Skip to content

Commit c60b8ad

Browse files
committed
🎉 integrated amcharts exporting functionality with new context and hooks, now able to export multiple amchart components
1 parent 77cd896 commit c60b8ad

File tree

6 files changed

+234
-40
lines changed

6 files changed

+234
-40
lines changed

src/App.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {MUILocalization} from 'components/shared/MUILocalization';
2323

2424
import AuthProvider from './components/AuthProvider';
2525
import BaseDataContext from 'context/BaseDataContext';
26+
import ExportingRegistry from 'context/ExportContext';
2627
/**
2728
* This is the root element of the React application. It divides the main screen area into the three main components.
2829
* The top bar, the sidebar and the main content area.
@@ -36,26 +37,28 @@ export default function App(): JSX.Element {
3637
<PersistGate loading={null} persistor={Persistor}>
3738
<I18nextProvider i18n={i18n}>
3839
<MUILocalization>
39-
<BaseDataContext>
40-
<Initializer />
41-
<WelcomeDialogWrapper />
42-
<Box id='app' display='flex' flexDirection='column' sx={{height: '100%', width: '100%'}}>
43-
<TopBar />
44-
<Box
45-
id='app-content'
46-
sx={{
47-
display: 'flex',
48-
flexDirection: 'row',
49-
flexGrow: 1,
50-
alignItems: 'stretch',
51-
width: '100%',
52-
}}
53-
>
54-
<SidebarContainer />
55-
<MainContent />
40+
<ExportingRegistry>
41+
<BaseDataContext>
42+
<Initializer />
43+
<WelcomeDialogWrapper />
44+
<Box id='app' display='flex' flexDirection='column' sx={{height: '100%', width: '100%'}}>
45+
<TopBar />
46+
<Box
47+
id='app-content'
48+
sx={{
49+
display: 'flex',
50+
flexDirection: 'row',
51+
flexGrow: 1,
52+
alignItems: 'stretch',
53+
width: '100%',
54+
}}
55+
>
56+
<SidebarContainer />
57+
<MainContent />
58+
</Box>
5659
</Box>
57-
</Box>
58-
</BaseDataContext>
60+
</BaseDataContext>
61+
</ExportingRegistry>
5962
</MUILocalization>
6063
</I18nextProvider>
6164
</PersistGate>

src/components/LineChartComponents/LineChart.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import useValueAxisRange from 'components/shared/LineChart/ValueAxisRange';
3030
import {LineChartData} from 'types/lineChart';
3131
import {LineSeries} from '@amcharts/amcharts5/xy';
3232
import {useSeriesRange} from 'components/shared/LineChart/SeriesRange';
33+
import {useExportingRegistry} from 'context/ExportContext';
34+
import useExporting from 'components/shared/Exporting';
3335

3436
interface LineChartProps {
3537
/** Optional unique identifier for the chart. Defaults to 'chartdiv'. */
@@ -688,25 +690,25 @@ export default function LineChart({
688690
});
689691
}
690692

691-
// Let's import this lazily, since it contains a lot of code.
692-
import('@amcharts/amcharts5/plugins/exporting')
693-
.then((module) => {
694-
// Update export menu
695-
module.Exporting.new(root as Root, {
696-
menu: module.ExportingMenu.new(root as Root, {}),
697-
filePrefix: exportedFileName,
698-
dataSource: data,
699-
dateFields: ['date'],
700-
dateFormat: `${
701-
memoizedLocalization.overrides?.['dateFormat']
702-
? customT(memoizedLocalization.overrides['dateFormat'])
703-
: defaultT('dateFormat')
704-
}`,
705-
dataFields: dataFields,
706-
dataFieldsOrder: dataFieldsOrder,
707-
});
708-
})
709-
.catch(() => console.warn("Couldn't load exporting functionality!"));
693+
// // Let's import this lazily, since it contains a lot of code.
694+
// import('@amcharts/amcharts5/plugins/exporting')
695+
// .then((module) => {
696+
// // Update export menu
697+
// const exporting = module.Exporting.new(root as Root, {
698+
// menu: module.ExportingMenu.new(root as Root, {}),
699+
// filePrefix: exportedFileName,
700+
// dataSource: data,
701+
// dateFields: ['date'],
702+
// dateFormat: `${
703+
// memoizedLocalization.overrides?.['dateFormat']
704+
// ? customT(memoizedLocalization.overrides['dateFormat'])
705+
// : defaultT('dateFormat')
706+
// }`,
707+
// dataFields: dataFields,
708+
// dataFieldsOrder: dataFieldsOrder,
709+
// });
710+
// })
711+
// .catch(() => console.warn("Couldn't load exporting functionality!"));
710712

