Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ describe('LineChart', () => {
]);

expect(html).toContain('<div><div>2024</div>');
expect(html).toContain('Men: 10');
expect(html).toContain('Women: 12');
expect(html).toContain('Men: <b>10</b>');
expect(html).toContain('Women: <b>12</b>');
expect(html).toContain('fill="#666666"');
expect(html).toContain('fill="#ff0000"');
expect(html).toContain('<circle');
Expand Down
164 changes: 138 additions & 26 deletions packages/pxweb2-ui/src/lib/components/Chart/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,49 @@ import { useEChartOption } from '../Utils/useEChartOption';
import type { PxTable } from '../../../shared-types/pxTable';
import { mapPxTableToChartDataset } from '../Utils/chartDataMapper';
import { getChartColorsFromCssVariables } from '../Utils/chartHelper';
import { EChartsDataset } from '../Utils/chartTypes';

type TooltipType = 'axis' | 'item';

interface LineChartProps {
readonly pxtable: PxTable;
readonly colors?: string[];
readonly tooltipType?: TooltipType;
}

type TooltipParam = {
axisValueLabel?: string;
axisValue?: string | number;
name?: string;
seriesIndex: number;
seriesName: string;
data?: Record<string, string | number>;
data?: Record<string, string | number | null>;
color?: string;
};

const getConfinedTooltipPosition: NonNullable<
echarts.TooltipComponentOption['position']
> = (point, _params, _dom, _rect, size) => {
const padding = 8;
const offset = 12;

const [rawX, rawY] = Array.isArray(point) ? point : [padding, padding];

const [contentWidth, contentHeight] = size?.contentSize ?? [0, 0];
const [viewWidth, viewHeight] = size?.viewSize ?? [0, 0];

const desiredX = rawX + offset;
const desiredY = rawY + offset;

const maxX = Math.max(padding, viewWidth - contentWidth - padding);
const maxY = Math.max(padding, viewHeight - contentHeight - padding);

const x = Math.min(Math.max(desiredX, padding), maxX);
const y = Math.min(Math.max(desiredY, padding), maxY);

return [x, y];
};

function getTooltipSymbolSvg(symbol: string, color: string): string {
switch (symbol) {
case 'rect':
Expand All @@ -41,29 +70,13 @@ function getTooltipSymbolSvg(symbol: string, color: string): string {
}
}

export function LineChart({ pxtable, colors }: LineChartProps) {
const dataset = useMemo(() => mapPxTableToChartDataset(pxtable), [pxtable]);

const resolvedColors = useMemo(() => {
return colors && colors.length > 0
? colors
: getChartColorsFromCssVariables();
}, [colors]);

const option = useMemo<echarts.EChartsOption>(
() => ({
...buildDatasetOption(dataset),
grid: { top: 0, bottom: 200, left: '0', right: '0', containLabel: false },
xAxis: { type: 'category' as const, axisLabel: { rotate: 45 } },
yAxis: {
name: dataset.unit,
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', resolvedColors),
tooltip: {
function getTooltip(
tooltipType: TooltipType,
dataset: EChartsDataset,
): echarts.EChartsOption['tooltip'] {
switch (tooltipType) {
case 'axis':
return {
trigger: 'axis',
formatter: (params: unknown) => {
const axisParams = (Array.isArray(params) ? params : [params]) as
Expand All @@ -85,15 +98,114 @@ export function LineChart({ pxtable, colors }: LineChartProps) {
];
const color = param.color ?? '#666666';

return `<div style="display:flex;align-items:center;gap:6px"><span style="display:inline-flex;align-items:center">${getTooltipSymbolSvg(symbol, color)}</span><span>${param.seriesName}: ${value ?? ''}</span></div>`;
return `<div style="display:flex;align-items:flex-start;gap:6px"><span style="display:inline-flex;align-items:flex-start;line-height:1;transform:translateY(2px)">${getTooltipSymbolSvg(symbol, color)}</span><span>${param.seriesName}: <b>${value ?? ''}</b></span></div>`;
})
.join('');

return `<div><div>${title}</div>${rows}</div>`;
},
};
case 'item':
return {
trigger: 'item',
axisPointer: {
type: 'cross',
},
formatter: (params: unknown) => {
const param = params as TooltipParam;
const seriesMeta = dataset.series[param.seriesIndex];

if (!seriesMeta) {
return '';
}

const row = param.data;
const title =
param.axisValueLabel ??
(param.axisValue != null ? String(param.axisValue) : undefined) ??
param.name ??
(row?.name != null ? String(row.name) : '');
const value = row?.[seriesMeta.key] ?? '';
const symbol =
LINE_SERIES_SYMBOLS[param.seriesIndex % LINE_SERIES_SYMBOLS.length];
const color = param.color ?? '#666666';

return `
<div>${title}</div>
<div style="display:flex;align-items:flex-start;gap:6px">
<span style="display:inline-flex;align-items:flex-start;line-height:1;transform:translateY(2px)">
${getTooltipSymbolSvg(symbol, color)}
</span>
<span>${param.seriesName}: <b>${value}</b></span>
</div>
`;
},
};
default:
return {
trigger: 'axis',
};
}
}

export function LineChart({
pxtable,
colors,
tooltipType = 'axis',
}: LineChartProps) {
const dataset = useMemo(() => mapPxTableToChartDataset(pxtable), [pxtable]);

const resolvedColors = useMemo(() => {
return colors && colors.length > 0
? colors
: getChartColorsFromCssVariables();
}, [colors]);

const series = useMemo<echarts.SeriesOption[]>(() => {
const baseSeries = buildSeriesOption(dataset, 'line', resolvedColors);

if (tooltipType !== 'item') {
return baseSeries;
}

// Show all symbols for 'item' tooltip type to ensure that the tooltip is displayed for all data points
return baseSeries.map((seriesOption) => {
if (seriesOption.type !== 'line') {
return seriesOption;
}

return {
...seriesOption,
showSymbol: true,
showAllSymbol: true,
symbolSize: 7,
} as echarts.LineSeriesOption;
});
}, [dataset, resolvedColors, tooltipType]);

const option = useMemo<echarts.EChartsOption>(
() => ({
...buildDatasetOption(dataset),
grid: { top: 0, bottom: 200, left: '0', right: '0', containLabel: false },
xAxis: { type: 'category' as const, axisLabel: { rotate: 45 } },
yAxis: {
name: dataset.unit,
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,
tooltip: {
...getTooltip(tooltipType, dataset),
confine: true,
appendToBody: true,
extraCssText:
'max-width:min(92vw,340px);white-space:normal;word-break:break-word;overflow-wrap:anywhere;',
position: getConfinedTooltipPosition,
},
}),
[dataset, resolvedColors],
[dataset, series, tooltipType],
);

const { divRef } = useEChartOption(option);
Expand Down
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,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ export function Presentation({
[classes.fadeChart]: isFadingTable,
})}
>
<LineChart pxtable={tableData.data} />
<LineChart pxtable={tableData.data} tooltipType="axis" />
<LineChart pxtable={tableData.data} tooltipType="item" />
</div>
</Activity>
</>
Expand Down