@hisptz/dhis2-analytics 1.0.49 → 1.0.51

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.
Files changed (125) hide show
  1. package/.gitignore +5 -0
  2. package/build/cjs/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
  3. package/build/cjs/components/ChartAnalytics/models/bar.js +24 -0
  4. package/build/cjs/components/ChartAnalytics/utils/chart.js +5 -0
  5. package/build/es/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
  6. package/build/es/components/ChartAnalytics/models/bar.js +16 -0
  7. package/build/es/components/ChartAnalytics/utils/chart.js +5 -0
  8. package/build/types/components/ChartAnalytics/models/bar.d.ts +8 -0
  9. package/build/types/components/ChartAnalytics/types/props.d.ts +1 -1
  10. package/d2.config.js +8 -0
  11. package/i18n/en.pot +439 -0
  12. package/package.json +5 -5
  13. package/src/components/ChartAnalytics/ChartAnalytics.test.tsx +51 -0
  14. package/src/components/ChartAnalytics/components/DownloadMenu/components/Menu.tsx +48 -0
  15. package/src/components/ChartAnalytics/components/DownloadMenu/constants/menu.ts +38 -0
  16. package/src/components/ChartAnalytics/components/DownloadMenu/index.tsx +65 -0
  17. package/src/components/ChartAnalytics/components/DownloadMenu/interfaces/menu.ts +1 -0
  18. package/src/components/ChartAnalytics/hooks/useChart.ts +35 -0
  19. package/src/components/ChartAnalytics/index.tsx +28 -0
  20. package/src/components/ChartAnalytics/models/bar.ts +20 -0
  21. package/src/components/ChartAnalytics/models/column.ts +52 -0
  22. package/src/components/ChartAnalytics/models/index.ts +111 -0
  23. package/src/components/ChartAnalytics/models/line.ts +31 -0
  24. package/src/components/ChartAnalytics/models/multi-series.ts +115 -0
  25. package/src/components/ChartAnalytics/models/pie.ts +54 -0
  26. package/src/components/ChartAnalytics/services/export.ts +38 -0
  27. package/src/components/ChartAnalytics/styles/custom-highchart.css +48 -0
  28. package/src/components/ChartAnalytics/types/props.tsx +48 -0
  29. package/src/components/ChartAnalytics/utils/chart.ts +128 -0
  30. package/src/components/CircularProgressDashboard/CircularProgressIndicator.test.tsx +9 -0
  31. package/src/components/CircularProgressDashboard/index.tsx +36 -0
  32. package/src/components/CircularProgressDashboard/types/props.tsx +17 -0
  33. package/src/components/CustomPivotTable/components/Table/index.tsx +23 -0
  34. package/src/components/CustomPivotTable/components/TableBody/TableBody.module.css +12 -0
  35. package/src/components/CustomPivotTable/components/TableBody/index.tsx +96 -0
  36. package/src/components/CustomPivotTable/components/TableHeaders/TableHeaders.module.css +10 -0
  37. package/src/components/CustomPivotTable/components/TableHeaders/index.tsx +94 -0
  38. package/src/components/CustomPivotTable/index.tsx +63 -0
  39. package/src/components/CustomPivotTable/interfaces/index.ts +1 -0
  40. package/src/components/CustomPivotTable/services/engine.ts +102 -0
  41. package/src/components/CustomPivotTable/state/engine.tsx +22 -0
  42. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfigModal.stories.tsx +28 -0
  43. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfiguration.stories.tsx +34 -0
  44. package/src/components/Map/components/EarthEngineLayerConfiguration/index.tsx +412 -0
  45. package/src/components/Map/components/MapArea/index.tsx +83 -0
  46. package/src/components/Map/components/MapArea/interfaces/index.ts +39 -0
  47. package/src/components/Map/components/MapControls/components/CustomControl/index.tsx +24 -0
  48. package/src/components/Map/components/MapControls/components/DownloadControl/index.tsx +11 -0
  49. package/src/components/Map/components/MapControls/components/FullscreenControl/index.tsx +7 -0
  50. package/src/components/Map/components/MapControls/index.tsx +24 -0
  51. package/src/components/Map/components/MapLayer/components/BoundaryLayer/hooks/useBoundaryData.ts +7 -0
  52. package/src/components/Map/components/MapLayer/components/BoundaryLayer/index.tsx +55 -0
  53. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/components/EarthEngineLegend.tsx +74 -0
  54. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/constants/index.ts +430 -0
  55. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/hooks/index.ts +34 -0
  56. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/index.tsx +185 -0
  57. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/interfaces/index.ts +56 -0
  58. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/api.js +34241 -0
  59. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/engine.ts +431 -0
  60. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/utils/index.ts +105 -0
  61. package/src/components/Map/components/MapLayer/components/LegendArea/LegendArea.module.css +12 -0
  62. package/src/components/Map/components/MapLayer/components/LegendArea/components/LegendCardHeader/index.tsx +17 -0
  63. package/src/components/Map/components/MapLayer/components/LegendArea/index.tsx +167 -0
  64. package/src/components/Map/components/MapLayer/components/PointLayer/components/PointLegend/index.tsx +44 -0
  65. package/src/components/Map/components/MapLayer/components/PointLayer/hooks/index.ts +8 -0
  66. package/src/components/Map/components/MapLayer/components/PointLayer/index.tsx +36 -0
  67. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubble.tsx +48 -0
  68. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubbles.tsx +150 -0
  69. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/index.tsx +39 -0
  70. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/index.tsx +57 -0
  71. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/components/ChoroplethLegend.tsx +43 -0
  72. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/index.tsx +38 -0
  73. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/CustomTooltip/index.tsx +26 -0
  74. package/src/components/Map/components/MapLayer/components/ThematicLayer/hooks/config.ts +10 -0
  75. package/src/components/Map/components/MapLayer/components/ThematicLayer/index.tsx +46 -0
  76. package/src/components/Map/components/MapLayer/components/ThematicLayer/styles/legends.css +62 -0
  77. package/src/components/Map/components/MapLayer/index.tsx +32 -0
  78. package/src/components/Map/components/MapLayer/interfaces/index.ts +139 -0
  79. package/src/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.tsx +368 -0
  80. package/src/components/Map/components/MapProvider/components/MapLayerProvider/index.tsx +105 -0
  81. package/src/components/Map/components/MapProvider/hooks/index.ts +14 -0
  82. package/src/components/Map/components/MapProvider/index.tsx +93 -0
  83. package/src/components/Map/components/MapUpdater/index.tsx +8 -0
  84. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfigModal.stories.tsx +28 -0
  85. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfiguration.stories.tsx +34 -0
  86. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/components/ColorScale/index.tsx +24 -0
  87. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/constants/colors.ts +433 -0
  88. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/index.tsx +50 -0
  89. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScale.module.css +15 -0
  90. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScaleSelect.module.css +12 -0
  91. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/utils/colors.ts +91 -0
  92. package/src/components/Map/components/ThematicLayerConfiguration/components/CustomLegend/index.tsx +45 -0
  93. package/src/components/Map/components/ThematicLayerConfiguration/components/IndicatorSelectorModal/index.tsx +47 -0
  94. package/src/components/Map/components/ThematicLayerConfiguration/components/LegendSetSelector/index.tsx +57 -0
  95. package/src/components/Map/components/ThematicLayerConfiguration/index.tsx +248 -0
  96. package/src/components/Map/constants/colors.ts +434 -0
  97. package/src/components/Map/constants/legendSet.ts +19 -0
  98. package/src/components/Map/hooks/map.ts +47 -0
  99. package/src/components/Map/index.tsx +65 -0
  100. package/src/components/Map/interfaces/index.ts +57 -0
  101. package/src/components/Map/state/index.tsx +31 -0
  102. package/src/components/Map/utils/colors.ts +95 -0
  103. package/src/components/Map/utils/helpers.ts +15 -0
  104. package/src/components/Map/utils/map.ts +150 -0
  105. package/src/components/SingleValueContainer/SingleValueContainer.test.tsx +24 -0
  106. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValueItem.tsx +46 -0
  107. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValuePercentage.tsx +12 -0
  108. package/src/components/SingleValueContainer/index.tsx +37 -0
  109. package/src/components/SingleValueContainer/styles/SingleValueContainer.module.css +39 -0
  110. package/src/components/SingleValueContainer/types/props.tsx +16 -0
  111. package/src/components/Visualization/components/AnalyticsDataProvider/index.tsx +76 -0
  112. package/src/components/Visualization/components/DimensionsProvider/index.tsx +51 -0
  113. package/src/components/Visualization/components/LayoutProvider/index.tsx +34 -0
  114. package/src/components/Visualization/components/VisualizationDimensionSelector/index.tsx +59 -0
  115. package/src/components/Visualization/components/VisualizationProvider/index.tsx +31 -0
  116. package/src/components/Visualization/components/VisualizationSelector/index.tsx +157 -0
  117. package/src/components/Visualization/components/VisualizationTypeProvider/index.tsx +40 -0
  118. package/src/components/Visualization/components/VisualizationTypeSelector/index.tsx +46 -0
  119. package/src/components/Visualization/index.tsx +103 -0
  120. package/src/index.ts +6 -0
  121. package/src/locales/en/translations.json +138 -0
  122. package/src/locales/index.js +16 -0
  123. package/tsconfig.build.json +46 -0
  124. package/tsconfig.json +51 -0
  125. package/LICENSE +0 -29
