diff --git a/ui/src/components/BridgeView.jsx b/ui/src/components/BridgeView.jsx index 3a71a83..2b286a8 100644 --- a/ui/src/components/BridgeView.jsx +++ b/ui/src/components/BridgeView.jsx @@ -193,7 +193,40 @@ export function buildSteps(rows, { // Plot geometry, also pure: given the steps and a canvas size, where does each // bar and label land? Exported so collisions and overflow can be checked. -export function layoutSteps(steps, width, H = 340, PAD = { t: 24, r: 16, b: 64, l: 68 }) { +// Break a label to the bar's width, on word boundaries. +// +// SVG text does not wrap, so the labels were cut at twelve characters and +// "04 - Forecast" and "04 - Forecasting" read the same. Character width is +// estimated rather than measured -- measuring means a DOM round trip per label +// on every render, and at this font a digit is about 0.55em, which is close +// enough for a centred label with a bar's width to play with. +// +// Three lines maximum, and a word longer than the line is left to overflow +// rather than broken: a truncated word helps nobody, and the bars are wide +// enough that it only happens to things like a pasted part number. +function wrapLabel(label, barW, fontSize = 10, maxLines = 3) { + const perLine = Math.max(6, Math.floor(barW / (fontSize * 0.55))) + const words = String(label || '').split(/\s+/).filter(Boolean) + const lines = [] + let line = '' + for (const w of words) { + const next = line ? `${line} ${w}` : w + if (next.length <= perLine) { line = next; continue } + if (line) lines.push(line) + line = w + if (lines.length === maxLines) break + } + if (line && lines.length < maxLines) lines.push(line) + if (!lines.length) return [''] + // anything that did not fit is marked on the last line rather than dropped + const used = lines.join(' ').length + if (used < String(label).replace(/\s+/g, ' ').length) { + lines[lines.length - 1] = `${lines[lines.length - 1]}…` + } + return lines +} + +export function layoutSteps(steps, width, H = 340, PAD = { t: 24, r: 16, b: 84, l: 68 }) { const plotW = Math.max(120, width - PAD.l - PAD.r) const plotH = H - PAD.t - PAD.b const values = steps.flatMap(s => [s.start, s.end]) @@ -427,6 +460,8 @@ export default function BridgeView({ const h = Math.max(2, bot - top) const x = xOf(i) const on = hover?.key === s.key + const labelLines = wrapLabel(s.label, barW) + const subY = PAD.t + plotH + 16 + labelLines.length * 11 + 2 return ( setHover({ ...s, x: x + barW / 2, y: top })} @@ -447,15 +482,18 @@ export default function BridgeView({ {isAnchor ? fmt(s.end, 0) : fmtSigned(s.delta, 0)} - {s.label.length > 12 ? `${s.label.slice(0, 11)}…` : s.label} + {labelLines.map((ln, li) => ( + {ln} + ))} + {/* below the label, however many lines it took */} {!isAnchor && s.entries > 1 && ( - + ×{s.entries} )} {!s.tagged && !isAnchor && ( - + untagged )}