@hisptz/dhis2-analytics 1.0.48 → 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 (127) 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/models/index.js +1 -1
  5. package/build/cjs/components/ChartAnalytics/utils/chart.js +5 -0
  6. package/build/es/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
  7. package/build/es/components/ChartAnalytics/models/bar.js +16 -0
  8. package/build/es/components/ChartAnalytics/models/index.js +1 -1
  9. package/build/es/components/ChartAnalytics/utils/chart.js +5 -0
  10. package/build/types/components/ChartAnalytics/models/bar.d.ts +8 -0
  11. package/build/types/components/ChartAnalytics/types/props.d.ts +1 -1
  12. package/d2.config.js +8 -0
  13. package/i18n/en.pot +439 -0
  14. package/package.json +5 -5
  15. package/src/components/ChartAnalytics/ChartAnalytics.test.tsx +51 -0
  16. package/src/components/ChartAnalytics/components/DownloadMenu/components/Menu.tsx +48 -0
  17. package/src/components/ChartAnalytics/components/DownloadMenu/constants/menu.ts +38 -0
  18. package/src/components/ChartAnalytics/components/DownloadMenu/index.tsx +65 -0
  19. package/src/components/ChartAnalytics/components/DownloadMenu/interfaces/menu.ts +1 -0
  20. package/src/components/ChartAnalytics/hooks/useChart.ts +35 -0
  21. package/src/components/ChartAnalytics/index.tsx +28 -0
  22. package/src/components/ChartAnalytics/models/bar.ts +20 -0
  23. package/src/components/ChartAnalytics/models/column.ts +52 -0
  24. package/src/components/ChartAnalytics/models/index.ts +111 -0
  25. package/src/components/ChartAnalytics/models/line.ts +31 -0
  26. package/src/components/ChartAnalytics/models/multi-series.ts +115 -0
  27. package/src/components/ChartAnalytics/models/pie.ts +54 -0
  28. package/src/components/ChartAnalytics/services/export.ts +38 -0
  29. package/src/components/ChartAnalytics/styles/custom-highchart.css +48 -0
  30. package/src/components/ChartAnalytics/types/props.tsx +48 -0
  31. package/src/components/ChartAnalytics/utils/chart.ts +128 -0
  32. package/src/components/CircularProgressDashboard/CircularProgressIndicator.test.tsx +9 -0
  33. package/src/components/CircularProgressDashboard/index.tsx +36 -0
  34. package/src/components/CircularProgressDashboard/types/props.tsx +17 -0
  35. package/src/components/CustomPivotTable/components/Table/index.tsx +23 -0
  36. package/src/components/CustomPivotTable/components/TableBody/TableBody.module.css +12 -0
  37. package/src/components/CustomPivotTable/components/TableBody/index.tsx +96 -0
  38. package/src/components/CustomPivotTable/components/TableHeaders/TableHeaders.module.css +10 -0
  39. package/src/components/CustomPivotTable/components/TableHeaders/index.tsx +94 -0
  40. package/src/components/CustomPivotTable/index.tsx +63 -0
  41. package/src/components/CustomPivotTable/interfaces/index.ts +1 -0
  42. package/src/components/CustomPivotTable/services/engine.ts +102 -0
  43. package/src/components/CustomPivotTable/state/engine.tsx +22 -0
  44. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfigModal.stories.tsx +28 -0
  45. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfiguration.stories.tsx +34 -0
  46. package/src/components/Map/components/EarthEngineLayerConfiguration/index.tsx +412 -0
  47. package/src/components/Map/components/MapArea/index.tsx +83 -0
  48. package/src/components/Map/components/MapArea/interfaces/index.ts +39 -0
  49. package/src/components/Map/components/MapControls/components/CustomControl/index.tsx +24 -0
  50. package/src/components/Map/components/MapControls/components/DownloadControl/index.tsx +11 -0
  51. package/src/components/Map/components/MapControls/components/FullscreenControl/index.tsx +7 -0
  52. package/src/components/Map/components/MapControls/index.tsx +24 -0
  53. package/src/components/Map/components/MapLayer/components/BoundaryLayer/hooks/useBoundaryData.ts +7 -0
  54. package/src/components/Map/components/MapLayer/components/BoundaryLayer/index.tsx +55 -0
  55. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/components/EarthEngineLegend.tsx +74 -0
  56. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/constants/index.ts +430 -0
  57. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/hooks/index.ts +34 -0
  58. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/index.tsx +185 -0
  59. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/interfaces/index.ts +56 -0
  60. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/api.js +34241 -0
  61. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/engine.ts +431 -0
  62. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/utils/index.ts +105 -0
  63. package/src/components/Map/components/MapLayer/components/LegendArea/LegendArea.module.css +12 -0
  64. package/src/components/Map/components/MapLayer/components/LegendArea/components/LegendCardHeader/index.tsx +17 -0
  65. package/src/components/Map/components/MapLayer/components/LegendArea/index.tsx +167 -0
  66. package/src/components/Map/components/MapLayer/components/PointLayer/components/PointLegend/index.tsx +44 -0
  67. package/src/components/Map/components/MapLayer/components/PointLayer/hooks/index.ts +8 -0
  68. package/src/components/Map/components/MapLayer/components/PointLayer/index.tsx +36 -0
  69. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubble.tsx +48 -0
  70. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubbles.tsx +150 -0
  71. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/index.tsx +39 -0
  72. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/index.tsx +57 -0
  73. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/components/ChoroplethLegend.tsx +43 -0
  74. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/index.tsx +38 -0
  75. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/CustomTooltip/index.tsx +26 -0
  76. package/src/components/Map/components/MapLayer/components/ThematicLayer/hooks/config.ts +10 -0
  77. package/src/components/Map/components/MapLayer/components/ThematicLayer/index.tsx +46 -0
  78. package/src/components/Map/components/MapLayer/components/ThematicLayer/styles/legends.css +62 -0
  79. package/src/components/Map/components/MapLayer/index.tsx +32 -0
  80. package/src/components/Map/components/MapLayer/interfaces/index.ts +139 -0
  81. package/src/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.tsx +368 -0
  82. package/src/components/Map/components/MapProvider/components/MapLayerProvider/index.tsx +105 -0
  83. package/src/components/Map/components/MapProvider/hooks/index.ts +14 -0
  84. package/src/components/Map/components/MapProvider/index.tsx +93 -0
  85. package/src/components/Map/components/MapUpdater/index.tsx +8 -0
  86. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfigModal.stories.tsx +28 -0
  87. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfiguration.stories.tsx +34 -0
  88. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/components/ColorScale/index.tsx +24 -0
  89. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/constants/colors.ts +433 -0
  90. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/index.tsx +50 -0
  91. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScale.module.css +15 -0
  92. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScaleSelect.module.css +12 -0
  93. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/utils/colors.ts +91 -0
  94. package/src/components/Map/components/ThematicLayerConfiguration/components/CustomLegend/index.tsx +45 -0
  95. package/src/components/Map/components/ThematicLayerConfiguration/components/IndicatorSelectorModal/index.tsx +47 -0
  96. package/src/components/Map/components/ThematicLayerConfiguration/components/LegendSetSelector/index.tsx +57 -0
  97. package/src/components/Map/components/ThematicLayerConfiguration/index.tsx +248 -0
  98. package/src/components/Map/constants/colors.ts +434 -0
  99. package/src/components/Map/constants/legendSet.ts +19 -0
  100. package/src/components/Map/hooks/map.ts +47 -0
  101. package/src/components/Map/index.tsx +65 -0
  102. package/src/components/Map/interfaces/index.ts +57 -0
  103. package/src/components/Map/state/index.tsx +31 -0
  104. package/src/components/Map/utils/colors.ts +95 -0
  105. package/src/components/Map/utils/helpers.ts +15 -0
  106. package/src/components/Map/utils/map.ts +150 -0
  107. package/src/components/SingleValueContainer/SingleValueContainer.test.tsx +24 -0
  108. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValueItem.tsx +46 -0
  109. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValuePercentage.tsx +12 -0
  110. package/src/components/SingleValueContainer/index.tsx +37 -0
  111. package/src/components/SingleValueContainer/styles/SingleValueContainer.module.css +39 -0
  112. package/src/components/SingleValueContainer/types/props.tsx +16 -0
  113. package/src/components/Visualization/components/AnalyticsDataProvider/index.tsx +76 -0
  114. package/src/components/Visualization/components/DimensionsProvider/index.tsx +51 -0
  115. package/src/components/Visualization/components/LayoutProvider/index.tsx +34 -0
  116. package/src/components/Visualization/components/VisualizationDimensionSelector/index.tsx +59 -0
  117. package/src/components/Visualization/components/VisualizationProvider/index.tsx +31 -0
  118. package/src/components/Visualization/components/VisualizationSelector/index.tsx +157 -0
  119. package/src/components/Visualization/components/VisualizationTypeProvider/index.tsx +40 -0
  120. package/src/components/Visualization/components/VisualizationTypeSelector/index.tsx +46 -0
  121. package/src/components/Visualization/index.tsx +103 -0
  122. package/src/index.ts +6 -0
  123. package/src/locales/en/translations.json +138 -0
  124. package/src/locales/index.js +16 -0
  125. package/tsconfig.build.json +46 -0
  126. package/tsconfig.json +51 -0
  127. package/LICENSE +0 -29