@@ -0,0 +1,48 @@
1
+ import type {Analytics} from "@hisptz/dhis2-utils";
2
+ import HighCharts, {DashStyleValue, YAxisOptions, YAxisPlotLinesLabelOptions} from "highcharts";
3
+
4
+ export type ChartType = "column" | "pie" | "stacked-column" | "line" | "multi-series" | "bar" | "stacked-bar";
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?: Partial<HighCharts.Options> | ((config: HighCharts.Options) => Partial<HighCharts.Options>);
41
+ multiSeries?: MultiSeriesConfig;
42
+ };
43
+
44
+ export type ChartAnalyticsProps = {
45
+ analytics: Analytics;
46
+ config: ChartConfig;
47
+ containerProps?: Record<string, any>;
48
+ };
@@ -0,0 +1,128 @@
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
+ import {DHIS2BarChart, DHIS2StackedBarChart} from "../models/bar";
10
+
11
+ export function getDimensionHeaderIndex(headers: AnalyticsHeader[], name: string): number {
12
+ return findIndex(headers, { name });
13
+ }
14
+
15
+ export function getPointSeries(analytics: Analytics, config: ChartConfig, highchartsType: string) {
16
+ const series: string[] = config.layout.series;
17
+
18
+ return series.map((seriesName: string) => {
19
+ const header = analytics?.headers?.find((header: any) => header.name === seriesName);
20
+ if (!header) {
21
+ return undefined;
22
+ }
23
+ if (analytics?.metaData) {
24
+ return getColumnSeries(analytics, header, config, highchartsType);
25
+ }
26
+ })[0];
27
+ }
28
+
29
+ export function getColumnSeries(analytics: Analytics, header: AnalyticsHeader, config: ChartConfig, highchartsType: string): any {
30
+ const headerIndex = analytics?.headers?.findIndex((h) => header.name === h.name);
31
+ const valueIndex = analytics?.headers?.findIndex((h) => h.name === "value");
32
+
33
+ const colors = config.colors ?? [];
34
+
35
+ const { items, dimensions } = analytics?.metaData ?? {};
36
+ const categoriesDimension = config.layout.category;
37
+
38
+ const seriesDimensionValues: string[] = dimensions?.[header.name as "dx" | "ou" | "pe"] ?? [];
39
+
40
+ return head(
41
+ categoriesDimension?.map((categoryDimension: string) => {
42
+ const categories: string[] = dimensions?.[categoryDimension as "dx" | "ou" | "pe"] as any;
43
+ const categoryDimensionIndex = analytics?.headers?.findIndex((h) => h.name === categoryDimension);
44
+ return seriesDimensionValues?.map((seriesDimensionValue: string, index) => {
45
+ const data = categories?.map((category: string) => {
46
+ const row = find(analytics?.rows, (row: any) => row[headerIndex ?? -1] === seriesDimensionValue && row[categoryDimensionIndex ?? -1] === category);
47
+ return row?.[valueIndex ?? -1] ? parseFloat(row?.[valueIndex ?? -1]) : 0;
48
+ });
49
+ return {
50
+ name: items?.[seriesDimensionValue as any]?.name,
51
+ data,
52
+ type: highchartsType,
53
+ color: colors[index % colors.length],
54
+ };
55
+ });
56
+ })
57
+ );
58
+ }
59
+
60
+ function getCategories({ name }: AnalyticsHeader, { items, dimensions }: AnalyticsMetadata): string[] {
61
+ const categories: string[] = dimensions?.[name as "dx" | "ou" | "pe"] as any;
62
+
63
+ return categories?.map((category: string) => {
64
+ return items[category as any]?.name ?? "";
65
+ }) as unknown as string[];
66
+ }
67
+
68
+ export function getAllCategories(analytics: Analytics, config: ChartConfig): string[] {
69
+ const categories = config.layout.category;
70
+
71
+ return compact(
72
+ categories?.map((category: string) => {
73
+ const header = analytics?.headers?.find((header: any) => header.name === category);
74
+ if (!header) {
75
+ return undefined;
76
+ }
77
+ if (analytics?.metaData) {
78
+ return getCategories(header, analytics?.metaData);
79
+ }
80
+ })
81
+ )[0];
82
+ }
83
+
84
+ export function updateLayout(config: ChartConfig, { type }: { type: ChartType }) {
85
+ if (type === config.type) {
86
+ return config.layout;
87
+ }
88
+
89
+ const updatedLayout = { ...config.layout };
90
+
91
+ switch (type) {
92
+ case "pie":
93
+ set(updatedLayout, "category", []);
94
+ if (isEmpty(updatedLayout.series)) {
95
+ if (!isEmpty(config.layout.category)) {
96
+ set(updatedLayout, "series", [head(config.layout.category)]);
97
+ } else {
98
+ throw new Error("Invalid layout for pie chart");
99
+ }
100
+ }
101
+ if (updatedLayout.series.length > 1) {
102
+ set(updatedLayout, "series", [head(updatedLayout.series)]);
103
+ }
104
+ }
105
+
106
+ return updatedLayout;
107
+ }
108
+
109
+ export function getChartInstance(id: string, analytics: Analytics, config: ChartConfig): DHIS2Chart {
110
+ switch (config.type) {
111
+ case "column":
112
+ return new DHIS2ColumnChart(id, analytics, config);
113
+ case "bar":
114
+ return new DHIS2BarChart(id, analytics, config);
115
+ case "stacked-bar":
116
+ return new DHIS2StackedBarChart(id, analytics, config);
117
+ case "stacked-column":
118
+ return new DHIS2StackedColumnChart(id, analytics, config);
119
+ case "pie":
120
+ return new DHIS2PieChart(id, analytics, config);
121
+ case "line":
122
+ return new DHIS2LineChart(id, analytics, config);
123
+ case "multi-series":
124
+ return new DHIS2MultiSeriesChart(id, analytics, config);
125
+ default:
126
+ throw new Error(`Unsupported chart type: ${config.type}`);
127
+ }
128
+ }
@@ -0,0 +1,9 @@
1
+ import {mount} from "@cypress/react";
2
+ import React from "react";
3
+ import {CircularProgressDashboard} from ".";
4
+
5
+ describe("CircularProgressDashboard", () => {
6
+ it("should render", () => {
7
+ mount(<CircularProgressDashboard size="100px" />);
8
+ });
9
+ });
@@ -0,0 +1,36 @@
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 * from "./types/props";
7
+ export function CircularProgressDashboard({ numerator, size, denominator, value, textStyle, strokeStyle }: CircularDashboardProps): React.ReactElement {
8
+ const filledSectionFieldsPercentage = useMemo(() => {
9
+ return value !== undefined ? value : Math.floor(((numerator ?? 0) / (denominator ?? 1)) * 100);
10
+ }, [numerator, denominator, value]);
11
+ return (
12
+ <div
13
+ style={{
14
+ backgroundColor: "transparent",
15
+ width: size,
16
+ }}>
17
+ <CircularProgressbarWithChildren
18
+ styles={{
19
+ path: {
20
+ stroke: strokeStyle?.color ?? colors.blue700,
21
+ },
22
+ }}
23
+ strokeWidth={7}
24
+ value={filledSectionFieldsPercentage}>
25
+ <div
26
+ style={{
27
+ ...(textStyle ?? {}),
28
+ fontSize: textStyle?.fontSize ?? typeof size === "number" ? 0.3 * (size as number) : "100%",
29
+ marginTop: -11,
30
+ }}>
31
+ <strong style={{ color: textStyle?.color ?? strokeStyle?.color ?? colors.blue700 }}>{filledSectionFieldsPercentage}%</strong>
32
+ </div>
33
+ </CircularProgressbarWithChildren>
34
+ </div>
35
+ );
36
+ }
@@ -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
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import {DataTable} from '@dhis2/ui'
3
+
4
+
5
+ export interface PivotTableLayoutProps {
6
+ scrollHeight?: string;
7
+ scrollWidth?: string;
8
+ layout?: string;
9
+ width?: string
10
+ }
11
+
12
+ export interface PivotTableProps {
13
+ children: React.ReactNode;
14
+ tableProps?: PivotTableLayoutProps;
15
+ }
16
+
17
+ export function PivotTable({tableProps, children}: PivotTableProps) {
18
+ return (
19
+ <DataTable {...(tableProps ?? {})} >
20
+ {children}
21
+ </DataTable>
22
+ )
23
+ }
@@ -0,0 +1,12 @@
1
+ .nested-table-cell {
2
+ padding: 0 !important;
3
+ }
4
+
5
+ .nested-table {
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ .header-cell {
11
+ border-right: 1px solid rgb(232, 237, 242) !important;
12
+ }
@@ -0,0 +1,96 @@
1
+ import {Header} from "../../services/engine";
2
+ import {isEmpty, slice} from "lodash";
3
+ import {useCustomPivotTableEngine} from "../../state/engine";
4
+ import {DataTableCell, DataTableRow, TableBody} from '@dhis2/ui'
5
+ import React, {Fragment} from "react";
6
+ import {AnalyticsItem} from "@hisptz/dhis2-utils";
7
+ import classes from "./TableBody.module.css"
8
+ import {useElementSize} from "usehooks-ts";
9
+ import {DHIS2Dimension} from "../../interfaces";
10
+
11
+ function DataRowRenderer({
12
+ mapper,
13
+ item,
14
+ dimension,
15
+ }: { mapper?: { [key: string]: any }; item: AnalyticsItem, dimension: DHIS2Dimension }) {
16
+ const engine = useCustomPivotTableEngine();
17
+
18
+ const columnMappers = engine?.columnMap;
19
+ const completeMapper = columnMappers?.map((map) => ({...map, ...mapper, [dimension]: item.uid}));
20
+ return <>
21
+ {
22
+ completeMapper?.map((mapper) => (
23
+ <DataTableCell key={`${Object.values(mapper).join('-')}-value`} align="center" bordered>
24
+ {engine?.getValue(mapper) ?? ''}
25
+ </DataTableCell>
26
+ ))
27
+ }
28
+ </>;
29
+ }
30
+
31
+ function RowRenderer({
32
+ row,
33
+ index,
34
+ config: {rows, columns, mapper, prevWidth, fixRowHeaders}
35
+ }: { row: Header, index: number, config: { rows: Header[], columns?: Header[]; mapper?: { [key: string]: string | undefined }, prevWidth?: number; fixRowHeaders?: boolean } }): React.ReactElement | null {
36
+ const [cellRef, {width}] = useElementSize();
37
+
38
+ const rowSpan = slice(rows, index + 1).reduce((acc, column) => {
39
+ return acc * (column.items?.length ?? 1);
40
+ }, 1)
41
+ const hasSubRows = !isEmpty(rows[index + 1]);
42
+ const nextRow = rows[index + 1];
43
+
44
+ return (
45
+ <>
46
+
47
+ {
48
+ row?.items?.map((item) => {
49
+ return (
50
+ <Fragment key={`${item.name}-${row.dimension}-fragment`}>
51
+ <DataTableRow key={`${item.name}-${row.dimension}-row`}>
52
+ <DataTableCell ref={cellRef} fixed={fixRowHeaders} left={`${prevWidth}px`}
53
+ className={classes['header-cell']} tag="th"
54
+ bordered
55
+ rowSpan={(rowSpan + (hasSubRows ? 1 : 0)).toString()}>
56
+ {item.name}
57
+ </DataTableCell>
58
+ {
59
+ !hasSubRows ?
60
+ <DataRowRenderer dimension={row.dimension} mapper={mapper} item={item}/> : null
61
+ }
62
+ </DataTableRow>
63
+ {
64
+ hasSubRows ? (<RowRenderer row={nextRow} index={index + 1} config={{
65
+ rows,
66
+ columns,
67
+ prevWidth: width,
68
+ mapper: {...mapper, [row.dimension]: item.uid as unknown as string}
69
+ }}/>) : null
70
+ }
71
+ </Fragment>
72
+ )
73
+ })
74
+ }
75
+ </>
76
+ )
77
+
78
+ }
79
+
80
+
81
+ export function CustomPivotTableBody() {
82
+ const engine = useCustomPivotTableEngine();
83
+ const rows = engine?.rowHeaders;
84
+ const columns = engine?.columnHeaders;
85
+ const fixRowHeaders = engine?.fixRowHeaders;
86
+
87
+ if (!rows || isEmpty(rows)) {
88
+ return null;
89
+ }
90
+
91
+ return (
92
+ <TableBody>
93
+ <RowRenderer row={rows[0]} index={0} config={{rows, columns, fixRowHeaders}}/>
94
+ </TableBody>
95
+ )
96
+ }
@@ -0,0 +1,10 @@
1
+ .table-header{
2
+ border-right: 1px solid rgb(232, 237, 242);
3
+ }
4
+ .table-header:last-child{
5
+ border-right: none;
6
+ }
7
+
8
+ .table-header > span > span{
9
+ justify-content: center;
10
+ }
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import {useCustomPivotTableEngine} from "../../state/engine";
3
+ import {DataTableColumnHeader, DataTableRow, TableHead} from '@dhis2/ui'
4
+ import {isEmpty, slice, times} from "lodash";
5
+ import {Header} from "../../services/engine";
6
+ import classes from "./TableHeaders.module.css"
7
+ import {useElementSize} from "usehooks-ts";
8
+
9
+ function ColumnRenderer({
10
+ column,
11
+ index,
12
+ config: {fixColumnHeaders, rowHeaders, prevHeight = 0, columns}
13
+ }: { column: Header, index: number, config: { columns: Header[], rowHeaders?: Header[], prevHeight?: number; fixColumnHeaders?: boolean } }): React.ReactElement | null {
14
+
15
+ const [columnHeaderRef, {height}] = useElementSize();
16
+
17
+ if (!column) {
18
+ return null;
19
+ }
20
+ const colSpan = slice(columns, index + 1).reduce((acc, column) => {
21
+ return acc * (column.items?.length ?? 1);
22
+ }, 1);
23
+
24
+ const hasSubColumns = !isEmpty(columns[index + 1]);
25
+ const nextColumn = columns[index + 1];
26
+
27
+ const multiplicationFactor = slice(columns, 0, index).reduce((acc, column) => {
28
+ return acc * (column.items?.length ?? 1);
29
+ }, 1);
30
+
31
+ return (
32
+ <>
33
+ <DataTableRow>
34
+ {
35
+ ((index === 0) && rowHeaders?.map((header) => {
36
+ return (
37
+ <DataTableColumnHeader
38
+ fixed={fixColumnHeaders}
39
+ top={"0"}
40
+ className={classes['table-header']}
41
+ rowSpan={columns.length.toString()}
42
+ key={`${header.dimension}-header-column`}>
43
+ {header.label ?? ""}
44
+ </DataTableColumnHeader>
45
+ )
46
+ }))
47
+ }
48
+ {
49
+ (times(multiplicationFactor, (colNo) => {
50
+ return (column.items?.map((item) => (
51
+ <DataTableColumnHeader
52
+ fixed={fixColumnHeaders}
53
+ top={`${prevHeight.toString()}px`}
54
+ ref={index === 0 ? columnHeaderRef : undefined}
55
+ className={classes['table-header']} align="center"
56
+ colSpan={colSpan.toString()}
57
+ key={`${colNo}-${item.name}-column-header`}>
58
+ {item.name}
59
+ </DataTableColumnHeader>
60
+ )))
61
+ }))
62
+ }
63
+ </DataTableRow>
64
+ {
65
+ hasSubColumns ? (<ColumnRenderer column={nextColumn} index={index + 1} config={{
66
+ columns,
67
+ rowHeaders,
68
+ prevHeight: height,
69
+ fixColumnHeaders
70
+ }}/>) : null
71
+ }
72
+ </>
73
+ )
74
+
75
+ }
76
+
77
+
78
+ export function TableHeaders() {
79
+ const engine = useCustomPivotTableEngine();
80
+ const columns = engine?.columnHeaders;
81
+ const rowHeaders = engine?.rowHeaders;
82
+ const fixColumnHeaders = engine?.fixColumnHeaders;
83
+
84
+ if (!columns || isEmpty(columns)) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ <TableHead>
90
+ <ColumnRenderer column={columns[0]} index={0} config={{rowHeaders, columns, fixColumnHeaders}}/>
91
+ </TableHead>
92
+ )
93
+
94
+ }
@@ -0,0 +1,63 @@
1
+ import {Analytics, LegendSet} from "@hisptz/dhis2-utils";
2
+ import {CustomPivotTableEngine} from "./services/engine";
3
+ import React, {useMemo} from "react";
4
+ import {CustomPivotTableEngineProvider} from "./state/engine";
5
+ import {PivotTable} from "./components/Table";
6
+ import {TableHeaders} from "./components/TableHeaders";
7
+ import {CustomPivotTableBody} from "./components/TableBody";
8
+ import {DHIS2Dimension} from "./interfaces";
9
+
10
+
11
+ export interface CustomPivotTableOptions {
12
+ legendSets?: LegendSet[],
13
+ hideEmptyColumns?: boolean,
14
+ hideEmptyRows?: boolean,
15
+ showRowTotals?: boolean,
16
+ showColumnTotals?: boolean,
17
+ showRowSubtotals?: boolean,
18
+ showColumnSubtotals?: boolean,
19
+ fixColumnHeaders?: boolean,
20
+ fixRowHeaders?: boolean,
21
+ }
22
+
23
+ export interface CustomPivotTableProps {
24
+ analytics: Analytics;
25
+ tableProps?: {
26
+ scrollHeight?: string;
27
+ scrollWidth?: string;
28
+ layout?: string;
29
+ width?: string
30
+ };
31
+ config: {
32
+ layout: {
33
+ columns: {
34
+ dimension: DHIS2Dimension,
35
+ label?: string;
36
+ }[];
37
+ rows: {
38
+ dimension: DHIS2Dimension,
39
+ label?: string;
40
+ }[];
41
+ filter?: {
42
+ dimension: DHIS2Dimension,
43
+ label?: string;
44
+ }[]
45
+ },
46
+ options?: CustomPivotTableOptions
47
+ }
48
+ }
49
+
50
+
51
+ export function CustomPivotTable({analytics, config, tableProps}: CustomPivotTableProps) {
52
+ const engine = useMemo(() => new CustomPivotTableEngine({analytics, config}), [analytics, config]);
53
+
54
+ return (
55
+ <CustomPivotTableEngineProvider engine={engine}>
56
+ <PivotTable tableProps={tableProps}>
57
+ <TableHeaders/>
58
+ <CustomPivotTableBody/>
59
+ </PivotTable>
60
+ </CustomPivotTableEngineProvider>
61
+ )
62
+
63
+ }
@@ -0,0 +1 @@
1
+ export type DHIS2Dimension = "ou" | "pe" | "dx" | "co" | string;
@@ -0,0 +1,102 @@
1
+ import {Analytics, AnalyticsItem, LegendSet} from "@hisptz/dhis2-utils";
2
+ import {compact, findIndex, intersection, times, zip} from "lodash";
3
+ import {DHIS2Dimension} from "../interfaces";
4
+
5
+
6
+ export interface EngineConfig {
7
+ layout: {
8
+ columns: { dimension: DHIS2Dimension, label?: string }[];
9
+ rows: { dimension: DHIS2Dimension, label?: string }[]
10
+ filter?: { dimension: DHIS2Dimension, label?: string }[]
11
+ }
12
+ options?: {
13
+ legendSets?: LegendSet[],
14
+ hideEmptyColumns?: boolean,
15
+ hideEmptyRows?: boolean,
16
+ showRowTotals?: boolean,
17
+ showColumnTotals?: boolean,
18
+ showRowSubtotals?: boolean,
19
+ showColumnSubtotals?: boolean,
20
+ fixColumnHeaders?: boolean,
21
+ fixRowHeaders?: boolean,
22
+ [key: string]: any
23
+ }
24
+ }
25
+
26
+ export interface Header {
27
+ dimension: DHIS2Dimension;
28
+ label?: string;
29
+ items?: AnalyticsItem[]
30
+ }
31
+
32
+ export class CustomPivotTableEngine {
33
+ analyticsData: Analytics;
34
+ valueIndex: number;
35
+ config: EngineConfig;
36
+ rowHeaders?: Header[];
37
+ columnHeaders?: Header[];
38
+
39
+ columnMap?: { [key: string]: any }[]
40
+
41
+ constructor({analytics, config}: { analytics: Analytics, config: EngineConfig }) {
42
+ this.config = config;
43
+ this.analyticsData = analytics;
44
+ this.valueIndex = findIndex(this.analyticsData.headers, ['name', 'value'])
45
+ this.getHeaders();
46
+ this.getColumnMap();
47
+ }
48
+
49
+ get fixColumnHeaders() {
50
+ return this.config.options?.fixColumnHeaders ?? true;
51
+ }
52
+
53
+ get fixRowHeaders() {
54
+ return this.config.options?.fixRowHeaders ?? true;
55
+ }
56
+
57
+ getDimensionItems(dimension: DHIS2Dimension) {
58
+ return this.analyticsData.metaData?.dimensions[dimension] ?? [];
59
+ }
60
+
61
+ getItem(id: string) {
62
+ return this.analyticsData.metaData?.items[id as any]
63
+ }
64
+
65
+
66
+ getValue(mapper: { [key: string]: any }) {
67
+ const dimensions = Object.values(mapper);
68
+ const data = this.analyticsData.rows?.filter((row) => intersection(row, dimensions).length >= dimensions.length);
69
+ return data?.reduce((acc, row) => {
70
+ return acc + parseFloat(row[this.valueIndex])
71
+ }, 0)
72
+ }
73
+
74
+ getHeaders() {
75
+ this.rowHeaders = this.config.layout.rows.map((rowConfig) => {
76
+ return {
77
+ ...rowConfig,
78
+ items: compact(this.getDimensionItems(rowConfig.dimension).map((itemId: string) => this.getItem(itemId)))
79
+ }
80
+ });
81
+ this.columnHeaders = this.config.layout.columns.map((columnConfig) => {
82
+ return {
83
+ ...columnConfig,
84
+ items: compact(this.getDimensionItems(columnConfig.dimension).map((itemId: string) => this.getItem(itemId)))
85
+ }
86
+ });
87
+ }
88
+
89
+
90
+ getColumnMap() {
91
+ const columns = this.columnHeaders;
92
+ const sanitizedColumns = compact(columns?.map((column) => (column.items?.map(item => ({[column.dimension]: item.uid}))))) ?? [];
93
+ const size = sanitizedColumns.reduce((acc, items) => {
94
+ return acc * (items?.length ?? 1);
95
+ }, 1);
96
+ const standardiseColumns = sanitizedColumns.map(items => ([...(times(size / items.length, () => items).flat())]));
97
+ this.columnMap = compact(zip(...standardiseColumns).map(arr => arr.reduce((obj, value) => {
98
+ return {...obj, ...value}
99
+ })));
100
+ }
101
+
102
+ }
@@ -0,0 +1,22 @@
1
+ import React, {createContext, ReactNode, useContext} from "react";
2
+ import {CustomPivotTableEngine} from "../services/engine";
3
+
4
+
5
+ const CustomPivotTableEngineContext = createContext<CustomPivotTableEngine | null>(null);
6
+
7
+
8
+ export function useCustomPivotTableEngine() {
9
+ return useContext(CustomPivotTableEngineContext)
10
+ }
11
+
12
+ export function CustomPivotTableEngineProvider({
13
+ children,
14
+ engine
15
+ }: { children: ReactNode, engine: CustomPivotTableEngine }) {
16
+
17
+ return (
18
+ <CustomPivotTableEngineContext.Provider value={engine}>
19
+ {children}
20
+ </CustomPivotTableEngineContext.Provider>
21
+ )
22
+ }