Reading map: a legend under the figure
Five marks, and until now the only way to learn what any of them meant was to hover a dot. The last row is the one worth having: it says out loud that the fuchsia node is where the quote at the top of the page came from, which is otherwise a connection nobody would guess was being made. Three things it does not do: Counts come from the payload at mount, not from the markup — they move every time a book is indexed, and a hardcoded 8,424 would go stale on the next one. Swatches take the palette the renderer resolved rather than re-reading the CSS vars, so a scheme modifier on the container (`--ember` swaps the newest hue) cannot leave the legend describing a different picture than the one on screen. The quoted row follows markQuoted's return value, not the attempt. A quote whose source book was never indexed leaves the map unmarked, and the legend now stays quiet about it instead of pointing at a colour that is not there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,17 @@ const {
|
||||
<figure class="reading-map-figure">
|
||||
<div class="reading-map" data-src={src}></div>
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
|
||||
{/* Counts are filled in from the payload at mount, not hardcoded — they move every time a
|
||||
book is indexed. The `quoted` row stays hidden until markQuoted() actually finds a node,
|
||||
so the legend never names a mark that is not on screen. */}
|
||||
<ul class="reading-map-legend" hidden>
|
||||
<li><i data-swatch="nebula"></i><b data-count="nodes"></b> passages read</li>
|
||||
<li><i data-swatch="signal"></i><b data-count="highlights"></b> highlighted</li>
|
||||
<li><i data-swatch="trace"></i>in the order I marked them</li>
|
||||
<li><i data-swatch="newest"></i>most recent</li>
|
||||
<li data-row="quoted" hidden><i data-swatch="quoted"></i>the quote at the top of this page</li>
|
||||
</ul>
|
||||
</figure>
|
||||
|
||||
<script>
|
||||
@@ -65,13 +76,38 @@ const {
|
||||
});
|
||||
if (!map) continue;
|
||||
|
||||
// Ring the node the header's quote came from. The two are independent fetches on
|
||||
const figure = el.closest('.reading-map-figure');
|
||||
const legend = figure?.querySelector<HTMLElement>('.reading-map-legend');
|
||||
if (legend) {
|
||||
for (const b of legend.querySelectorAll<HTMLElement>('[data-count]')) {
|
||||
const n = map.stats[b.dataset.count as keyof typeof map.stats];
|
||||
b.textContent = n.toLocaleString();
|
||||
}
|
||||
// Swatches take the palette the renderer actually resolved, so a scheme modifier on
|
||||
// the container (`--ember` swaps the newest colour) can never leave the legend
|
||||
// describing a picture that is not the one on screen.
|
||||
for (const i of legend.querySelectorAll<HTMLElement>('[data-swatch]')) {
|
||||
i.style.background = map.colors[i.dataset.swatch as keyof typeof map.colors];
|
||||
}
|
||||
legend.hidden = false;
|
||||
}
|
||||
const quotedRow = legend?.querySelector<HTMLElement>('[data-row="quoted"]');
|
||||
|
||||
// Tint the node the header's quote came from. The two are independent fetches on
|
||||
// every page view and either can land first, so handle both orders: read the hash if
|
||||
// it is already there, and listen in case it is not. `garden:quote` is dispatched by
|
||||
// the header script in Base.astro.
|
||||
const ring = (hash?: string) => hash && map.markQuoted(hash);
|
||||
ring(window.__gardenQuoteHash);
|
||||
addEventListener('garden:quote', (e) => ring((e as CustomEvent).detail?.text_hash));
|
||||
//
|
||||
// The legend row follows markQuoted's return value rather than the attempt, so a quote
|
||||
// whose source book was never indexed leaves the map unmarked AND unexplained, instead
|
||||
// of pointing at a colour that is not there.
|
||||
const mark = (hash?: string) => {
|
||||
if (!hash) return;
|
||||
const found = map.markQuoted(hash);
|
||||
if (quotedRow) quotedRow.hidden = !found;
|
||||
};
|
||||
mark(window.__gardenQuoteHash);
|
||||
addEventListener('garden:quote', (e) => mark((e as CustomEvent).detail?.text_hash));
|
||||
}
|
||||
} catch (err) {
|
||||
/* chunk failed to load: the figure stays hidden */
|
||||
@@ -90,8 +126,49 @@ const {
|
||||
line-height: 1.5;
|
||||
opacity: 0.72;
|
||||
}
|
||||
/* Hide the caption too when the map itself failed to mount. */
|
||||
.reading-map-figure:not(:has([data-ready])) figcaption {
|
||||
/* Hide the caption and legend too when the map itself failed to mount. */
|
||||
.reading-map-figure:not(:has([data-ready])) figcaption,
|
||||
.reading-map-figure:not(:has([data-ready])) .reading-map-legend {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The legend is the one place colour is *allowed* to be the whole encoding: it is where the
|
||||
mapping from hue to meaning is stated, rather than left to be inferred off the cloud. */
|
||||
.reading-map-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 1.15rem;
|
||||
margin: 0.7rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0.72;
|
||||
}
|
||||
.reading-map-legend[hidden] { display: none; }
|
||||
|
||||
.reading-map-legend li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.42rem;
|
||||
}
|
||||
.reading-map-legend b {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* Backgrounds are set from the resolved palette at mount — see the script above. The dot is
|
||||
nudged down because `align-items: baseline` aligns it on its own box, not the text's. */
|
||||
.reading-map-legend i {
|
||||
flex: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transform: translateY(0.06rem);
|
||||
}
|
||||
/* The trace is a line in the picture, so it is a line here too rather than a third dot. */
|
||||
.reading-map-legend i[data-swatch='trace'] {
|
||||
width: 0.85rem;
|
||||
height: 0.14rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,6 +74,8 @@ export function build(container, data) {
|
||||
signal: cssVar(container, '--map-signal', '#ea580c'),
|
||||
trace: cssVar(container, '--map-trace', '#2563eb'),
|
||||
fog: cssVar(container, '--map-fog', '#fcfcfb'),
|
||||
newest: cssVar(container, '--map-newest', '#f59e0b'),
|
||||
quoted: cssVar(container, '--map-quoted', '#e879f9'),
|
||||
};
|
||||
|
||||
// --- scene ------------------------------------------------------------------------------
|
||||
@@ -246,13 +248,12 @@ export function build(container, data) {
|
||||
// trace_order is chronological and pre-sorted, `t` is date-only, and many highlights share a
|
||||
// day — sorting on it would quietly pick a different passage from the same date.
|
||||
const newestNode = data.trace.length ? data.trace[data.trace.length - 1].node : null;
|
||||
if (newestNode != null) paint(newestNode, cssVar(container, '--map-newest', colors.signal));
|
||||
if (newestNode != null) paint(newestNode, colors.newest);
|
||||
|
||||
// Set by the page once the header's random quote has loaded; the two fetches are independent
|
||||
// and either can win. `h` is the highlight's text_hash, which the API returns alongside the
|
||||
// quote — an exact key, not a text match.
|
||||
const nodeOfHash = new Map(data.trace.map((t) => [t.h, t.node]));
|
||||
const quotedHex = cssVar(container, '--map-quoted', colors.signal);
|
||||
let quotedNode = null;
|
||||
|
||||
// --- overlays ----------------------------------------------------------------------------
|
||||
@@ -426,9 +427,13 @@ export function build(container, data) {
|
||||
quotedNode = node;
|
||||
// Quoted wins a tie: if the header happens to quote the newest highlight, the passage
|
||||
// the reader is actually looking at is the one worth pointing at.
|
||||
return node != null && paint(node, quotedHex);
|
||||
return node != null && paint(node, colors.quoted);
|
||||
},
|
||||
stats: { nodes: nodeCount, highlights: total, chunks: signalNodes.length },
|
||||
// The resolved palette, so a legend can colour its swatches from the same values this
|
||||
// renderer drew with — including whichever `--map-*` scheme modifier is on the container.
|
||||
// Two sources for one colour is how a legend ends up quietly lying about the picture.
|
||||
colors,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user