From cbe7faffe68db579b3a6914b157ab8bdb93b64bb Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 10:33:56 +0200 Subject: [PATCH 01/24] Added echart add barchart --- package-lock.json | 32 +++++++++++++++ packages/pxweb2-ui/package.json | 1 + packages/pxweb2-ui/src/index.ts | 1 + .../src/lib/components/Chart/Chart.tsx | 24 +++++++++++ .../lib/components/Chart/Charts/BarChart.tsx | 40 +++++++++++++++++++ .../components/Presentation/Presentation.tsx | 11 ++++- 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx diff --git a/package-lock.json b/package-lock.json index aa60f3b5a..4f3fab606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6120,6 +6120,22 @@ "node": ">= 0.4" } }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/electron-to-chromium": { "version": "1.5.321", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", @@ -13684,6 +13700,21 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", @@ -13729,6 +13760,7 @@ "dependencies": { "@vitejs/plugin-react": "^5.2.0", "clsx": "^2.1.1", + "echarts": "^6.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "motion": "^12.38.0", "vite-plugin-dts": "^4.5.4" diff --git a/packages/pxweb2-ui/package.json b/packages/pxweb2-ui/package.json index 9a9b6f147..aea41c90f 100644 --- a/packages/pxweb2-ui/package.json +++ b/packages/pxweb2-ui/package.json @@ -18,6 +18,7 @@ "dependencies": { "@vitejs/plugin-react": "^5.2.0", "clsx": "^2.1.1", + "echarts": "^6.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "motion": "^12.38.0", "vite-plugin-dts": "^4.5.4" diff --git a/packages/pxweb2-ui/src/index.ts b/packages/pxweb2-ui/src/index.ts index 3c8afc3f1..2275e039b 100644 --- a/packages/pxweb2-ui/src/index.ts +++ b/packages/pxweb2-ui/src/index.ts @@ -4,6 +4,7 @@ export * from './lib/components/ActionItem/ActionItem'; export * from './lib/components/BottomSheet/BottomSheet'; export * from './lib/components/Breadcrumbs/Breadcrumbs'; export * from './lib/components/Button/Button'; +export * from './lib/components/Chart/Chart'; export * from './lib/components/Checkbox/Checkbox'; export * from './lib/components/CheckCircle/CheckCircleIcon'; export * from './lib/components/CheckCircle/CheckCircleToggle'; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx new file mode 100644 index 000000000..8e02467b1 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -0,0 +1,24 @@ +import BarChart from './Charts/BarChart'; + +export function Chart() { + // const data = { + // title: { + // text: 'ECharts Getting Started Example', + // }, + // tooltip: {}, + // xAxis: { + // data: ['shirt', 'cardigan', 'chiffon', 'pants', 'heels', 'socks'], + // }, + // yAxis: {}, + // series: [ + // { + // name: 'sales', + // type: 'bar', + // data: [5, 20, 36, 10, 10, 20], + // }, + // ], + // }; + + return ; +} +export default Chart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx new file mode 100644 index 000000000..4834d6bf5 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -0,0 +1,40 @@ +import { useRef, useEffect } from 'react'; +import * as echarts from 'echarts'; + +export function BarChart() { + const divRef = useRef(null); + + useEffect(() => { + if (!divRef.current) { + return; + } + // Create the echarts instance + const myChart = echarts.init(divRef.current as HTMLElement); + // Draw the chart + myChart.setOption({ + title: { + text: 'ECharts Getting Started Example', + }, + tooltip: {}, + xAxis: { + data: ['shirt', 'cardigan', 'chiffon', 'pants', 'heels', 'socks'], + }, + yAxis: {}, + series: [ + { + name: 'sales', + type: 'bar', + data: [5, 20, 36, 10, 10, 20], + }, + ], + }); + }, [divRef]); + return ( +
+ ); +} +export default BarChart; diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index 3f48d6014..1e7de0509 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -6,7 +6,13 @@ import isEqual from 'lodash/isEqual'; import classes from './Presentation.module.scss'; import useApp from '../../context/useApp'; import { ContentTop } from '../ContentTop/ContentTop'; -import { Table, EmptyState, PxTable, LocalAlert } from '@pxweb2/pxweb2-ui'; +import { + Table, + EmptyState, + PxTable, + LocalAlert, + Chart, +} from '@pxweb2/pxweb2-ui'; import useTableData from '../../context/useTableData'; import useVariables from '../../context/useVariables'; import { useDebounce } from '@uidotdev/usehooks'; @@ -269,6 +275,9 @@ export function Presentation({ className={classes.gradientContainer} ref={gradientContainerRef} > +
+ +
From 83d7f9398acbc8f045ba6e3a8fdb8997262afdab Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 11:04:48 +0200 Subject: [PATCH 02/24] Send dataset to BarChart --- .../src/lib/components/Chart/Chart.tsx | 29 +++++++------------ .../lib/components/Chart/Charts/BarChart.tsx | 28 ++++++++++-------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index 8e02467b1..f2345b644 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -1,24 +1,17 @@ import BarChart from './Charts/BarChart'; export function Chart() { - // const data = { - // title: { - // text: 'ECharts Getting Started Example', - // }, - // tooltip: {}, - // xAxis: { - // data: ['shirt', 'cardigan', 'chiffon', 'pants', 'heels', 'socks'], - // }, - // yAxis: {}, - // series: [ - // { - // name: 'sales', - // type: 'bar', - // data: [5, 20, 36, 10, 10, 20], - // }, - // ], - // }; + const dataset = { + // Provide a set of data. + source: [ + ['product', '2015', '2016', '2017'], + ['Matcha Latte', 43.3, 85.8, 93.7], + ['Milk Tea', 83.1, 73.4, 55.1], + ['Cheese Cocoa', 86.4, 65.2, 82.5], + ['Walnut Brownie', 72.4, 53.9, 39.1], + ], + }; - return ; + return ; } export default Chart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 4834d6bf5..75f66da6c 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -1,7 +1,10 @@ import { useRef, useEffect } from 'react'; import * as echarts from 'echarts'; -export function BarChart() { +interface BarChartProps { + readonly dataset: any; +} +export function BarChart({ dataset }: BarChartProps) { const divRef = useRef(null); useEffect(() => { @@ -9,24 +12,25 @@ export function BarChart() { return; } // Create the echarts instance - const myChart = echarts.init(divRef.current as HTMLElement); + const myChart = echarts.init(divRef.current as HTMLElement, null, { + renderer: 'svg', + }); // Draw the chart myChart.setOption({ title: { text: 'ECharts Getting Started Example', }, + dataset: dataset, + legend: {}, tooltip: {}, - xAxis: { - data: ['shirt', 'cardigan', 'chiffon', 'pants', 'heels', 'socks'], - }, + // Declare an x-axis (category axis). + // The category map the first column in the dataset by default. + xAxis: { type: 'category' }, + // Declare a y-axis (value axis). yAxis: {}, - series: [ - { - name: 'sales', - type: 'bar', - data: [5, 20, 36, 10, 10, 20], - }, - ], + // Declare several 'bar' series, + // every series will auto-map to each column by default. + series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], }); }, [divRef]); return ( From bf09ac0283da3134193d6570b991a3f6d860d61a Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 12:34:50 +0200 Subject: [PATCH 03/24] Added horizontal bar chart --- packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx | 7 ++++++- .../src/lib/components/Chart/Charts/BarChart.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index f2345b644..0fb9b1cdb 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -12,6 +12,11 @@ export function Chart() { ], }; - return ; + return ( + <> + ; + ; + + ); } export default Chart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 75f66da6c..842203b6a 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -3,9 +3,12 @@ import * as echarts from 'echarts'; interface BarChartProps { readonly dataset: any; + readonly isHorizontal?: boolean; } -export function BarChart({ dataset }: BarChartProps) { +export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { const divRef = useRef(null); + const xAxisType = isHorizontal ? { type: 'category' } : {}; + const yAxisType = isHorizontal ? {} : { type: 'category' }; useEffect(() => { if (!divRef.current) { @@ -25,9 +28,9 @@ export function BarChart({ dataset }: BarChartProps) { tooltip: {}, // Declare an x-axis (category axis). // The category map the first column in the dataset by default. - xAxis: { type: 'category' }, + xAxis: xAxisType, // Declare a y-axis (value axis). - yAxis: {}, + yAxis: yAxisType, // Declare several 'bar' series, // every series will auto-map to each column by default. series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], From 4f5e7259680fcd99ee39b45b9701ce7a83b3642f Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 12:54:45 +0200 Subject: [PATCH 04/24] Added LineChart Fix dependencies --- .../src/lib/components/Chart/Chart.tsx | 2 + .../lib/components/Chart/Charts/BarChart.tsx | 14 ++----- .../lib/components/Chart/Charts/LineChart.tsx | 38 +++++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index 0fb9b1cdb..f08b179ad 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -1,4 +1,5 @@ import BarChart from './Charts/BarChart'; +import LineChart from './Charts/LineChart'; export function Chart() { const dataset = { @@ -16,6 +17,7 @@ export function Chart() { <> ; ; + ; ); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 842203b6a..e13cfb36f 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -7,13 +7,13 @@ interface BarChartProps { } export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { const divRef = useRef(null); - const xAxisType = isHorizontal ? { type: 'category' } : {}; - const yAxisType = isHorizontal ? {} : { type: 'category' }; useEffect(() => { if (!divRef.current) { return; } + const xAxisType = isHorizontal ? { type: 'category' } : {}; + const yAxisType = isHorizontal ? {} : { type: 'category' }; // Create the echarts instance const myChart = echarts.init(divRef.current as HTMLElement, null, { renderer: 'svg', @@ -35,13 +35,7 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { // every series will auto-map to each column by default. series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], }); - }, [divRef]); - return ( -
- ); + }, [divRef, dataset, isHorizontal]); + return
; } export default BarChart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx new file mode 100644 index 000000000..79ce6e21e --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -0,0 +1,38 @@ +import { useRef, useEffect } from 'react'; +import * as echarts from 'echarts'; + +interface LineChartProps { + readonly dataset: any; +} +export function LineChart({ dataset }: LineChartProps) { + const divRef = useRef(null); + + useEffect(() => { + if (!divRef.current) { + return; + } + // Create the echarts instance + const myChart = echarts.init(divRef.current as HTMLElement, null, { + renderer: 'svg', + }); + // Draw the chart + myChart.setOption({ + title: { + text: 'ECharts Getting Started Example', + }, + dataset: dataset, + legend: {}, + tooltip: {}, + // Declare an x-axis (category axis). + // The category map the first column in the dataset by default. + xAxis: { type: 'category' }, + // Declare a y-axis (value axis). + yAxis: {}, + // Declare several 'bar' series, + // every series will auto-map to each column by default. + series: [{ type: 'line' }, { type: 'line' }, { type: 'line' }], + }); + }, [divRef, dataset]); + return
; +} +export default LineChart; From c8cf131dd2ddb2e16a84fc5f66cade2bbff121db Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 13:12:15 +0200 Subject: [PATCH 05/24] Use pxtable as dataset --- .../lib/components/Chart/chartDataMapper.ts | 106 ++++++++++++++++++ .../src/lib/components/Chart/chartTypes.ts | 43 +++++++ .../components/Presentation/Presentation.tsx | 2 +- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts new file mode 100644 index 000000000..8e39d6c53 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -0,0 +1,106 @@ +import { getPxTableData } from '../Table/cubeHelper'; +import type { PxTable } from '../../shared-types/pxTable'; +import type { DataCell } from '../../shared-types/pxTableData'; + +import type { ChartConfig, ChartDataPoint, ChartSeries } from './chartTypes'; + +interface CombinationItem { + readonly variableId: string; + readonly code: string; + readonly label: string; +} + +interface Combination { + readonly items: CombinationItem[]; +} + +function buildCombinations( + variables: PxTable['metadata']['variables'], +): Combination[] { + if (variables.length === 0) { + return [{ items: [] }]; + } + + return variables.reduce( + (acc, variable) => { + const next: Combination[] = []; + const values = variable.values.length > 0 ? variable.values : []; + + for (const combo of acc) { + for (const value of values) { + next.push({ + items: [ + ...combo.items, + { + variableId: variable.id, + code: value.code, + label: value.label, + }, + ], + }); + } + } + + return next; + }, + [{ items: [] }], + ); +} + +function toCodeMap(items: CombinationItem[]): Record { + return Object.fromEntries(items.map((item) => [item.variableId, item.code])); +} + +function getLabel(items: CombinationItem[], fallback: string): string { + if (items.length === 0) { + return fallback; + } + + return items.map((item) => item.label).join(' / '); +} + +export function mapPxTableToChart(pxtable: PxTable): ChartConfig { + const rowCombinations = buildCombinations(pxtable.stub); + const seriesCombinations = buildCombinations(pxtable.heading); + + const series: ChartSeries[] = seriesCombinations.map( + (combination, index) => ({ + key: + combination.items.map((item) => item.code).join('|') || + `series-${index}`, + name: getLabel(combination.items, 'Value'), + }), + ); + + const data = rowCombinations.map((rowCombination) => { + const rowMap = toCodeMap(rowCombination.items); + const point: Record = { + name: getLabel(rowCombination.items, 'Value'), + }; + + seriesCombinations.forEach((seriesCombination, seriesIndex) => { + const seriesKey = series[seriesIndex].key; + const allCodes = { + ...rowMap, + ...toCodeMap(seriesCombination.items), + }; + + const dimensions = pxtable.data.variableOrder.map( + (variableId) => allCodes[variableId], + ); + + if (dimensions.some((dimension) => !dimension)) { + point[seriesKey] = null; + return; + } + + const dataCell = getPxTableData(pxtable.data.cube, dimensions); + + point[seriesKey] = dataCell?.value ?? null; + }); + + return point as ChartDataPoint; + }); + + return { data, series }; +} diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts new file mode 100644 index 000000000..da31715f8 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts @@ -0,0 +1,43 @@ +export interface ChartDataPoint { + readonly name: string; + readonly [seriesKey: string]: number | string | null; +} + +export interface ChartSeries { + readonly key: string; + readonly name: string; +} + +export interface ChartConfig { + readonly data: ChartDataPoint[]; + readonly series: ChartSeries[]; +} + +export interface PopulationPyramidDataPoint { + readonly name: string; + readonly left: number | null; + readonly right: number | null; +} + +export interface PopulationPyramidConfig { + readonly data: PopulationPyramidDataPoint[]; + readonly leftSeriesName: string; + readonly rightSeriesName: string; +} + +export interface PopulationPyramidValidationResult { + readonly isValid: boolean; + readonly reason?: + | 'MISSING_TWO_VALUE_DIMENSION' + | 'MULTIPLE_TWO_VALUE_DIMENSIONS' + | 'MISSING_MULTI_VALUE_DIMENSION' + | 'MULTIPLE_MULTI_VALUE_DIMENSIONS' + | 'NON_SINGLE_VALUE_REMAINING_DIMENSIONS'; + readonly twoValueDimensionId?: string; + readonly multiValueDimensionId?: string; +} + +export interface PopulationPyramidMappingResult { + readonly validation: PopulationPyramidValidationResult; + readonly config?: PopulationPyramidConfig; +} diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index 1e7de0509..f36596315 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -276,7 +276,7 @@ export function Presentation({ ref={gradientContainerRef} >
- +
From 75bcc0e63eed4f2309f2d08c46c14e65c3325fa5 Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 13:44:05 +0200 Subject: [PATCH 06/24] Use real data from pxtable --- .../src/lib/components/Chart/Chart.tsx | 25 ++++++----- .../lib/components/Chart/Charts/BarChart.tsx | 20 ++++++--- .../lib/components/Chart/Charts/LineChart.tsx | 20 ++++++--- .../components/Chart/chartDataMapper.spec.ts | 45 +++++++++++++++++++ .../lib/components/Chart/chartDataMapper.ts | 36 ++++++++++++++- .../src/lib/components/Chart/chartTypes.ts | 6 +++ 6 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index f08b179ad..542e647a8 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -1,17 +1,20 @@ import BarChart from './Charts/BarChart'; import LineChart from './Charts/LineChart'; +import { useMemo } from 'react'; +import { mapChartConfigToEChartsDataset, mapPxTableToChart } from './chartDataMapper'; -export function Chart() { - const dataset = { - // Provide a set of data. - source: [ - ['product', '2015', '2016', '2017'], - ['Matcha Latte', 43.3, 85.8, 93.7], - ['Milk Tea', 83.1, 73.4, 55.1], - ['Cheese Cocoa', 86.4, 65.2, 82.5], - ['Walnut Brownie', 72.4, 53.9, 39.1], - ], - }; +import type { PxTable } from '../../shared-types/pxTable'; + + +interface ChartProps { + readonly pxtable: PxTable; +} +export function Chart({ pxtable }: ChartProps) { + const chartConfig = useMemo(() => mapPxTableToChart(pxtable), [pxtable]); + const dataset = useMemo( + () => mapChartConfigToEChartsDataset(chartConfig), + [chartConfig], + ); return ( <> diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index e13cfb36f..b1315e197 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -1,8 +1,10 @@ import { useRef, useEffect } from 'react'; import * as echarts from 'echarts'; +import type { EChartsDataset } from '../chartTypes'; + interface BarChartProps { - readonly dataset: any; + readonly dataset: EChartsDataset; readonly isHorizontal?: boolean; } export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { @@ -23,7 +25,10 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { title: { text: 'ECharts Getting Started Example', }, - dataset: dataset, + dataset: { + dimensions: dataset.dimensions, + source: dataset.source, + }, legend: {}, tooltip: {}, // Declare an x-axis (category axis). @@ -31,10 +36,15 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { xAxis: xAxisType, // Declare a y-axis (value axis). yAxis: yAxisType, - // Declare several 'bar' series, - // every series will auto-map to each column by default. - series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], + series: dataset.series.map((series) => ({ + name: series.name, + type: 'bar', + })), }); + + return () => { + myChart.dispose(); + }; }, [divRef, dataset, isHorizontal]); return
; } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 79ce6e21e..f6ea6e785 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -1,8 +1,10 @@ import { useRef, useEffect } from 'react'; import * as echarts from 'echarts'; +import type { EChartsDataset } from '../chartTypes'; + interface LineChartProps { - readonly dataset: any; + readonly dataset: EChartsDataset; } export function LineChart({ dataset }: LineChartProps) { const divRef = useRef(null); @@ -20,7 +22,10 @@ export function LineChart({ dataset }: LineChartProps) { title: { text: 'ECharts Getting Started Example', }, - dataset: dataset, + dataset: { + dimensions: dataset.dimensions, + source: dataset.source, + }, legend: {}, tooltip: {}, // Declare an x-axis (category axis). @@ -28,10 +33,15 @@ export function LineChart({ dataset }: LineChartProps) { xAxis: { type: 'category' }, // Declare a y-axis (value axis). yAxis: {}, - // Declare several 'bar' series, - // every series will auto-map to each column by default. - series: [{ type: 'line' }, { type: 'line' }, { type: 'line' }], + series: dataset.series.map((series) => ({ + name: series.name, + type: 'line', + })), }); + + return () => { + myChart.dispose(); + }; }, [divRef, dataset]); return
; } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts new file mode 100644 index 000000000..6d106faba --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { mapChartConfigToEChartsDataset } from './chartDataMapper'; + +import type { ChartConfig } from './chartTypes'; + +describe('mapChartConfigToEChartsDataset', () => { + it('maps dimensions and values using chart series order', () => { + const chartConfig: ChartConfig = { + series: [ + { key: 'year-2024', name: '2024' }, + { key: 'year-2025', name: '2025' }, + ], + data: [ + { name: 'Product A', 'year-2024': 10, 'year-2025': 20 }, + { name: 'Product B', 'year-2024': 30, 'year-2025': 40 }, + ], + }; + + const dataset = mapChartConfigToEChartsDataset(chartConfig); + + expect(dataset.dimensions).toEqual(['name', 'year-2024', 'year-2025']); + expect(dataset.series).toEqual(chartConfig.series); + expect(dataset.source).toEqual([ + { name: 'Product A', 'year-2024': 10, 'year-2025': 20 }, + { name: 'Product B', 'year-2024': 30, 'year-2025': 40 }, + ]); + }); + + it('preserves null values and normalizes missing/non-numeric series values to null', () => { + const chartConfig: ChartConfig = { + series: [ + { key: 'first', name: 'First' }, + { key: 'second', name: 'Second' }, + ], + data: [{ name: 'Category 1', first: null }], + }; + + const dataset = mapChartConfigToEChartsDataset(chartConfig); + + expect(dataset.source).toEqual([ + { name: 'Category 1', first: null, second: null }, + ]); + }); +}); diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index 8e39d6c53..b3770a610 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -2,7 +2,12 @@ import { getPxTableData } from '../Table/cubeHelper'; import type { PxTable } from '../../shared-types/pxTable'; import type { DataCell } from '../../shared-types/pxTableData'; -import type { ChartConfig, ChartDataPoint, ChartSeries } from './chartTypes'; +import type { + ChartConfig, + ChartDataPoint, + ChartSeries, + EChartsDataset, +} from './chartTypes'; interface CombinationItem { readonly variableId: string; @@ -104,3 +109,32 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { return { data, series }; } + +export function mapChartConfigToEChartsDataset( + chartConfig: ChartConfig, +): EChartsDataset { + const dimensions = [ + 'name', + ...chartConfig.series.map((series) => series.key), + ]; + + const source = chartConfig.data.map((point) => { + const row: Record = { + name: point.name, + }; + + for (const series of chartConfig.series) { + const value = point[series.key]; + row[series.key] = + typeof value === 'number' || value === null ? value : null; + } + + return row; + }); + + return { + dimensions, + source, + series: chartConfig.series, + }; +} diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts index da31715f8..aef06726b 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts @@ -13,6 +13,12 @@ export interface ChartConfig { readonly series: ChartSeries[]; } +export interface EChartsDataset { + readonly dimensions: string[]; + readonly source: Array>; + readonly series: ChartSeries[]; +} + export interface PopulationPyramidDataPoint { readonly name: string; readonly left: number | null; From af5c36811aedbd1f1eb61159c7ac1d2a0511cea3 Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 13:47:12 +0200 Subject: [PATCH 07/24] formatting --- packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index 542e647a8..b2dcce533 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -1,11 +1,13 @@ import BarChart from './Charts/BarChart'; import LineChart from './Charts/LineChart'; import { useMemo } from 'react'; -import { mapChartConfigToEChartsDataset, mapPxTableToChart } from './chartDataMapper'; +import { + mapChartConfigToEChartsDataset, + mapPxTableToChart, +} from './chartDataMapper'; import type { PxTable } from '../../shared-types/pxTable'; - interface ChartProps { readonly pxtable: PxTable; } From 9e6d491ec964a21d7ff47bec7cc6ae47bedf455a Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 14:23:35 +0200 Subject: [PATCH 08/24] Add Population Pyramid chart and validation logic - Implemented PopulationPyramid component for visualizing demographic data. - Added validation for population pyramid input to ensure correct dimensions. - Refactored BarChart and LineChart components to use hooks for chart options. - Introduced utility functions for building dataset and series options. - Enhanced chart data mapping functions to support population pyramid configuration. --- .../src/lib/components/Chart/Chart.tsx | 37 ++- .../lib/components/Chart/Charts/BarChart.tsx | 49 +--- .../lib/components/Chart/Charts/LineChart.tsx | 49 +--- .../Chart/Charts/PopulationPyramid.tsx | 63 ++++ .../Chart/Charts/useEChartOption.ts | 24 ++ .../components/Chart/chartDataMapper.spec.ts | 252 +++++++++++++++- .../lib/components/Chart/chartDataMapper.ts | 273 ++++++++++++++++++ .../components/Chart/chartOptionBuilder.ts | 29 ++ 8 files changed, 700 insertions(+), 76 deletions(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index b2dcce533..536f0b51f 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -1,9 +1,12 @@ import BarChart from './Charts/BarChart'; import LineChart from './Charts/LineChart'; +import { PopulationPyramid } from './Charts/PopulationPyramid'; import { useMemo } from 'react'; +import LocalAlert from '../LocalAlert/LocalAlert'; import { mapChartConfigToEChartsDataset, mapPxTableToChart, + mapPxTableToPopulationPyramid, } from './chartDataMapper'; import type { PxTable } from '../../shared-types/pxTable'; @@ -17,12 +20,40 @@ export function Chart({ pxtable }: ChartProps) { () => mapChartConfigToEChartsDataset(chartConfig), [chartConfig], ); + const populationPyramidResult = useMemo( + () => mapPxTableToPopulationPyramid(pxtable), + [pxtable], + ); + + const pyramidWarningText = useMemo(() => { + switch (populationPyramidResult.validation.reason) { + case 'MISSING_TWO_VALUE_DIMENSION': + return 'Population pyramid requires exactly one dimension with two selected values.'; + case 'MULTIPLE_TWO_VALUE_DIMENSIONS': + return 'Population pyramid supports only one two-value dimension.'; + case 'MISSING_MULTI_VALUE_DIMENSION': + return 'Population pyramid requires one dimension with several selected values.'; + case 'MULTIPLE_MULTI_VALUE_DIMENSIONS': + return 'Population pyramid supports only one multi-value dimension.'; + case 'NON_SINGLE_VALUE_REMAINING_DIMENSIONS': + return 'All remaining dimensions must have exactly one selected value for population pyramid.'; + default: + return ''; + } + }, [populationPyramidResult.validation.reason]); return ( <> - ; - ; - ; + + + + {populationPyramidResult.config ? ( + + ) : ( + + {pyramidWarningText} + + )} ); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index b1315e197..3d2a0fd89 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -1,51 +1,28 @@ -import { useRef, useEffect } from 'react'; -import * as echarts from 'echarts'; +import { useMemo } from 'react'; +import type * as echarts from 'echarts'; +import { buildDatasetOption, buildSeriesOption } from '../chartOptionBuilder'; import type { EChartsDataset } from '../chartTypes'; +import { useEChartOption } from './useEChartOption'; interface BarChartProps { readonly dataset: EChartsDataset; readonly isHorizontal?: boolean; } export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { - const divRef = useRef(null); + const option = useMemo(() => { + const xAxisType = isHorizontal ? ({ type: 'category' } as const) : {}; + const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const); - useEffect(() => { - if (!divRef.current) { - return; - } - const xAxisType = isHorizontal ? { type: 'category' } : {}; - const yAxisType = isHorizontal ? {} : { type: 'category' }; - // Create the echarts instance - const myChart = echarts.init(divRef.current as HTMLElement, null, { - renderer: 'svg', - }); - // Draw the chart - myChart.setOption({ - title: { - text: 'ECharts Getting Started Example', - }, - dataset: { - dimensions: dataset.dimensions, - source: dataset.source, - }, - legend: {}, - tooltip: {}, - // Declare an x-axis (category axis). - // The category map the first column in the dataset by default. + return { + ...buildDatasetOption(dataset), xAxis: xAxisType, - // Declare a y-axis (value axis). yAxis: yAxisType, - series: dataset.series.map((series) => ({ - name: series.name, - type: 'bar', - })), - }); - - return () => { - myChart.dispose(); + series: buildSeriesOption(dataset, 'bar'), }; - }, [divRef, dataset, isHorizontal]); + }, [dataset, isHorizontal]); + + const divRef = useEChartOption(option); return
; } export default BarChart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index f6ea6e785..677f18433 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -1,48 +1,25 @@ -import { useRef, useEffect } from 'react'; -import * as echarts from 'echarts'; +import { useMemo } from 'react'; +import type * as echarts from 'echarts'; +import { buildDatasetOption, buildSeriesOption } from '../chartOptionBuilder'; import type { EChartsDataset } from '../chartTypes'; +import { useEChartOption } from './useEChartOption'; interface LineChartProps { readonly dataset: EChartsDataset; } export function LineChart({ dataset }: LineChartProps) { - const divRef = useRef(null); - - useEffect(() => { - if (!divRef.current) { - return; - } - // Create the echarts instance - const myChart = echarts.init(divRef.current as HTMLElement, null, { - renderer: 'svg', - }); - // Draw the chart - myChart.setOption({ - title: { - text: 'ECharts Getting Started Example', - }, - dataset: { - dimensions: dataset.dimensions, - source: dataset.source, - }, - legend: {}, - tooltip: {}, - // Declare an x-axis (category axis). - // The category map the first column in the dataset by default. - xAxis: { type: 'category' }, - // Declare a y-axis (value axis). + const option = useMemo( + () => ({ + ...buildDatasetOption(dataset), + xAxis: { type: 'category' as const }, yAxis: {}, - series: dataset.series.map((series) => ({ - name: series.name, - type: 'line', - })), - }); + series: buildSeriesOption(dataset, 'line'), + }), + [dataset], + ); - return () => { - myChart.dispose(); - }; - }, [divRef, dataset]); + const divRef = useEChartOption(option); return
; } export default LineChart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx new file mode 100644 index 000000000..e508ef7a6 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; + +import type * as echarts from 'echarts'; + +import type { PopulationPyramidConfig } from '../chartTypes'; +import { useEChartOption } from './useEChartOption'; + +interface PopulationPyramidProps { + readonly dataset: PopulationPyramidConfig; +} + +export function PopulationPyramid({ dataset }: PopulationPyramidProps) { + const option = useMemo(() => { + const categories = dataset.data.map((point) => point.name); + const leftData = dataset.data.map((point) => + point.left === null ? null : -Math.abs(point.left), + ); + const rightData = dataset.data.map((point) => + point.right === null ? null : Math.abs(point.right), + ); + + return { + title: { + text: 'Population pyramid', + }, + legend: { + data: [dataset.leftSeriesName, dataset.rightSeriesName], + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + valueFormatter: (value) => `${Math.abs(Number(value ?? 0))}`, + }, + xAxis: { + type: 'value', + axisLabel: { + formatter: (value: number) => `${Math.abs(value)}`, + }, + }, + yAxis: { + type: 'category', + data: categories, + }, + series: [ + { + name: dataset.leftSeriesName, + type: 'bar', + data: leftData, + stack: 'quantity', + }, + { + name: dataset.rightSeriesName, + type: 'bar', + data: rightData, + stack: 'quantity', + }, + ], + }; + }, [dataset]); + + const divRef = useEChartOption(option); + return
; +} diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts new file mode 100644 index 000000000..20220e8ee --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts @@ -0,0 +1,24 @@ +import { useEffect, useRef } from 'react'; +import * as echarts from 'echarts'; + +export function useEChartOption( + option: echarts.EChartsOption, + renderer: 'canvas' | 'svg' = 'svg', +) { + const divRef = useRef(null); + + useEffect(() => { + if (!divRef.current) { + return; + } + + const chart = echarts.init(divRef.current, null, { renderer }); + chart.setOption(option); + + return () => { + chart.dispose(); + }; + }, [option, renderer]); + + return divRef; +} diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts index 6d106faba..69c8ee98f 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.spec.ts @@ -1,8 +1,163 @@ import { describe, expect, it } from 'vitest'; -import { mapChartConfigToEChartsDataset } from './chartDataMapper'; +import { + mapChartConfigToEChartsDataset, + mapPxTableToPopulationPyramid, + validatePopulationPyramidInput, +} from './chartDataMapper'; import type { ChartConfig } from './chartTypes'; +import type { PxTable } from '../../shared-types/pxTable'; +import { VartypeEnum } from '../../shared-types/vartypeEnum'; + +function createPyramidPxTable(overrides?: { + sexCount?: number; + ageCount?: number; + regionCount?: number; + timeCount?: number; +}): PxTable { + const sexCount = overrides?.sexCount ?? 2; + const ageCount = overrides?.ageCount ?? 3; + const regionCount = overrides?.regionCount ?? 1; + const timeCount = overrides?.timeCount ?? 1; + + const sexValues = Array.from({ length: sexCount }, (_, index) => ({ + code: `S${index + 1}`, + label: `Sex ${index + 1}`, + })); + const ageValues = Array.from({ length: ageCount }, (_, index) => ({ + code: `A${index + 1}`, + label: `Age ${index + 1}`, + })); + const regionValues = Array.from({ length: regionCount }, (_, index) => ({ + code: `R${index + 1}`, + label: `Region ${index + 1}`, + })); + const timeValues = Array.from({ length: timeCount }, (_, index) => ({ + code: `T${index + 1}`, + label: `Time ${index + 1}`, + })); + + const cube: Record< + string, + Record>> + > = {}; + for (let sexIndex = 0; sexIndex < sexValues.length; sexIndex += 1) { + const sexCode = sexValues[sexIndex].code; + cube[sexCode] = {}; + for (let ageIndex = 0; ageIndex < ageValues.length; ageIndex += 1) { + const ageCode = ageValues[ageIndex].code; + cube[sexCode][ageCode] = {}; + for ( + let regionIndex = 0; + regionIndex < regionValues.length; + regionIndex += 1 + ) { + const regionCode = regionValues[regionIndex].code; + cube[sexCode][ageCode][regionCode] = {}; + for (let timeIndex = 0; timeIndex < timeValues.length; timeIndex += 1) { + const timeCode = timeValues[timeIndex].code; + cube[sexCode][ageCode][regionCode][timeCode] = { + value: + (sexIndex + 1) * 100 + + (ageIndex + 1) * 10 + + (regionIndex + 1) + + timeIndex, + }; + } + } + } + } + + return { + metadata: { + id: 'pyramid-test', + language: 'en', + label: 'Population pyramid', + updated: new Date('2026-01-01'), + source: '', + infofile: '', + decimals: 0, + officialStatistics: false, + aggregationAllowed: false, + contents: '', + descriptionDefault: false, + matrix: '', + subjectCode: '', + subjectArea: '', + variables: [ + { + id: 'sex', + label: 'Sex', + type: VartypeEnum.REGULAR_VARIABLE, + mandatory: false, + values: sexValues, + }, + { + id: 'age', + label: 'Age', + type: VartypeEnum.REGULAR_VARIABLE, + mandatory: false, + values: ageValues, + }, + { + id: 'region', + label: 'Region', + type: VartypeEnum.GEOGRAPHICAL_VARIABLE, + mandatory: false, + values: regionValues, + }, + { + id: 'time', + label: 'Time', + type: VartypeEnum.TIME_VARIABLE, + mandatory: false, + values: timeValues, + }, + ], + contacts: [], + definitions: {}, + notes: [], + }, + data: { + cube, + variableOrder: ['sex', 'age', 'region', 'time'], + isLoaded: true, + }, + heading: [ + { + id: 'sex', + label: 'Sex', + type: VartypeEnum.REGULAR_VARIABLE, + mandatory: false, + values: sexValues, + }, + ], + stub: [ + { + id: 'age', + label: 'Age', + type: VartypeEnum.REGULAR_VARIABLE, + mandatory: false, + values: ageValues, + }, + { + id: 'region', + label: 'Region', + type: VartypeEnum.GEOGRAPHICAL_VARIABLE, + mandatory: false, + values: regionValues, + }, + { + id: 'time', + label: 'Time', + type: VartypeEnum.TIME_VARIABLE, + mandatory: false, + values: timeValues, + }, + ], + }; +} describe('mapChartConfigToEChartsDataset', () => { it('maps dimensions and values using chart series order', () => { @@ -43,3 +198,98 @@ describe('mapChartConfigToEChartsDataset', () => { ]); }); }); + +describe('validatePopulationPyramidInput', () => { + it('returns valid result for one two-value dimension, one multi-value dimension and single-value remaining dimensions', () => { + const pxtable = createPyramidPxTable(); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result).toEqual({ + isValid: true, + twoValueDimensionId: 'sex', + multiValueDimensionId: 'age', + }); + }); + + it('returns missing two-value dimension when no dimension has exactly two values', () => { + const pxtable = createPyramidPxTable({ sexCount: 1 }); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result.isValid).toBe(false); + expect(result.reason).toBe('MISSING_TWO_VALUE_DIMENSION'); + }); + + it('returns multiple two-value dimensions when more than one dimension has exactly two values', () => { + const pxtable = createPyramidPxTable({ regionCount: 2 }); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result.isValid).toBe(false); + expect(result.reason).toBe('MULTIPLE_TWO_VALUE_DIMENSIONS'); + }); + + it('returns missing multi-value dimension when no dimension has several values', () => { + const pxtable = createPyramidPxTable({ + ageCount: 1, + regionCount: 1, + timeCount: 1, + }); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result.isValid).toBe(false); + expect(result.reason).toBe('MISSING_MULTI_VALUE_DIMENSION'); + }); + + it('returns multiple multi-value dimensions when more than one dimension has several values', () => { + const pxtable = createPyramidPxTable({ regionCount: 3 }); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result.isValid).toBe(false); + expect(result.reason).toBe('MULTIPLE_MULTI_VALUE_DIMENSIONS'); + }); + + it('returns non-single remaining dimensions when dimensions outside pyramid split are not single-valued', () => { + const pxtable = createPyramidPxTable({ timeCount: 0 }); + + const result = validatePopulationPyramidInput(pxtable); + + expect(result.isValid).toBe(false); + expect(result.reason).toBe('NON_SINGLE_VALUE_REMAINING_DIMENSIONS'); + }); +}); + +describe('mapPxTableToPopulationPyramid', () => { + it('maps valid pyramid data with left and right series names from the two-value dimension', () => { + const pxtable = createPyramidPxTable(); + + const result = mapPxTableToPopulationPyramid(pxtable); + + expect(result.validation.isValid).toBe(true); + expect(result.config).toBeDefined(); + expect(result.config?.leftSeriesName).toBe('Sex 1'); + expect(result.config?.rightSeriesName).toBe('Sex 2'); + expect(result.config?.data).toEqual([ + { name: 'Age 1', left: 111, right: 211 }, + { name: 'Age 2', left: 121, right: 221 }, + { name: 'Age 3', left: 131, right: 231 }, + ]); + }); + + it('returns only validation result when input is invalid', () => { + const pxtable = createPyramidPxTable({ + ageCount: 1, + regionCount: 1, + timeCount: 1, + }); + + const result = mapPxTableToPopulationPyramid(pxtable); + + expect(result.validation.isValid).toBe(false); + expect(result.validation.reason).toBe('MISSING_MULTI_VALUE_DIMENSION'); + expect(result.config).toBeUndefined(); + }); +}); diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index b3770a610..b39171dbe 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -7,6 +7,9 @@ import type { ChartDataPoint, ChartSeries, EChartsDataset, + PopulationPyramidConfig, + PopulationPyramidMappingResult, + PopulationPyramidValidationResult, } from './chartTypes'; interface CombinationItem { @@ -64,6 +67,87 @@ function getLabel(items: CombinationItem[], fallback: string): string { return items.map((item) => item.label).join(' / '); } +type SelectionEntry = string | { code: string }; + +interface VariableWithOptionalSelection { + readonly selectedValues?: unknown; + readonly selection?: unknown; +} + +interface ValueWithOptionalSelectedFlag { + readonly code: string; + readonly selected?: boolean; +} + +function isCodeObject(entry: unknown): entry is { code: string } { + return ( + typeof entry === 'object' && + entry !== null && + 'code' in entry && + typeof entry.code === 'string' + ); +} + +function selectionEntriesToCodes(entries: SelectionEntry[]): string[] { + return entries + .map((entry) => (typeof entry === 'string' ? entry : entry.code)) + .filter((code) => code.length > 0); +} + +function selectedFromEntries( + values: Array<{ code: string; label: string }>, + entries: unknown, +): Array<{ code: string; label: string }> { + if (!Array.isArray(entries) || entries.length === 0) { + return []; + } + + const normalizedEntries = entries.filter( + (entry): entry is SelectionEntry => + typeof entry === 'string' || isCodeObject(entry), + ); + + if (normalizedEntries.length === 0) { + return []; + } + + const selectedCodes = new Set(selectionEntriesToCodes(normalizedEntries)); + return values.filter((value) => selectedCodes.has(value.code)); +} + +function resolveSelectedValues( + variable: PxTable['metadata']['variables'][number], +) { + const variableWithOptionalSelection = + variable as unknown as VariableWithOptionalSelection; + + const selectedFromSelectedValues = selectedFromEntries( + variable.values, + variableWithOptionalSelection.selectedValues, + ); + if (selectedFromSelectedValues.length > 0) { + return selectedFromSelectedValues; + } + + const selectedFromSelection = selectedFromEntries( + variable.values, + variableWithOptionalSelection.selection, + ); + if (selectedFromSelection.length > 0) { + return selectedFromSelection; + } + + const selectedByFlag = variable.values.filter( + (value) => + (value as unknown as ValueWithOptionalSelectedFlag).selected === true, + ); + if (selectedByFlag.length > 0) { + return selectedByFlag; + } + + return variable.values; +} + export function mapPxTableToChart(pxtable: PxTable): ChartConfig { const rowCombinations = buildCombinations(pxtable.stub); const seriesCombinations = buildCombinations(pxtable.heading); @@ -138,3 +222,192 @@ export function mapChartConfigToEChartsDataset( series: chartConfig.series, }; } + +export function validatePopulationPyramidInput( + pxtable: PxTable, +): PopulationPyramidValidationResult { + const variables = [...pxtable.stub, ...pxtable.heading]; + const selectedCountById = new Map( + variables.map((variable) => [ + variable.id, + resolveSelectedValues(variable).length, + ]), + ); + + const twoValueDimensions = variables.filter( + (variable) => selectedCountById.get(variable.id) === 2, + ); + if (twoValueDimensions.length === 0) { + return { + isValid: false, + reason: 'MISSING_TWO_VALUE_DIMENSION', + }; + } + if (twoValueDimensions.length > 1) { + return { + isValid: false, + reason: 'MULTIPLE_TWO_VALUE_DIMENSIONS', + }; + } + + const multiValueDimensions = variables.filter( + (variable) => (selectedCountById.get(variable.id) ?? 0) > 2, + ); + if (multiValueDimensions.length === 0) { + return { + isValid: false, + reason: 'MISSING_MULTI_VALUE_DIMENSION', + twoValueDimensionId: twoValueDimensions[0].id, + }; + } + if (multiValueDimensions.length > 1) { + return { + isValid: false, + reason: 'MULTIPLE_MULTI_VALUE_DIMENSIONS', + twoValueDimensionId: twoValueDimensions[0].id, + }; + } + + const twoValueDimensionId = twoValueDimensions[0].id; + const multiValueDimensionId = multiValueDimensions[0].id; + const nonSingleRemainingDimension = variables.find((variable) => { + if ( + variable.id === twoValueDimensionId || + variable.id === multiValueDimensionId + ) { + return false; + } + + return (selectedCountById.get(variable.id) ?? 0) !== 1; + }); + + if (nonSingleRemainingDimension) { + return { + isValid: false, + reason: 'NON_SINGLE_VALUE_REMAINING_DIMENSIONS', + twoValueDimensionId, + multiValueDimensionId, + }; + } + + return { + isValid: true, + twoValueDimensionId, + multiValueDimensionId, + }; +} + +function getPopulationPyramidValue( + pxtable: PxTable, + codesByVariableId: Record, +): number | null { + const dimensions = pxtable.data.variableOrder.map( + (variableId) => codesByVariableId[variableId], + ); + + if (dimensions.some((dimension) => !dimension)) { + return null; + } + + const dataCell = getPxTableData(pxtable.data.cube, dimensions); + return dataCell?.value ?? null; +} + +export function mapPxTableToPopulationPyramid( + pxtable: PxTable, +): PopulationPyramidMappingResult { + const validation = validatePopulationPyramidInput(pxtable); + if (!validation.isValid) { + return { validation }; + } + + const variables = [...pxtable.stub, ...pxtable.heading]; + const variableById = new Map( + variables.map((variable) => [variable.id, variable]), + ); + const twoValueVariable = validation.twoValueDimensionId + ? variableById.get(validation.twoValueDimensionId) + : undefined; + const multiValueVariable = validation.multiValueDimensionId + ? variableById.get(validation.multiValueDimensionId) + : undefined; + + if (!twoValueVariable || !multiValueVariable) { + return { + validation: { + isValid: false, + reason: 'MISSING_TWO_VALUE_DIMENSION', + }, + }; + } + + const twoValueDimensionValues = resolveSelectedValues(twoValueVariable); + const multiValueDimensionValues = resolveSelectedValues(multiValueVariable); + + if (twoValueDimensionValues.length !== 2) { + return { + validation: { + isValid: false, + reason: 'MISSING_TWO_VALUE_DIMENSION', + }, + }; + } + + if (multiValueDimensionValues.length < 3) { + return { + validation: { + isValid: false, + reason: 'MISSING_MULTI_VALUE_DIMENSION', + }, + }; + } + + const fixedSelectionCodes = Object.fromEntries( + variables + .filter( + (variable) => + variable.id !== twoValueVariable.id && + variable.id !== multiValueVariable.id, + ) + .map((variable) => { + const selectedValues = resolveSelectedValues(variable); + return [variable.id, selectedValues[0]?.code]; + }) + .filter( + (entry): entry is [string, string] => typeof entry[1] === 'string', + ), + ); + + const leftValue = twoValueDimensionValues[0]; + const rightValue = twoValueDimensionValues[1]; + + const data: PopulationPyramidConfig['data'] = multiValueDimensionValues.map( + (categoryValue) => { + const sharedCodes = { + ...fixedSelectionCodes, + [multiValueVariable.id]: categoryValue.code, + }; + + return { + name: categoryValue.label, + left: getPopulationPyramidValue(pxtable, { + ...sharedCodes, + [twoValueVariable.id]: leftValue.code, + }), + right: getPopulationPyramidValue(pxtable, { + ...sharedCodes, + [twoValueVariable.id]: rightValue.code, + }), + }; + }, + ); + + return { + validation, + config: { + data, + leftSeriesName: leftValue.label, + rightSeriesName: rightValue.label, + }, + }; +} diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts new file mode 100644 index 000000000..a891cf1e4 --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts @@ -0,0 +1,29 @@ +import type * as echarts from 'echarts'; + +import type { EChartsDataset } from './chartTypes'; + +export function buildDatasetOption( + dataset: EChartsDataset, +): echarts.EChartsOption { + return { + title: { + text: 'ECharts Getting Started Example', + }, + dataset: { + dimensions: dataset.dimensions, + source: dataset.source, + }, + legend: {}, + tooltip: {}, + }; +} + +export function buildSeriesOption( + dataset: EChartsDataset, + type: 'bar' | 'line', +): echarts.SeriesOption[] { + return dataset.series.map((series) => ({ + name: series.name, + type, + })); +} From 8a5f3c4a465fd011e01b9b1ed7f45f85c5c2e1b0 Mon Sep 17 00:00:00 2001 From: PerIngeVaaje Date: Wed, 15 Apr 2026 14:24:49 +0200 Subject: [PATCH 09/24] Refactor Chart component for improved readability and formatting --- packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index 536f0b51f..a898fc9f3 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -48,9 +48,15 @@ export function Chart({ pxtable }: ChartProps) { {populationPyramidResult.config ? ( - + ) : ( - + {pyramidWarningText} )} From 7dce690164bcd4b7b4e9fc585e7cb888d46b8da6 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Wed, 15 Apr 2026 15:17:15 +0200 Subject: [PATCH 10/24] Add save to png and svg buttons --- .../lib/components/Chart/Charts/BarChart.tsx | 14 ++- .../Chart/Charts/ChartExportButtons.tsx | 116 ++++++++++++++++++ .../lib/components/Chart/Charts/LineChart.tsx | 11 +- .../Chart/Charts/PopulationPyramid.tsx | 11 +- .../Chart/Charts/useEChartOption.ts | 5 +- 5 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 packages/pxweb2-ui/src/lib/components/Chart/Charts/ChartExportButtons.tsx diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 3d2a0fd89..f37d9e315 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -3,6 +3,7 @@ import type * as echarts from 'echarts'; import { buildDatasetOption, buildSeriesOption } from '../chartOptionBuilder'; import type { EChartsDataset } from '../chartTypes'; +import ChartExportButtons from './ChartExportButtons'; import { useEChartOption } from './useEChartOption'; interface BarChartProps { @@ -22,7 +23,16 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { }; }, [dataset, isHorizontal]); - const divRef = useEChartOption(option); - return
; + const { divRef, chartRef } = useEChartOption(option); + + return ( +
+ +
+
+ ); } export default BarChart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/ChartExportButtons.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/ChartExportButtons.tsx new file mode 100644 index 000000000..b02c71bfd --- /dev/null +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/ChartExportButtons.tsx @@ -0,0 +1,116 @@ +import { useCallback } from 'react'; +import type * as echarts from 'echarts'; + +import { Button } from '../../Button/Button'; + +interface ChartExportButtonsProps { + readonly chartRef: React.RefObject; + readonly fileName: string; +} + +export function ChartExportButtons({ + chartRef, + fileName, +}: ChartExportButtonsProps) { + const triggerDownload = useCallback((href: string, downloadName: string) => { + const link = document.createElement('a'); + link.href = href; + link.download = downloadName; + document.body.appendChild(link); + link.click(); + link.remove(); + }, []); + + const exportPngFromSvgDataUrl = useCallback( + (svgDataUrl: string, outputName: string, pixelRatio = 2) => { + const chart = chartRef.current; + if (!chart) { + return; + } + + const image = new Image(); + + image.onload = () => { + const width = Math.max(1, Math.round(chart.getWidth() * pixelRatio)); + const height = Math.max(1, Math.round(chart.getHeight() * pixelRatio)); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + const context = canvas.getContext('2d'); + if (!context) { + return; + } + + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + context.drawImage(image, 0, 0, chart.getWidth(), chart.getHeight()); + + canvas.toBlob((blob) => { + if (!blob) { + return; + } + + const pngBlobUrl = URL.createObjectURL(blob); + triggerDownload(pngBlobUrl, outputName); + URL.revokeObjectURL(pngBlobUrl); + }, 'image/png'); + }; + + image.src = svgDataUrl; + }, + [chartRef, triggerDownload], + ); + + const downloadImage = useCallback( + (type: 'png' | 'svg') => { + const chart = chartRef.current; + if (!chart) { + return; + } + + const dataUrl = chart.getDataURL({ + type, + pixelRatio: type === 'png' ? 2 : 1, + }); + + if (type === 'png' && !dataUrl.startsWith('data:image/png')) { + const svgDataUrl = chart.getDataURL({ type: 'svg' }); + exportPngFromSvgDataUrl(svgDataUrl, `${fileName}.png`); + return; + } + + triggerDownload(dataUrl, `${fileName}.${type}`); + }, + [chartRef, exportPngFromSvgDataUrl, fileName, triggerDownload], + ); + + return ( +
+ + +
+ ); +} + +export default ChartExportButtons; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 677f18433..d3a336902 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -3,6 +3,7 @@ import type * as echarts from 'echarts'; import { buildDatasetOption, buildSeriesOption } from '../chartOptionBuilder'; import type { EChartsDataset } from '../chartTypes'; +import ChartExportButtons from './ChartExportButtons'; import { useEChartOption } from './useEChartOption'; interface LineChartProps { @@ -19,7 +20,13 @@ export function LineChart({ dataset }: LineChartProps) { [dataset], ); - const divRef = useEChartOption(option); - return
; + const { divRef, chartRef } = useEChartOption(option); + + return ( +
+ +
+
+ ); } export default LineChart; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx index e508ef7a6..520de9352 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import type * as echarts from 'echarts'; import type { PopulationPyramidConfig } from '../chartTypes'; +import ChartExportButtons from './ChartExportButtons'; import { useEChartOption } from './useEChartOption'; interface PopulationPyramidProps { @@ -58,6 +59,12 @@ export function PopulationPyramid({ dataset }: PopulationPyramidProps) { }; }, [dataset]); - const divRef = useEChartOption(option); - return
; + const { divRef, chartRef } = useEChartOption(option); + + return ( +
+ +
+
+ ); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts index 20220e8ee..14ead1d13 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts @@ -6,6 +6,7 @@ export function useEChartOption( renderer: 'canvas' | 'svg' = 'svg', ) { const divRef = useRef(null); + const chartRef = useRef(null); useEffect(() => { if (!divRef.current) { @@ -13,12 +14,14 @@ export function useEChartOption( } const chart = echarts.init(divRef.current, null, { renderer }); + chartRef.current = chart; chart.setOption(option); return () => { + chartRef.current = null; chart.dispose(); }; }, [option, renderer]); - return divRef; + return { divRef, chartRef }; } From e8f265993b682575a713b08685d12b2a3a3782b5 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Wed, 15 Apr 2026 15:24:56 +0200 Subject: [PATCH 11/24] Update Content-Security-Policy to include img src directive --- packages/pxweb2/public/_headers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pxweb2/public/_headers b/packages/pxweb2/public/_headers index dc4d3611d..3aef5f063 100644 --- a/packages/pxweb2/public/_headers +++ b/packages/pxweb2/public/_headers @@ -8,4 +8,4 @@ Cache-Control: no-cache, must-revalidate Pragma: no-cache Expires: 0 - Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.scb.se https://data.ssb.no https://data.qa.ssb.no; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net ajax.googleapis.com; + Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.scb.se https://data.ssb.no https://data.qa.ssb.no; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net ajax.googleapis.com;img src 'self' data:; From f740dded4abaa93acd586ab158900b540c162d39 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Wed, 15 Apr 2026 15:31:12 +0200 Subject: [PATCH 12/24] fix typo --- packages/pxweb2/public/_headers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pxweb2/public/_headers b/packages/pxweb2/public/_headers index 3aef5f063..e747ac19b 100644 --- a/packages/pxweb2/public/_headers +++ b/packages/pxweb2/public/_headers @@ -8,4 +8,4 @@ Cache-Control: no-cache, must-revalidate Pragma: no-cache Expires: 0 - Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.scb.se https://data.ssb.no https://data.qa.ssb.no; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net ajax.googleapis.com;img src 'self' data:; + Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.scb.se https://data.ssb.no https://data.qa.ssb.no; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net ajax.googleapis.com; img-src 'self' data:; From bda2f0995bf13446964e61f108bd928be5a8ace8 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 11:39:34 +0200 Subject: [PATCH 13/24] Refactor chart components to use dynamic titles and adjust width for better responsiveness --- .../lib/components/Chart/Charts/BarChart.tsx | 2 +- .../lib/components/Chart/Charts/LineChart.tsx | 2 +- .../Chart/Charts/PopulationPyramid.tsx | 4 +- .../Chart/Charts/useEChartOption.ts | 46 ++++++++++++++++++- .../lib/components/Chart/chartDataMapper.ts | 6 ++- .../components/Chart/chartOptionBuilder.ts | 10 +++- .../src/lib/components/Chart/chartTypes.ts | 3 ++ 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index f37d9e315..0923c6789 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -31,7 +31,7 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { chartRef={chartRef} fileName={isHorizontal ? 'bar-chart-horizontal' : 'bar-chart-vertical'} /> -
+
); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index d3a336902..eb4c0c0e6 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -25,7 +25,7 @@ export function LineChart({ dataset }: LineChartProps) { return (
-
+
); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx index 520de9352..5b74d482a 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx @@ -22,7 +22,7 @@ export function PopulationPyramid({ dataset }: PopulationPyramidProps) { return { title: { - text: 'Population pyramid', + text: dataset.title, }, legend: { data: [dataset.leftSeriesName, dataset.rightSeriesName], @@ -64,7 +64,7 @@ export function PopulationPyramid({ dataset }: PopulationPyramidProps) { return (
-
+
); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts index 14ead1d13..2d1741bd4 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/useEChartOption.ts @@ -1,6 +1,41 @@ import { useEffect, useRef } from 'react'; import * as echarts from 'echarts'; +function isSingleTitleOption( + title: echarts.EChartsOption['title'], +): title is echarts.TitleComponentOption { + return typeof title === 'object' && title !== null && !Array.isArray(title); +} + +function applyOptionWithWrappedTitle( + chart: echarts.EChartsType, + option: echarts.EChartsOption, +) { + if (!isSingleTitleOption(option.title)) { + chart.setOption(option); + return; + } + + const titleTextStyle = option.title.textStyle ?? {}; + const titleWidth = Math.max(80, chart.getWidth() - 32); + + chart.setOption({ + ...option, + title: { + ...option.title, + left: 0, + right: 0, + width: '100%', + textStyle: { + ...titleTextStyle, + overflow: 'break', + width: titleWidth, + align: titleTextStyle.align ?? 'center', + }, + }, + }); +} + export function useEChartOption( option: echarts.EChartsOption, renderer: 'canvas' | 'svg' = 'svg', @@ -15,9 +50,18 @@ export function useEChartOption( const chart = echarts.init(divRef.current, null, { renderer }); chartRef.current = chart; - chart.setOption(option); + + applyOptionWithWrappedTitle(chart, option); + + const handleResize = () => { + chart.resize(); + applyOptionWithWrappedTitle(chart, option); + }; + + window.addEventListener('resize', handleResize); return () => { + window.removeEventListener('resize', handleResize); chartRef.current = null; chart.dispose(); }; diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index b39171dbe..ca0ef97f7 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -152,6 +152,7 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { const rowCombinations = buildCombinations(pxtable.stub); const seriesCombinations = buildCombinations(pxtable.heading); + const title = pxtable.metadata.label; const series: ChartSeries[] = seriesCombinations.map( (combination, index) => ({ key: @@ -191,12 +192,13 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { return point as ChartDataPoint; }); - return { data, series }; + return { title, data, series }; } export function mapChartConfigToEChartsDataset( chartConfig: ChartConfig, ): EChartsDataset { + const title = chartConfig.title; const dimensions = [ 'name', ...chartConfig.series.map((series) => series.key), @@ -217,6 +219,7 @@ export function mapChartConfigToEChartsDataset( }); return { + title, dimensions, source, series: chartConfig.series, @@ -405,6 +408,7 @@ export function mapPxTableToPopulationPyramid( return { validation, config: { + title: pxtable.metadata.label, data, leftSeriesName: leftValue.label, rightSeriesName: rightValue.label, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts index a891cf1e4..711bece2d 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts @@ -7,7 +7,15 @@ export function buildDatasetOption( ): echarts.EChartsOption { return { title: { - text: 'ECharts Getting Started Example', + text: dataset.title, + left: 0, + right: 0, + width: '100%', + textStyle: { + overflow: 'break', + align: 'center', + lineHeight: 20, + }, }, dataset: { dimensions: dataset.dimensions, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts index aef06726b..88cf37d2c 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts @@ -9,11 +9,13 @@ export interface ChartSeries { } export interface ChartConfig { + readonly title: string; readonly data: ChartDataPoint[]; readonly series: ChartSeries[]; } export interface EChartsDataset { + readonly title: string; readonly dimensions: string[]; readonly source: Array>; readonly series: ChartSeries[]; @@ -26,6 +28,7 @@ export interface PopulationPyramidDataPoint { } export interface PopulationPyramidConfig { + readonly title: string; readonly data: PopulationPyramidDataPoint[]; readonly leftSeriesName: string; readonly rightSeriesName: string; From f06cd75dfde26de9aa6f2680edfc8adc626ede35 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 13:16:32 +0200 Subject: [PATCH 14/24] Add origin source to charts and update related types --- .../components/Chart/Charts/PopulationPyramid.tsx | 14 ++++++++++++++ .../src/lib/components/Chart/chartDataMapper.ts | 6 +++++- .../src/lib/components/Chart/chartOptionBuilder.ts | 14 ++++++++++++++ .../src/lib/components/Chart/chartTypes.ts | 3 +++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx index 5b74d482a..14d578029 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/PopulationPyramid.tsx @@ -24,6 +24,20 @@ export function PopulationPyramid({ dataset }: PopulationPyramidProps) { title: { text: dataset.title, }, + // Add origin in bottom-left corner of the chart canvas + graphic: [ + { + type: 'text', + left: 8, + bottom: 8, + silent: true, // don't capture mouse events + style: { + text: `Source: ${dataset.origin}`, + fill: '#6b7280', + font: '12px sans-serif', + }, + }, + ], legend: { data: [dataset.leftSeriesName, dataset.rightSeriesName], }, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index ca0ef97f7..c303926df 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -153,6 +153,7 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { const seriesCombinations = buildCombinations(pxtable.heading); const title = pxtable.metadata.label; + const origin = pxtable.metadata.source; const series: ChartSeries[] = seriesCombinations.map( (combination, index) => ({ key: @@ -192,13 +193,14 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { return point as ChartDataPoint; }); - return { title, data, series }; + return { title, origin, data, series }; } export function mapChartConfigToEChartsDataset( chartConfig: ChartConfig, ): EChartsDataset { const title = chartConfig.title; + const origin = chartConfig.origin; const dimensions = [ 'name', ...chartConfig.series.map((series) => series.key), @@ -220,6 +222,7 @@ export function mapChartConfigToEChartsDataset( return { title, + origin, dimensions, source, series: chartConfig.series, @@ -409,6 +412,7 @@ export function mapPxTableToPopulationPyramid( validation, config: { title: pxtable.metadata.label, + origin: pxtable.metadata.source, data, leftSeriesName: leftValue.label, rightSeriesName: rightValue.label, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts index 711bece2d..b89d96973 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts @@ -17,6 +17,20 @@ export function buildDatasetOption( lineHeight: 20, }, }, + // Add origin in bottom-left corner of the chart canvas + graphic: [ + { + type: 'text', + left: 8, + bottom: 8, + silent: true, // don't capture mouse events + style: { + text: `Source: ${dataset.origin}`, + fill: '#6b7280', + font: '12px sans-serif', + }, + }, + ], dataset: { dimensions: dataset.dimensions, source: dataset.source, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts index 88cf37d2c..f981582b7 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts @@ -10,12 +10,14 @@ export interface ChartSeries { export interface ChartConfig { readonly title: string; + readonly origin: string; readonly data: ChartDataPoint[]; readonly series: ChartSeries[]; } export interface EChartsDataset { readonly title: string; + readonly origin: string; readonly dimensions: string[]; readonly source: Array>; readonly series: ChartSeries[]; @@ -32,6 +34,7 @@ export interface PopulationPyramidConfig { readonly data: PopulationPyramidDataPoint[]; readonly leftSeriesName: string; readonly rightSeriesName: string; + readonly origin: string; } export interface PopulationPyramidValidationResult { From a735f7701795d57b8aa7e1d0913d7ff2b9c071bc Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 14:20:17 +0200 Subject: [PATCH 15/24] Add color customization to chart components and update configuration --- .../src/lib/components/Chart/Chart.tsx | 9 +++++---- .../lib/components/Chart/Charts/BarChart.tsx | 7 ++++--- .../lib/components/Chart/Charts/LineChart.tsx | 7 ++++--- .../lib/components/Chart/chartOptionBuilder.ts | 6 +++++- packages/pxweb2/public/config/config.js | 17 +++++++++++++++++ .../components/Presentation/Presentation.tsx | 2 +- .../pxweb2/src/app/util/config/configType.ts | 5 +++++ 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index a898fc9f3..ea5c38f6d 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -13,8 +13,9 @@ import type { PxTable } from '../../shared-types/pxTable'; interface ChartProps { readonly pxtable: PxTable; + readonly colors?: string[]; } -export function Chart({ pxtable }: ChartProps) { +export function Chart({ pxtable, colors }: ChartProps) { const chartConfig = useMemo(() => mapPxTableToChart(pxtable), [pxtable]); const dataset = useMemo( () => mapChartConfigToEChartsDataset(chartConfig), @@ -44,9 +45,9 @@ export function Chart({ pxtable }: ChartProps) { return ( <> - - - + + + {populationPyramidResult.config ? ( (() => { const xAxisType = isHorizontal ? ({ type: 'category' } as const) : {}; const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const); @@ -19,9 +20,9 @@ export function BarChart({ dataset, isHorizontal = false }: BarChartProps) { ...buildDatasetOption(dataset), xAxis: xAxisType, yAxis: yAxisType, - series: buildSeriesOption(dataset, 'bar'), + series: buildSeriesOption(dataset, 'bar', colors), }; - }, [dataset, isHorizontal]); + }, [dataset, isHorizontal, colors]); const { divRef, chartRef } = useEChartOption(option); diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index eb4c0c0e6..82f4ab728 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -8,16 +8,17 @@ import { useEChartOption } from './useEChartOption'; interface LineChartProps { readonly dataset: EChartsDataset; + readonly colors?: string[]; } -export function LineChart({ dataset }: LineChartProps) { +export function LineChart({ dataset, colors }: LineChartProps) { const option = useMemo( () => ({ ...buildDatasetOption(dataset), xAxis: { type: 'category' as const }, yAxis: {}, - series: buildSeriesOption(dataset, 'line'), + series: buildSeriesOption(dataset, 'line', colors), }), - [dataset], + [dataset, colors], ); const { divRef, chartRef } = useEChartOption(option); diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts index b89d96973..99a3d8096 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts @@ -40,12 +40,16 @@ export function buildDatasetOption( }; } + + export function buildSeriesOption( dataset: EChartsDataset, type: 'bar' | 'line', + colors?: string[], ): echarts.SeriesOption[] { - return dataset.series.map((series) => ({ + return dataset.series.map((series, index) => ({ name: series.name, type, + color: colors ? colors[index % colors.length] : undefined, })); } diff --git a/packages/pxweb2/public/config/config.js b/packages/pxweb2/public/config/config.js index 87ec89910..6ab5be5be 100644 --- a/packages/pxweb2/public/config/config.js +++ b/packages/pxweb2/public/config/config.js @@ -38,4 +38,21 @@ globalThis.PxWeb2Config = { sv: '', // Set to your Swedish homepage URL en: '', // Set to your English homepage URL }, + presentationPage: { + chart: { + colors: [ + '#3b82f6', + '#ef4444', + '#f59e0b', + '#10b981', + '#8b5cf6', + '#ec4899', + '#14b8a6', + '#f97316', + '#6366f1', + '#e11d48', + '#22d3ee', + ], + }, + }, }; diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index f36596315..3ffe02031 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -276,7 +276,7 @@ export function Presentation({ ref={gradientContainerRef} >
- +
diff --git a/packages/pxweb2/src/app/util/config/configType.ts b/packages/pxweb2/src/app/util/config/configType.ts index 783d29b57..4c69fc2cb 100644 --- a/packages/pxweb2/src/app/util/config/configType.ts +++ b/packages/pxweb2/src/app/util/config/configType.ts @@ -16,4 +16,9 @@ export type Config = { homePage?: { [lang: string]: string; }; + presentationPage?: { + chart: { + colors: string[]; + }; + }; }; From 80050eda2635e55e40a1370e429054745e40d1ce Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 14:21:03 +0200 Subject: [PATCH 16/24] Prettier code --- packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx | 6 +++++- .../pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx | 6 +++++- .../pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx | 2 +- .../src/lib/components/Chart/chartOptionBuilder.ts | 2 -- .../pxweb2/src/app/components/Presentation/Presentation.tsx | 5 ++++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx index ea5c38f6d..594708308 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx @@ -45,7 +45,11 @@ export function Chart({ pxtable, colors }: ChartProps) { return ( <> - + {populationPyramidResult.config ? ( diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 9f5c9883e..3a1bed104 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -11,7 +11,11 @@ interface BarChartProps { readonly isHorizontal?: boolean; readonly colors?: string[]; } -export function BarChart({ dataset, colors, isHorizontal = false }: BarChartProps) { +export function BarChart({ + dataset, + colors, + isHorizontal = false, +}: BarChartProps) { const option = useMemo(() => { const xAxisType = isHorizontal ? ({ type: 'category' } as const) : {}; const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const); diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 82f4ab728..1a27694c5 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -8,7 +8,7 @@ import { useEChartOption } from './useEChartOption'; interface LineChartProps { readonly dataset: EChartsDataset; - readonly colors?: string[]; + readonly colors?: string[]; } export function LineChart({ dataset, colors }: LineChartProps) { const option = useMemo( diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts index 99a3d8096..2e1770e4a 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartOptionBuilder.ts @@ -40,8 +40,6 @@ export function buildDatasetOption( }; } - - export function buildSeriesOption( dataset: EChartsDataset, type: 'bar' | 'line', diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index 3ffe02031..b34b20d5f 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -276,7 +276,10 @@ export function Presentation({ ref={gradientContainerRef} >
- +
From f145cce01dcf64ad4ed80f45fca93a16919ad3d5 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 15:06:54 +0200 Subject: [PATCH 17/24] Add unit property to ChartConfig and EChartsDataset; update LineChart to utilize unit from dataset --- .../src/lib/components/Chart/Charts/LineChart.tsx | 12 +++++++++++- .../src/lib/components/Chart/chartDataMapper.ts | 6 +++++- .../pxweb2-ui/src/lib/components/Chart/chartTypes.ts | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 1a27694c5..3f21ae744 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -14,8 +14,18 @@ export function LineChart({ dataset, colors }: LineChartProps) { const option = useMemo( () => ({ ...buildDatasetOption(dataset), + grid: { top: 100, bottom: 50, right: '4%', containLabel: true }, xAxis: { type: 'category' as const }, - yAxis: {}, + yAxis: { + name: dataset.unit, + // For line charts with small values, start y-axis at 0 to avoid misleading representation + // axisLabel: { + // formatter: '{value} kg', + // align: 'center', + // }, + // min: 100, + min: (value) => value.min, + }, series: buildSeriesOption(dataset, 'line', colors), }), [dataset, colors], diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index c303926df..b8d650810 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -11,6 +11,7 @@ import type { PopulationPyramidMappingResult, PopulationPyramidValidationResult, } from './chartTypes'; +import { VartypeEnum } from '../../shared-types/vartypeEnum'; interface CombinationItem { readonly variableId: string; @@ -154,6 +155,7 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { const title = pxtable.metadata.label; const origin = pxtable.metadata.source; + const unit = pxtable.metadata.variables.find((variable) => variable.type === VartypeEnum.CONTENTS_VARIABLE)?.values[0]?.contentInfo?.unit ?? ''; const series: ChartSeries[] = seriesCombinations.map( (combination, index) => ({ key: @@ -193,7 +195,7 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { return point as ChartDataPoint; }); - return { title, origin, data, series }; + return { title, origin, unit, data, series }; } export function mapChartConfigToEChartsDataset( @@ -201,6 +203,7 @@ export function mapChartConfigToEChartsDataset( ): EChartsDataset { const title = chartConfig.title; const origin = chartConfig.origin; + const unit = chartConfig.unit; const dimensions = [ 'name', ...chartConfig.series.map((series) => series.key), @@ -223,6 +226,7 @@ export function mapChartConfigToEChartsDataset( return { title, origin, + unit, dimensions, source, series: chartConfig.series, diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts index f981582b7..4b7d3416e 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartTypes.ts @@ -11,6 +11,7 @@ export interface ChartSeries { export interface ChartConfig { readonly title: string; readonly origin: string; + readonly unit: string; readonly data: ChartDataPoint[]; readonly series: ChartSeries[]; } @@ -18,6 +19,7 @@ export interface ChartConfig { export interface EChartsDataset { readonly title: string; readonly origin: string; + readonly unit: string; readonly dimensions: string[]; readonly source: Array>; readonly series: ChartSeries[]; From 5449d2dfd75f739839719dd0d16e959eb7eed93a Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 15:07:26 +0200 Subject: [PATCH 18/24] Prettier code --- .../pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx | 2 +- .../pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 3f21ae744..935285881 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -24,7 +24,7 @@ export function LineChart({ dataset, colors }: LineChartProps) { // align: 'center', // }, // min: 100, - min: (value) => value.min, + min: (value) => value.min, }, series: buildSeriesOption(dataset, 'line', colors), }), diff --git a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts index b8d650810..c8b488a50 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/chartDataMapper.ts @@ -155,7 +155,10 @@ export function mapPxTableToChart(pxtable: PxTable): ChartConfig { const title = pxtable.metadata.label; const origin = pxtable.metadata.source; - const unit = pxtable.metadata.variables.find((variable) => variable.type === VartypeEnum.CONTENTS_VARIABLE)?.values[0]?.contentInfo?.unit ?? ''; + const unit = + pxtable.metadata.variables.find( + (variable) => variable.type === VartypeEnum.CONTENTS_VARIABLE, + )?.values[0]?.contentInfo?.unit ?? ''; const series: ChartSeries[] = seriesCombinations.map( (combination, index) => ({ key: From f92677129f1f448317212287643a2e21e3355c25 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 15:50:52 +0200 Subject: [PATCH 19/24] Enhance BarChart and LineChart components with dynamic grid and legend height adjustments --- .../src/lib/components/Chart/Charts/BarChart.tsx | 8 +++++++- .../src/lib/components/Chart/Charts/LineChart.tsx | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 3a1bed104..5f67d6466 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -22,13 +22,19 @@ export function BarChart({ return { ...buildDatasetOption(dataset), + grid: { top: 100, bottom: 200, right: '4%', containLabel: true }, xAxis: xAxisType, yAxis: yAxisType, series: buildSeriesOption(dataset, 'bar', colors), + legend: { + height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels + }, + }; }, [dataset, isHorizontal, colors]); const { divRef, chartRef } = useEChartOption(option); + const height = 400 + dataset.series.length * 20; // increase chart height based on number of series to prevent legend overlap return (
@@ -36,7 +42,7 @@ export function BarChart({ chartRef={chartRef} fileName={isHorizontal ? 'bar-chart-horizontal' : 'bar-chart-vertical'} /> -
+
); } diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 935285881..d59632187 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -14,7 +14,7 @@ export function LineChart({ dataset, colors }: LineChartProps) { const option = useMemo( () => ({ ...buildDatasetOption(dataset), - grid: { top: 100, bottom: 50, right: '4%', containLabel: true }, + grid: { top: 100, bottom: 200, right: '4%', containLabel: true }, xAxis: { type: 'category' as const }, yAxis: { name: dataset.unit, @@ -26,17 +26,21 @@ export function LineChart({ dataset, colors }: LineChartProps) { // min: 100, min: (value) => value.min, }, + legend: { + height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels + }, series: buildSeriesOption(dataset, 'line', colors), }), [dataset, colors], ); const { divRef, chartRef } = useEChartOption(option); + const height = 400 + dataset.series.length * 20; // increase chart height based on number of series to prevent legend overlap return (
-
+
); } From 999e561fc14f9ade57b240c6c3f51d79e8f7cc06 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 15:51:27 +0200 Subject: [PATCH 20/24] prettier code --- packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 5f67d6466..bc44439c9 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -29,7 +29,6 @@ export function BarChart({ legend: { height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels }, - }; }, [dataset, isHorizontal, colors]); From 935f9e57251f43046ce3c5f370b92426306ea3fb Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 16:23:21 +0200 Subject: [PATCH 21/24] Add dataZoom and tooltip configuration to LineChart for improved interactivity --- .../src/lib/components/Chart/Charts/LineChart.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index d59632187..57027b33e 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -30,6 +30,19 @@ export function LineChart({ dataset, colors }: LineChartProps) { height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels }, series: buildSeriesOption(dataset, 'line', colors), + dataZoom: [ + { + id: 'dataZoomX', + type: 'slider', + xAxisIndex: [0], + filterMode: 'filter', + bottom: 60, + }, + ], + // For line charts, tooltips are more useful when triggered by axis to show values of all series at a given category + tooltip: { + trigger: 'axis', + }, }), [dataset, colors], ); From bd832af4b919aad895df2ae5633282cc45805156 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 16:33:39 +0200 Subject: [PATCH 22/24] Add tooltip configuration to BarChart for enhanced interactivity --- .../pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index bc44439c9..a88a9e333 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -29,6 +29,12 @@ export function BarChart({ legend: { height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, }; }, [dataset, isHorizontal, colors]); From 3a0937e9015443d6b20ce7653656b370a45cc281 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 17:11:03 +0200 Subject: [PATCH 23/24] Enhance BarChart and LineChart components with improved axis label rotation and dynamic height adjustments --- .../pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx | 4 ++-- .../pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index a88a9e333..592cf55f9 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -17,7 +17,7 @@ export function BarChart({ isHorizontal = false, }: BarChartProps) { const option = useMemo(() => { - const xAxisType = isHorizontal ? ({ type: 'category' } as const) : {}; + const xAxisType = isHorizontal ? ({ type: 'category', axisLabel: { rotate: 45 } } as const) : {}; const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const); return { @@ -39,7 +39,7 @@ export function BarChart({ }, [dataset, isHorizontal, colors]); const { divRef, chartRef } = useEChartOption(option); - const height = 400 + dataset.series.length * 20; // increase chart height based on number of series to prevent legend overlap + const height = 800 + dataset.series.length * 10; // increase chart height based on number of series to prevent legend overlap return (
diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx index 57027b33e..c8dcc6747 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx @@ -15,7 +15,7 @@ export function LineChart({ dataset, colors }: LineChartProps) { () => ({ ...buildDatasetOption(dataset), grid: { top: 100, bottom: 200, right: '4%', containLabel: true }, - xAxis: { type: 'category' as const }, + xAxis: { type: 'category' as const, axisLabel: { rotate: 45 } }, yAxis: { name: dataset.unit, // For line charts with small values, start y-axis at 0 to avoid misleading representation @@ -48,7 +48,7 @@ export function LineChart({ dataset, colors }: LineChartProps) { ); const { divRef, chartRef } = useEChartOption(option); - const height = 400 + dataset.series.length * 20; // increase chart height based on number of series to prevent legend overlap + const height = 600 + dataset.series.length * 10; // increase chart height based on number of series to prevent legend overlap return (
From d3d01c34f5ef12855b6e25be83ccd7f16e0883e3 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Mon, 18 May 2026 17:11:44 +0200 Subject: [PATCH 24/24] Prettier code --- .../pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx index 592cf55f9..29aee5ecc 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx @@ -17,7 +17,9 @@ export function BarChart({ isHorizontal = false, }: BarChartProps) { const option = useMemo(() => { - const xAxisType = isHorizontal ? ({ type: 'category', axisLabel: { rotate: 45 } } as const) : {}; + const xAxisType = isHorizontal + ? ({ type: 'category', axisLabel: { rotate: 45 } } as const) + : {}; const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const); return {