Skip to content

Commit 8d06dd4

Browse files
committed
re-implement tooltip for bar chart, and template ref breaks updates in histoire
1 parent b1d754a commit 8d06dd4

File tree

2 files changed

+86
-23
lines changed

2 files changed

+86
-23
lines changed

lib/components/SChartBar.vue

+81-18
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,28 @@ function renderChart({
7979
.append('g')
8080
.attr('transform', `translate(${margin.left},${margin.top})`)
8181
82-
// X scale
83-
const x = d3
84-
.scaleBand()
82+
// Create a padded scale for the bars (with padding for visual spacing)
83+
const paddedScale = d3
84+
.scaleBand<string>()
8585
.domain(props.data.map((d) => d.key))
8686
.range(vertical ? [0, width] : [0, height])
8787
.padding(0.4)
8888
89-
// Y scale
89+
// Y scale for bar values
9090
const y = d3
9191
.scaleLinear()
9292
.domain([0, d3.max(props.data, (d) => d.value)!])
9393
.nice()
9494
.range(vertical ? [height, 0] : [0, width])
9595
96-
// Add X axis
96+
// Compute a constant offset to center the colored bar inside its full band.
97+
const groupOffset = (paddedScale.step() - paddedScale.bandwidth()) / 2
98+
99+
// For the axes, use the paddedScale so ticks remain centered on the bars.
97100
svg
98101
.append('g')
99102
.attr('transform', `translate(0,${height})`)
100-
.call(vertical ? d3.axisBottom(x) : d3.axisBottom(y).ticks(props.ticks))
103+
.call(vertical ? d3.axisBottom(paddedScale) : d3.axisBottom(y).ticks(props.ticks))
101104
.selectAll('text')
102105
.attr('fill', c.text2)
103106
.style('font-size', '14px')
@@ -111,7 +114,7 @@ function renderChart({
111114
// Add Y axis
112115
svg
113116
.append('g')
114-
.call(vertical ? d3.axisLeft(y).ticks(props.ticks) : d3.axisLeft(x))
117+
.call(vertical ? d3.axisLeft(y).ticks(props.ticks) : d3.axisLeft(paddedScale))
115118
.selectAll('text')
116119
.attr('fill', c.text2)
117120
.style('font-size', '14px')
@@ -123,7 +126,7 @@ function renderChart({
123126
124127
// Add horizontal grid lines
125128
const gridLines = svg
126-
.selectAll('line.grid')
129+
.selectAll()
127130
.data(y.ticks(props.ticks))
128131
.enter()
129132
.append('line')
@@ -168,10 +171,30 @@ function renderChart({
168171
}
169172
170173
// Add bars
171-
const bars = svg
172-
.selectAll('rect')
174+
const barGroups = svg
175+
.selectAll()
173176
.data(props.data)
174177
.enter()
178+
.append('g')
179+
.attr('transform', (d) =>
180+
vertical
181+
? `translate(${(paddedScale(d.key)! - groupOffset)},0)`
182+
: `translate(0,${(paddedScale(d.key)! - groupOffset)})`
183+
)
184+
185+
// Each group gets a transparent rect covering the full band (using paddedScale.step())
186+
barGroups
187+
.append('rect')
188+
.attr('fill', 'transparent')
189+
.attr('pointer-events', 'all')
190+
.attr('x', 0)
191+
.attr('y', (d) => vertical ? y(d.value) - groupOffset : 0)
192+
.attr('width', (d) => vertical ? paddedScale.step() : y(d.value) + groupOffset)
193+
.attr('height', (d) => vertical ? height - y(d.value) + groupOffset : paddedScale.step())
194+
195+
// Append the colored bar rect inside each group.
196+
// We now offset it by groupOffset so its left edge is at paddedScale(d.key)
197+
const bars = barGroups
175198
.append('rect')
176199
.attr('fill', (d) => color(d))
177200
.attr('rx', 2)
@@ -180,24 +203,24 @@ function renderChart({
180203
if (!animate) {
181204
if (vertical) {
182205
bars
183-
.attr('x', (d) => x(d.key)!)
206+
.attr('x', groupOffset)
184207
.attr('y', (d) => y(d.value))
185-
.attr('width', x.bandwidth())
208+
.attr('width', paddedScale.bandwidth())
186209
.attr('height', (d) => height - y(d.value))
187210
} else {
188211
bars
189212
.attr('x', 0)
190-
.attr('y', (d) => x(d.key)!)
213+
.attr('y', groupOffset)
191214
.attr('width', (d) => y(d.value))
192-
.attr('height', x.bandwidth())
215+
.attr('height', paddedScale.bandwidth())
193216
}
194217
} else {
195218
// Animate the bars
196219
if (vertical) {
197220
bars
198-
.attr('x', (d) => x(d.key)!)
221+
.attr('x', groupOffset)
199222
.attr('y', height)
200-
.attr('width', x.bandwidth())
223+
.attr('width', paddedScale.bandwidth())
201224
.attr('height', 0)
202225
.transition()
203226
.duration(800)
@@ -207,16 +230,43 @@ function renderChart({
207230
} else {
208231
bars
209232
.attr('x', 0)
210-
.attr('y', (d) => x(d.key)!)
233+
.attr('y', groupOffset)
211234
.attr('width', 0)
212-
.attr('height', x.bandwidth())
235+
.attr('height', paddedScale.bandwidth())
213236
.transition()
214237
.duration(800)
215238
.delay((_, i) => i * 100)
216239
.attr('width', (d) => y(d.value))
217240
}
218241
}
219242
243+
if (props.tooltip) {
244+
const Tooltip = d3
245+
.select(chartRef.value)
246+
.append('div')
247+
.attr('class', 'tooltip')
248+
249+
function updatePos(event: PointerEvent) {
250+
const [x, y] = d3.pointer(event, chartRef.value)
251+
Tooltip
252+
.style('left', `${x + 14}px`)
253+
.style('top', `${y + 14}px`)
254+
}
255+
256+
barGroups
257+
.on('pointerenter', (event: PointerEvent, d) => {
258+
Tooltip
259+
.html(props.tooltipFormat(d, color(d)))
260+
.style('opacity', '1')
261+
updatePos(event)
262+
})
263+
.on('pointermove', updatePos)
264+
.on('pointerleave', () => {
265+
Tooltip
266+
.style('opacity', '0')
267+
})
268+
}
269+
220270
// Render outline for debugging
221271
if (props.debug) {
222272
d3
@@ -261,6 +311,19 @@ watch(
261311
position: relative;
262312
}
263313
314+
:deep(.tooltip) {
315+
opacity: 0;
316+
pointer-events: none;
317+
position: absolute;
318+
top: 0;
319+
left: 0;
320+
padding: 2px 8px;
321+
background-color: var(--c-bg-elv-2);
322+
border: 1px solid var(--c-divider);
323+
border-radius: 6px;
324+
font-size: 12px;
325+
}
326+
264327
:deep(.tick line) {
265328
display: none;
266329
}

stories/components/SChart.01_Playground.story.vue

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SControlLeft from 'sefirot/components/SControlLeft.vue'
1111
import SControlRight from 'sefirot/components/SControlRight.vue'
1212
import SControlText from 'sefirot/components/SControlText.vue'
1313
import { type KV, c, exportAsPng } from 'sefirot/support/Chart'
14-
import { ref, useTemplateRef } from 'vue'
14+
import { ref } from 'vue'
1515
1616
const title = 'Components / SChart / 01. Playground'
1717
@@ -26,8 +26,8 @@ const data = ref<KV[]>([
2626
{ key: '2025', value: 45 }
2727
])
2828
29-
const barRef = useTemplateRef('bar-chart')
30-
const pieRef = useTemplateRef('pie-chart')
29+
const barRef = ref()
30+
const pieRef = ref()
3131
3232
function tooltipFormat(d: KV) {
3333
return `${d.key} &ndash; <span class="tooltip-number">${d.value}</span>`
@@ -233,7 +233,7 @@ function state() {
233233
<SCardBlock bg="elv-2">
234234
<div class="s-w-full s-h-320">
235235
<SChartBar
236-
ref="bar-chart"
236+
ref="barRef"
237237
:data
238238
:margins="{
239239
top: state.barMarginTop,
@@ -277,7 +277,7 @@ function state() {
277277
<SCardBlock bg="elv-2">
278278
<div class="s-w-full s-h-320 s-pb-24">
279279
<SChartPie
280-
ref="pie-chart"
280+
ref="pieRef"
281281
:data
282282
:margins="{
283283
top: state.pieMarginTop,

0 commit comments

Comments
 (0)