711713
setReferenceDayX();
712714
// Re-run this effect whenever the data itself changes (or any variable the effect uses)
@@ -724,6 +726,21 @@ export default function LineChart({
724726
yAxisLabel,
725727
]);
726728

729+
const {register} = useExportingRegistry();
730+
const exportSettings = useMemo(() => {
731+
return {
732+
filePrefix: exportedFileName,
733+
};
734+
}, [exportedFileName]);
735+
736+
const exporting = useExporting(root, exportSettings);
737+
738+
useEffect(() => {
739+
if (exporting) {
740+
register('lineChart', exporting);
741+
}
742+
}, [exporting, register]);
743+
727744
return (
728745
<Box
729746
id={chartId}

src/components/Sidebar/MapComponents/HeatMap.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {Localization} from 'types/localization';
2323

2424
// Utils
2525
import {useConst} from 'util/hooks';
26+
import useExporting from 'components/shared/Exporting';
27+
import {useExportingRegistry} from 'context/ExportContext';
2628

2729
interface MapProps {
2830
/** The data to be displayed on the map, in GeoJSON format. */
@@ -123,6 +125,7 @@ export default function HeatMap({
123125
const lastSelectedPolygon = useRef<am5map.MapPolygon | null>(null);
124126
const [longLoadTimeout, setLongLoadTimeout] = useState<number>();
125127

128+
const {register} = useExportingRegistry();
126129
const root = useRoot(mapId);
127130

128131
// MapControlBar.tsx
@@ -353,6 +356,20 @@ export default function HeatMap({
353356
isDataFetching,
354357
]);
355358

359+
const exportSettings = useMemo(() => {
360+
return {
361+
filePrefix: 'map',
362+
};
363+
}, []);
364+
365+
const exporting = useExporting(root, exportSettings);
366+
367+
useEffect(() => {
368+
if (exporting) {
369+
register('map', exporting);
370+
}
371+
}, [exporting, register]);
372+
356373
return (
357374
<Box
358375
id={mapId}

src/components/TopBar/PopUps/ExportDialog.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,97 @@
11
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import React from 'react';
4+
import React, {useCallback} from 'react';
55
import Box from '@mui/material/Box';
66
import useTheme from '@mui/material/styles/useTheme';
77
import {useTranslation} from 'react-i18next';
88
import Typography from '@mui/material/Typography';
99
import Button from '@mui/material/Button';
10+
import {useExportingRegistry} from 'context/ExportContext';
11+
import type {Content, TDocumentDefinitions, ContentImage, Column} from 'pdfmake/interfaces';
12+
13+
const toDataUrl = (img: unknown): string | undefined => {
14+
if (!img) return undefined;
15+
if (typeof img === 'string') return img;
16+
if (typeof img === 'object' && img !== null && 'data' in (img as Record<string, unknown>)) {
17+
const d = (img as Record<string, unknown>).data;
18+
return typeof d === 'string' ? d : undefined;
19+
}
20+
return undefined;
21+
};
1022

1123
export default function ExportDialog(): JSX.Element {
1224
const {t} = useTranslation();
1325
const theme = useTheme();
26+
const {get} = useExportingRegistry();
27+
28+
const handleExport = useCallback(() => {
29+
void (async () => {
30+
const lineExp = get('lineChart');
31+
const mapExp = get('map');
32+
33+
const pdfMake =
34+
(await (lineExp as unknown as {getPDFMake?: () => Promise<unknown>})?.getPDFMake?.()) ||
35+
(await (lineExp as unknown as {getPdfmake?: () => Promise<unknown>})?.getPdfmake?.()) ||
36+
(await (mapExp as unknown as {getPDFMake?: () => Promise<unknown>})?.getPDFMake?.()) ||
37+
(await (mapExp as unknown as {getPdfmake?: () => Promise<unknown>})?.getPdfmake?.());
38+
39+
const [lineImg, mapImg] = await Promise.all([lineExp?.export?.('png'), mapExp?.export?.('png')]);
40+
41+
const lineDataUrl = toDataUrl(lineImg);
42+
const mapDataUrl = toDataUrl(mapImg);
43+
44+
const doc: TDocumentDefinitions = {
45+
pageSize: 'A4',
46+
pageOrientation: 'portrait',
47+
pageMargins: [30, 30, 30, 30],
48+
content: [],
49+
styles: {header: {fontSize: 18, bold: true, margin: [0, 0, 0, 10]}},
50+
};
51+
52+
(doc.content as Content[]).push({text: t('export.header'), style: 'header'});
53+
54+
if (lineDataUrl) {
55+
(doc.content as ContentImage[]).push({
56+
image: lineDataUrl,
57+
width: 500,
58+
});
59+
}
60+
61+
if (mapDataUrl) {
62+
(doc.content as ContentImage[]).push({
63+
image: mapDataUrl,
64+
fit: [300, 300],
65+
});
66+
}
67+
68+
// (doc.content as ContentTable[]).push({
69+
// table: {
70+
// body: [
71+
// [{text: 'Line Chart Data'}, {text: 'Line Chart Data'}],
72+
// [{text: 'Line Chart Data'}, {text: 'Line Chart Data'}],
73+
// ],
74+
// },
75+
// });
76+
77+
const columns: Column[] = [
78+
{
79+
text: 'Line Chart Data',
80+
},
81+
{
82+
text: 'Map Data',
83+
},
84+
];
85+
86+
(doc.content as Content[]).push({
87+
columns: columns,
88+
columnGap: 10,
89+
});
90+
91+
const pdfmake = pdfMake as {createPdf?: (doc: unknown) => {download: (name: string) => void}};
92+
pdfmake?.createPdf?.(doc)?.download('ESID-export.pdf');
93+
})();
94+
}, [get, t]);
1495

1596
return (
1697
<Box
@@ -23,7 +104,7 @@ export default function ExportDialog(): JSX.Element {
23104
<br />
24105
<Typography>{t('export.description')}</Typography>
25106
<br />
26-
<Button variant='contained' color='primary'>
107+
<Button variant='contained' color='primary' onClick={handleExport}>
27108
{t('export.button')}
28109
</Button>
29110
</Box>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {useLayoutEffect, useState} from 'react';
5+
import {Root} from '@amcharts/amcharts5/.internal/core/Root';
6+
import {Exporting} from '@amcharts/amcharts5/.internal/plugins/exporting/Exporting';
7+
import {IExportingSettings} from '@amcharts/amcharts5/.internal/plugins/exporting/Exporting';
8+
9+
export default function useExporting(
10+
root: Root | null,
11+
settings: IExportingSettings,
12+
initializer?: (exporting: Exporting) => void
13+
): Exporting | null {
14+
const [exporting, setExporting] = useState<Exporting>();
15+
16+
useLayoutEffect(() => {
17+
if (!root) {
18+
return;
19+
}
20+
21+
const newExporting = Exporting.new(root, settings);
22+
setExporting(newExporting);
23+
24+
if (initializer) {
25+
initializer(newExporting);
26+
}
27+
28+
return () => {
29+
newExporting.dispose();
30+
};
31+
}, [root, settings, initializer]);
32+
33+
return exporting || null;
34+
}

src/context/ExportContext.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, {createContext, useCallback, useContext, useState} from 'react';
5+
import {Exporting} from '@amcharts/amcharts5/.internal/plugins/exporting/Exporting';
6+
7+
type ExportRegistry = Record<string, Exporting>;
8+
9+
interface ExportContextAPI {
10+
register: (name: string, exporting: Exporting) => void;
11+
unregister: (name: string) => void;
12+
get: (name: string) => Exporting | null;
13+
}
14+
15+
export const ExportContext = createContext<ExportContextAPI | null>(null);
16+
17+
export default function ExportingRegistry({children}: {children: React.ReactNode}): JSX.Element {
18+
const [exporting, setExporting] = useState<ExportRegistry>({});
19+
20+
const register = useCallback((name: string, exporting: Exporting) => {
21+
setExporting((prev) => ({...prev, [name]: exporting}));
22+
}, []);
23+
24+
const unregister = useCallback((name: string) => {
25+
setExporting((prev) => {
26+
const {[name]: _, ...rest} = prev;
27+
return rest;
28+
});
29+
}, []);
30+
31+
const get = useCallback((name: string) => exporting[name] ?? null, [exporting]);
32+
33+
return <ExportContext.Provider value={{register, unregister, get}}>{children}</ExportContext.Provider>;
34+
}
35+
36+
export function useExportingRegistry(): ExportContextAPI {
37+
const context = useContext(ExportContext);
38+
if (!context) {
39+
throw new Error('useExportingRegistry must be used within a ExportContext');
40+
}
41+
return context;
42+
}

0 commit comments

Comments
 (0)