Skip to content

Commit 7563c4a

Browse files
committed
fix(charts): preserve stable series identities
1 parent 380f7a0 commit 7563c4a

5 files changed

Lines changed: 37 additions & 13 deletions

File tree

‎packages/emcn/src/components/charts/bar-chart.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ function BarChartComponent({
169169
<div className='relative' style={{ width, height }}>
170170
<ChartDataTable
171171
label={label || 'Values by date'}
172-
series={[{ label: unit || 'Value', data }]}
172+
series={[{ id: 'base', label: unit || 'Value', data }]}
173173
timeZone={timeZone}
174174
/>
175175
<svg

‎packages/emcn/src/components/charts/chart-accessibility.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('Chart accessibility', () => {
1010
label='Cost'
1111
series={[
1212
{
13+
id: 'credits',
1314
label: 'Credits',
1415
data: [
1516
{ timestamp: '2026-01-01T00:00:00Z', value: 0.00000123456789 },

‎packages/emcn/src/components/charts/chart-data-table.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { formatChartTimestamp } from '@sim/emcn'
22

33
interface ChartDataTableProps {
44
label: string
5-
series: { label: string; data: { timestamp: string; value: number }[] }[]
5+
series: { id: string; label: string; data: { timestamp: string; value: number }[] }[]
66
timeZone?: string
77
}
88

@@ -21,7 +21,7 @@ export function ChartDataTable({ label, series, timeZone }: ChartDataTableProps)
2121
<tr>
2222
<th scope='col'>Date</th>
2323
{series.map((item) => (
24-
<th key={item.label} scope='col'>
24+
<th key={item.id} scope='col'>
2525
{item.label}
2626
</th>
2727
))}
@@ -32,7 +32,7 @@ export function ChartDataTable({ label, series, timeZone }: ChartDataTableProps)
3232
<tr key={timestamp}>
3333
<th scope='row'>{formatChartTimestamp(timestamp, timeZone)}</th>
3434
{series.map((item, index) => (
35-
<td key={item.label}>
35+
<td key={item.id}>
3636
{values[index]
3737
.get(timestamp)
3838
?.toLocaleString(undefined, { maximumSignificantDigits: 21 }) ?? '—'}

‎packages/emcn/src/components/charts/chart-layout.test.tsx‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ describe('RadarChart rendered geometry', () => {
241241
})
242242

243243
describe('Dashboard chart states', () => {
244+
it('preserves distinct cells when equally named series change order', () => {
245+
const data = dailySeries(3, 10)
246+
const first = { id: 'base', label: 'Runs', color: 'red', data: dailySeries(3, 20) }
247+
const second = { id: 'second', label: 'Runs', color: 'green', data: dailySeries(3, 30) }
248+
mountAtWidth(400, <LineChart label='Runs' color='blue' data={data} series={[first, second]} />)
249+
const headers = [...container.querySelectorAll('thead th')]
250+
const cells = [...container.querySelectorAll('tbody tr:first-child td')]
251+
expect(cells.map((cell) => cell.textContent)).toEqual(['10', '20', '30'])
252+
act(() => container.querySelector('button')?.click())
253+
act(() =>
254+
root.render(<LineChart label='Runs' color='blue' data={data} series={[second, first]} />)
255+
)
256+
const updatedHeaders = [...container.querySelectorAll('thead th')]
257+
const updatedCells = [...container.querySelectorAll('tbody tr:first-child td')]
258+
expect(updatedHeaders[2]).toBe(headers[3])
259+
expect(updatedHeaders[3]).toBe(headers[2])
260+
expect(updatedCells[1]).toBe(cells[2])
261+
expect(updatedCells[2]).toBe(cells[1])
262+
expect(updatedCells.map((cell) => cell.textContent)).toEqual(['10', '30', '20'])
263+
expect(container.querySelector('path[stroke="red"]')?.getAttribute('opacity')).toBe('1')
264+
expect(container.querySelector('path[stroke="green"]')).toBeNull()
265+
})
266+
244267
it('previews, pins, and clears a distribution without changing its proportions', () => {
245268
const segments = [
246269
{ label: 'Completed', value: 90, color: 'blue' },

‎packages/emcn/src/components/charts/line-chart.tsx‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface LineChartPoint {
3333
}
3434

3535
export interface LineChartMultiSeries {
36-
id?: string
36+
id: string
3737
label: string
3838
color: string
3939
data: LineChartPoint[]
@@ -94,11 +94,10 @@ function LineChartComponent({
9494
const hasExternalWrapper = !label || label === ''
9595

9696
const allSeries = useMemo(
97-
() =>
98-
(Array.isArray(series) && series.length > 0
99-
? [{ id: 'base', label, color, data }, ...series]
100-
: [{ id: 'base', label, color, data }]
101-
).map((s, idx) => ({ ...s, id: s.id || s.label || String(idx) })),
97+
() => [
98+
{ id: 'base', sourceId: 'base', label, color, data },
99+
...(series ?? []).map((item) => ({ ...item, id: `series:${item.id}`, sourceId: item.id })),
100+
],
102101
[series, label, color, data]
103102
)
104103

@@ -292,6 +291,7 @@ function LineChartComponent({
292291
<ChartDataTable
293292
label={label || 'Values by date'}
294293
series={allSeries.map((item) => ({
294+
id: item.id,
295295
label: item.label || unit || 'Value',
296296
data: item.data,
297297
}))}
@@ -378,7 +378,7 @@ function LineChartComponent({
378378
const baseOpacity = isActive ? 1 : 0.12
379379
const strokeOpacity = hoverSeriesId ? (isHovered ? 1 : 0.2) : baseOpacity
380380
const sw = (() => {
381-
switch ((s.id || '').toLowerCase()) {
381+
switch (s.sourceId.toLowerCase()) {
382382
case 'p50':
383383
return isDark ? 1.5 : 1.7
384384
case 'p90':
@@ -539,7 +539,7 @@ function LineChartComponent({
539539
const seriesIndex = allSeries.findIndex((x) => x.id === s.id)
540540
const v = allSeries[seriesIndex]?.data?.[hoverIndex]?.value
541541
const valueStr = fmt(v)
542-
const labelStr = s.label || String(s.id || '')
542+
const labelStr = s.label || s.sourceId
543543
const len = `${labelStr} ${valueStr}`.length
544544
return Math.max(m, len)
545545
}, 0)
@@ -557,7 +557,7 @@ function LineChartComponent({
557557
{toDisplay.map((s) => {
558558
const seriesIndex = allSeries.findIndex((x) => x.id === s.id)
559559
const val = allSeries[seriesIndex]?.data?.[hoverIndex]?.value
560-
const seriesLabel = s.label || s.id
560+
const seriesLabel = s.label || s.sourceId
561561
const showLabel =
562562
seriesLabel && seriesLabel !== 'base' && seriesLabel.trim() !== ''
563563
return (

0 commit comments

Comments
 (0)