From c0a00bde9c303c6e25770f8348f2e94205c6fa20 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Thu, 2 Jul 2026 10:35:40 +0200 Subject: [PATCH 1/6] feat: add chartEnabled feature flag to configuration --- packages/pxweb2/public/config/config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/pxweb2/public/config/config.js b/packages/pxweb2/public/config/config.js index 87ec89910..26a9c5b36 100644 --- a/packages/pxweb2/public/config/config.js +++ b/packages/pxweb2/public/config/config.js @@ -38,4 +38,7 @@ globalThis.PxWeb2Config = { sv: '', // Set to your Swedish homepage URL en: '', // Set to your English homepage URL }, + features: { + chartEnabled: true, + }, }; From 34737ea8db606da54b845a463487f2dfa16d02ae Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Thu, 2 Jul 2026 16:43:22 +0200 Subject: [PATCH 2/6] feat: implement adaptive Y-axis scaling for LineChart --- .../components/Chart/LineChart/LineChart.tsx | 10 ++- .../lib/components/Chart/Utils/chartHelper.ts | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx index 123d414d4..a7b9cc59f 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx @@ -9,7 +9,11 @@ import { import { useEChartOption } from '../Utils/useEChartOption'; import type { PxTable } from '../../../shared-types/pxTable'; import { mapPxTableToChartDataset } from '../Utils/chartDataMapper'; -import { getChartColorsFromCssVariables } from '../Utils/chartHelper'; +import { + getAdaptiveYAxisMax, + getAdaptiveYAxisMin, + getChartColorsFromCssVariables, +} from '../Utils/chartHelper'; interface LineChartProps { readonly pxtable: PxTable; @@ -57,7 +61,9 @@ export function LineChart({ pxtable, colors }: LineChartProps) { xAxis: { type: 'category' as const, axisLabel: { rotate: 45 } }, yAxis: { name: dataset.unit, - min: (value) => value.min, + scale: true, + min: getAdaptiveYAxisMin, + max: getAdaptiveYAxisMax, }, legend: { height: 40 * dataset.series.length, // increase legend height based on number of series to prevent overlap with x-axis labels diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.ts b/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.ts index a78da3e8f..1d5addebc 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.ts @@ -48,3 +48,69 @@ export function getChartColorsFromCssVariables(): string[] | undefined { return undefined; } + +function getNiceNumber(value: number): number { + if (!Number.isFinite(value) || value <= 0) { + return 1; + } + + const exponent = Math.floor(Math.log10(value)); + const base = 10 ** exponent; + const fraction = value / base; + + if (fraction <= 1) { + return 1 * base; + } + if (fraction <= 2) { + return 2 * base; + } + if (fraction <= 5) { + return 5 * base; + } + return 10 * base; +} + +function getAdaptiveSnapUnit(min: number, max: number): number { + const span = Math.max(max - min, 1); + + // No fixed tick count: just derive a clean rounding grain from data span. + // For spans around 1.3M this typically becomes 500k. + return getNiceNumber(span / 3); +} + +export function getAdaptiveYAxisMin(value: { + min: number; + max: number; +}): number { + const min = Number(value.min); + const max = Number(value.max); + + const span = Math.max(max - min, 1); + const pad = span * 0.03; + const snap = getAdaptiveSnapUnit(min, max); + + const paddedMin = min - pad; + const roundedMin = Math.floor(paddedMin / snap) * snap; + + // Keep non-negative axes non-negative. + if (min >= 0) { + return Math.max(0, roundedMin); + } + + return roundedMin; +} + +export function getAdaptiveYAxisMax(value: { + min: number; + max: number; +}): number { + const min = Number(value.min); + const max = Number(value.max); + + const span = Math.max(max - min, 1); + const pad = span * 0.03; + const snap = getAdaptiveSnapUnit(min, max); + + const paddedMax = max + pad; + return Math.ceil(paddedMax / snap) * snap; +} From 9106e14c18b6f265bc56b06426fbf536a1688801 Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Thu, 2 Jul 2026 16:57:10 +0200 Subject: [PATCH 3/6] feat: mock getAdaptiveYAxisMin and getAdaptiveYAxisMax in LineChart tests --- .../src/lib/components/Chart/LineChart/LineChart.spec.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx index 94ad83347..e2a914c96 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx @@ -34,6 +34,8 @@ vi.mock('../Utils/chartOptionBuilder', async () => { vi.mock('../Utils/chartHelper', () => ({ getChartColorsFromCssVariables: vi.fn(), + getAdaptiveYAxisMin: vi.fn(), + getAdaptiveYAxisMax: vi.fn(), })); const mockDataset: EChartsDataset = { From d79b78b61983ab5badca0f27afc3bdf93da07f7e Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Thu, 2 Jul 2026 17:33:18 +0200 Subject: [PATCH 4/6] feat: enhance LineChart x-axis configuration with dynamic naming and layout adjustments --- .../lib/components/Chart/LineChart/LineChart.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx index a7b9cc59f..d2655604d 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx @@ -48,6 +48,10 @@ function getTooltipSymbolSvg(symbol: string, color: string): string { export function LineChart({ pxtable, colors }: LineChartProps) { const dataset = useMemo(() => mapPxTableToChartDataset(pxtable), [pxtable]); + const xAxisName = useMemo(() => { + return pxtable.stub.map((variable) => variable.label).join(' / '); + }, [pxtable]); + const resolvedColors = useMemo(() => { return colors && colors.length > 0 ? colors @@ -58,7 +62,13 @@ export function LineChart({ pxtable, colors }: LineChartProps) { () => ({ ...buildDatasetOption(dataset), grid: { top: 0, bottom: 200, left: '0', right: '0', containLabel: false }, - xAxis: { type: 'category' as const, axisLabel: { rotate: 45 } }, + xAxis: { + type: 'category' as const, + name: xAxisName, + nameLocation: 'end', + nameGap: 70, + axisLabel: { rotate: 45 }, + }, yAxis: { name: dataset.unit, scale: true, @@ -100,7 +110,7 @@ export function LineChart({ pxtable, colors }: LineChartProps) { }, }, }), - [dataset, resolvedColors], + [dataset, resolvedColors, xAxisName], ); const { divRef } = useEChartOption(option); From 417e3fb12222873bb303b598f8d614fdc3eb99ba Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Thu, 2 Jul 2026 17:39:30 +0200 Subject: [PATCH 5/6] test: update LineChart tests to use mockPxTable for consistent data representation --- .../Chart/LineChart/LineChart.spec.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx index e2a914c96..3d5ebd409 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx +++ b/packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.spec.tsx @@ -51,6 +51,10 @@ const mockDataset: EChartsDataset = { ], }; +const mockPxTable = { + stub: [{ label: 'Year' }], +} as PxTable; + function getTooltipFormatter(option: { tooltip?: unknown }) { const tooltip = Array.isArray(option.tooltip) ? option.tooltip[0] @@ -97,9 +101,9 @@ describe('LineChart', () => { it('builds chart option with mapped dataset and provided colors', () => { const colors = ['#111111', '#222222']; - render(); + render(); - expect(mapPxTableToChartDataset).toHaveBeenCalledWith({}); + expect(mapPxTableToChartDataset).toHaveBeenCalledWith(mockPxTable); expect(buildDatasetOption).toHaveBeenCalledWith(mockDataset); expect(buildSeriesOption).toHaveBeenCalledWith(mockDataset, 'line', colors); expect(getChartColorsFromCssVariables).not.toHaveBeenCalled(); @@ -125,7 +129,7 @@ describe('LineChart', () => { const fallbackColors = ['#abcdef', '#fedcba']; vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors); - render(); + render(); expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1); expect(buildSeriesOption).toHaveBeenCalledWith( @@ -139,7 +143,7 @@ describe('LineChart', () => { const fallbackColors = ['#121212', '#343434']; vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors); - render(); + render(); expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1); expect(buildSeriesOption).toHaveBeenCalledWith( @@ -150,7 +154,7 @@ describe('LineChart', () => { }); it('renders chart container with height based on number of series', () => { - const { container } = render(); + const { container } = render(); const chartDiv = Array.from(container.querySelectorAll('div')).find( (element) => element.style.height, @@ -161,7 +165,7 @@ describe('LineChart', () => { }); it('returns empty tooltip text for empty params', () => { - render(); + render(); const option = vi.mocked(useEChartOption).mock.calls[0][0]; const formatter = getTooltipFormatter(option); @@ -171,7 +175,7 @@ describe('LineChart', () => { }); it('formats tooltip rows with symbol svg, labels, values and fallback color', () => { - render(); + render(); const option = vi.mocked(useEChartOption).mock.calls[0][0]; const formatter = getTooltipFormatter(option); From 906277f1b83de610054809a7963daa83b42efcef Mon Sep 17 00:00:00 2001 From: MikaelNordberg Date: Fri, 3 Jul 2026 10:39:12 +0200 Subject: [PATCH 6/6] feat: add tests for adaptive Y-axis min and max calculations in chartHelper --- .../Chart/Utils/chartHelper.spec.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.spec.ts b/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.spec.ts index 8ff8b61d2..f9edf2c25 100644 --- a/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.spec.ts +++ b/packages/pxweb2-ui/src/lib/components/Chart/Utils/chartHelper.spec.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { getChartColorsFromCssVariables } from './chartHelper'; +import { + getAdaptiveYAxisMax, + getAdaptiveYAxisMin, + getChartColorsFromCssVariables, +} from './chartHelper'; function mockStyles(values: Record): CSSStyleDeclaration { return { @@ -75,3 +79,31 @@ describe('getChartColorsFromCssVariables', () => { expect(getChartColorsFromCssVariables()).toBeUndefined(); }); }); + +describe('getAdaptiveYAxisMin', () => { + it('clamps to zero when the source range is non-negative', () => { + expect(getAdaptiveYAxisMin({ min: 0, max: 1 })).toBe(0); + }); + + it('rounds down to a clean snap value for positive ranges', () => { + expect(getAdaptiveYAxisMin({ min: 100, max: 200 })).toBe(50); + }); + + it('keeps negative ranges negative and rounds down', () => { + expect(getAdaptiveYAxisMin({ min: -120, max: 80 })).toBe(-200); + }); +}); + +describe('getAdaptiveYAxisMax', () => { + it('rounds up to a clean snap value for positive ranges', () => { + expect(getAdaptiveYAxisMax({ min: 100, max: 200 })).toBe(250); + }); + + it('adds headroom and rounds up for small decimal ranges', () => { + expect(getAdaptiveYAxisMax({ min: 0, max: 1 })).toBe(1.5); + }); + + it('works when min and max are equal', () => { + expect(getAdaptiveYAxisMax({ min: 5, max: 5 })).toBe(5.5); + }); +});