feat(ui): themed hover tooltip on the composition bar segments
The Current composition stacked bar (and the request-detail mini bar) now shows a theme-native tooltip on hover per segment — category, token count, and share of the total — positioned at the pointer and clamped inside the bar. Replaces the native title attribute; the wrapper keeps the tooltip outside the rounded/clipped stack.
This commit is contained in:
1 parent
f8ac2f45cb
commit
ed63b7f0d8
2 files changed
+54
-12
No files matched your search
+15
-1
@@ -276,4 +276,18 @@ ctxSlots[3][1](null) // leave the plot
|
||||
tr = renderView()
|
||||
assert.match(detailStep(tr), /T3/, 'leaving the plot reverts the detail to the newest request')
|
||||
|
||||
console.log('✔ chart render test passed (fixed-width bars, scroll container, turn ranges, hover linking)')
|
||||
// ---- overview stacked bar: themed hover tooltip per segment ----
|
||||
const overviewStack = byClass(tr, 'lc-stacked').find(s => s.args[1].style.height === '16px')
|
||||
assert.ok(overviewStack, 'overview stacked bar present')
|
||||
const segment = overviewStack.args.slice(2).flat().find(s => s !== null)
|
||||
assert.ok(segment, 'overview has segments')
|
||||
assert.equal(segment.args[1].title, undefined, 'native title replaced by the custom tooltip')
|
||||
assert.equal(typeof segment.args[1].onMouseEnter, 'function', 'segments carry onMouseEnter')
|
||||
segment.args[1].onMouseEnter({ clientX: 120 }) // fake pointer; ref is null in tests -> centered fallback
|
||||
tr = renderView()
|
||||
const tip = byClass(tr, 'lc-bar-tip')
|
||||
assert.equal(tip.length, 1, 'hovering a segment shows the tooltip')
|
||||
assert.match(textOf(tip[0]), /\(10%\)/, 'tooltip shows the segment share of the total')
|
||||
assert.equal(typeof tip[0].args[1].style.left, 'string', 'tooltip is positioned along the pointer')
|
||||
|
||||
console.log('✔ chart render test passed (fixed-width bars, scroll container, turn ranges, hover linking, overview tooltip)')
|
||||
+39
-11
@@ -292,17 +292,43 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
|
||||
let total = 0
|
||||
for (const p of props.parts) total += p.value
|
||||
const scale = props.max !== undefined && props.max > total ? props.max : total
|
||||
return h('div', { className: 'lc-stacked', style: { height: (props.height || 14) + 'px' } },
|
||||
total <= 0
|
||||
? null
|
||||
: props.parts.map(p => {
|
||||
if (!p.value) return null
|
||||
return h('div', {
|
||||
key: p.key,
|
||||
title: catLabel(p.key) + ' ' + fmt(p.value) + ' (' + Math.round(p.value / total * 100) + '%)',
|
||||
style: { width: (p.value / scale * 100) + '%', background: p.color },
|
||||
})
|
||||
}))
|
||||
|
||||
// Themed hover tooltip per segment: shows the category, its tokens, and
|
||||
// its share of the parts' total. The tooltip is positioned at the
|
||||
// pointer's x within the bar (clamped) and floats above the stack — the
|
||||
// wrapper keeps it outside the rounded/clipped stack itself.
|
||||
const wrapRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const [tip, setTip] = React.useState<{ key: string; text: string; left: number } | null>(null)
|
||||
const showTip = (e: ReactNS.MouseEvent<HTMLDivElement>, p: PartsPart): void => {
|
||||
const rect = wrapRef.current?.getBoundingClientRect()
|
||||
const left = rect ? e.clientX - rect.left : 50
|
||||
const width = rect?.width ?? 200
|
||||
setTip({
|
||||
key: p.key,
|
||||
text: catLabel(p.key) + ' ' + fmt(p.value) + ' (' + Math.round(p.value / total * 100) + '%)',
|
||||
left: Math.max(48, Math.min(left, width - 48)),
|
||||
})
|
||||
}
|
||||
|
||||
return h('div', { className: 'lc-stacked-wrap' },
|
||||
h('div', {
|
||||
className: 'lc-stacked',
|
||||
style: { height: (props.height || 14) + 'px' },
|
||||
ref: wrapRef,
|
||||
onMouseLeave: () => { setTip(null) },
|
||||
},
|
||||
total <= 0
|
||||
? null
|
||||
: props.parts.map(p => {
|
||||
if (!p.value) return null
|
||||
return h('div', {
|
||||
key: p.key,
|
||||
style: { width: (p.value / scale * 100) + '%', background: p.color },
|
||||
onMouseEnter: (e: ReactNS.MouseEvent<HTMLDivElement>) => { showTip(e, p) },
|
||||
onMouseMove: (e: ReactNS.MouseEvent<HTMLDivElement>) => { showTip(e, p) },
|
||||
})
|
||||
})),
|
||||
tip ? h('div', { className: 'lc-bar-tip', style: { left: tip.left + 'px' } }, tip.text) : null)
|
||||
}
|
||||
|
||||
function Legend(props: { parts: PartsPart[] }): ReactNS.ReactElement {
|
||||
@@ -602,7 +628,9 @@ const STYLES = [
|
||||
'.lc-overview-num { margin-bottom: 8px; }',
|
||||
'.lc-overview-num b { font-size: 20px; }',
|
||||
'.lc-overview-num span { color: var(--dsw-alias-label-secondary); }',
|
||||
'.lc-stacked-wrap { position: relative; width: 100%; }',
|
||||
'.lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }',
|
||||
'.lc-bar-tip { position: absolute; bottom: calc(100% + 6px); transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }',
|
||||
'.lc-stacked > div { height: 100%; }',
|
||||
'.lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }',
|
||||
'.lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }',
|
||||
|
||||
Reference in new issue
Block a user