Skip to content

Commit 3f9ed2e

Browse files
ryan-williamsclaude
andcommitted
FatalitiesPerYearPlot: pin unified hover label at top of plot
Plotly's `x unified` hover label is anchored to the topmost trace value at each x, so it bobs up/down as the user moves across bars. A `MutationObserver` now overrides the hover label's SVG `transform` attribute with a CSS transform (which wins per spec) to lock its y at a fixed offset from the plot's top — Plotly's re-renders no longer fight back. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 99592e8 commit 3f9ed2e

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

www/src/njsp/FatalitiesPerYearPlot.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,38 @@ export function FatalitiesPerYearPlot({ id = "per-year", initialCounty = null, c
217217
return () => observer.disconnect()
218218
}, [])
219219

220+
// Pin the unified hover label at a constant y so it doesn't bob up/down
221+
// with bar heights as the user moves across x. Plotly anchors the label
222+
// to the topmost trace value at each x; we override its SVG `transform`
223+
// attribute with a CSS transform (which wins per spec), so Plotly's
224+
// re-renders don't fight back.
225+
useEffect(() => {
226+
const container = containerRef.current
227+
if (!container) return
228+
const ANCHOR_Y = 24 // pixels from plot top
229+
const reposition = (lg: SVGGElement) => {
230+
const transform = lg.getAttribute('transform') ?? ''
231+
const match = transform.match(/translate\(([-\d.]+)[ ,]\s*([-\d.]+)\)/)
232+
if (!match) return
233+
const x = parseFloat(match[1])
234+
lg.style.transform = `translate(${x}px, ${ANCHOR_Y}px)`
235+
}
236+
const scan = () => {
237+
const lg = container.querySelector(
238+
'.js-plotly-plot .hoverlayer g.legend',
239+
) as SVGGElement | null
240+
if (lg) reposition(lg)
241+
}
242+
const observer = new MutationObserver(scan)
243+
observer.observe(container, {
244+
childList: true,
245+
subtree: true,
246+
attributes: true,
247+
attributeFilter: ['transform'],
248+
})
249+
return () => observer.disconnect()
250+
}, [])
251+
220252
// Load data sources
221253
const ytcDb = useRegisteredDb({ db, table: "ytc", url: YtcCsv })
222254
const monthlyDb = useRegisteredDb({ db, table: "monthly", url: MonthlyCsv })
@@ -400,6 +432,7 @@ export function FatalitiesPerYearPlot({ id = "per-year", initialCounty = null, c
400432
hovertemplate: `%{y:.2f}<extra>12-mo avg</extra>`,
401433
} as PlotData)
402434

435+
403436
// Optional population overlay on y2.
404437
if (showPop) {
405438
const popX: string[] = []

0 commit comments

Comments
 (0)