@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.
- package/.gitignore +5 -0
- package/build/cjs/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
- package/build/cjs/components/ChartAnalytics/models/bar.js +24 -0
- package/build/cjs/components/ChartAnalytics/utils/chart.js +5 -0
- package/build/es/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
- package/build/es/components/ChartAnalytics/models/bar.js +16 -0
- package/build/es/components/ChartAnalytics/utils/chart.js +5 -0
- package/build/types/components/ChartAnalytics/models/bar.d.ts +8 -0
- package/build/types/components/ChartAnalytics/types/props.d.ts +1 -1
- package/d2.config.js +8 -0
- package/i18n/en.pot +439 -0
- package/package.json +5 -5
- 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 +65 -0
- package/src/components/ChartAnalytics/components/DownloadMenu/interfaces/menu.ts +1 -0
- package/src/components/ChartAnalytics/hooks/useChart.ts +35 -0
- package/src/components/ChartAnalytics/index.tsx +28 -0
- package/src/components/ChartAnalytics/models/bar.ts +20 -0
- package/src/components/ChartAnalytics/models/column.ts +52 -0
- package/src/components/ChartAnalytics/models/index.ts +111 -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 +128 -0
- package/src/components/CircularProgressDashboard/CircularProgressIndicator.test.tsx +9 -0
- package/src/components/CircularProgressDashboard/index.tsx +36 -0
- package/src/components/CircularProgressDashboard/types/props.tsx +17 -0
- package/src/components/CustomPivotTable/components/Table/index.tsx +23 -0
- package/src/components/CustomPivotTable/components/TableBody/TableBody.module.css +12 -0
- package/src/components/CustomPivotTable/components/TableBody/index.tsx +96 -0
- package/src/components/CustomPivotTable/components/TableHeaders/TableHeaders.module.css +10 -0
- package/src/components/CustomPivotTable/components/TableHeaders/index.tsx +94 -0
- package/src/components/CustomPivotTable/index.tsx +63 -0
- package/src/components/CustomPivotTable/interfaces/index.ts +1 -0
- package/src/components/CustomPivotTable/services/engine.ts +102 -0
- package/src/components/CustomPivotTable/state/engine.tsx +22 -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 +11 -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 +74 -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 +34241 -0
- package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/engine.ts +431 -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 +167 -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 +368 -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 +434 -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.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 +37 -0
- package/src/components/SingleValueContainer/styles/SingleValueContainer.module.css +39 -0
- package/src/components/SingleValueContainer/types/props.tsx +16 -0
- package/src/components/Visualization/components/AnalyticsDataProvider/index.tsx +76 -0
- package/src/components/Visualization/components/DimensionsProvider/index.tsx +51 -0
- package/src/components/Visualization/components/LayoutProvider/index.tsx +34 -0
- package/src/components/Visualization/components/VisualizationDimensionSelector/index.tsx +59 -0
- package/src/components/Visualization/components/VisualizationProvider/index.tsx +31 -0
- package/src/components/Visualization/components/VisualizationSelector/index.tsx +157 -0
- package/src/components/Visualization/components/VisualizationTypeProvider/index.tsx +40 -0
- package/src/components/Visualization/components/VisualizationTypeSelector/index.tsx +46 -0
- package/src/components/Visualization/index.tsx +103 -0
- package/src/index.ts +6 -0
- package/src/locales/en/translations.json +138 -0
- package/src/locales/index.js +16 -0
- package/tsconfig.build.json +46 -0
- package/tsconfig.json +51 -0
- package/LICENSE +0 -29
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {OrgUnitSelection} from "@hisptz/dhis2-utils";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type {MapContainerProps} from "react-leaflet";
|
|
4
|
+
import {MapControls, MapLegendConfig} from "../components/MapArea/interfaces";
|
|
5
|
+
import {EarthEngineLayerConfig, ThematicLayerConfig} from "../components/MapLayer/interfaces";
|
|
6
|
+
|
|
7
|
+
export interface MapProviderProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
orgUnitSelection: OrgUnitSelection;
|
|
10
|
+
periodSelection?: { periods?: string[]; range?: { start: Date; end: Date } };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MapProps {
|
|
14
|
+
key?: string;
|
|
15
|
+
orgUnitSelection: OrgUnitSelection; //Organisation unit selection
|
|
16
|
+
pointLayer?: {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
label?: string;
|
|
19
|
+
level?: number | string;
|
|
20
|
+
group?: string;
|
|
21
|
+
style?: {
|
|
22
|
+
icon?: string;
|
|
23
|
+
groupSet?: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
boundaryLayer?: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
};
|
|
29
|
+
controls?: MapControls[];
|
|
30
|
+
legends?: MapLegendConfig;
|
|
31
|
+
thematicLayers?: ThematicLayerConfig[];
|
|
32
|
+
earthEngineLayers?: EarthEngineLayerConfig[];
|
|
33
|
+
periodSelection?: { periods?: string[]; range?: { start: Date; end: Date } };
|
|
34
|
+
mapOptions?: MapContainerProps;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MapOrgUnit {
|
|
38
|
+
id: string;
|
|
39
|
+
path: string;
|
|
40
|
+
name: string;
|
|
41
|
+
bounds: any[];
|
|
42
|
+
geoJSON: any;
|
|
43
|
+
children?: MapOrgUnit[];
|
|
44
|
+
level?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PointOrgUnit {
|
|
48
|
+
id: string;
|
|
49
|
+
path: string;
|
|
50
|
+
name: string;
|
|
51
|
+
bounds: any[];
|
|
52
|
+
geoJSON: any;
|
|
53
|
+
icon: {
|
|
54
|
+
type: "custom" | "groupIcon";
|
|
55
|
+
icon: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {OrgUnitSelection} from "@hisptz/dhis2-utils";
|
|
2
|
+
import {BasePeriod} from "@hisptz/dhis2-utils";
|
|
3
|
+
import {createContext} from "react";
|
|
4
|
+
import {CustomMapLayer} from "../components/MapLayer/interfaces";
|
|
5
|
+
import {MapOrgUnit} from "../interfaces";
|
|
6
|
+
|
|
7
|
+
export const MapOrgUnitContext = createContext<{
|
|
8
|
+
orgUnitSelection: OrgUnitSelection;
|
|
9
|
+
orgUnits?: MapOrgUnit[];
|
|
10
|
+
}>({
|
|
11
|
+
orgUnitSelection: { orgUnits: [] },
|
|
12
|
+
orgUnits: [],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const MapPeriodContext = createContext<
|
|
16
|
+
| {
|
|
17
|
+
periods?: BasePeriod[];
|
|
18
|
+
range?: { start: Date; end: Date };
|
|
19
|
+
}
|
|
20
|
+
| undefined
|
|
21
|
+
>({
|
|
22
|
+
periods: [],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const MapLayersContext = createContext<{
|
|
26
|
+
layers: CustomMapLayer[];
|
|
27
|
+
updateLayer: (id: string, updatedLayer: CustomMapLayer) => void;
|
|
28
|
+
}>({
|
|
29
|
+
layers: [],
|
|
30
|
+
updateLayer: () => {},
|
|
31
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {hcl} from "d3-color";
|
|
2
|
+
import {isString} from "lodash";
|
|
3
|
+
import {COLOR_PALETTES} from "../constants/colors";
|
|
4
|
+
|
|
5
|
+
const colorbrewer: Record<string, any> = COLOR_PALETTES;
|
|
6
|
+
|
|
7
|
+
// Returns a color brewer scale for a number of classes
|
|
8
|
+
export const getColorPalette = (scale: string, classes: number) => {
|
|
9
|
+
return colorbrewer?.[scale]?.[classes];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getColorClasses = (palette: string) => {
|
|
13
|
+
return palette?.split(",")?.length;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Returns color scale name for a palette
|
|
17
|
+
export const getColorScale = (palette: string) => {
|
|
18
|
+
const classes = palette.split(",").length;
|
|
19
|
+
return colorScales.find((name) => colorbrewer[name][classes]?.join(",") === palette);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const defaultColorScaleName = "YlOrBr";
|
|
23
|
+
export const defaultClasses = 5;
|
|
24
|
+
export const defaultColorScale = getColorPalette(defaultColorScaleName, defaultClasses);
|
|
25
|
+
|
|
26
|
+
// Correct colors not adhering to the css standard (add missing #)
|
|
27
|
+
export const cssColor = (color: any) => {
|
|
28
|
+
if (!isString(color)) {
|
|
29
|
+
return color;
|
|
30
|
+
} else if (color === "##normal") {
|
|
31
|
+
// ##normal is used in old map favorites
|
|
32
|
+
return null; // Will apply default color
|
|
33
|
+
}
|
|
34
|
+
return (/(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color) ? "#" : "") + color;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Returns an unique color (first from an array, then random but still unique)
|
|
38
|
+
export const getUniqueColor = (defaultColors: any) => {
|
|
39
|
+
const colors = [...defaultColors];
|
|
40
|
+
|
|
41
|
+
function randomColor(): string {
|
|
42
|
+
const color = "#000000".replace(/0/g, () => (~~(Math.random() * 16)).toString(16));
|
|
43
|
+
|
|
44
|
+
// Recursive until color is unique
|
|
45
|
+
if (colors.includes(color)) {
|
|
46
|
+
return randomColor();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
colors.push(color);
|
|
50
|
+
|
|
51
|
+
return color;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (index: number) => colors[index] || randomColor();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Returns true if a color is dark
|
|
58
|
+
export const isDarkColor = (color: string) => hcl(color).l < 70;
|
|
59
|
+
|
|
60
|
+
// Returns constrasting color
|
|
61
|
+
export const getContrastColor = (color: string) => (isDarkColor(color) ? "#fff" : "#000");
|
|
62
|
+
export type LegendColorScale = typeof colorScales[number];
|
|
63
|
+
export const colorScales = [
|
|
64
|
+
"YlOrBr",
|
|
65
|
+
"Reds",
|
|
66
|
+
"YlGn",
|
|
67
|
+
"Greens",
|
|
68
|
+
"Blues",
|
|
69
|
+
"BuPu",
|
|
70
|
+
"RdPu",
|
|
71
|
+
"PuRd",
|
|
72
|
+
"Greys",
|
|
73
|
+
"YlOrBr_reverse",
|
|
74
|
+
"Reds_reverse",
|
|
75
|
+
"YlGn_reverse",
|
|
76
|
+
"Greens_reverse",
|
|
77
|
+
"Blues_reverse",
|
|
78
|
+
"BuPu_reverse",
|
|
79
|
+
"RdPu_reverse",
|
|
80
|
+
"PuRd_reverse",
|
|
81
|
+
"Greys_reverse",
|
|
82
|
+
"PuOr",
|
|
83
|
+
"BrBG",
|
|
84
|
+
"PRGn",
|
|
85
|
+
"PiYG",
|
|
86
|
+
"RdBu",
|
|
87
|
+
"RdGy",
|
|
88
|
+
"RdYlBu",
|
|
89
|
+
"Spectral",
|
|
90
|
+
"RdYlGn",
|
|
91
|
+
"Paired",
|
|
92
|
+
"Pastel1",
|
|
93
|
+
"Set1",
|
|
94
|
+
"Set3",
|
|
95
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Get the longest text length from an object property in an array
|
|
2
|
+
import L from "leaflet";
|
|
3
|
+
|
|
4
|
+
export const getLongestTextLength = (array: Array<any>, key: string | number) =>
|
|
5
|
+
array.reduce((text, curr) => (curr[key] && String(curr[key]).length > text.length ? String(curr[key]) : text), "").length;
|
|
6
|
+
|
|
7
|
+
export function getIconUrl(icon: string, { baseUrl }: { baseUrl: string }) {
|
|
8
|
+
return `${baseUrl}/images/orgunitgroup/${icon ?? "01.png"}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getIcon(url: string): L.Icon | undefined {
|
|
12
|
+
return new L.Icon({
|
|
13
|
+
iconUrl: url,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {Legend, OrganisationUnit, OrgUnitSelection} from "@hisptz/dhis2-utils";
|
|
2
|
+
import {LeafletMouseEvent} from "leaflet";
|
|
3
|
+
import {compact, filter, find, forEach, isEmpty, isString, sortBy} from "lodash";
|
|
4
|
+
import {defaultClasses, defaultColorScaleName, getColorPalette} from "./colors";
|
|
5
|
+
|
|
6
|
+
export function highlightFeature(e: LeafletMouseEvent, style: any) {
|
|
7
|
+
const layer = e.target;
|
|
8
|
+
layer.setStyle(style);
|
|
9
|
+
// layer.bringToFront();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resetHighlight(e: LeafletMouseEvent, defaultStyle: any) {
|
|
13
|
+
const layer = e.target;
|
|
14
|
+
layer.setStyle(defaultStyle);
|
|
15
|
+
// layer.bringToBack();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getColorFromLegendSet(legends: Legend[], value?: number): string {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
const legend: any = find(legends ?? [], (legend: any) => legend?.startValue <= value && legend?.endValue >= value) ?? {};
|
|
23
|
+
return legend.color ? legend.color : "transparent";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLegendCount(legend: any, data: any) {
|
|
27
|
+
const { startValue, endValue } = legend;
|
|
28
|
+
return filter(data, (d: any) => d.data >= startValue && d.data <= endValue).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getOrgUnitsSelection(orgUnitSelection: OrgUnitSelection) {
|
|
32
|
+
const orgUnits = [];
|
|
33
|
+
if (orgUnitSelection.userOrgUnit) {
|
|
34
|
+
orgUnits.push("USER_ORGUNIT");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (orgUnitSelection.userSubUnit) {
|
|
38
|
+
orgUnits.push("USER_ORGUNIT_CHILDREN");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (orgUnitSelection.userSubX2Unit) {
|
|
42
|
+
orgUnits.push("USER_ORGUNIT_GRANDCHILDREN");
|
|
43
|
+
}
|
|
44
|
+
if (!isEmpty(orgUnitSelection.levels)) {
|
|
45
|
+
forEach(orgUnitSelection.levels, (level) => orgUnits.push(`LEVEL-${level}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [...orgUnits, ...(orgUnitSelection?.orgUnits?.map((ou: OrganisationUnit) => `${ou.id}`) ?? [])];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sanitizeOrgUnits(metaData: any) {
|
|
52
|
+
if (metaData) {
|
|
53
|
+
return metaData?.dimensions?.ou?.map((ouId: string) => ({
|
|
54
|
+
id: ouId,
|
|
55
|
+
name: metaData?.items[ouId]?.name,
|
|
56
|
+
path: metaData?.ouHierarchy?.[ouId],
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function toGeoJson(organisationUnits: any) {
|
|
63
|
+
return sortBy(organisationUnits, "le").map((ou: any) => {
|
|
64
|
+
try {
|
|
65
|
+
const coord = JSON.parse(ou.co);
|
|
66
|
+
let gpid = "";
|
|
67
|
+
let gppg = "";
|
|
68
|
+
let type = "Point";
|
|
69
|
+
|
|
70
|
+
if (ou.ty === 2) {
|
|
71
|
+
type = "Polygon";
|
|
72
|
+
if (ou.co.substring(0, 4) === "[[[[") {
|
|
73
|
+
type = "MultiPolygon";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Grand parent
|
|
78
|
+
if (isString(ou.pg) && ou.pg.length) {
|
|
79
|
+
const ids = compact(ou.pg.split("/"));
|
|
80
|
+
|
|
81
|
+
// Grand parent id
|
|
82
|
+
if (ids.length >= 2) {
|
|
83
|
+
gpid = ids[ids.length - 2] as string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Grand parent parent graph
|
|
87
|
+
if (ids.length > 2) {
|
|
88
|
+
gppg = "/" + ids.slice(0, ids.length - 2).join("/");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
type: "Feature",
|
|
94
|
+
id: ou.id,
|
|
95
|
+
geometry: {
|
|
96
|
+
type,
|
|
97
|
+
coordinates: coord,
|
|
98
|
+
},
|
|
99
|
+
properties: {
|
|
100
|
+
type,
|
|
101
|
+
id: ou.id,
|
|
102
|
+
name: ou.na,
|
|
103
|
+
hasCoordinatesDown: ou.hcd,
|
|
104
|
+
hasCoordinatesUp: ou.hcu,
|
|
105
|
+
level: ou.le,
|
|
106
|
+
grandParentParentGraph: gppg,
|
|
107
|
+
grandParentId: gpid,
|
|
108
|
+
parentGraph: ou.pg,
|
|
109
|
+
parentId: ou.pi,
|
|
110
|
+
parentName: ou.pn,
|
|
111
|
+
dimensions: ou.dimensions,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function sanitizeDate(startDate: string): string {
|
|
121
|
+
if (startDate?.split("-")?.[0]?.length < 4) {
|
|
122
|
+
return startDate?.split("-")?.reverse()?.join("-");
|
|
123
|
+
}
|
|
124
|
+
return startDate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateLegends(maxValue: number, minValue: number, { classesCount, colorClass }: { classesCount: number; colorClass: string }): Array<Legend> {
|
|
128
|
+
const count: number = classesCount ?? defaultClasses;
|
|
129
|
+
const color = colorClass ?? defaultColorScaleName;
|
|
130
|
+
|
|
131
|
+
const colorScale = [...getColorPalette(color, count)].reverse();
|
|
132
|
+
|
|
133
|
+
const maxLegendValue = 5 * Math.ceil(maxValue / 5);
|
|
134
|
+
const range = maxLegendValue / count;
|
|
135
|
+
|
|
136
|
+
const values = [];
|
|
137
|
+
let legendColorsIterator = colorScale.length - 1;
|
|
138
|
+
for (let i = 0; i < maxLegendValue; i += range) {
|
|
139
|
+
const id = colorScale[legendColorsIterator];
|
|
140
|
+
values.push({
|
|
141
|
+
startValue: Math.floor(i),
|
|
142
|
+
endValue: Math.floor(i + range),
|
|
143
|
+
id,
|
|
144
|
+
color: id,
|
|
145
|
+
});
|
|
146
|
+
legendColorsIterator--;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return values.reverse();
|
|
150
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {mount} from "@cypress/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {SingleValueContainer} from ".";
|
|
4
|
+
|
|
5
|
+
describe("Single Value Container Tests", () => {
|
|
6
|
+
it("should render", function () {
|
|
7
|
+
const arg = {
|
|
8
|
+
title: "PRIORITY INDICATORS",
|
|
9
|
+
singleValueItems: [
|
|
10
|
+
{
|
|
11
|
+
label: "Total Bookings",
|
|
12
|
+
value: 136,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: "At least one dose",
|
|
16
|
+
value: 45,
|
|
17
|
+
percentage: 23,
|
|
18
|
+
color: "#0D47A1",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
mount(<SingleValueContainer {...arg} />);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Tooltip} from "@dhis2/ui";
|
|
2
|
+
import {capitalize} from "lodash";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {animated, useSpring} from "react-spring";
|
|
5
|
+
import styles from "../../styles/SingleValueContainer.module.css";
|
|
6
|
+
import {SingleValue} from "../../types/props";
|
|
7
|
+
import SingleValuePercentage from "./SingleValuePercentage";
|
|
8
|
+
|
|
9
|
+
interface SingleValueProps extends SingleValue {
|
|
10
|
+
globalAnimationDelay?: number;
|
|
11
|
+
globalAnimationDuration?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function SingleValueItem({
|
|
15
|
+
label,
|
|
16
|
+
value,
|
|
17
|
+
color,
|
|
18
|
+
percentage,
|
|
19
|
+
animationDuration,
|
|
20
|
+
animationDelay,
|
|
21
|
+
globalAnimationDelay,
|
|
22
|
+
decimalPlaces,
|
|
23
|
+
globalAnimationDuration,
|
|
24
|
+
}: SingleValueProps): React.ReactElement {
|
|
25
|
+
const numberFormatter = (value: number) => Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: decimalPlaces ?? 1 }).format(value);
|
|
26
|
+
|
|
27
|
+
const sanitizedValue = useSpring({
|
|
28
|
+
val: value,
|
|
29
|
+
from: { val: 0 },
|
|
30
|
+
config: { duration: animationDuration ?? globalAnimationDuration ?? 1000 },
|
|
31
|
+
delay: animationDelay ?? globalAnimationDelay ?? 10,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const tooltipContent = `${label}: ${value}`;
|
|
35
|
+
return (
|
|
36
|
+
<div className={`${styles["single-value-item"]} text-center`}>
|
|
37
|
+
<div className={styles["font-large"]}>{label}</div>
|
|
38
|
+
<Tooltip content={capitalize(tooltipContent)}>
|
|
39
|
+
<animated.div className={`${styles["font-bold"]} ${styles["font-xx-large"]} ${styles["padding-top"]}`}>
|
|
40
|
+
{sanitizedValue.val.to((value) => numberFormatter(Math.floor(value)))}
|
|
41
|
+
</animated.div>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
{percentage ? <SingleValuePercentage color={color} percentage={percentage} /> : <span></span>}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/components/SingleValueContainer/components/SingleValueItem/SingleValuePercentage.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {LinearLoader} from "@dhis2/ui";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import styles from "../../styles/SingleValueContainer.module.css";
|
|
4
|
+
|
|
5
|
+
export default function SingleValuePercentage({ percentage, color }: any): React.ReactElement {
|
|
6
|
+
const width = "100%";
|
|
7
|
+
return (
|
|
8
|
+
<div className="w-100">
|
|
9
|
+
<LinearLoader className={styles["percent-value"]} width={width} amount={percentage} />
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import i18n from "@dhis2/d2-i18n";
|
|
2
|
+
import {CssReset} from "@dhis2/ui";
|
|
3
|
+
import React, {Suspense} from "react";
|
|
4
|
+
import SingleValueItem from "./components/SingleValueItem/SingleValueItem";
|
|
5
|
+
import styles from "./styles/SingleValueContainer.module.css";
|
|
6
|
+
import {SingleValue, SingleValueContainerProps} from "./types/props";
|
|
7
|
+
|
|
8
|
+
export * from "./types/props";
|
|
9
|
+
|
|
10
|
+
export function SingleValueContainer({
|
|
11
|
+
title,
|
|
12
|
+
singleValueItems,
|
|
13
|
+
animationDuration,
|
|
14
|
+
animationDelay
|
|
15
|
+
}: SingleValueContainerProps): React.ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<div className="w-100 h-100">
|
|
18
|
+
<CssReset/>
|
|
19
|
+
<Suspense fallback={<div>{i18n.t("Loading ...")}</div>}>
|
|
20
|
+
<div>
|
|
21
|
+
<span className={`${styles["font-x-large"]} ${styles["font-bold"]}`}>{title}</span>
|
|
22
|
+
<div className={styles["single-value-list"]}>
|
|
23
|
+
{singleValueItems.map((singleValueItem: SingleValue) => (
|
|
24
|
+
<SingleValueItem
|
|
25
|
+
key={`${singleValueItem.label}-${singleValueItem.value}`}
|
|
26
|
+
{...singleValueItem}
|
|
27
|
+
globalAnimationDuration={animationDuration}
|
|
28
|
+
globalAnimationDelay={animationDelay}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</Suspense>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
export { SingleValueItem };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.single-value-list {
|
|
2
|
+
padding: 10px 2px;
|
|
3
|
+
width: 100%;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
justify-content: space-around;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.padding-top {
|
|
10
|
+
padding-top: 12px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.percent-value {
|
|
14
|
+
border-radius: 1rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.percent-value div {
|
|
18
|
+
height: .5rem !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.font-large {
|
|
22
|
+
font-size: 1rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.font-x-large {
|
|
26
|
+
font-size: 1.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.font-xx-large {
|
|
30
|
+
font-size: 3rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.font-bold {
|
|
34
|
+
font-weight: bold;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.single-value-item {
|
|
38
|
+
margin: 0 1vw;
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SingleValueContainerProps = {
|
|
2
|
+
title: string;
|
|
3
|
+
singleValueItems: Array<SingleValue>;
|
|
4
|
+
animationDuration?: number;
|
|
5
|
+
animationDelay?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SingleValue = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: number;
|
|
11
|
+
percentage?: number;
|
|
12
|
+
decimalPlaces?: number;
|
|
13
|
+
color?: string;
|
|
14
|
+
animationDuration?: number;
|
|
15
|
+
animationDelay?: number;
|
|
16
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, {createContext, useContext, useEffect, useMemo} from "react";
|
|
2
|
+
import {Analytics, AnalyticsDimension} from "@hisptz/dhis2-utils";
|
|
3
|
+
import {useDimensions} from "../DimensionsProvider";
|
|
4
|
+
import {useDataQuery} from "@dhis2/app-runtime";
|
|
5
|
+
import {useLayout} from "../LayoutProvider";
|
|
6
|
+
import {forEach, set} from "lodash";
|
|
7
|
+
|
|
8
|
+
const AnalyticsContext = createContext<{ loading: boolean; analytics: Analytics } | undefined>(undefined);
|
|
9
|
+
|
|
10
|
+
const analyticsQuery = {
|
|
11
|
+
analytics: {
|
|
12
|
+
resource: "analytics",
|
|
13
|
+
params: ({dimensions, filters}: any) => {
|
|
14
|
+
return {
|
|
15
|
+
dimension: Object.keys(dimensions).map((dimension) => `${dimension}:${dimensions[dimension]?.join(';')}`),
|
|
16
|
+
filters: Object.keys(filters).map((dimension) => `${dimension}:${dimensions[dimension]?.join(';')}`),
|
|
17
|
+
includeMetadataDetails: true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DataProviderProps {
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useAnalyticsData() {
|
|
28
|
+
return useContext(AnalyticsContext) ?? {analytics: {}, loading: false}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AnalyticsDataProvider({children}: DataProviderProps) {
|
|
32
|
+
const [analyticsDimensions] = useDimensions();
|
|
33
|
+
const [layout] = useLayout();
|
|
34
|
+
const {dimensions, filters} = useMemo(() => {
|
|
35
|
+
const dimensions = {};
|
|
36
|
+
const filters = {};
|
|
37
|
+
|
|
38
|
+
forEach([...(layout?.columns ?? []), ...(layout?.rows ?? [])], (dimension) => {
|
|
39
|
+
set(dimensions, [dimension], (analyticsDimensions as AnalyticsDimension)?.[dimension])
|
|
40
|
+
})
|
|
41
|
+
forEach([...(layout?.filters ?? [])], (dimension) => {
|
|
42
|
+
set(dimensions, [dimension], (analyticsDimensions as AnalyticsDimension)?.[dimension])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
dimensions,
|
|
47
|
+
filters
|
|
48
|
+
}
|
|
49
|
+
}, [layout, analyticsDimensions.pe, analyticsDimensions.ou]);
|
|
50
|
+
const {data: analytics, error, loading, refetch, called} = useDataQuery(analyticsQuery, {
|
|
51
|
+
variables: {
|
|
52
|
+
dimensions,
|
|
53
|
+
filters
|
|
54
|
+
},
|
|
55
|
+
lazy: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
refetch({
|
|
60
|
+
dimensions,
|
|
61
|
+
filters
|
|
62
|
+
})
|
|
63
|
+
}, [dimensions, filters]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (error) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}, [error])
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<AnalyticsContext.Provider value={{analytics: analytics?.analytics as Analytics, loading}}>
|
|
73
|
+
{children}
|
|
74
|
+
</AnalyticsContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, {createContext, useContext, useReducer} from "react";
|
|
2
|
+
import {AnalyticsDimension} from "@hisptz/dhis2-utils";
|
|
3
|
+
import {set} from "lodash";
|
|
4
|
+
import {useUpdateEffect} from "usehooks-ts";
|
|
5
|
+
|
|
6
|
+
export type Dimension = "ou" | "pe" | "dx" | "co";
|
|
7
|
+
|
|
8
|
+
export type DimensionUpdater = (data: { dimension: Dimension; value: string[] }) => void
|
|
9
|
+
export const DimensionState = createContext<AnalyticsDimension>({
|
|
10
|
+
dx: [],
|
|
11
|
+
pe: [],
|
|
12
|
+
ou: []
|
|
13
|
+
})
|
|
14
|
+
export const DimensionUpdateState = createContext<DimensionUpdater | undefined>(undefined);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function reducer(state: AnalyticsDimension, {dimension, value}: { dimension: Dimension, value: string[] }) {
|
|
18
|
+
const updatedState = {...(state ?? {})};
|
|
19
|
+
set(updatedState, [dimension], value);
|
|
20
|
+
return updatedState
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DimensionProviderProps {
|
|
24
|
+
children: React.ReactNode,
|
|
25
|
+
dimensions: AnalyticsDimension
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useDimensions() {
|
|
29
|
+
return [
|
|
30
|
+
useContext(DimensionState),
|
|
31
|
+
useContext(DimensionUpdateState)
|
|
32
|
+
] as [AnalyticsDimension, DimensionUpdater]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function DimensionsProvider({children, dimensions}: DimensionProviderProps) {
|
|
36
|
+
const [state, dispatch] = useReducer(reducer, dimensions);
|
|
37
|
+
|
|
38
|
+
useUpdateEffect(() => {
|
|
39
|
+
Object.keys(dimensions).forEach((dimension: string) => {
|
|
40
|
+
console.log("updating")
|
|
41
|
+
dispatch({dimension: dimension as Dimension, value: dimensions[dimension] ?? []})
|
|
42
|
+
})
|
|
43
|
+
}, [dimensions]);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
return <DimensionState.Provider value={state}>
|
|
47
|
+
<DimensionUpdateState.Provider value={dispatch}>
|
|
48
|
+
{children}
|
|
49
|
+
</DimensionUpdateState.Provider>
|
|
50
|
+
</DimensionState.Provider>
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, {createContext, useContext, useState} from "react";
|
|
2
|
+
import {Dimension} from "../DimensionsProvider";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export interface Layout {
|
|
6
|
+
rows: Dimension[];
|
|
7
|
+
columns: Dimension[];
|
|
8
|
+
filters: Dimension[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LayoutProviderProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
defaultLayout: Layout
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LayoutState = createContext<Layout | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export function useLayout() {
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
useContext(LayoutState) ?? {rows: [], filters: [], columns: []}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LayoutProvider({defaultLayout, children}: LayoutProviderProps) {
|
|
27
|
+
const [layout, setLayout] = useState(defaultLayout);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<LayoutState.Provider value={layout}>
|
|
31
|
+
{children}
|
|
32
|
+
</LayoutState.Provider>
|
|
33
|
+
)
|
|
34
|
+
}
|