@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,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);
|
package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/index.tsx
ADDED
|
@@ -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
|
+
}
|