Wrap the bridge's bar labels instead of cutting them
SVG text does not wrap, so labels were cut at twelve characters -- which with the sort prefixes now on every bucket meant "04 - Forecas…" and told you nothing the position of the bar had not already. Word wrapping to the bar's width, three lines at most, with the ×n and untagged markers moved below however many lines the label took and the plot's bottom padding raised to make room. Character width is estimated rather than measured: measuring means a DOM round trip per label on every render, and at this size a digit is about 0.55em, which is close enough for a centred label with a bar's width to play with. A word longer than the line overflows rather than breaking, since half a word helps nobody. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
96342f3527
commit
5cc59b5792
@ -193,7 +193,40 @@ export function buildSteps(rows, {
|
|||||||
|
|
||||||
// Plot geometry, also pure: given the steps and a canvas size, where does each
|
// 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.
|
// 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 plotW = Math.max(120, width - PAD.l - PAD.r)
|
||||||
const plotH = H - PAD.t - PAD.b
|
const plotH = H - PAD.t - PAD.b
|
||||||
const values = steps.flatMap(s => [s.start, s.end])
|
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 h = Math.max(2, bot - top)
|
||||||
const x = xOf(i)
|
const x = xOf(i)
|
||||||
const on = hover?.key === s.key
|
const on = hover?.key === s.key
|
||||||
|
const labelLines = wrapLabel(s.label, barW)
|
||||||
|
const subY = PAD.t + plotH + 16 + labelLines.length * 11 + 2
|
||||||
return (
|
return (
|
||||||
<g key={s.key}
|
<g key={s.key}
|
||||||
onMouseEnter={() => setHover({ ...s, x: x + barW / 2, y: top })}
|
onMouseEnter={() => 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)}
|
{isAnchor ? fmt(s.end, 0) : fmtSigned(s.delta, 0)}
|
||||||
</text>
|
</text>
|
||||||
<text x={x + barW / 2} y={PAD.t + plotH + 16} textAnchor="middle" fontSize="10" fill={INK}>
|
<text x={x + barW / 2} y={PAD.t + plotH + 16} textAnchor="middle" fontSize="10" fill={INK}>
|
||||||
{s.label.length > 12 ? `${s.label.slice(0, 11)}…` : s.label}
|
{labelLines.map((ln, li) => (
|
||||||
|
<tspan key={li} x={x + barW / 2} dy={li === 0 ? 0 : 11}>{ln}</tspan>
|
||||||
|
))}
|
||||||
</text>
|
</text>
|
||||||
|
{/* below the label, however many lines it took */}
|
||||||
{!isAnchor && s.entries > 1 && (
|
{!isAnchor && s.entries > 1 && (
|
||||||
<text x={x + barW / 2} y={PAD.t + plotH + 29} textAnchor="middle" fontSize="9" fill={INK_DIM}>
|
<text x={x + barW / 2} y={subY} textAnchor="middle" fontSize="9" fill={INK_DIM}>
|
||||||
×{s.entries}
|
×{s.entries}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
{!s.tagged && !isAnchor && (
|
{!s.tagged && !isAnchor && (
|
||||||
<text x={x + barW / 2} y={PAD.t + plotH + 29} textAnchor="middle" fontSize="9" fill={INK_DIM}>
|
<text x={x + barW / 2} y={subY} textAnchor="middle" fontSize="9" fill={INK_DIM}>
|
||||||
untagged
|
untagged
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user