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..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
@@ -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 = {
@@ -49,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]
@@ -95,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();
@@ -123,7 +129,7 @@ describe('LineChart', () => {
const fallbackColors = ['#abcdef', '#fedcba'];
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);
- render();
+ render();
expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
expect(buildSeriesOption).toHaveBeenCalledWith(
@@ -137,7 +143,7 @@ describe('LineChart', () => {
const fallbackColors = ['#121212', '#343434'];
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);
- render();
+ render();
expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
expect(buildSeriesOption).toHaveBeenCalledWith(
@@ -148,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,
@@ -159,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);
@@ -169,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);
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..d2655604d 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;
@@ -44,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
@@ -54,10 +62,18 @@ 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,
- 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
@@ -94,7 +110,7 @@ export function LineChart({ pxtable, colors }: LineChartProps) {
},
},
}),
- [dataset, resolvedColors],
+ [dataset, resolvedColors, xAxisName],
);
const { divRef } = useEChartOption(option);
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);
+ });
+});
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;
+}
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,
+ },
};