Skip to content

Commit 6114df7

Browse files
feat: Chart colors (#1334)
Implement chart color retrieval from CSS variable.
1 parent 0e96397 commit 6114df7

8 files changed

Lines changed: 409 additions & 164 deletions

File tree

package-lock.json

Lines changed: 0 additions & 162 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
buildDatasetOption,
99
buildSeriesOption,
1010
} from '../Utils/chartOptionBuilder';
11+
import { getChartColorsFromCssVariables } from '../Utils/chartHelper';
1112
import type { EChartsDataset } from '../Utils/chartTypes';
1213
import type { PxTable } from '../../../shared-types/pxTable';
1314

@@ -31,6 +32,10 @@ vi.mock('../Utils/chartOptionBuilder', async () => {
3132
};
3233
});
3334

35+
vi.mock('../Utils/chartHelper', () => ({
36+
getChartColorsFromCssVariables: vi.fn(),
37+
}));
38+
3439
const mockDataset: EChartsDataset = {
3540
title: 'Population by year',
3641
origin: 'Statistics Demo',
@@ -77,6 +82,10 @@ describe('LineChart', () => {
7782
{ name: 'Women', type: 'line', symbol: 'rect', symbolSize: 8 },
7883
{ name: 'Total', type: 'line', symbol: 'triangle', symbolSize: 8 },
7984
]);
85+
vi.mocked(getChartColorsFromCssVariables).mockReturnValue([
86+
'#333333',
87+
'#444444',
88+
]);
8089
vi.mocked(useEChartOption).mockReturnValue({
8190
divRef: { current: null },
8291
chartRef: { current: null },
@@ -91,6 +100,7 @@ describe('LineChart', () => {
91100
expect(mapPxTableToChartDataset).toHaveBeenCalledWith({});
92101
expect(buildDatasetOption).toHaveBeenCalledWith(mockDataset);
93102
expect(buildSeriesOption).toHaveBeenCalledWith(mockDataset, 'line', colors);
103+
expect(getChartColorsFromCssVariables).not.toHaveBeenCalled();
94104

95105
const option = vi.mocked(useEChartOption).mock.calls[0][0];
96106

@@ -109,6 +119,34 @@ describe('LineChart', () => {
109119
});
110120
});
111121

122+
it('uses fallback colors when colors are not provided', () => {
123+
const fallbackColors = ['#abcdef', '#fedcba'];
124+
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);
125+
126+
render(<LineChart pxtable={{} as PxTable} />);
127+
128+
expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
129+
expect(buildSeriesOption).toHaveBeenCalledWith(
130+
mockDataset,
131+
'line',
132+
fallbackColors,
133+
);
134+
});
135+
136+
it('uses fallback colors when provided colors array is empty', () => {
137+
const fallbackColors = ['#121212', '#343434'];
138+
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);
139+
140+
render(<LineChart pxtable={{} as PxTable} colors={[]} />);
141+
142+
expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
143+
expect(buildSeriesOption).toHaveBeenCalledWith(
144+
mockDataset,
145+
'line',
146+
fallbackColors,
147+
);
148+
});
149+
112150
it('renders chart container with height based on number of series', () => {
113151
const { container } = render(<LineChart pxtable={{} as PxTable} />);
114152

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import LineChart from './LineChart';
4+
import { setPxTableData } from '../../Table/Utils/cubeHelper';
5+
import type { PxTable } from '../../../shared-types/pxTable';
6+
import type { DataCell, PxData } from '../../../shared-types/pxTableData';
7+
import type { Variable } from '../../../shared-types/variable';
8+
import { VartypeEnum } from '../../../shared-types/vartypeEnum';
9+
10+
type Story = StoryObj<typeof LineChart>;
11+
12+
function createVariable(
13+
id: string,
14+
type: VartypeEnum,
15+
values: Array<{ code: string; label: string }>,
16+
): Variable {
17+
return {
18+
id,
19+
label: id,
20+
type,
21+
mandatory: true,
22+
values,
23+
};
24+
}
25+
26+
function createLineChartPxTable(
27+
years: string[],
28+
seriesValues: Array<{ code: string; label: string }>,
29+
getValue: (
30+
year: string,
31+
seriesCode: string,
32+
yearIndex: number,
33+
) => number | null,
34+
): PxTable {
35+
const year = createVariable(
36+
'year',
37+
VartypeEnum.TIME_VARIABLE,
38+
years.map((value) => ({ code: value, label: value })),
39+
);
40+
const series = createVariable(
41+
'series',
42+
VartypeEnum.REGULAR_VARIABLE,
43+
seriesValues,
44+
);
45+
const contents = createVariable('contents', VartypeEnum.CONTENTS_VARIABLE, [
46+
{
47+
code: 'POP',
48+
label: 'Population',
49+
},
50+
]);
51+
52+
contents.values[0] = {
53+
...contents.values[0],
54+
contentInfo: {
55+
unit: 'persons',
56+
decimals: 0,
57+
referencePeriod: '',
58+
basePeriod: '',
59+
alternativeText: '',
60+
},
61+
};
62+
63+
const cube: PxData<DataCell> = {};
64+
years.forEach((yearCode, yearIndex) => {
65+
seriesValues.forEach((seriesValue) => {
66+
const value = getValue(yearCode, seriesValue.code, yearIndex);
67+
if (value === null) {
68+
return;
69+
}
70+
71+
setPxTableData(cube, [yearCode, seriesValue.code], {
72+
value,
73+
});
74+
});
75+
});
76+
77+
return {
78+
metadata: {
79+
id: 'storybook-line-chart',
80+
language: 'en',
81+
availableLanguages: ['en'],
82+
label: 'Population by year',
83+
updated: new Date('2024-01-01'),
84+
source: 'Storybook demo source',
85+
infofile: '',
86+
decimals: 0,
87+
officialStatistics: true,
88+
aggregationAllowed: true,
89+
contents: 'Population',
90+
descriptionDefault: true,
91+
matrix: 'M1',
92+
subjectCode: 'BE',
93+
subjectArea: 'Population',
94+
variables: [year, series, contents],
95+
contacts: [],
96+
definitions: {},
97+
notes: [],
98+
},
99+
stub: [year],
100+
heading: [series],
101+
data: {
102+
cube,
103+
variableOrder: ['year', 'series'],
104+
isLoaded: true,
105+
},
106+
};
107+
}
108+
109+
const defaultPxTable = createLineChartPxTable(
110+
['2020', '2021', '2022', '2023', '2024'],
111+
[
112+
{ code: 'M', label: 'Men' },
113+
{ code: 'F', label: 'Women' },
114+
],
115+
(year, seriesCode) => {
116+
const baseByYear: Record<string, number> = {
117+
'2020': 120,
118+
'2021': 125,
119+
'2022': 130,
120+
'2023': 136,
121+
'2024': 141,
122+
};
123+
124+
const base = baseByYear[year] ?? 100;
125+
return seriesCode === 'M' ? base : base + 6;
126+
},
127+
);
128+
129+
const manySeriesPxTable = createLineChartPxTable(
130+
['2019', '2020', '2021', '2022', '2023', '2024'],
131+
[
132+
{ code: 'A', label: 'Region A' },
133+
{ code: 'B', label: 'Region B' },
134+
{ code: 'C', label: 'Region C' },
135+
{ code: 'D', label: 'Region D' },
136+
{ code: 'E', label: 'Region E' },
137+
{ code: 'F', label: 'Region F' },
138+
],
139+
(_year, seriesCode, yearIndex) => {
140+
const seriesOffset: Record<string, number> = {
141+
A: 20,
142+
B: 35,
143+
C: 50,
144+
D: 65,
145+
E: 80,
146+
F: 95,
147+
};
148+
149+
return 100 + yearIndex * 8 + (seriesOffset[seriesCode] ?? 0);
150+
},
151+
);
152+
153+
const sparseDataPxTable = createLineChartPxTable(
154+
['2020', '2021', '2022', '2023', '2024'],
155+
[
156+
{ code: 'Urban', label: 'Urban' },
157+
{ code: 'Rural', label: 'Rural' },
158+
],
159+
(year, seriesCode) => {
160+
if ((year === '2022' && seriesCode === 'Urban') || year === '2021') {
161+
return null;
162+
}
163+
164+
const valueByYear: Record<string, number> = {
165+
'2020': 80,
166+
'2021': 84,
167+
'2022': 89,
168+
'2023': 93,
169+
'2024': 99,
170+
};
171+
172+
const base = valueByYear[year] ?? 75;
173+
return seriesCode === 'Urban' ? base + 10 : base;
174+
},
175+
);
176+
177+
const meta: Meta<typeof LineChart> = {
178+
component: LineChart,
179+
title: 'Components/Chart/LineChart',
180+
decorators: [
181+
(StoryComponent) => (
182+
<div style={{ maxWidth: '960px', margin: '0 auto' }}>
183+
<StoryComponent />
184+
</div>
185+
),
186+
],
187+
};
188+
189+
export default meta;
190+
191+
export const Default: Story = {
192+
args: {
193+
pxtable: defaultPxTable,
194+
},
195+
};
196+
197+
export const CssVariableFallbackColors: Story = {
198+
args: {
199+
pxtable: defaultPxTable,
200+
},
201+
};
202+
203+
export const CustomColors: Story = {
204+
args: {
205+
pxtable: defaultPxTable,
206+
colors: ['#0b5fff', '#fa4d56'],
207+
},
208+
};
209+
210+
export const EmptyColorsArrayUsesFallback: Story = {
211+
args: {
212+
pxtable: defaultPxTable,
213+
colors: [],
214+
},
215+
};
216+
217+
export const ManySeries: Story = {
218+
args: {
219+
pxtable: manySeriesPxTable,
220+
},
221+
};
222+
223+
export const SparseData: Story = {
224+
args: {
225+
pxtable: sparseDataPxTable,
226+
colors: ['#24a148', '#8a3ffc'],
227+
},
228+
};

packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { useEChartOption } from '../Utils/useEChartOption';
1010
import type { PxTable } from '../../../shared-types/pxTable';
1111
import { mapPxTableToChartDataset } from '../Utils/chartDataMapper';
12+
import { getChartColorsFromCssVariables } from '../Utils/chartHelper';
1213

1314
interface LineChartProps {
1415
readonly pxtable: PxTable;
@@ -42,6 +43,13 @@ function getTooltipSymbolSvg(symbol: string, color: string): string {
4243

4344
export function LineChart({ pxtable, colors }: LineChartProps) {
4445
const dataset = useMemo(() => mapPxTableToChartDataset(pxtable), [pxtable]);
46+
47+
const resolvedColors = useMemo(() => {
48+
return colors && colors.length > 0
49+
? colors
50+
: getChartColorsFromCssVariables();
51+
}, [colors]);
52+
4553
const option = useMemo<echarts.EChartsOption>(
4654
() => ({
4755
...buildDatasetOption(dataset),
@@ -54,7 +62,7 @@ export function LineChart({ pxtable, colors }: LineChartProps) {
5462
legend: {
5563
height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels
5664
},
57-
series: buildSeriesOption(dataset, 'line', colors),
65+
series: buildSeriesOption(dataset, 'line', resolvedColors),
5866
tooltip: {
5967
trigger: 'axis',
6068
formatter: (params: unknown) => {
@@ -86,7 +94,7 @@ export function LineChart({ pxtable, colors }: LineChartProps) {
8694
},
8795
},
8896
}),
89-
[dataset, colors],
97+
[dataset, resolvedColors],
9098
);
9199

92100
const { divRef } = useEChartOption(option);

0 commit comments

Comments
 (0)