Skip to content

Commit bd2a9b5

Browse files
Roméo MaignalRoméo Maignal
authored andcommitted
feat(Viz2): the widgets in hover mode now glow with the secondary color and the bubbles are now multicolor
1 parent b4acc04 commit bd2a9b5

4 files changed

Lines changed: 67 additions & 18 deletions

File tree

src/App.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ section:has(.about-content)::after {
165165
.widget:hover {
166166
border-color: var(--border-hover);
167167
background: var(--bg-elevated);
168-
box-shadow: 0 0 40px -4px var(--glow-dim), 0 0 20px -2px var(--glow-dim);
168+
box-shadow: 0 0 40px -4px var(--secondary-glow-dim), 0 0 20px -2px var(--secondary-glow-dim);
169169
}
170170

171171
.widget:hover::before {

src/components/Viz2.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
position: relative;
4646
}
4747

48+
.bbl circle {
49+
cursor: grab;
50+
}
51+
52+
.bbl circle:active {
53+
cursor: grabbing;
54+
}
55+
4856
/* ── tooltip ── */
4957
.viz2-tooltip {
5058
position: absolute;

src/components/Viz2.tsx

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ interface BubbleNode extends d3.SimulationNodeDatum {
1818
name: string
1919
value: number
2020
r: number
21+
colorIdx: number
22+
__restX?: number
23+
__restY?: number
24+
}
25+
26+
const PALETTE = [
27+
'#a78bfa', '#22d3ee', '#fb923c', '#f87171', '#4ade80',
28+
'#facc15', '#60a5fa', '#f472b6', '#a3e635', '#34d399',
29+
'#e879f9', '#38bdf8', '#fbbf24', '#fb7185', '#c084fc',
30+
]
31+
32+
function hexToRgba(hex: string, a: number): string {
33+
const r = parseInt(hex.slice(1, 3), 16)
34+
const g = parseInt(hex.slice(3, 5), 16)
35+
const b = parseInt(hex.slice(5, 7), 16)
36+
return `rgba(${r},${g},${b},${a})`
2137
}
2238

2339
const YEAR_MIN = 1920
@@ -34,7 +50,7 @@ const Viz2: React.FC = () => {
3450

3551
const [data, setData] = useState<AirlinePoint[]>([])
3652
const [metric, setMetric] = useState<Metric>('incidents')
37-
const [year, setYear] = useState(2000)
53+
const [year, setYear] = useState(1949)
3854
const [playing, setPlaying] = useState(false)
3955
const [chartSize, setChartSize] = useState({ w: 0, h: 0 })
4056

@@ -128,28 +144,25 @@ const Viz2: React.FC = () => {
128144

129145
/* merge with existing nodes to preserve positions */
130146
const existing = new Map(nodesRef.current.map(n => [n.key, n]))
131-
const newNodes: BubbleNode[] = yearData.map((d: AirlinePoint) => {
147+
const newNodes: BubbleNode[] = yearData.map((d: AirlinePoint, i: number) => {
132148
const prev = existing.get(d.key)
133149
const r = Math.max(rScale(d[metric]), 6)
134150
if (prev) {
135-
prev.value = d[metric]
136-
prev.name = d.name
137-
prev.r = r
151+
prev.value = d[metric]
152+
prev.name = d.name
153+
prev.r = r
154+
prev.colorIdx = i
138155
return prev
139156
}
140157
return {
141-
key: d.key, name: d.name, value: d[metric], r,
158+
key: d.key, name: d.name, value: d[metric], r, colorIdx: i,
142159
x: w / 2 + (Math.random() - 0.5) * w * 0.3,
143160
y: h / 2 + (Math.random() - 0.5) * h * 0.3,
144161
}
145162
})
146163
nodesRef.current = newNodes
147164

148-
const accent = getComputedStyle(document.documentElement)
149-
.getPropertyValue('--accent').trim() || 'rgb(0,234,255)'
150-
const fill = accent.startsWith('rgb(')
151-
? accent.replace('rgb(', 'rgba(').replace(')', ',0.12)')
152-
: 'rgba(0,234,255,0.12)'
165+
const color = (d: BubbleNode) => PALETTE[d.colorIdx % PALETTE.length]
153166

154167
/* D3 join */
155168
const groups = svg.selectAll<SVGGElement, BubbleNode>('.bbl')
@@ -177,8 +190,8 @@ const Viz2: React.FC = () => {
177190
all.select<SVGCircleElement>('circle')
178191
.transition().duration(350)
179192
.attr('r', (d: BubbleNode) => d.r)
180-
.attr('fill', fill)
181-
.attr('stroke', accent)
193+
.attr('fill', (d: BubbleNode) => hexToRgba(color(d), 0.15))
194+
.attr('stroke', (d: BubbleNode) => color(d))
182195
.attr('stroke-width', 1.5)
183196

184197
all.select<SVGTextElement>('text')
@@ -190,12 +203,12 @@ const Viz2: React.FC = () => {
190203
all
191204
.on('mouseover', (event: MouseEvent, d: BubbleNode) => {
192205
d3.select(event.currentTarget as SVGGElement)
193-
.select('circle').attr('stroke-width', 2.5).attr('fill', fill.replace('0.12', '0.22'))
206+
.select('circle').attr('stroke-width', 2.5).attr('fill', hexToRgba(color(d), 0.28))
194207
if (!chartRef.current) return
195208
const tip = d3.select<HTMLDivElement, unknown>('.viz2-tooltip')
196209
const rect = chartRef.current.getBoundingClientRect()
197210
tip.style('display', 'block')
198-
.html(`<span class="viz2-tt-name">${d.name}</span><br/>${d.value} ${metric}`)
211+
.html(`<span class="viz2-tt-name" style="color:${color(d)}">${d.name}</span><br/>${d.value} ${metric}`)
199212
.style('left', `${Math.min(event.clientX - rect.left + 12, chartSize.w - 160)}px`)
200213
.style('top', `${Math.max(event.clientY - rect.top - 50, 4)}px`)
201214
})
@@ -206,12 +219,36 @@ const Viz2: React.FC = () => {
206219
.style('left', `${Math.min(event.clientX - rect.left + 12, chartSize.w - 160)}px`)
207220
.style('top', `${Math.max(event.clientY - rect.top - 50, 4)}px`)
208221
})
209-
.on('mouseout', (event: MouseEvent) => {
222+
.on('mouseout', (event: MouseEvent, d: BubbleNode) => {
210223
d3.select(event.currentTarget as SVGGElement)
211-
.select('circle').attr('stroke-width', 1.5).attr('fill', fill)
224+
.select('circle').attr('stroke-width', 1.5).attr('fill', hexToRgba(color(d), 0.15))
212225
d3.select('.viz2-tooltip').style('display', 'none')
213226
})
214227

228+
/* drag — free movement, snaps back on release */
229+
const drag = d3.drag<SVGGElement, BubbleNode>()
230+
.on('start', (_ev: d3.D3DragEvent<SVGGElement, BubbleNode, BubbleNode>, d: BubbleNode) => {
231+
simRef.current?.alphaTarget(0.3).restart()
232+
d.fx = d.x; d.fy = d.y
233+
d.__restX = d.x ?? 0; d.__restY = d.y ?? 0
234+
d3.select(_ev.sourceEvent.target as SVGCircleElement).attr('stroke-width', 2.5)
235+
})
236+
.on('drag', (ev: d3.D3DragEvent<SVGGElement, BubbleNode, BubbleNode>, d: BubbleNode) => {
237+
d.fx = ev.x
238+
d.fy = ev.y
239+
})
240+
.on('end', (ev: d3.D3DragEvent<SVGGElement, BubbleNode, BubbleNode>, d: BubbleNode) => {
241+
simRef.current?.alphaTarget(0)
242+
d3.select(ev.sourceEvent.target as SVGCircleElement).attr('stroke-width', 1.5)
243+
// give an initial velocity toward rest position for a snappy return
244+
d.vx = ((d.__restX ?? 0) - (d.x ?? 0)) * 0.35
245+
d.vy = ((d.__restY ?? 0) - (d.y ?? 0)) * 0.35
246+
d.fx = null; d.fy = null
247+
simRef.current?.alpha(0.45).restart()
248+
})
249+
250+
all.call(drag)
251+
215252
/* restart simulation */
216253
if (simRef.current) {
217254
simRef.current

src/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
--text-muted: #52525b;
88
--accent: rgb(0, 234, 255);
99
--accent-dim: rgba(0, 234, 255, 0.12);
10+
--second-accent: rgb(124, 59, 237);
11+
--second-accent-dim: rgba(124, 59, 237, 0.12);
1012
--glow-dim: rgba(0, 234, 255, 0.6);
13+
--secondary-glow-dim: rgba(124, 59, 237, 0.6);
1114
--border: rgba(255, 255, 255, 0.15);
1215
--border-hover: rgba(255, 255, 255, 0.3);
1316
--border-sep: rgba(255, 255, 255, 0.15);
@@ -44,6 +47,7 @@
4447
--second-accent: rgb(124, 59, 237);
4548
--second-accent-dim: rgba(124, 59, 237, 0.12);
4649
--glow-dim: rgba(0, 170, 190, 0.6);
50+
--secondary-glow-dim: rgba(124, 59, 237, 0.6);
4751
--border: rgba(0, 0, 0, 0.15);
4852
--border-hover: rgba(0, 0, 0, 0.3);
4953
--border-sep: rgba(0, 0, 0, 0.15);

0 commit comments

Comments
 (0)