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
1 change: 1 addition & 0 deletions packages/pxweb2-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "npm run storybook",
"build": "npm run build-style-dictionary && storybook build && mv ./storybook-static ../pxweb2/dist/storybook",
"build:web-component": "vite build --config vite.web-component.config.ts",
"build-style-dictionary": "node ./style-dictionary/build.mjs",
"storybook": "storybook dev -p 6006",
"test": "vitest run",
Expand Down
5 changes: 5 additions & 0 deletions packages/pxweb2-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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/Chart/Charts/LineChart';
export * from './lib/components/Chart/Charts/Standalone/StandaloneLineChart';
export * from './lib/components/Chart/Charts/Standalone/parseTableDataUrl';
export * from './lib/components/Chart/Charts/Standalone/PxwebLineChartElement';
export * from './lib/components/Chart/Charts/Standalone/pxwebLineChartGlobal';
export * from './lib/components/Checkbox/Checkbox';
export * from './lib/components/CheckCircle/CheckCircleIcon';
export * from './lib/components/CheckCircle/CheckCircleToggle';
Expand Down
4 changes: 2 additions & 2 deletions packages/pxweb2-ui/src/lib/components/Chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BarChart from './Charts/BarChart';
import LineChart from './Charts/LineChart';
// import LineChart from './Charts/LineChart';
import { PopulationPyramid } from './Charts/PopulationPyramid';
import { useMemo } from 'react';
import LocalAlert from '../LocalAlert/LocalAlert';
Expand Down Expand Up @@ -51,7 +51,7 @@ export function Chart({ pxtable, colors }: ChartProps) {
isHorizontal={true}
></BarChart>
<BarChart dataset={dataset} colors={colors}></BarChart>
<LineChart dataset={dataset} colors={colors}></LineChart>
{/* <LineChart dataset={dataset} colors={colors}></LineChart> */}
{populationPyramidResult.config ? (
<PopulationPyramid
dataset={populationPyramidResult.config}
Expand Down
34 changes: 22 additions & 12 deletions packages/pxweb2-ui/src/lib/components/Chart/Charts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@ import { useMemo } from 'react';
import type * as echarts from 'echarts';

import { buildDatasetOption, buildSeriesOption } from '../chartOptionBuilder';
import type { EChartsDataset } from '../chartTypes';
// import type { EChartsDataset } from '../chartTypes';
import ChartExportButtons from './ChartExportButtons';
import { useEChartOption } from './useEChartOption';
import { PxTable } from '../../../shared-types/pxTable';
import {
mapChartConfigToEChartsDataset,
mapPxTableToChart,
} from '../chartDataMapper';

interface LineChartProps {
readonly dataset: EChartsDataset;
readonly pxtable: PxTable;
readonly colors?: string[];
}
export function LineChart({ dataset, colors }: LineChartProps) {
export function LineChart({ pxtable, colors }: LineChartProps) {
const chartConfig = useMemo(() => mapPxTableToChart(pxtable), [pxtable]);
const dataset = useMemo(
() => mapChartConfigToEChartsDataset(chartConfig),
[chartConfig],
);
const option = useMemo<echarts.EChartsOption>(
() => ({
...buildDatasetOption(dataset),
Expand All @@ -30,15 +40,15 @@ export function LineChart({ dataset, colors }: LineChartProps) {
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,
},
],
// 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',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// This file implements the web component <pxweb-line-chart> that renders a line chart from a given data URL, using React under the hood.
// Some of the functions in this file are web component specific and should exist for web components.
import { createRoot, type Root } from 'react-dom/client';

import { StandaloneLineChart } from './StandaloneLineChart';

const ELEMENT_NAME = 'pxweb-line-chart';

function parseBooleanAttribute(
value: string | null,
defaultValue: boolean,
): boolean {
if (value === null) {
return defaultValue;
}

if (value === '' || value.toLowerCase() === 'true') {
return true;
}

if (value.toLowerCase() === 'false') {
return false;
}

return defaultValue;
}

function parseColorsAttribute(value: string | null): string[] | undefined {
if (!value) {
return undefined;
}

const colors = value
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);

return colors.length > 0 ? colors : undefined;
}

class PxwebLineChartElement extends HTMLElement {
static get observedAttributes() {
return ['data-url', 'colors', 'strict-base-match'];
}

private root: Root | null = null;

connectedCallback() {
if (!this.root) {
this.root = createRoot(this);
}

this.renderComponent();
}

attributeChangedCallback() {
this.renderComponent();
}

disconnectedCallback() {
this.root?.unmount();
this.root = null;
}

private renderComponent() {
if (!this.root) {
return;
}

const dataUrl = this.getAttribute('data-url');
if (!dataUrl) {
this.root.render(<div>Missing required attribute: data-url</div>);
return;
}

this.root.render(
<StandaloneLineChart
dataUrl={dataUrl}
colors={parseColorsAttribute(this.getAttribute('colors'))}
strictBaseMatch={parseBooleanAttribute(
this.getAttribute('strict-base-match'),
true,
)}
/>,
);
}
}

// Registers the custom element safely
export function definePxwebLineChartElement() {
if (!customElements.get(ELEMENT_NAME)) {
customElements.define(ELEMENT_NAME, PxwebLineChartElement);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// This file is the UI wrapper for rendering a standalone line chart from a dataUrl.
import LineChart from '../LineChart';
import { PXWEB_LINE_CHART_CONFIG_CHANGED_EVENT } from './pxwebLineChartGlobal';
import { useStandalonePxTableData } from './useStandalonePxTableData';

export interface StandaloneLineChartProps {
readonly dataUrl: string;
readonly colors?: string[];
readonly strictBaseMatch?: boolean;
readonly loadingRenderer?: React.ReactNode;
readonly errorRenderer?: (errorMessage: string) => React.ReactNode;
}

export function StandaloneLineChart({
dataUrl,
colors,
strictBaseMatch = true,
loadingRenderer,
errorRenderer,
}: StandaloneLineChartProps) {
const { pxTable, error, isLoading } = useStandalonePxTableData({
dataUrl,
strictBaseMatch,
reloadEventName: PXWEB_LINE_CHART_CONFIG_CHANGED_EVENT,
});

if (isLoading) {
return loadingRenderer ?? <div>Loading chart...</div>;
}

if (error) {
return errorRenderer ? (
<>{errorRenderer(error)}</>
) : (
<div role="alert">Unable to load chart: {error}</div>
);
}

if (!pxTable) {
return null;
}

return <LineChart pxtable={pxTable} colors={colors}></LineChart>;
}

export default StandaloneLineChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//This file is the shared data-loading utility for standalone chart components.
//It centralizes all non-UI work needed to get chart-ready data from a table URL.
import {
OpenAPI,
OutputFormatType,
TablesService,
type ApiError,
type Dataset,
} from '@pxweb2/pxweb2-api-client';

import { parseTableDataUrl } from './parseTableDataUrl';
import { mapJsonStat2DatasetToPxTable } from './mapJsonStat2DatasetToPxTable';
import type { PxTable } from '../../../../shared-types/pxTable';

export interface LoadPxTableDataOptions {
readonly dataUrl: string;
readonly strictBaseMatch?: boolean;
}

export function getBaseOrigin(): string {
if (!OpenAPI.BASE) {
return '';
}

try {
const fallbackOrigin =
globalThis.window?.location.origin ?? 'http://localhost';
return new URL(OpenAPI.BASE, fallbackOrigin).origin;
} catch {
return '';
}
}

export function getLoadPxTableDataErrorMessage(error: unknown): string {
if (
typeof error === 'object' &&
error !== null &&
'status' in error &&
'message' in error
) {
const apiError = error as ApiError;
return `Request failed (${apiError.status}): ${apiError.message}`;
}

if (error instanceof Error) {
return error.message;
}

return 'Unknown error while loading chart data.';
}

export async function loadPxTableData({
dataUrl,
strictBaseMatch = true,
}: LoadPxTableDataOptions): Promise<PxTable> {
const parsedUrl = parseTableDataUrl(dataUrl);

const baseOrigin = getBaseOrigin();
if (!baseOrigin) {
throw new Error(
'OpenAPI.BASE is not configured. Configure OpenAPI.BASE before rendering StandaloneLineChart.',
);
}

if (strictBaseMatch && parsedUrl.origin !== baseOrigin) {
throw new Error(
`dataUrl origin (${parsedUrl.origin}) does not match OpenAPI.BASE origin (${baseOrigin}).`,
);
}

const response = await TablesService.getTableData(
parsedUrl.tableId,
parsedUrl.lang,
parsedUrl.valuecodes,
parsedUrl.codelist,
OutputFormatType.JSON_STAT2,
parsedUrl.outputFormatParams,
parsedUrl.heading?.length ? parsedUrl.heading : undefined,
parsedUrl.stub?.length ? parsedUrl.stub : undefined,
);

const dataset = response as unknown as Dataset;
return mapJsonStat2DatasetToPxTable(dataset);
}
Loading