@hisptz/dhis2-analytics 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/d2.config.js +8 -0
- package/package.json +55 -0
- package/src/components/ChartAnalytics/ChartAnalytics.stories.tsx +250 -0
- package/src/components/ChartAnalytics/ChartAnalytics.test.tsx +51 -0
- package/src/components/ChartAnalytics/components/DownloadMenu/components/Menu.tsx +48 -0
- package/src/components/ChartAnalytics/components/DownloadMenu/constants/menu.ts +38 -0
- package/src/components/ChartAnalytics/components/DownloadMenu/index.tsx +67 -0
- package/src/components/ChartAnalytics/components/DownloadMenu/interfaces/menu.ts +1 -0
- package/src/components/ChartAnalytics/data/column-data.json +210 -0
- package/src/components/ChartAnalytics/data/complex-multi-series-data.json +124 -0
- package/src/components/ChartAnalytics/data/multi-series-data.json +536 -0
- package/src/components/ChartAnalytics/data/pie-data.json +115 -0
- package/src/components/ChartAnalytics/data/stacked-chart-data.json +415 -0
- package/src/components/ChartAnalytics/hooks/useChart.ts +35 -0
- package/src/components/ChartAnalytics/index.tsx +23 -0
- package/src/components/ChartAnalytics/models/column.ts +50 -0
- package/src/components/ChartAnalytics/models/index.ts +78 -0
- package/src/components/ChartAnalytics/models/line.ts +31 -0
- package/src/components/ChartAnalytics/models/multi-series.ts +115 -0
- package/src/components/ChartAnalytics/models/pie.ts +54 -0
- package/src/components/ChartAnalytics/services/export.ts +38 -0
- package/src/components/ChartAnalytics/styles/custom-highchart.css +48 -0
- package/src/components/ChartAnalytics/types/props.tsx +48 -0
- package/src/components/ChartAnalytics/utils/chart.ts +123 -0
- package/src/components/CircularProgressDashboard/CircularProgressIndicator.stories.tsx +41 -0
- package/src/components/CircularProgressDashboard/CircularProgressIndicator.test.tsx +9 -0
- package/src/components/CircularProgressDashboard/index.tsx +35 -0
- package/src/components/CircularProgressDashboard/types/props.tsx +17 -0
- package/src/components/Map/Map.stories.tsx +339 -0
- package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfigModal.stories.tsx +28 -0
- package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfiguration.stories.tsx +34 -0
- package/src/components/Map/components/EarthEngineLayerConfiguration/index.tsx +412 -0
- package/src/components/Map/components/MapArea/index.tsx +83 -0
- package/src/components/Map/components/MapArea/interfaces/index.ts +39 -0
- package/src/components/Map/components/MapControls/components/CustomControl/index.tsx +24 -0
- package/src/components/Map/components/MapControls/components/DownloadControl/index.tsx +10 -0
- package/src/components/Map/components/MapControls/components/FullscreenControl/index.tsx +7 -0
- package/src/components/Map/components/MapControls/index.tsx +24 -0
- package/src/components/Map/components/MapLayer/components/BoundaryLayer/hooks/useBoundaryData.ts +7 -0
- package/src/components/Map/components/MapLayer/components/BoundaryLayer/index.tsx +55 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/components/EarthEngineLegend.tsx +76 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/constants/index.ts +430 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/hooks/index.ts +34 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/index.tsx +185 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/interfaces/index.ts +56 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/api.js +34233 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/engine.ts +423 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/utils/index.ts +105 -0
- package/src/components/Map/components/MapLayer/components/LegendArea/LegendArea.module.css +12 -0
- package/src/components/Map/components/MapLayer/components/LegendArea/components/LegendCardHeader/index.tsx +17 -0
- package/src/components/Map/components/MapLayer/components/LegendArea/index.tsx +168 -0
- package/src/components/Map/components/MapLayer/components/PointLayer/components/PointLegend/index.tsx +44 -0
- package/src/components/Map/components/MapLayer/components/PointLayer/hooks/index.ts +8 -0
- package/src/components/Map/components/MapLayer/components/PointLayer/index.tsx +36 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubble.tsx +48 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubbles.tsx +150 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/index.tsx +39 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/index.tsx +57 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/components/ChoroplethLegend.tsx +43 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/index.tsx +38 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/components/CustomTooltip/index.tsx +26 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/hooks/config.ts +10 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/index.tsx +46 -0
- package/src/components/Map/components/MapLayer/components/ThematicLayer/styles/legends.css +62 -0
- package/src/components/Map/components/MapLayer/index.tsx +32 -0
- package/src/components/Map/components/MapLayer/interfaces/index.ts +139 -0
- package/src/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.tsx +359 -0
- package/src/components/Map/components/MapProvider/components/MapLayerProvider/index.tsx +105 -0
- package/src/components/Map/components/MapProvider/hooks/index.ts +14 -0
- package/src/components/Map/components/MapProvider/index.tsx +93 -0
- package/src/components/Map/components/MapUpdater/index.tsx +8 -0
- package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfigModal.stories.tsx +28 -0
- package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfiguration.stories.tsx +34 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/components/ColorScale/index.tsx +24 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/constants/colors.ts +433 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/index.tsx +50 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScale.module.css +15 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScaleSelect.module.css +12 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/utils/colors.ts +91 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/CustomLegend/index.tsx +45 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/IndicatorSelectorModal/index.tsx +47 -0
- package/src/components/Map/components/ThematicLayerConfiguration/components/LegendSetSelector/index.tsx +57 -0
- package/src/components/Map/components/ThematicLayerConfiguration/index.tsx +248 -0
- package/src/components/Map/constants/colors.ts +433 -0
- package/src/components/Map/constants/legendSet.ts +19 -0
- package/src/components/Map/hooks/map.ts +47 -0
- package/src/components/Map/index.tsx +65 -0
- package/src/components/Map/interfaces/index.ts +57 -0
- package/src/components/Map/state/index.tsx +31 -0
- package/src/components/Map/utils/colors.ts +95 -0
- package/src/components/Map/utils/helpers.ts +15 -0
- package/src/components/Map/utils/map.ts +150 -0
- package/src/components/SingleValueContainer/SingleValueContainer.stories.tsx +146 -0
- package/src/components/SingleValueContainer/SingleValueContainer.test.tsx +24 -0
- package/src/components/SingleValueContainer/components/SingleValueItem/SingleValueItem.tsx +46 -0
- package/src/components/SingleValueContainer/components/SingleValueItem/SingleValuePercentage.tsx +12 -0
- package/src/components/SingleValueContainer/index.tsx +30 -0
- package/src/components/SingleValueContainer/styles/SingleValueContainer.module.css +39 -0
- package/src/components/SingleValueContainer/types/props.tsx +16 -0
- package/src/data/map.json +5984 -0
- package/src/dataProviders/map.tsx +24 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {PlotOptions, SeriesOptionsType, XAxisOptions, YAxisOptions} from "highcharts";
|
|
2
|
+
import {compact, head} from "lodash";
|
|
3
|
+
import {MultiSeriesConfig} from "../types/props";
|
|
4
|
+
import {getAllCategories, getDimensionHeaderIndex} from "../utils/chart";
|
|
5
|
+
import {DHIS2Chart} from "./index";
|
|
6
|
+
|
|
7
|
+
export class DHIS2MultiSeriesChart extends DHIS2Chart {
|
|
8
|
+
getHighchartsType(): string {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getPlotOptions(): PlotOptions {
|
|
13
|
+
return {
|
|
14
|
+
column: {},
|
|
15
|
+
line: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getSeries(): SeriesOptionsType[] {
|
|
20
|
+
const analytics = this.analytics;
|
|
21
|
+
const config = this.config;
|
|
22
|
+
|
|
23
|
+
const categoryDimension = head(config.layout.category);
|
|
24
|
+
const seriesConfig: MultiSeriesConfig | undefined = this.config.multiSeries;
|
|
25
|
+
|
|
26
|
+
const categoryIndex = getDimensionHeaderIndex(analytics.headers ?? [], categoryDimension ?? "");
|
|
27
|
+
const seriesIndex = getDimensionHeaderIndex(analytics.headers ?? [], head(config.layout.series) ?? "");
|
|
28
|
+
const valueIndex = getDimensionHeaderIndex(analytics.headers ?? [], "value");
|
|
29
|
+
|
|
30
|
+
if (!categoryDimension) {
|
|
31
|
+
throw new Error("At least one category dimension is required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!seriesConfig) {
|
|
35
|
+
throw new Error("MultiSeries config is required for chart type multi-series");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return compact(
|
|
39
|
+
seriesConfig?.series?.map(({ id, as, cumulative, yAxis }) => {
|
|
40
|
+
const dataItem = analytics.metaData?.items[id as any];
|
|
41
|
+
const categoryItems = analytics.metaData?.dimensions[categoryDimension as "dx" | "ou" | "pe"];
|
|
42
|
+
|
|
43
|
+
const data = categoryItems?.map((item: string) => {
|
|
44
|
+
const row = analytics.rows?.find((row: any) => row[categoryIndex] === item && row[seriesIndex] === id);
|
|
45
|
+
return row?.[valueIndex] ? parseFloat(row?.[valueIndex]) : 0;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let cumulativeData: number[] = [];
|
|
49
|
+
|
|
50
|
+
if (cumulative) {
|
|
51
|
+
cumulativeData =
|
|
52
|
+
data?.reduce((acc, curr, index) => {
|
|
53
|
+
if (index === 0) {
|
|
54
|
+
return [...acc, curr];
|
|
55
|
+
}
|
|
56
|
+
return [...acc, acc[index - 1] + curr];
|
|
57
|
+
}, [] as number[]) ?? [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name: dataItem?.name,
|
|
62
|
+
data: cumulative ? cumulativeData : data,
|
|
63
|
+
type: as,
|
|
64
|
+
yAxis: yAxis ?? 0,
|
|
65
|
+
};
|
|
66
|
+
})
|
|
67
|
+
) as SeriesOptionsType[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getXAxis(): XAxisOptions | undefined {
|
|
71
|
+
return {
|
|
72
|
+
type: "category",
|
|
73
|
+
categories: getAllCategories(this.analytics, this.config),
|
|
74
|
+
crosshair: true,
|
|
75
|
+
labels: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
},
|
|
78
|
+
title: { text: "" },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getYAxis(): YAxisOptions[] {
|
|
83
|
+
let yAxes: YAxisOptions[] = [];
|
|
84
|
+
|
|
85
|
+
if (this.config.multiSeries?.yAxes) {
|
|
86
|
+
yAxes = this.config.multiSeries?.yAxes;
|
|
87
|
+
} else {
|
|
88
|
+
yAxes = super.getYAxis();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!this.config.multiSeries?.target) {
|
|
92
|
+
return yAxes;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { value, styles, label } = this.config.multiSeries?.target ?? {};
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
...yAxes[0],
|
|
100
|
+
plotLines: [
|
|
101
|
+
...(yAxes[0].plotLines ?? []),
|
|
102
|
+
{
|
|
103
|
+
color: styles?.color ?? "#00FF00",
|
|
104
|
+
dashStyle: styles?.dashStyle ?? "Solid",
|
|
105
|
+
value,
|
|
106
|
+
width: styles?.width ?? 2,
|
|
107
|
+
zIndex: styles?.zIndex ?? 1000,
|
|
108
|
+
label: label,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
...yAxes.slice(1),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {PlotOptions, SeriesOptionsType, XAxisOptions} from "highcharts";
|
|
2
|
+
import {head} from "lodash";
|
|
3
|
+
import {DHIS2Chart} from "./index";
|
|
4
|
+
|
|
5
|
+
export class DHIS2PieChart extends DHIS2Chart {
|
|
6
|
+
getHighchartsType(): string {
|
|
7
|
+
return "pie";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getPlotOptions(): PlotOptions {
|
|
11
|
+
return {
|
|
12
|
+
pie: {
|
|
13
|
+
allowPointSelect: true,
|
|
14
|
+
cursor: "pointer",
|
|
15
|
+
dataLabels: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
format: "<b>{point.name}</b>: {point.percentage:.1f} %",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getSeries(): SeriesOptionsType[] {
|
|
24
|
+
const analytics = this.analytics;
|
|
25
|
+
const config = this.config;
|
|
26
|
+
const seriesDimension = head(config.layout.series);
|
|
27
|
+
const seriesIndex = analytics?.headers?.findIndex((h) => h.name === seriesDimension) ?? -1;
|
|
28
|
+
const valueIndex = analytics?.headers?.findIndex((h) => h.name === "value") ?? -1;
|
|
29
|
+
|
|
30
|
+
if (!seriesDimension) {
|
|
31
|
+
throw new Error("Pie chart must have a series dimension");
|
|
32
|
+
}
|
|
33
|
+
const seriesValues = analytics.metaData?.dimensions?.[seriesDimension as "dx" | "ou" | "pe"];
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
id: seriesDimension ?? "",
|
|
38
|
+
name: analytics.metaData?.items?.[seriesDimension as any]?.name ?? "",
|
|
39
|
+
data: seriesValues?.map((value: string) => {
|
|
40
|
+
const row = analytics?.rows?.find((row: any) => row[seriesIndex] === value);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: analytics.metaData?.items?.[value as any]?.name,
|
|
44
|
+
y: row?.[valueIndex] ? parseFloat(row?.[valueIndex] ?? "") : 0,
|
|
45
|
+
};
|
|
46
|
+
}),
|
|
47
|
+
} as SeriesOptionsType,
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getXAxis(): XAxisOptions | undefined {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Highcharts from "highcharts";
|
|
2
|
+
import HighchartsReact from "highcharts-react-official";
|
|
3
|
+
import HighChartsExportCSV from "highcharts/modules/export-data";
|
|
4
|
+
import HighChartsExport from "highcharts/modules/exporting";
|
|
5
|
+
import HighChartsFullScreen from "highcharts/modules/full-screen";
|
|
6
|
+
|
|
7
|
+
export function setupHighchartsModules(highcharts: typeof Highcharts) {
|
|
8
|
+
HighChartsExport(highcharts);
|
|
9
|
+
HighChartsExportCSV(highcharts);
|
|
10
|
+
HighChartsFullScreen(highcharts);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function onFullScreenView(chartRef: HighchartsReact.RefObject, options?: Highcharts.Options) {
|
|
14
|
+
chartRef?.chart?.fullscreen.toggle();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function onPDFDownload(chartRef: HighchartsReact.RefObject, options?: Highcharts.Options) {
|
|
18
|
+
chartRef?.chart.exportChart({ type: "application/pdf" }, options ?? {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function onCSVDownload(chartRef: HighchartsReact.RefObject, options?: Highcharts.Options) {
|
|
22
|
+
chartRef?.chart.downloadCSV();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const onImageDownload = (chartRef: HighchartsReact.RefObject, type: "png" | "svg+xml" | "jpeg", options?: Highcharts.Options) => {
|
|
26
|
+
chartRef?.chart.exportChart({ type: `image/${type}` }, options ?? {});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const onViewAsTable = (chartRef: HighchartsReact.RefObject, show: boolean) => {
|
|
30
|
+
const options = chartRef?.chart.options;
|
|
31
|
+
chartRef?.chart?.update({
|
|
32
|
+
...options,
|
|
33
|
+
exporting: {
|
|
34
|
+
...options?.exporting,
|
|
35
|
+
showTable: show,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
.highcharts-data-table table {
|
|
3
|
+
border: 1px solid rgb(232, 237, 242);
|
|
4
|
+
text-align: left;
|
|
5
|
+
min-width: 100%;
|
|
6
|
+
border-collapse: collapse;
|
|
7
|
+
vertical-align: top;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.highcharts-data-table td,
|
|
11
|
+
.highcharts-data-table th,
|
|
12
|
+
.highcharts-data-table caption {
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
line-height: 16px;
|
|
15
|
+
padding: 8px 6px;
|
|
16
|
+
height: 32px;
|
|
17
|
+
border-bottom: 1px solid rgb(232, 237, 242);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.highcharts-data-table thead tr {
|
|
21
|
+
background: transparent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.highcharts-data-table tr:nth-child(even) {
|
|
25
|
+
background: rgb(251, 252, 253);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.highcharts-data-table tr {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.highcharts-data-table tr:hover {
|
|
33
|
+
background: rgb(251, 252, 253);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.highcharts-data-table caption {
|
|
37
|
+
border-bottom: none;
|
|
38
|
+
font-size: 1.1em;
|
|
39
|
+
font-weight: bold;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.highcharts-sort-ascending::after {
|
|
43
|
+
content: " ↓";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.highcharts-sort-descending::after {
|
|
47
|
+
content: " ↑";
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {Analytics} from "@hisptz/dhis2-utils";
|
|
2
|
+
import {DashStyleValue, YAxisOptions, YAxisPlotLinesLabelOptions} from "highcharts";
|
|
3
|
+
|
|
4
|
+
export type ChartType = "column" | "pie" | "stacked-column" | "line" | "multi-series";
|
|
5
|
+
|
|
6
|
+
export interface MultiSeriesConfig {
|
|
7
|
+
series?: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
as: "column" | "line";
|
|
10
|
+
cumulative?: boolean;
|
|
11
|
+
yAxis?: number;
|
|
12
|
+
}>;
|
|
13
|
+
yAxes?: Array<YAxisOptions>;
|
|
14
|
+
target?: TargetConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TargetConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
value: number;
|
|
20
|
+
label?: YAxisPlotLinesLabelOptions;
|
|
21
|
+
styles: {
|
|
22
|
+
color?: string;
|
|
23
|
+
width?: number;
|
|
24
|
+
dashStyle?: DashStyleValue;
|
|
25
|
+
zIndex?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ChartConfig = {
|
|
30
|
+
layout: {
|
|
31
|
+
series: Array<string>;
|
|
32
|
+
category: Array<string>;
|
|
33
|
+
filter: Array<string>;
|
|
34
|
+
};
|
|
35
|
+
type?: ChartType;
|
|
36
|
+
height?: number;
|
|
37
|
+
colors?: Array<string>;
|
|
38
|
+
name?: string;
|
|
39
|
+
allowChartTypeChange?: boolean;
|
|
40
|
+
highChartOverrides?: Record<string, any>;
|
|
41
|
+
multiSeries?: MultiSeriesConfig;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ChartAnalyticsProps = {
|
|
45
|
+
analytics: Analytics;
|
|
46
|
+
config: ChartConfig;
|
|
47
|
+
containerProps?: Record<string, any>;
|
|
48
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {Analytics, AnalyticsHeader, AnalyticsMetadata} from "@hisptz/dhis2-utils";
|
|
2
|
+
import {compact, find, findIndex, head, isEmpty, set} from "lodash";
|
|
3
|
+
import {DHIS2Chart} from "../models";
|
|
4
|
+
import {DHIS2ColumnChart, DHIS2StackedColumnChart} from "../models/column";
|
|
5
|
+
import {DHIS2LineChart} from "../models/line";
|
|
6
|
+
import {DHIS2MultiSeriesChart} from "../models/multi-series";
|
|
7
|
+
import {DHIS2PieChart} from "../models/pie";
|
|
8
|
+
import {ChartConfig, ChartType} from "../types/props";
|
|
9
|
+
|
|
10
|
+
export function getDimensionHeaderIndex(headers: AnalyticsHeader[], name: string): number {
|
|
11
|
+
return findIndex(headers, { name });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getPointSeries(analytics: Analytics, config: ChartConfig, highchartsType: string) {
|
|
15
|
+
const series: string[] = config.layout.series;
|
|
16
|
+
|
|
17
|
+
return series.map((seriesName: string) => {
|
|
18
|
+
const header = analytics?.headers?.find((header: any) => header.name === seriesName);
|
|
19
|
+
if (!header) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (analytics?.metaData) {
|
|
23
|
+
return getColumnSeries(analytics, header, config, highchartsType);
|
|
24
|
+
}
|
|
25
|
+
})[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getColumnSeries(analytics: Analytics, header: AnalyticsHeader, config: ChartConfig, highchartsType: string): any {
|
|
29
|
+
const headerIndex = analytics?.headers?.findIndex((h) => header.name === h.name);
|
|
30
|
+
const valueIndex = analytics?.headers?.findIndex((h) => h.name === "value");
|
|
31
|
+
|
|
32
|
+
const colors = config.colors ?? [];
|
|
33
|
+
|
|
34
|
+
const { items, dimensions } = analytics?.metaData ?? {};
|
|
35
|
+
const categoriesDimension = config.layout.category;
|
|
36
|
+
|
|
37
|
+
const seriesDimensionValues: string[] = dimensions?.[header.name as "dx" | "ou" | "pe"] ?? [];
|
|
38
|
+
|
|
39
|
+
return head(
|
|
40
|
+
categoriesDimension?.map((categoryDimension: string) => {
|
|
41
|
+
const categories: string[] = dimensions?.[categoryDimension as "dx" | "ou" | "pe"] as any;
|
|
42
|
+
const categoryDimensionIndex = analytics?.headers?.findIndex((h) => h.name === categoryDimension);
|
|
43
|
+
return seriesDimensionValues?.map((seriesDimensionValue: string, index) => {
|
|
44
|
+
const data = categories?.map((category: string) => {
|
|
45
|
+
const row = find(analytics?.rows, (row: any) => row[headerIndex ?? -1] === seriesDimensionValue && row[categoryDimensionIndex ?? -1] === category);
|
|
46
|
+
return row?.[valueIndex ?? -1] ? parseFloat(row?.[valueIndex ?? -1]) : 0;
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
name: items?.[seriesDimensionValue as any]?.name,
|
|
50
|
+
data,
|
|
51
|
+
type: highchartsType,
|
|
52
|
+
color: colors[index % colors.length],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getCategories({ name }: AnalyticsHeader, { items, dimensions }: AnalyticsMetadata): string[] {
|
|
60
|
+
const categories: string[] = dimensions?.[name as "dx" | "ou" | "pe"] as any;
|
|
61
|
+
|
|
62
|
+
return categories?.map((category: string) => {
|
|
63
|
+
return items[category as any]?.name ?? "";
|
|
64
|
+
}) as unknown as string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getAllCategories(analytics: Analytics, config: ChartConfig): string[] {
|
|
68
|
+
const categories = config.layout.category;
|
|
69
|
+
|
|
70
|
+
return compact(
|
|
71
|
+
categories?.map((category: string) => {
|
|
72
|
+
const header = analytics?.headers?.find((header: any) => header.name === category);
|
|
73
|
+
if (!header) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (analytics?.metaData) {
|
|
77
|
+
return getCategories(header, analytics?.metaData);
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
)[0];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function updateLayout(config: ChartConfig, { type }: { type: ChartType }) {
|
|
84
|
+
if (type === config.type) {
|
|
85
|
+
return config.layout;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const updatedLayout = { ...config.layout };
|
|
89
|
+
|
|
90
|
+
switch (type) {
|
|
91
|
+
case "pie":
|
|
92
|
+
set(updatedLayout, "category", []);
|
|
93
|
+
if (isEmpty(updatedLayout.series)) {
|
|
94
|
+
if (!isEmpty(config.layout.category)) {
|
|
95
|
+
set(updatedLayout, "series", [head(config.layout.category)]);
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error("Invalid layout for pie chart");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (updatedLayout.series.length > 1) {
|
|
101
|
+
set(updatedLayout, "series", [head(updatedLayout.series)]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return updatedLayout;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getChartInstance(id: string, analytics: Analytics, config: ChartConfig): DHIS2Chart {
|
|
109
|
+
switch (config.type) {
|
|
110
|
+
case "column":
|
|
111
|
+
return new DHIS2ColumnChart(id, analytics, config);
|
|
112
|
+
case "stacked-column":
|
|
113
|
+
return new DHIS2StackedColumnChart(id, analytics, config);
|
|
114
|
+
case "pie":
|
|
115
|
+
return new DHIS2PieChart(id, analytics, config);
|
|
116
|
+
case "line":
|
|
117
|
+
return new DHIS2LineChart(id, analytics, config);
|
|
118
|
+
case "multi-series":
|
|
119
|
+
return new DHIS2MultiSeriesChart(id, analytics, config);
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unsupported chart type: ${config.type}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {Story} from "@storybook/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {CircularDashboardProps} from "./types/props";
|
|
4
|
+
import CircularProgressDashboard from ".";
|
|
5
|
+
|
|
6
|
+
const Template: Story<CircularDashboardProps> = (args) => <CircularProgressDashboard {...args} />;
|
|
7
|
+
|
|
8
|
+
export const Default = Template.bind({});
|
|
9
|
+
Default.args = {
|
|
10
|
+
numerator: 7,
|
|
11
|
+
denominator: 10,
|
|
12
|
+
size: 500,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const WithStrokeStyling = Template.bind({});
|
|
16
|
+
WithStrokeStyling.args = {
|
|
17
|
+
numerator: 7,
|
|
18
|
+
denominator: 10,
|
|
19
|
+
size: 500,
|
|
20
|
+
strokeStyle: {
|
|
21
|
+
width: "10%",
|
|
22
|
+
color: "red",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const WithTextStyling = Template.bind({});
|
|
27
|
+
WithTextStyling.args = {
|
|
28
|
+
numerator: 7,
|
|
29
|
+
denominator: 10,
|
|
30
|
+
size: 500,
|
|
31
|
+
textStyle: {
|
|
32
|
+
color: "red",
|
|
33
|
+
fontWeight: "bold",
|
|
34
|
+
fontSize: "20vh",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
title: "Components/Circular Progress Indicator",
|
|
40
|
+
component: CircularProgressDashboard,
|
|
41
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {colors} from "@dhis2/ui";
|
|
2
|
+
import React, {useMemo} from "react";
|
|
3
|
+
import {CircularProgressbarWithChildren} from "react-circular-progressbar";
|
|
4
|
+
import {CircularDashboardProps} from "./types/props";
|
|
5
|
+
|
|
6
|
+
export function CircularProgressDashboard({ numerator, size, denominator, value, textStyle, strokeStyle }: CircularDashboardProps): React.ReactElement {
|
|
7
|
+
const filledSectionFieldsPercentage = useMemo(() => {
|
|
8
|
+
return value !== undefined ? value : Math.floor(((numerator ?? 0) / (denominator ?? 1)) * 100);
|
|
9
|
+
}, [numerator, denominator, value]);
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
style={{
|
|
13
|
+
backgroundColor: "transparent",
|
|
14
|
+
width: size,
|
|
15
|
+
}}>
|
|
16
|
+
<CircularProgressbarWithChildren
|
|
17
|
+
styles={{
|
|
18
|
+
path: {
|
|
19
|
+
stroke: strokeStyle?.color ?? colors.blue700,
|
|
20
|
+
},
|
|
21
|
+
}}
|
|
22
|
+
strokeWidth={7}
|
|
23
|
+
value={filledSectionFieldsPercentage}>
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
...(textStyle ?? {}),
|
|
27
|
+
fontSize: textStyle?.fontSize ?? typeof size === "number" ? 0.3 * (size as number) : "100%",
|
|
28
|
+
marginTop: -11,
|
|
29
|
+
}}>
|
|
30
|
+
<strong style={{ color: textStyle?.color ?? strokeStyle?.color ?? colors.blue700 }}>{filledSectionFieldsPercentage}%</strong>
|
|
31
|
+
</div>
|
|
32
|
+
</CircularProgressbarWithChildren>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CircularDashboardProps = {
|
|
2
|
+
strokeStyle?: {
|
|
3
|
+
width?: number | string;
|
|
4
|
+
color?: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
textStyle?: {
|
|
8
|
+
color?: string;
|
|
9
|
+
fontSize?: number | string;
|
|
10
|
+
fontWeight?: number | string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
value?: number;
|
|
14
|
+
numerator?: number;
|
|
15
|
+
denominator?: number;
|
|
16
|
+
size: string | number;
|
|
17
|
+
};
|