fix: keep turn strip labels centered in their block's visible slice while scrolling
This commit is contained in:
1 parent
ef5166675a
commit
25fe067c2e
2 files changed
+57
-4
No files matched your search
@@ -219,6 +219,21 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
grp.span += req.stepCount ?? 1
|
||||
}
|
||||
|
||||
// Prefix geometry of the strip blocks in content px: the scroll handler
|
||||
// re-centers each label inside its block's VISIBLE slice analytically,
|
||||
// so it only measures the handful of labels currently on screen.
|
||||
const turnOffsets: number[] = []
|
||||
const turnWidths: number[] = []
|
||||
{
|
||||
let x = 0
|
||||
for (const grp of groups) {
|
||||
const w = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP
|
||||
turnOffsets.push(x)
|
||||
turnWidths.push(w)
|
||||
x += w + BAR_GAP
|
||||
}
|
||||
}
|
||||
|
||||
// Default anchor: the newest bars at the RIGHT edge. The first layout
|
||||
// after mount scrolls unconditionally; a GRANULARITY SWITCH re-anchors
|
||||
// the same way (returning to step mode must show the newest bars, not
|
||||
@@ -246,6 +261,38 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
edgesRef.current = { left, right }
|
||||
setEdges({ left, right })
|
||||
}
|
||||
/**
|
||||
* Keep each turn label centered within its block's VISIBLE slice, so a
|
||||
* label never scrolls out of view while any part of its block remains
|
||||
* on screen. Blocks narrower than their label stay put (the label is
|
||||
* already ellipsized). Reads (offsetWidth) are gathered before writes
|
||||
* (transform) to avoid layout thrash on the scroll path; out-of-view
|
||||
* blocks need no measurement at all.
|
||||
*/
|
||||
const updateTurnLabels = (el: HTMLDivElement): void => {
|
||||
const labels = el.querySelectorAll<HTMLElement>('.lc-turn-label')
|
||||
const n = Math.min(labels.length, turnOffsets.length)
|
||||
const sl = el.scrollLeft
|
||||
const vr = sl + el.clientWidth
|
||||
const writes: [HTMLElement, string][] = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const off = turnOffsets[i]
|
||||
const w = turnWidths[i]
|
||||
const visL = Math.max(off, sl)
|
||||
const visR = Math.min(off + w, vr)
|
||||
let dx = 0
|
||||
if (visR > visL) {
|
||||
const lw = labels[i].offsetWidth
|
||||
if (lw < w) {
|
||||
const center = (visL + visR) / 2 - off
|
||||
dx = Math.min(Math.max(center, lw / 2), w - lw / 2) - w / 2
|
||||
}
|
||||
}
|
||||
const next = dx !== 0 ? `translateX(${dx}px)` : ''
|
||||
if (labels[i].style.transform !== next) writes.push([labels[i], next])
|
||||
}
|
||||
for (const [label, next] of writes) label.style.transform = next
|
||||
}
|
||||
React.useLayoutEffect(() => {
|
||||
const el = scrollRef.current
|
||||
if (el === null) return
|
||||
@@ -271,6 +318,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
el.scrollLeft = el.scrollWidth
|
||||
}
|
||||
updateEdges(el)
|
||||
updateTurnLabels(el)
|
||||
})
|
||||
|
||||
// Compact single-line hover tooltip (shown instantly by the custom
|
||||
@@ -305,7 +353,10 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
// bars and strip blocks OUTSIDE the active turn fade to 35%.
|
||||
className={'lc-chart-scroll' + (props.activeTurn !== null ? ' lc-chart-dim' : '')}
|
||||
ref={scrollRef}
|
||||
onScroll={(e: ReactNS.UIEvent<HTMLDivElement>) => { updateEdges(e.currentTarget) }}
|
||||
onScroll={(e: ReactNS.UIEvent<HTMLDivElement>) => {
|
||||
updateEdges(e.currentTarget)
|
||||
updateTurnLabels(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
{/* Edge fades: visible whenever more history sits beyond the viewport,
|
||||
so the horizontal scroll affordance is obvious. */}
|
||||
@@ -351,19 +402,18 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
// blocks span their steps' columns. The flex gap between blocks
|
||||
// mirrors the bar gap, so blocks always line up with the bars.
|
||||
const on = props.activeTurn === grp.turn
|
||||
const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP
|
||||
return (
|
||||
<span
|
||||
key={`turn-${gi}`}
|
||||
className={'lc-turn' + (on ? ' lc-turn-on' : '')}
|
||||
style={{
|
||||
width: `${blockW}px`,
|
||||
width: `${turnWidths[gi]}px`,
|
||||
background: TURN_FILLS[gi % TURN_FILLS.length],
|
||||
}}
|
||||
title={`T${grp.turn}`}
|
||||
onMouseEnter={() => { props.onHoverTurn(grp.turn) }}
|
||||
onClick={() => { props.onPickTurn(grp.turn) }}
|
||||
>{`T${grp.turn}`}</span>
|
||||
><span className="lc-turn-label">{`T${grp.turn}`}</span></span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -207,6 +207,9 @@
|
||||
.lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }
|
||||
.lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: filter 120ms, opacity 120ms; }
|
||||
.lc-turn-on { filter: brightness(1.35); color: var(--dsw-alias-label-primary); }
|
||||
/* The label is translated by JS to stay centered in the block's visible
|
||||
slice; max-width keeps the block's ellipsis behavior for narrow blocks. */
|
||||
.lc-turn-label { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }
|
||||
.lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }
|
||||
.lc-detail-head b { color: var(--dsw-alias-label-primary); }
|
||||
|
||||
Reference in new issue
Block a user