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
19 changes: 19 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 @@ -17,6 +17,7 @@
"license": "MIT",
"dependencies": {
"@vitejs/plugin-react": "^5.2.0",
"chart.js": "^4.5.1",
"clsx": "^2.1.1",
"hast-util-to-jsx-runtime": "^2.3.6",
"motion": "^12.38.0",
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/ChartPx/ChartPx';
export * from './lib/components/Checkbox/Checkbox';
export * from './lib/components/CheckCircle/CheckCircleIcon';
export * from './lib/components/CheckCircle/CheckCircleToggle';
Expand Down
30 changes: 30 additions & 0 deletions packages/pxweb2-ui/src/lib/components/ChartPx/ChartPx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PxTable } from '../../shared-types/pxTable';
import { useMemo } from 'react';
import { mapPxTableToChart } from './chartDataMapper';
import { PopulationPyramid } from './Charts/PopulationPyramid';
import { mapPxTableToPopulationPyramid } from './populationPyramidMapper';
import { RegularCharts } from './Charts/RegularCharts';

interface ChartProps {
readonly pxtable: PxTable;
}

export function ChartPx({ pxtable }: ChartProps) {
const chartConfig = useMemo(() => mapPxTableToChart(pxtable), [pxtable]);
const populationPyramid = useMemo(
() => mapPxTableToPopulationPyramid(pxtable),
[pxtable],
);

return (
<>
<RegularCharts chartConfig={chartConfig} />
<PopulationPyramid
config={populationPyramid.config}
validation={populationPyramid.validation}
/>
</>
);
}

export default ChartPx;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';
import type { ChartConfig } from '../chartTypes';
import { downloadCanvasAsPng } from '../chartExport';

interface BarChartProps {
readonly chartConfig: ChartConfig;
readonly isHorizontal?: boolean;
}

export function BarChart({ chartConfig, isHorizontal = false }: BarChartProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);

const chartName = isHorizontal ? 'bar-chart-horizontal.png' : 'bar-chart.png';

const handleDownloadPng = () => {
if (!canvasRef.current) {
return;
}

downloadCanvasAsPng(canvasRef.current, chartName);
};

useEffect(() => {
if (!canvasRef.current) {
return;
}

const chart = new Chart(canvasRef.current, {
type: 'bar',
options: {
indexAxis: isHorizontal ? 'y' : 'x',
},
data: {
labels: chartConfig.data.map((row) => row.name),
datasets: chartConfig.series.map((series) => ({
label: series.name,
data: chartConfig.data.map((row) => row[series.key] as number | null),
})),
},
});

return () => {
chart.destroy();
};
}, [chartConfig, isHorizontal]);

return (
<>
<h1>Chart</h1>
<button onClick={handleDownloadPng} type="button">
Download PNG
</button>

<canvas ref={canvasRef} />
</>
);
}

export default BarChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';
import type { ChartConfig } from '../chartTypes';
import { downloadCanvasAsPng } from '../chartExport';

interface LineChartProps {
readonly chartConfig: ChartConfig;
}

export function LineChart({ chartConfig }: LineChartProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);

const handleDownloadPng = () => {
if (!canvasRef.current) {
return;
}

downloadCanvasAsPng(canvasRef.current, 'line-chart.png');
};

useEffect(() => {
if (!canvasRef.current) {
return;
}

const chart = new Chart(canvasRef.current, {
type: 'line',
data: {
labels: chartConfig.data.map((row) => row.name),
datasets: chartConfig.series.map((series) => ({
label: series.name,
data: chartConfig.data.map((row) => row[series.key] as number | null),
})),
},
});

return () => {
chart.destroy();
};
}, [chartConfig]);

return (
<>
<h1>Chart</h1>
<button onClick={handleDownloadPng} type="button">
Download PNG
</button>

<canvas ref={canvasRef} />
</>
);
}

export default LineChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useEffect, useMemo, useRef } from 'react';
import Chart from 'chart.js/auto';
import type {
PopulationPyramidConfig,
PopulationPyramidValidationResult,
} from '../chartTypes';
import { downloadCanvasAsPng } from '../chartExport';

interface PopulationPyramidProps {
readonly config?: PopulationPyramidConfig;
readonly validation: PopulationPyramidValidationResult;
}

function formatAbsoluteValue(value: number | string): string {
const numericValue = typeof value === 'number' ? value : Number(value);

if (Number.isNaN(numericValue)) {
return String(value);
}

return Math.abs(numericValue).toLocaleString();
}

function getValidationMessage(
validation: PopulationPyramidValidationResult,
): string {
if (!validation.reason) {
return 'Population pyramid data is not valid.';
}

const messages: Record<
NonNullable<PopulationPyramidValidationResult['reason']>,
string
> = {
MISSING_TWO_VALUE_DIMENSION:
'Population pyramid requires exactly one dimension with 2 values.',
MULTIPLE_TWO_VALUE_DIMENSIONS:
'Population pyramid supports only one dimension with exactly 2 values.',
MISSING_MULTI_VALUE_DIMENSION:
'Population pyramid requires exactly one dimension with more than 2 values.',
MULTIPLE_MULTI_VALUE_DIMENSIONS:
'Population pyramid supports only one dimension with more than 2 values.',
NON_SINGLE_VALUE_REMAINING_DIMENSIONS:
'All remaining dimensions must have exactly one value for population pyramid.',
};

return messages[validation.reason];
}

export function PopulationPyramid({
config,
validation,
}: PopulationPyramidProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const validationMessage = useMemo(
() => getValidationMessage(validation),
[validation],
);

const handleDownloadPng = () => {
if (!canvasRef.current) {
return;
}

downloadCanvasAsPng(canvasRef.current, 'population-pyramid.png');
};

useEffect(() => {
if (!validation.isValid || !config || !canvasRef.current) {
return;
}

const chart = new Chart(canvasRef.current, {
type: 'bar',
options: {
indexAxis: 'y',
scales: {
x: {
stacked: true,
ticks: {
callback: (value) => formatAbsoluteValue(value),
},
},
y: {
stacked: true,
},
},
plugins: {
tooltip: {
callbacks: {
label: (context) => {
const label = context.dataset.label
? `${context.dataset.label}: `
: '';
const rawValue = context.parsed?.x ?? 0;
return `${label}${formatAbsoluteValue(rawValue)}`;
},
},
},
},
},
data: {
labels: config.data.map((row) => row.name),
datasets: [
{
label: config.leftSeriesName,
data: config.data.map((row) =>
row.left === null ? null : -Math.abs(row.left),
),
},
{
label: config.rightSeriesName,
data: config.data.map((row) => row.right),
},
],
},
});

return () => {
chart.destroy();
};
}, [config, validation.isValid]);

if (!validation.isValid || !config) {
return (
<>
<h1>Population Pyramid</h1>
<p>{validationMessage}</p>
</>
);
}

return (
<>
<h1>Population Pyramid</h1>
<button onClick={handleDownloadPng} type="button">
Download PNG
</button>
<canvas ref={canvasRef} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ChartConfig } from '../chartTypes';
import { BarChart } from './BarChart';
import { LineChart } from './LineChart';

interface RegularChartsProps {
readonly chartConfig: ChartConfig;
}

export function RegularCharts({ chartConfig }: RegularChartsProps) {
return (
<>
<BarChart chartConfig={chartConfig} />
<BarChart chartConfig={chartConfig} isHorizontal={true} />
<LineChart chartConfig={chartConfig} />
</>
);
}

export default RegularCharts;
Loading
Loading