diff --git a/explorer/app.js b/explorer/app.js index 7bc14a5..8e2e584 100644 --- a/explorer/app.js +++ b/explorer/app.js @@ -311,8 +311,8 @@ function overview(root) { state.plan ? section('Diagram topics by area', areaSummary()) : null, ...groups.map((g) => section(g.title, topicChips(g.topics))), section('How to explore', h('ul', { class: 'howto' }, - h('li', {}, 'Map follows the roadmap diagram: topics sit in Software, Hardware or both, and are colored by importance.'), - h('li', {}, 'Click Required, Recommended or Possibility in the legend above the map to highlight topics of that importance.'), + h('li', {}, 'Map follows the roadmap diagram: topics sit in Software, Hardware or both, and are colored by learning priority, a general guide that varies with your specialty and role.'), + h('li', {}, 'Click Required, Recommended or Optional in the legend above the map to highlight topics of that priority.'), h('li', {}, 'Hover or select a topic to see solid lines to its related topics. Fainter dashed lines come from resources that share or mention a topic.'), h('li', {}, 'Outline lists every topic of the README; use it to browse topics that are not on the map.'), h('li', {}, 'Press ', h('kbd', {}, '/'), ' to search topics and resources.'))), diff --git a/explorer/dom.js b/explorer/dom.js index 91544fd..881595b 100644 --- a/explorer/dom.js +++ b/explorer/dom.js @@ -49,7 +49,7 @@ export const AREAS = { export const IMPORTANCE = { required: 'Required', recommended: 'Recommended', - possible: 'Possibility', + possible: 'Optional', }; export const areaClass = (area) => (AREAS[area] ? AREAS[area].cls : 'xx'); diff --git a/explorer/layout.js b/explorer/layout.js index 4a1d91d..8c61ab8 100644 --- a/explorer/layout.js +++ b/explorer/layout.js @@ -9,7 +9,7 @@ export const SPACE = 10; const COL = 170; const GUTTER = 26; const LEFT = 86; -const TOP = 159; +const TOP = 148; const BOX_H = 26; const BOX_H2 = 42; const HEAD_H = 20; diff --git a/explorer/map.css b/explorer/map.css index 6619283..7aaaf3a 100644 --- a/explorer/map.css +++ b/explorer/map.css @@ -203,21 +203,35 @@ font-weight: 600; } -.legend-note { +.legend-caption { fill: var(--muted); - font-size: 12px; - font-style: italic; + font-size: 12.5px; + font-weight: 600; +} + +.legend-info circle:first-child { + fill: none; + stroke: var(--muted); + stroke-width: 1.4; +} + +.legend-info path { + fill: none; + stroke: var(--muted); + stroke-width: 1.6; + stroke-linecap: round; +} + +.legend-info-dot { + fill: var(--muted); + stroke: var(--muted); + stroke-width: 1.2; } .legend-imp { stroke: var(--box-edge); stroke-width: 1; } .legend-imp.required { fill: var(--imp-required); } .legend-imp.recommended { fill: var(--imp-recommended); } .legend-imp.possible { fill: var(--imp-possible); stroke: var(--border-strong); } -.legend-area { fill: none; stroke-width: 2.5; } -.legend-area.sw { stroke: var(--sw); } -.legend-area.hw { stroke: var(--hw); } -.legend-area.ss { stroke: var(--ss); } -.legend-hatch { fill: url(#cross-hatch); stroke: var(--border-strong); } .legend-hit { fill: transparent; diff --git a/explorer/mapview.js b/explorer/mapview.js index 7e8a59d..4a25a7e 100644 --- a/explorer/mapview.js +++ b/explorer/mapview.js @@ -1,5 +1,5 @@ // Interactive roadmap diagram: Software / Hardware / Soft skills areas with their cross-sections, -// importance colors, filters and connections between topics. Laid out by layout.js to fit A4. +// priority colors, filters and connections between topics. Laid out by layout.js to fit A4. import { CREDITS, GITHUB_MARK, IMPORTANCE, TITLE, areaDots, areaLabel, h, plural, svg } from './dom.js'; import { SPACE, buildLayout } from './layout.js'; @@ -103,7 +103,7 @@ export function createMapView({ root, plan, data, build, onSelect }) { const names = Object.keys(IMPORTANCE).filter((l) => state.importance.has(l)).map((l) => IMPORTANCE[l]); filterLabel.textContent = names.join(' · '); filterPill.hidden = !names.length; - for (const part of legendParts) if (part.pressed) part.g.setAttribute('aria-pressed', String(part.pressed())); + for (const part of legendParts) part.g.setAttribute('aria-pressed', String(part.pressed())); legend.classList.toggle('filtering', names.length > 0); } @@ -132,47 +132,44 @@ export function createMapView({ root, plan, data, build, onSelect }) { rect({ x: 0, y: 0, w: layout.width, h: layout.height }, { class: 'page-bg' }, layers.page); - // Title and legend, so the diagram also stands on its own when printed. + // Title, then one legend row (priority keys, then an info icon and caption), so the diagram also stands on its own when printed. // On screen the header already names the roadmap, so the title only appears in print, PDF and PNG. const legendY = layout.titleY + 22; - const screenTop = legendY - 24; + // On screen the view starts just above the legend row, close to the toolbar. + const screenTop = legendY - 17; const title = svg('text', { x: 24, y: layout.titleY, class: 'page-title' }, layers.labels); title.textContent = TITLE; const legend = svg('g', { class: 'page-legend' }, layers.labels); - const legendItems = [ - ...Object.entries(IMPORTANCE).map(([level, label]) => ({ - kind: 'imp', cls: level, label, pressed: () => state.importance.has(level), act: () => toggle(state.importance, level), - })), - { kind: 'area', cls: 'sw', label: 'Software' }, - { kind: 'area', cls: 'hw', label: 'Hardware' }, - { kind: 'hatch', cls: '', label: 'Software ∩ Hardware' }, - { kind: 'area', cls: 'ss', label: 'Soft skills' }, - ]; - // On screen the importance entries are also filter buttons; areas are already visible as the map's regions. - for (const item of legendItems) { + const info = svg('g', { class: 'legend-info' }, legend); + svg('circle', { cx: 7.5, cy: legendY + 6.5, r: 7.5 }, info); + svg('path', { d: `M7.5 ${legendY + 6} v4.2` }, info); + svg('circle', { cx: 7.5, cy: legendY + 3.2, r: 0.6, class: 'legend-info-dot' }, info); + const caption = svg('text', { x: 0, y: legendY + 11, class: 'legend-caption' }, legend); + caption.textContent = 'A general guide: priorities vary with your specialty and role'; + // Areas need no legend: their regions are labeled in the margin. On screen each entry is also a filter button. + for (const [level, label] of Object.entries(IMPORTANCE)) { const y = legendY; - const cls = item.kind === 'imp' ? `legend-imp ${item.cls}` : item.kind === 'area' ? `legend-area ${item.cls}` : 'legend-hatch'; - const g = svg('g', item.act ? { - class: 'legend-item', role: 'button', tabindex: '0', 'aria-pressed': 'false', 'aria-label': `Highlight ${item.label} topics`, - } : { class: 'legend-entry' }, legend); - if (item.act) svg('title', {}, g).textContent = `Highlight ${item.label} topics`; - const hit = item.act ? rect({ x: 0, y: y - 5, w: 0, h: 23 }, { rx: 6, class: 'legend-hit' }, g) : null; - const key = rect({ x: 0, y, w: 22, h: 13 }, { rx: item.kind === 'imp' ? 3 : 4, class: cls }, g); + const pressed = () => state.importance.has(level); + const act = () => toggle(state.importance, level); + const g = svg('g', { + class: 'legend-item', role: 'button', tabindex: '0', 'aria-pressed': 'false', 'aria-label': `Highlight ${label} topics`, + }, legend); + svg('title', {}, g).textContent = `Highlight ${label} topics`; + const hit = rect({ x: 0, y: y - 5, w: 0, h: 23 }, { rx: 6, class: 'legend-hit' }, g); + const key = rect({ x: 0, y, w: 22, h: 13 }, { rx: 3, class: `legend-imp ${level}` }, g); const t = svg('text', { x: 0, y: y + 11, class: 'legend-text' }, g); - t.textContent = item.label; - if (item.act) { - g.addEventListener('click', () => { - // A drag that starts on the legend pans the map; it must not also filter. - if (state.drag && state.drag.moved) return; - item.act(); - }); - g.addEventListener('keydown', (e) => { - if (e.key !== 'Enter' && e.key !== ' ') return; - e.preventDefault(); - item.act(); - }); - } - legendParts.push({ g, hit, key, t, label: item.label, pressed: item.pressed }); + t.textContent = label; + g.addEventListener('click', () => { + // A drag that starts on the legend pans the map; it must not also filter. + if (state.drag && state.drag.moved) return; + act(); + }); + g.addEventListener('keydown', (e) => { + if (e.key !== 'Enter' && e.key !== ' ') return; + e.preventDefault(); + act(); + }); + legendParts.push({ g, hit, key, t, label, pressed }); } function layoutLegend() { @@ -181,15 +178,15 @@ export function createMapView({ root, plan, data, build, onSelect }) { const width = 28 + ((part.t.getComputedTextLength && part.t.getComputedTextLength()) || part.label.length * 6.5); part.key.setAttribute('x', x); part.t.setAttribute('x', x + 28); - if (part.hit) { - part.hit.setAttribute('x', x - 6); - part.hit.setAttribute('width', width + 12); - } + part.hit.setAttribute('x', x - 6); + part.hit.setAttribute('width', width + 12); x += width + 20; } + // The caption follows the keys, set apart by a wider gap. + x += 16; + info.setAttribute('transform', `translate(${x.toFixed(1)} 0)`); + caption.setAttribute('x', (x + 22).toFixed(1)); } - const note = svg('text', { x: 24, y: layout.titleY + 54, class: 'legend-note' }, layers.labels); - note.textContent = 'Colors show the average importance of each skill for an embedded systems engineer.'; // Areas and their cross-sections. const sw = layout.areas.SOFTWARE; @@ -461,7 +458,7 @@ export function createMapView({ root, plan, data, build, onSelect }) { const slackX = Math.max(0, page.w - v.w); const slackY = Math.max(0, page.h - v.h); v.x = slackX ? Math.min(Math.max(v.x, -40), slackX + 40) : (page.w - v.w) / 2; - v.y = slackY ? Math.min(Math.max(v.y, screenTop - 40), slackY + 40) : (page.h - v.h) / 2; + v.y = slackY ? Math.min(Math.max(v.y, screenTop - 24), slackY + 40) : (page.h - v.h) / 2; state.view = v; chart.setAttribute('viewBox', `${v.x.toFixed(1)} ${v.y.toFixed(1)} ${v.w.toFixed(1)} ${v.h.toFixed(1)}`); if (size) zoomLevel.textContent = `${Math.round((size.w / v.w) * 100)}%`; @@ -471,7 +468,7 @@ export function createMapView({ root, plan, data, build, onSelect }) { const size = canvasSize(); const width = size ? Math.max(page.w + 40, size.w / MAX_SCALE) : page.w; fitMode = 'width'; - setView({ x: (page.w - width) / 2, y: screenTop - 20, w: width }); + setView({ x: (page.w - width) / 2, y: screenTop, w: width }); fitButton.title = 'Show the whole page'; }