From 6fcebaea3fd3f82910349e8723b3963504e0f0b0 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 24 Aug 2026 18:35:36 +0000 Subject: [PATCH] Transition map: svg-scaled fonts, border-clipped pill text, tight crop - Fonts scale with the svg instead of the --u constant-screen-size compensation (ResizeObserver machinery removed) - Larger node text (slug 19px, count 15px) - Pill labels no longer ellipsis-truncated: text is clipped at the pill border via per-node clipPaths; captions center when they fit and anchor left on overflow so the title's beginning survives; count lines stay centered - Bounding box crops to pill half extents plus the ribbon halo instead of the diagonal radius, removing the large top/bottom margins --- frontend/src/TransitionGraph.vue | 97 +++++++++++---------------- frontend/src/analytics/transitions.js | 42 ++++++------ 2 files changed, 61 insertions(+), 78 deletions(-) diff --git a/frontend/src/TransitionGraph.vue b/frontend/src/TransitionGraph.vue index 200becd..e57e565 100644 --- a/frontend/src/TransitionGraph.vue +++ b/frontend/src/TransitionGraph.vue @@ -6,7 +6,7 @@ * before passing them in; `window` carries the absolute [t0, t1) window * so the visual scale can normalize against a one-week reference. */ -import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue' +import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue' import { DAY, WEEK } from './analytics/time.js' import { formatCount } from './analytics/format.js' import { @@ -140,45 +140,14 @@ onBeforeUnmount(() => cancelAnimationFrame(rafId)) // The svg never renders larger than its natural size (1 viewBox unit = 1 // px, max-width below): the layout geometry is designed in pixel-like -// units, and upscaling would blow the pills up around their constant-size -// text. Narrow panels still scale the graph down to fit (width: 100%). -// Text renders at a constant screen size regardless of that downscale: -// measure the unit→pixel ratio and expose it as --u on the svg, which the -// font-size rules divide by. Falls back to 1 (raw units) until measured. -const svgEl = ref(null) -const pxPerUnit = ref(1) -let resizeObs = null +// units, and upscaling would blow up the pills around their text. Narrow +// panels scale the graph down to fit (width: 100%), text along with it. -function updateScale() { - const el = svgEl.value - if (el && el.viewBox.baseVal.width) { - pxPerUnit.value = el.clientWidth / el.viewBox.baseVal.width - } -} - -onMounted(() => { - resizeObs = new ResizeObserver(updateScale) -}) -watch(svgEl, (el) => { - resizeObs?.disconnect() - if (el) resizeObs?.observe(el) -}) -watch(() => graph.value?.bounds, updateScale) -onBeforeUnmount(() => resizeObs?.disconnect()) - -// Label text shortened to fit inside the pill at the current zoom (fonts -// are fixed screen sizes): drop whole trailing words first, then hard-cut -// with an ellipsis. Width estimate: ~0.52 em per glyph, 12 px padding -// per side. -const fitLabel = (label, fontPx = 15) => { - const budget = Math.max(2, (TNODE_W * pxPerUnit.value - 24) / (0.52 * fontPx)) - if (label.length <= budget) return label - const words = label.split(' ') - while (words.length > 1 && words.join(' ').length + 1 > budget) words.pop() - let out = words.join(' ') - if (out.length + 1 > budget) out = out.slice(0, Math.floor(budget) - 1) - return `${out}…` -} +// Pill text is not truncated: text is clipped at the pill's rounded border +// (clipPath per node, inset a few units for padding). Captions center when +// they fit; overlong ones anchor left so their beginning (not their +// middle) survives the clip. Width estimate: ~0.52 em per glyph. +const fitsPill = (label, fontPx = 19) => label.length * 0.52 * fontPx <= TNODE_W - 16 const countLabel = (n) => n.readMin ? `${formatCount(n.views)}×${n.readMin}m` : formatCount(n.views) @@ -186,7 +155,7 @@ const countLabel = (n) =>