@@ -0,0 +1,167 @@
1
+ import {colors, IconLegend24, Popper, Portal} from "@dhis2/ui";
2
+ import {ControlPosition} from "leaflet";
3
+ import {compact, head} from "lodash";
4
+ import React, {useEffect, useRef, useState} from "react";
5
+ import {MapLegendConfig} from "../../../MapArea/interfaces";
6
+ import {CustomControl} from "../../../MapControls/components/CustomControl";
7
+ import {
8
+ CustomBubbleLayer,
9
+ CustomGoogleEngineLayer,
10
+ CustomPointLayer,
11
+ CustomThematicLayer,
12
+ SUPPORTED_EARTH_ENGINE_LAYERS
13
+ } from "../../interfaces";
14
+ import PointLegend from "../PointLayer/components/PointLegend";
15
+ import BubbleLegend from "../ThematicLayer/components/Bubble/components/BubbleLegend";
16
+ import ChoroplethLegend from "../ThematicLayer/components/Choropleth/components/ChoroplethLegend";
17
+ import EarthEngineLegend from "../GoogleEngineLayer/components/EarthEngineLegend";
18
+ import classes from "./LegendArea.module.css";
19
+ import {usePrintMedia} from "../../../../hooks/map";
20
+
21
+ const TOOLTIP_OFFSET = 4;
22
+
23
+ function getLegendComponent(layer: CustomThematicLayer | CustomPointLayer | CustomGoogleEngineLayer) {
24
+ if (layer.type === "point") {
25
+ return <PointLegend name={layer.label} />;
26
+ }
27
+
28
+ if (SUPPORTED_EARTH_ENGINE_LAYERS.includes(layer.type)) {
29
+ return <EarthEngineLegend name={layer.name ?? ""} layer={layer as CustomGoogleEngineLayer} />;
30
+ }
31
+
32
+ const { type, enabled, control, dataItem, name, data, legends } = (layer as CustomThematicLayer) ?? {};
33
+
34
+ if (!enabled || !control) {
35
+ return null;
36
+ }
37
+ switch (type) {
38
+ case "bubble":
39
+ return (
40
+ <BubbleLegend
41
+ radius={(layer as CustomBubbleLayer)?.radius ?? { min: 0, max: 50 }}
42
+ legends={legends ?? []}
43
+ name={name ?? dataItem.displayName}
44
+ data={data}
45
+ dataItem={head(data)?.dataItem ?? dataItem}
46
+ />
47
+ );
48
+ case "choropleth":
49
+ return <ChoroplethLegend legends={legends ?? []} name={name ?? dataItem.displayName} data={data} dataItem={head(data)?.dataItem ?? dataItem} />;
50
+ }
51
+ }
52
+
53
+ function CollapsedLegendIcon({ onCollapse, name }: { name: string; onCollapse: () => void }) {
54
+ const openDelay = 200;
55
+ const closeDelay = 200;
56
+ const [openTooltip, setOpenTooltip] = useState(false);
57
+ const openTimerRef = useRef<any>(null);
58
+ const closeTimerRef = useRef<any>(null);
59
+ const ref = useRef<HTMLDivElement>(null);
60
+
61
+ const hideModifier = { name: "hide" };
62
+ const offsetModifier = {
63
+ name: "offset",
64
+ options: {
65
+ offset: [0, TOOLTIP_OFFSET],
66
+ },
67
+ };
68
+
69
+ const flipModifier = {
70
+ name: "flip",
71
+ options: { altBoundary: true },
72
+ };
73
+
74
+ const onMouseOver = () => {
75
+ clearTimeout(closeTimerRef.current);
76
+
77
+ openTimerRef.current = setTimeout(() => {
78
+ setOpenTooltip(true);
79
+ }, openDelay);
80
+ };
81
+
82
+ const onMouseOut = () => {
83
+ clearTimeout(openTimerRef.current);
84
+
85
+ closeTimerRef.current = setTimeout(() => {
86
+ setOpenTooltip(false);
87
+ }, closeDelay);
88
+ };
89
+
90
+ useEffect(
91
+ () => () => {
92
+ clearTimeout(openTimerRef.current);
93
+ clearTimeout(closeTimerRef.current);
94
+ },
95
+ []
96
+ );
97
+
98
+ return (
99
+ <div ref={ref} onMouseOver={onMouseOver} onMouseOut={onMouseOut} onClick={onCollapse} style={{ width: 28, height: 28 }} className="legend-card collapsed">
100
+ <IconLegend24 />
101
+ {openTooltip && (
102
+ <Portal className={classes["map-tooltip"]}>
103
+ <Popper className={classes["map-tooltip"]} reference={ref} modifiers={[offsetModifier, flipModifier, hideModifier]}>
104
+ <div
105
+ style={{
106
+ backgroundColor: `${colors.grey900}`,
107
+ borderRadius: 3,
108
+ color: `${colors.white}`,
109
+ padding: "4px 6px",
110
+ }}
111
+ data-test={`content`}>
112
+ {name}
113
+ </div>
114
+ </Popper>
115
+ </Portal>
116
+ )}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function Legend({ children, collapsible }: { children: React.ReactElement; collapsible: boolean }) {
122
+ const [collapsed, setCollapsed] = useState(collapsible);
123
+ const inPrintMode = usePrintMedia();
124
+ const onCollapse = () => {
125
+ if (collapsible) {
126
+ setCollapsed((prevState) => !prevState);
127
+ }
128
+ };
129
+
130
+ const name = head(React.Children.toArray(children) as React.ReactElement[])?.props.name;
131
+
132
+ const shouldCollapse = collapsed && !inPrintMode;
133
+
134
+ return (
135
+ <div className="w-100">
136
+ {shouldCollapse ? (
137
+ <CollapsedLegendIcon name={name} onCollapse={onCollapse} />
138
+ ) : (
139
+ React.Children.map(children, (child) => React.cloneElement(child, { collapsible, onCollapse }))
140
+ )}
141
+ </div>
142
+ );
143
+ }
144
+
145
+ export default function LegendArea({
146
+ layers,
147
+ legends: legendConfig,
148
+ }: {
149
+ layers: Array<CustomThematicLayer | CustomPointLayer | CustomGoogleEngineLayer>;
150
+ position: ControlPosition;
151
+ legends?: MapLegendConfig;
152
+ }) {
153
+ const legends: JSX.Element[] = compact(layers.filter((layer) => layer.enabled).map(getLegendComponent));
154
+ const { position, collapsible } = legendConfig ?? {};
155
+
156
+ return (
157
+ <CustomControl position={position}>
158
+ <div className="column gap-16 align-items-end">
159
+ {legends?.map((legend: any, index) => (
160
+ <Legend collapsible={collapsible ?? true} key={`${index}-map-legend`}>
161
+ {legend}
162
+ </Legend>
163
+ ))}
164
+ </div>
165
+ </CustomControl>
166
+ );
167
+ }
@@ -0,0 +1,44 @@
1
+ import {useConfig} from "@dhis2/app-runtime";
2
+ import {Divider} from "@dhis2/ui";
3
+ import React, {forwardRef} from "react";
4
+ import {getIconUrl} from "../../../../../../utils/helpers";
5
+ import LegendCardHeader from "../../../LegendArea/components/LegendCardHeader";
6
+ import {usePointLayer} from "../../hooks";
7
+
8
+ function PointLegends({ orgUnitGroups, icon, label }: { orgUnitGroups: { name: string; symbol: string }[]; icon?: string; label?: string }) {
9
+ const { baseUrl } = useConfig();
10
+
11
+ return (
12
+ <div style={{ minWidth: 100, alignItems: "flex-start" }} className="w-100 p-8 legend-list column">
13
+ {icon && (
14
+ <div key={`${icon}-legend`} className="row gap-16 align-items-center">
15
+ <img height={20} width={20} alt={`${name}-icon`} src={getIconUrl(icon, { baseUrl })} />
16
+ <p>{label}</p>
17
+ </div>
18
+ )}
19
+ {orgUnitGroups.map(({ name, symbol }) => {
20
+ return (
21
+ <div key={`${name}-legend`} className="row gap-16 align-items-center">
22
+ <img height={20} width={20} alt={`${name}-icon`} src={getIconUrl(symbol ?? "", { baseUrl })} />
23
+ <p>{name}</p>
24
+ </div>
25
+ );
26
+ })}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ function PointLegend({ collapsible, onCollapse }: any, ref: React.LegacyRef<HTMLDivElement>) {
32
+ const pointLayer = usePointLayer();
33
+ const { label, style } = pointLayer ?? {};
34
+
35
+ return (
36
+ <div ref={ref} className="legend-card">
37
+ <LegendCardHeader collapsible={collapsible} onCollapse={onCollapse} title={label ?? "Points"} />
38
+ <Divider margin={"0"} />
39
+ <PointLegends label={label} orgUnitGroups={style?.orgUnitGroups ?? []} icon={style?.icon} />
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export default forwardRef(PointLegend);
@@ -0,0 +1,8 @@
1
+ import {find} from "lodash";
2
+ import {useMapLayers} from "../../../../MapProvider/hooks";
3
+ import {CustomPointLayer} from "../../../interfaces";
4
+
5
+ export function usePointLayer() {
6
+ const { layers } = useMapLayers();
7
+ return find(layers, ["type", "point"]) as CustomPointLayer;
8
+ }
@@ -0,0 +1,36 @@
1
+ import {useConfig} from "@dhis2/app-runtime";
2
+ import i18n from "@dhis2/d2-i18n";
3
+ import L from "leaflet";
4
+ import React from "react";
5
+ import {GeoJSON, LayerGroup, LayersControl, Popup, Tooltip} from "react-leaflet";
6
+ import {PointOrgUnit} from "../../../../interfaces";
7
+ import {getIcon, getIconUrl} from "../../../../utils/helpers";
8
+ import {usePointLayer} from "./hooks";
9
+
10
+ export function PointLayer() {
11
+ const pointLayer = usePointLayer();
12
+ const { enabled, label, points: orgUnits, style } = pointLayer ?? {};
13
+ const { baseUrl } = useConfig();
14
+ return (
15
+ <LayersControl.Overlay checked={enabled} name={label ?? i18n.t("Points")}>
16
+ <LayerGroup>
17
+ {orgUnits?.map((area: PointOrgUnit) => {
18
+ return (
19
+ <GeoJSON
20
+ pointToLayer={(_, coordinates) => {
21
+ return L.marker(coordinates, { icon: getIcon(getIconUrl(area.icon.icon ?? style?.icon, { baseUrl })) });
22
+ }}
23
+ data={area.geoJSON}
24
+ interactive
25
+ key={`${area.id}-polygon`}>
26
+ <Tooltip>{area.name}</Tooltip>
27
+ <Popup minWidth={80}>
28
+ <h3>{area.name}</h3>
29
+ </Popup>
30
+ </GeoJSON>
31
+ );
32
+ })}
33
+ </LayerGroup>
34
+ </LayersControl.Overlay>
35
+ );
36
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import {guideLength, textPadding} from "./Bubbles";
3
+
4
+ export interface BubbleProps {
5
+ radius: number;
6
+ maxRadius: number;
7
+ text?: string;
8
+ textAlign?: "left" | "right";
9
+ color?: string;
10
+ stroke?: string;
11
+ pattern?: string;
12
+ gap?: number;
13
+ }
14
+
15
+ const Bubble = ({ radius, maxRadius, text, textAlign, color, stroke, pattern }: BubbleProps) => {
16
+ const leftAlign = textAlign === "left";
17
+ const x = maxRadius;
18
+ const y = maxRadius * 2 - radius;
19
+ const x2 = leftAlign ? x - maxRadius - guideLength : x + maxRadius + guideLength;
20
+ const y2 = maxRadius * 2 - radius * 2;
21
+ const textX = x2 + (leftAlign ? -textPadding : textPadding);
22
+ const textAnchor = leftAlign ? "end" : "start";
23
+
24
+ return (
25
+ <g>
26
+ <circle
27
+ cx={x}
28
+ cy={y}
29
+ r={radius}
30
+ stroke={stroke || "#000"}
31
+ style={{
32
+ fill: pattern ? `url(#${pattern})` : color || "none",
33
+ strokeWidth: 0.5,
34
+ }}
35
+ />
36
+ {text && (
37
+ <g>
38
+ <line x1={x} x2={x2} y1={y2} y2={y2} stroke="black" style={{ strokeDasharray: "2, 2", strokeWidth: 0.5 }} />
39
+ <text x={textX} y={y2} textAnchor={textAnchor} alignmentBaseline="middle" style={{ fontSize: 12 }}>
40
+ {text}
41
+ </text>
42
+ </g>
43
+ )}
44
+ </g>
45
+ );
46
+ };
47
+
48
+ export default Bubble;
@@ -0,0 +1,150 @@
1
+ import i18n from "@dhis2/d2-i18n";
2
+ import {scaleSqrt} from "d3-scale";
3
+ import {reduce} from "lodash";
4
+ import React, {memo} from "react";
5
+ import {getContrastColor} from "../../../../../../../../../utils/colors";
6
+ import {getLongestTextLength} from "../../../../../../../../../utils/helpers";
7
+ import Bubble, {BubbleProps} from "./Bubble";
8
+
9
+ const style = {
10
+ paddingTop: 10,
11
+ display: "flex",
12
+ alignItems: "center",
13
+ justifyContent: "center",
14
+ };
15
+
16
+ const legendWidth = 200;
17
+ const digitWidth = 6.8;
18
+ export const guideLength = 16;
19
+ export const textPadding = 4;
20
+
21
+ const Bubbles = ({ radiusLow, radiusHigh, color, classes }: { radiusLow: number; radiusHigh: number; color?: string; classes: Array<any> }) => {
22
+ const height = radiusHigh * 2 + 4;
23
+ const scale = scaleSqrt().range([radiusLow, radiusHigh]);
24
+ const radiusMid = scale(0.5);
25
+
26
+ if (isNaN(radiusLow) || isNaN(radiusHigh)) {
27
+ return null;
28
+ }
29
+
30
+ let bubbles: Array<BubbleProps> = [];
31
+
32
+ // If color legend
33
+ if (Array.isArray(classes) && classes.length) {
34
+ const startValue = classes[0].startValue;
35
+ const endValue = classes[classes.length - 1].endValue;
36
+ const itemScale = scale.domain([startValue, endValue]);
37
+
38
+ bubbles = [...classes].reverse().map((c) => ({
39
+ radius: itemScale(c.endValue),
40
+ maxRadius: radiusHigh,
41
+ color: c.color,
42
+ text: String(c.endValue),
43
+ }));
44
+
45
+ // Add the smallest bubble for the lowest value
46
+ bubbles.push({
47
+ radius: itemScale(startValue),
48
+ maxRadius: radiusHigh,
49
+ text: String(startValue),
50
+ });
51
+ } else {
52
+ // If single color
53
+ const stroke = color && getContrastColor(color);
54
+
55
+ bubbles = [
56
+ {
57
+ radius: radiusHigh,
58
+ maxRadius: radiusHigh,
59
+ color,
60
+ stroke,
61
+ text: i18n.t("Max"),
62
+ },
63
+ {
64
+ radius: radiusMid,
65
+ maxRadius: radiusHigh,
66
+ color,
67
+ stroke,
68
+ text: i18n.t("Mid"),
69
+ },
70
+ {
71
+ radius: radiusLow,
72
+ maxRadius: radiusHigh,
73
+ color,
74
+ stroke,
75
+ text: i18n.t("Min"),
76
+ },
77
+ ];
78
+ }
79
+
80
+ // Calculate the pixel length of the longest number
81
+ let textLength = Math.ceil(Math.max(getLongestTextLength(classes, "startValue"), getLongestTextLength(classes, "endValue")) * digitWidth);
82
+
83
+ // Calculate the total length if numbers are alternate on each side
84
+ const alternateLength = (radiusHigh + guideLength + textPadding + textLength) * 2;
85
+
86
+ let smallestGap = reduce(bubbles, (prev, curr: any, i) => {
87
+ const gap = prev.radius - curr.radius;
88
+ const smallestGap = prev.gap === undefined || gap < prev.gap ? gap : prev.gap;
89
+
90
+ return i === bubbles.length - 1
91
+ ? Math.round(smallestGap * 2)
92
+ : {
93
+ radius: curr.radius,
94
+ gap: smallestGap,
95
+ };
96
+ });
97
+
98
+ const alternateFit = alternateLength < legendWidth;
99
+
100
+ const alternate = alternateFit && smallestGap > 5 && smallestGap < 12;
101
+
102
+ if (!alternateFit) {
103
+ smallestGap = smallestGap / 2;
104
+ }
105
+
106
+ // Too cramped to show number for each bubble
107
+ if (smallestGap < 4) {
108
+ const [maxBubble] = bubbles;
109
+ const minBubble = bubbles[bubbles.length - 1];
110
+ const gap = maxBubble.radius - minBubble.radius;
111
+ const showNumbers = [0]; // Always show the largest number
112
+
113
+ if (gap > 4) {
114
+ showNumbers.push(bubbles.length - 1);
115
+ }
116
+
117
+ if (gap > 15) {
118
+ const midRadius = minBubble.radius + gap / 2;
119
+
120
+ // Find the closest bubble above the mid-radius
121
+ const midBubble = bubbles.reduce((prev, curr) => (curr.radius >= midRadius && curr.radius - midRadius < prev.radius - midRadius ? curr : prev));
122
+
123
+ showNumbers.push(bubbles.indexOf(midBubble));
124
+ }
125
+
126
+ bubbles.forEach((b, i) => {
127
+ if (!showNumbers.includes(i)) {
128
+ delete b.text;
129
+ }
130
+ });
131
+ }
132
+
133
+ textLength = Math.ceil(getLongestTextLength(bubbles, "text") * digitWidth);
134
+
135
+ const offset = textLength + guideLength + textPadding;
136
+
137
+ return (
138
+ <div style={style}>
139
+ <svg width={legendWidth} height={height + 50}>
140
+ <g transform={`translate(${alternate ? offset : "16"} 24)`}>
141
+ {bubbles.map((bubble, i) => (
142
+ <Bubble key={i} {...bubble} textAlign={alternate && i % 2 == 0 ? "left" : "right"} />
143
+ ))}
144
+ </g>
145
+ </svg>
146
+ </div>
147
+ );
148
+ };
149
+
150
+ export default memo(Bubbles);
@@ -0,0 +1,39 @@
1
+ import {Divider} from "@dhis2/ui";
2
+ import type {Legend} from "@hisptz/dhis2-utils";
3
+ import React, {forwardRef} from "react";
4
+ import {ThematicLayerData, ThematicLayerDataItem} from "../../../../../../interfaces";
5
+ import LegendCardHeader from "../../../../../LegendArea/components/LegendCardHeader";
6
+ import Bubbles from "./components/Bubbles";
7
+
8
+ function BubbleLegend(
9
+ {
10
+ radius,
11
+ dataItem,
12
+ data,
13
+ name,
14
+ collapsible,
15
+ onCollapse,
16
+ legends,
17
+ }: {
18
+ radius: { min: number; max: number };
19
+ dataItem: ThematicLayerDataItem;
20
+ data: ThematicLayerData[];
21
+ name?: string;
22
+ collapsible?: boolean;
23
+ onCollapse?: () => void;
24
+ legends: Legend[];
25
+ },
26
+ ref: React.LegacyRef<HTMLDivElement> | undefined
27
+ ) {
28
+ return (
29
+ <div className="legend-card" ref={ref}>
30
+ <LegendCardHeader title={dataItem.displayName} onCollapse={onCollapse} collapsible={collapsible} />
31
+ <Divider margin={"0"} />
32
+ <div className="legend-list pt-8">
33
+ <Bubbles classes={legends.reverse()} radiusHigh={radius.max} radiusLow={radius.min} color={"#FF0000"} />
34
+ </div>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export default forwardRef(BubbleLegend);
@@ -0,0 +1,57 @@
1
+ import {colors} from "@dhis2/ui";
2
+ import type {Legend} from "@hisptz/dhis2-utils";
3
+ import {geoJSON} from "leaflet";
4
+ import React, {useMemo} from "react";
5
+ import {CircleMarker} from "react-leaflet";
6
+ import {getColorFromLegendSet, highlightFeature, resetHighlight} from "../../../../../../utils/map";
7
+ import {ThematicLayerData} from "../../../../interfaces";
8
+ import CustomTooltip from "../CustomTooltip";
9
+
10
+ const defaultStyle = {
11
+ weight: 1,
12
+ };
13
+ const highlightStyle = {
14
+ weight: 2,
15
+ };
16
+
17
+ export default function Bubble({
18
+ data,
19
+ highestData,
20
+ legends,
21
+ radius,
22
+ }: {
23
+ data: ThematicLayerData;
24
+ highestData: number;
25
+ legends: Legend[];
26
+ radius?: { min: number; max: number };
27
+ }) {
28
+ const { orgUnit, data: value } = data ?? {};
29
+
30
+ const geoJSONObject = orgUnit.geoJSON;
31
+ const center = geoJSON(geoJSONObject).getBounds().getCenter();
32
+
33
+ const circleRadius = useMemo(() => {
34
+ return ((value ?? 0) * (radius?.max ?? 50)) / highestData;
35
+ }, [radius, data, highestData]);
36
+
37
+ return (
38
+ <>
39
+ <CircleMarker
40
+ interactive
41
+ eventHandlers={{
42
+ mouseover: (e) => highlightFeature(e, highlightStyle),
43
+ mouseout: (e) => resetHighlight(e, defaultStyle),
44
+ }}
45
+ pathOptions={{
46
+ fillColor: getColorFromLegendSet(legends, data.data),
47
+ fillOpacity: 1,
48
+ color: colors.grey900,
49
+ weight: 1,
50
+ }}
51
+ radius={circleRadius}
52
+ center={center}>
53
+ <CustomTooltip data={data} />
54
+ </CircleMarker>
55
+ </>
56
+ );
57
+ }
@@ -0,0 +1,43 @@
1
+ import "../../../styles/legends.css";
2
+ import {Divider} from "@dhis2/ui";
3
+ import type {Legend} from "@hisptz/dhis2-utils";
4
+ import React, {forwardRef} from "react";
5
+ import {getLegendCount} from "../../../../../../../utils/map";
6
+ import {ThematicLayerData, ThematicLayerDataItem} from "../../../../../interfaces";
7
+ import LegendCardHeader from "../../../../LegendArea/components/LegendCardHeader";
8
+
9
+ export function LegendItem({ legend, value }: { legend: { startValue: number; endValue: number; color: string }; value: number }) {
10
+ return (
11
+ <div className="legend-item">
12
+ <div className="legend-item-color" style={{ backgroundColor: legend.color }} />
13
+ <div className="legend-item-label">{`${legend.startValue} - ${legend.endValue}`}</div>
14
+ <div className="legend-item-value">{`(${value})`}</div>
15
+ </div>
16
+ );
17
+ }
18
+
19
+ function ChoroplethLegend(
20
+ {
21
+ dataItem,
22
+ data,
23
+ name,
24
+ collapsible,
25
+ onCollapse,
26
+ legends,
27
+ }: { data: ThematicLayerData[]; dataItem: ThematicLayerDataItem; name?: string; collapsible?: boolean; onCollapse?: () => void; legends: Legend[] },
28
+ ref: React.LegacyRef<HTMLDivElement> | undefined
29
+ ) {
30
+ return (
31
+ <div className="legend-card" ref={ref}>
32
+ <LegendCardHeader title={dataItem.displayName} collapsible={collapsible} onCollapse={onCollapse} />
33
+ <Divider margin={"0"} />
34
+ <div className="legend-list pt-8">
35
+ {legends?.map((legend: any) => (
36
+ <LegendItem key={`${legend?.color}-legend-list`} legend={legend} value={getLegendCount(legend, data)} />
37
+ ))}
38
+ </div>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export default forwardRef(ChoroplethLegend);
@@ -0,0 +1,38 @@
1
+ import {colors} from "@dhis2/ui";
2
+ import type {Legend} from "@hisptz/dhis2-utils";
3
+ import React from "react";
4
+ import {GeoJSON} from "react-leaflet";
5
+ import {MapOrgUnit} from "../../../../../../interfaces";
6
+ import {getColorFromLegendSet, highlightFeature, resetHighlight} from "../../../../../../utils/map";
7
+ import {ThematicLayerDataItem} from "../../../../interfaces";
8
+ import CustomTooltip from "../CustomTooltip";
9
+
10
+ const defaultStyle = {
11
+ weight: 1,
12
+ };
13
+ const highlightStyle = {
14
+ weight: 2,
15
+ };
16
+
17
+ export default function Choropleth({ data, legends }: { data: { orgUnit: MapOrgUnit; data?: number; dataItem: ThematicLayerDataItem }; legends: Legend[] }) {
18
+ const { orgUnit } = data;
19
+ return (
20
+ <>
21
+ <GeoJSON
22
+ data={orgUnit.geoJSON}
23
+ eventHandlers={{
24
+ mouseover: (e) => highlightFeature(e, highlightStyle),
25
+ mouseout: (e) => resetHighlight(e, defaultStyle),
26
+ }}
27
+ pathOptions={{
28
+ fillColor: getColorFromLegendSet(legends, data.data),
29
+ fillOpacity: 1,
30
+ color: colors.grey900,
31
+ weight: 1,
32
+ }}
33
+ key={`${data.dataItem.id}-layer`}>
34
+ <CustomTooltip data={data} />
35
+ </GeoJSON>
36
+ </>
37
+ );
38
+ }
@@ -0,0 +1,26 @@
1
+ import i18n from "@dhis2/d2-i18n";
2
+ import React from "react";
3
+ import {Pane, Popup, Tooltip} from "react-leaflet";
4
+ import {useMapPeriods} from "../../../../../MapProvider/hooks";
5
+ import {ThematicLayerData} from "../../../../interfaces";
6
+
7
+ export default function CustomTooltip({ data: dataObject }: { data: ThematicLayerData }) {
8
+ const { dataItem, orgUnit, data } = dataObject ?? {};
9
+ const { periods } = useMapPeriods() ?? {};
10
+
11
+ return (
12
+ <Pane name={`${dataItem.displayName}-${orgUnit.id}-popup-pane`} pane="popupPane">
13
+ <Tooltip>
14
+ {orgUnit?.name} ({data})
15
+ </Tooltip>
16
+ <Popup minWidth={80}>
17
+ <h3 style={{ margin: 0 }}>{orgUnit?.name}</h3>
18
+ <div>{dataItem?.displayName}</div>
19
+ <div>{periods?.map((period) => period.name).join(",")}</div>
20
+ <div>
21
+ {i18n.t("Value")}: {data}
22
+ </div>
23
+ </Popup>
24
+ </Pane>
25
+ );
26
+ }
@@ -0,0 +1,10 @@
1
+ import {find} from "lodash";
2
+ import {useContext} from "react";
3
+ import {MapLayersContext} from "../../../../../state";
4
+ import {CustomThematicLayer} from "../../../interfaces";
5
+
6
+ export default function useThematicLayer(layerId: string): CustomThematicLayer | undefined {
7
+ const { layers } = useContext(MapLayersContext);
8
+
9
+ return find(layers as CustomThematicLayer[], ["id", layerId]);
10
+ }