Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cbe7faf
Added echart
PerIngeVaaje Apr 15, 2026
83d7f93
Send dataset to BarChart
PerIngeVaaje Apr 15, 2026
bf09ac0
Added horizontal bar chart
PerIngeVaaje Apr 15, 2026
4f5e725
Added LineChart
PerIngeVaaje Apr 15, 2026
c8cf131
Use pxtable as dataset
PerIngeVaaje Apr 15, 2026
75bcc0e
Use real data from pxtable
PerIngeVaaje Apr 15, 2026
af5c368
formatting
PerIngeVaaje Apr 15, 2026
9e6d491
Add Population Pyramid chart and validation logic
PerIngeVaaje Apr 15, 2026
8a5f3c4
Refactor Chart component for improved readability and formatting
PerIngeVaaje Apr 15, 2026
7dce690
Add save to png and svg buttons
SjurSutterudSagen Apr 15, 2026
e8f2659
Update Content-Security-Policy to include img src directive
SjurSutterudSagen Apr 15, 2026
f740dde
fix typo
SjurSutterudSagen Apr 15, 2026
bda2f09
Refactor chart components to use dynamic titles and adjust width for …
MikaelNordberg May 18, 2026
f06cd75
Add origin source to charts and update related types
MikaelNordberg May 18, 2026
a735f77
Add color customization to chart components and update configuration
MikaelNordberg May 18, 2026
80050ed
Prettier code
MikaelNordberg May 18, 2026
f145cce
Add unit property to ChartConfig and EChartsDataset; update LineChart…
MikaelNordberg May 18, 2026
5449d2d
Prettier code
MikaelNordberg May 18, 2026
f926771
Enhance BarChart and LineChart components with dynamic grid and legen…
MikaelNordberg May 18, 2026
999e561
prettier code
MikaelNordberg May 18, 2026
935f9e5
Add dataZoom and tooltip configuration to LineChart for improved inte…
MikaelNordberg May 18, 2026
bd832af
Add tooltip configuration to BarChart for enhanced interactivity
MikaelNordberg May 18, 2026
3a0937e
Enhance BarChart and LineChart components with improved axis label ro…
MikaelNordberg May 18, 2026
d3d01c3
Prettier code
MikaelNordberg May 18, 2026
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
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/pxweb2-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/pxweb2-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
71 changes: 71 additions & 0 deletions packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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';

interface ChartProps {
readonly pxtable: PxTable;
readonly colors?: string[];
}
export function Chart({ pxtable, colors }: ChartProps) {
const chartConfig = useMemo(() => mapPxTableToChart(pxtable), [pxtable]);
const dataset = useMemo(
() => 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 (
<>
<BarChart
dataset={dataset}
colors={colors}
isHorizontal={true}
></BarChart>
<BarChart dataset={dataset} colors={colors}></BarChart>
<LineChart dataset={dataset} colors={colors}></LineChart>
{populationPyramidResult.config ? (
<PopulationPyramid
dataset={populationPyramidResult.config}
></PopulationPyramid>
) : (
<LocalAlert
variant="warning"
size="small"
heading="Population pyramid unavailable"
>
{pyramidWarningText}
</LocalAlert>
)}
</>
);
}
export default Chart;
56 changes: 56 additions & 0 deletions packages/pxweb2-ui/src/lib/components/Chart/Charts/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useMemo } from 'react';
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 {
readonly dataset: EChartsDataset;
readonly isHorizontal?: boolean;
readonly colors?: string[];
}
export function BarChart({
dataset,
colors,
isHorizontal = false,
}: BarChartProps) {
const option = useMemo<echarts.EChartsOption>(() => {
const xAxisType = isHorizontal
? ({ type: 'category', axisLabel: { rotate: 45 } } as const)
: {};
const yAxisType = isHorizontal ? {} : ({ type: 'category' } as const);

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
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
};
}, [dataset, isHorizontal, colors]);

const { divRef, chartRef } = useEChartOption(option);
const height = 800 + dataset.series.length * 10; // increase chart height based on number of series to prevent legend overlap

return (
<div>
<ChartExportButtons
chartRef={chartRef}
fileName={isHorizontal ? 'bar-chart-horizontal' : 'bar-chart-vertical'}
/>
<div ref={divRef} style={{ width: '100%', height: `${height}px` }}></div>
</div>
);
}
export default BarChart;
Original file line number Diff line number Diff line change
@@ -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<echarts.EChartsType | null>;
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 (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '0.5rem',
marginBottom: '0.5rem',
}}
>
<Button
type="button"
variant="tertiary"
size="small"
onClick={() => downloadImage('png')}
>
Download PNG
</Button>
<Button
type="button"
variant="tertiary"
size="small"
onClick={() => downloadImage('svg')}
>
Download SVG
</Button>
</div>
);
}

export default ChartExportButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from 'react';
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 {
readonly dataset: EChartsDataset;
readonly colors?: string[];
}
export function LineChart({ dataset, colors }: LineChartProps) {
const option = useMemo<echarts.EChartsOption>(
() => ({
...buildDatasetOption(dataset),
grid: { top: 100, bottom: 200, right: '4%', containLabel: true },
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
// axisLabel: {
// formatter: '{value} kg',
// align: 'center',
// },
// 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),
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],
);

const { divRef, chartRef } = useEChartOption(option);
const height = 600 + dataset.series.length * 10; // increase chart height based on number of series to prevent legend overlap

return (
<div>
<ChartExportButtons chartRef={chartRef} fileName="line-chart" />
<div ref={divRef} style={{ width: '100%', height: `${height}px` }}></div>
</div>
);
}
export default LineChart;
Loading
Loading