Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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]
Expand Down Expand Up @@ -95,9 +101,9 @@ describe('LineChart', () => {
it('builds chart option with mapped dataset and provided colors', () => {
const colors = ['#111111', '#222222'];

render(<LineChart pxtable={{} as PxTable} colors={colors} />);
render(<LineChart pxtable={mockPxTable} colors={colors} />);

expect(mapPxTableToChartDataset).toHaveBeenCalledWith({});
expect(mapPxTableToChartDataset).toHaveBeenCalledWith(mockPxTable);
expect(buildDatasetOption).toHaveBeenCalledWith(mockDataset);
expect(buildSeriesOption).toHaveBeenCalledWith(mockDataset, 'line', colors);
expect(getChartColorsFromCssVariables).not.toHaveBeenCalled();
Expand All @@ -123,7 +129,7 @@ describe('LineChart', () => {
const fallbackColors = ['#abcdef', '#fedcba'];
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);

render(<LineChart pxtable={{} as PxTable} />);
render(<LineChart pxtable={mockPxTable} />);

expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
expect(buildSeriesOption).toHaveBeenCalledWith(
Expand All @@ -137,7 +143,7 @@ describe('LineChart', () => {
const fallbackColors = ['#121212', '#343434'];
vi.mocked(getChartColorsFromCssVariables).mockReturnValue(fallbackColors);

render(<LineChart pxtable={{} as PxTable} colors={[]} />);
render(<LineChart pxtable={mockPxTable} colors={[]} />);

expect(getChartColorsFromCssVariables).toHaveBeenCalledTimes(1);
expect(buildSeriesOption).toHaveBeenCalledWith(
Expand All @@ -148,7 +154,7 @@ describe('LineChart', () => {
});

it('renders chart container with height based on number of series', () => {
const { container } = render(<LineChart pxtable={{} as PxTable} />);
const { container } = render(<LineChart pxtable={mockPxTable} />);

const chartDiv = Array.from(container.querySelectorAll('div')).find(
(element) => element.style.height,
Expand All @@ -159,7 +165,7 @@ describe('LineChart', () => {
});

it('returns empty tooltip text for empty params', () => {
render(<LineChart pxtable={{} as PxTable} />);
render(<LineChart pxtable={mockPxTable} />);

const option = vi.mocked(useEChartOption).mock.calls[0][0];
const formatter = getTooltipFormatter(option);
Expand All @@ -169,7 +175,7 @@ describe('LineChart', () => {
});

it('formats tooltip rows with symbol svg, labels, values and fallback color', () => {
render(<LineChart pxtable={{} as PxTable} />);
render(<LineChart pxtable={mockPxTable} />);

const option = vi.mocked(useEChartOption).mock.calls[0][0];
const formatter = getTooltipFormatter(option);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -94,7 +110,7 @@ export function LineChart({ pxtable, colors }: LineChartProps) {
},
},
}),
[dataset, resolvedColors],
[dataset, resolvedColors, xAxisName],
);

const { divRef } = useEChartOption(option);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string>): CSSStyleDeclaration {
return {
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions packages/pxweb2/public/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ globalThis.PxWeb2Config = {
sv: '', // Set to your Swedish homepage URL
en: '', // Set to your English homepage URL
},
features: {
chartEnabled: true,
},
};
Loading