@hisptz/dhis2-analytics 2.1.36 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/Map/DHIS2Map.js +3 -1
  2. package/dist/components/Map/DHIS2Map.js.map +1 -1
  3. package/dist/components/Map/components/MapControls/components/TimelineControl/index.js +236 -0
  4. package/dist/components/Map/components/MapControls/components/TimelineControl/index.js.map +1 -0
  5. package/dist/components/Map/components/MapControls/index.js +17 -12
  6. package/dist/components/Map/components/MapControls/index.js.map +1 -1
  7. package/dist/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.js +62 -55
  8. package/dist/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.js.map +1 -1
  9. package/dist/components/Map/components/MapProvider/components/MapLayerProvider/index.js +14 -2
  10. package/dist/components/Map/components/MapProvider/components/MapLayerProvider/index.js.map +1 -1
  11. package/dist/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.js +19 -0
  12. package/dist/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.js.map +1 -0
  13. package/dist/components/Map/components/MapProvider/hooks/index.js +4 -0
  14. package/dist/components/Map/components/MapProvider/hooks/index.js.map +1 -1
  15. package/dist/components/Map/components/MapProvider/index.js +22 -5
  16. package/dist/components/Map/components/MapProvider/index.js.map +1 -1
  17. package/dist/components/Map/index.js +21 -0
  18. package/dist/components/Map/state/index.js +9 -0
  19. package/dist/components/Map/state/index.js.map +1 -1
  20. package/dist/components/Map/utils/helpers.js +74 -0
  21. package/dist/components/Map/utils/helpers.js.map +1 -1
  22. package/dist/esm/components/Map/DHIS2Map.js +3 -1
  23. package/dist/esm/components/Map/DHIS2Map.js.map +1 -1
  24. package/dist/esm/components/Map/components/MapControls/components/TimelineControl/index.js +234 -0
  25. package/dist/esm/components/Map/components/MapControls/components/TimelineControl/index.js.map +1 -0
  26. package/dist/esm/components/Map/components/MapControls/index.js +17 -12
  27. package/dist/esm/components/Map/components/MapControls/index.js.map +1 -1
  28. package/dist/esm/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.js +66 -59
  29. package/dist/esm/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.js.map +1 -1
  30. package/dist/esm/components/Map/components/MapProvider/components/MapLayerProvider/index.js +15 -3
  31. package/dist/esm/components/Map/components/MapProvider/components/MapLayerProvider/index.js.map +1 -1
  32. package/dist/esm/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.js +17 -0
  33. package/dist/esm/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.js.map +1 -0
  34. package/dist/esm/components/Map/components/MapProvider/hooks/index.js +5 -2
  35. package/dist/esm/components/Map/components/MapProvider/hooks/index.js.map +1 -1
  36. package/dist/esm/components/Map/components/MapProvider/index.js +22 -5
  37. package/dist/esm/components/Map/components/MapProvider/index.js.map +1 -1
  38. package/dist/esm/components/Map/index.js +3 -0
  39. package/dist/esm/components/Map/state/index.js +9 -1
  40. package/dist/esm/components/Map/state/index.js.map +1 -1
  41. package/dist/esm/components/Map/utils/helpers.js +72 -1
  42. package/dist/esm/components/Map/utils/helpers.js.map +1 -1
  43. package/dist/types/components/Map/DHIS2Map.d.ts.map +1 -1
  44. package/dist/types/components/Map/components/MapArea/interfaces/index.d.ts +5 -2
  45. package/dist/types/components/Map/components/MapArea/interfaces/index.d.ts.map +1 -1
  46. package/dist/types/components/Map/components/MapControls/components/TimelineControl/index.d.ts +26 -0
  47. package/dist/types/components/Map/components/MapControls/components/TimelineControl/index.d.ts.map +1 -0
  48. package/dist/types/components/Map/components/MapControls/index.d.ts +3 -4
  49. package/dist/types/components/Map/components/MapControls/index.d.ts.map +1 -1
  50. package/dist/types/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.d.ts.map +1 -1
  51. package/dist/types/components/Map/components/MapProvider/components/MapLayerProvider/index.d.ts.map +1 -1
  52. package/dist/types/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.d.ts +8 -0
  53. package/dist/types/components/Map/components/MapProvider/components/MapPeriodFilterProvider/index.d.ts.map +1 -0
  54. package/dist/types/components/Map/components/MapProvider/hooks/index.d.ts +1 -0
  55. package/dist/types/components/Map/components/MapProvider/hooks/index.d.ts.map +1 -1
  56. package/dist/types/components/Map/components/MapProvider/index.d.ts +2 -2
  57. package/dist/types/components/Map/components/MapProvider/index.d.ts.map +1 -1
  58. package/dist/types/components/Map/index.d.ts +3 -0
  59. package/dist/types/components/Map/index.d.ts.map +1 -1
  60. package/dist/types/components/Map/interfaces/index.d.ts +2 -0
  61. package/dist/types/components/Map/interfaces/index.d.ts.map +1 -1
  62. package/dist/types/components/Map/state/index.d.ts +8 -0
  63. package/dist/types/components/Map/state/index.d.ts.map +1 -1
  64. package/dist/types/components/Map/utils/helpers.d.ts +7 -0
  65. package/dist/types/components/Map/utils/helpers.d.ts.map +1 -1
  66. package/package.json +10 -6
@@ -0,0 +1,234 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { axisBottom } from 'd3-axis';
3
+ import { scaleTime } from 'd3-scale';
4
+ import { select } from 'd3-selection';
5
+ import { DomUtil, DomEvent } from 'leaflet';
6
+ import { DateTime } from 'luxon';
7
+ import { createPortal } from 'react-dom';
8
+ import { useContext, useState, useEffect, useMemo, useCallback, useRef } from 'react';
9
+ import { useMap } from 'react-leaflet';
10
+ import { MapPeriodFilterContext } from '../../../../state/index.js';
11
+ import { dateToDHIS2PeriodId } from '../../../../utils/helpers.js';
12
+
13
+ function resolveDates(tl) {
14
+ if ("step" in tl) {
15
+ const [start, end] = tl.range;
16
+ const dates = [];
17
+ let cur = DateTime.fromJSDate(start);
18
+ const endDt = DateTime.fromJSDate(end);
19
+ while (cur <= endDt) {
20
+ dates.push(cur.toJSDate());
21
+ cur = cur.plus(tl.step);
22
+ }
23
+ return dates;
24
+ }
25
+ return [...tl.range];
26
+ }
27
+ const ACCENT = "#2c6693";
28
+ const PADDING_LEFT = 40;
29
+ const PADDING_RIGHT = 20;
30
+ const LABEL_WIDTH = 80;
31
+ const PERIOD_RECT_HEIGHT = 8;
32
+ const PERIOD_RECT_GAP = 1;
33
+ const AXIS_OFFSET = 4;
34
+ function TimelineUI({
35
+ autoplay: initialAutoplay,
36
+ button,
37
+ dates,
38
+ interval,
39
+ onStep
40
+ }) {
41
+ const [index, setIndex] = useState(0);
42
+ const [playing, setPlaying] = useState(initialAutoplay);
43
+ const [svgWidth, setSvgWidth] = useState(null);
44
+ const svgRef = useRef(null);
45
+ const axisRef = useRef(null);
46
+ const intervalRef = useRef(null);
47
+ useEffect(() => {
48
+ const el = svgRef.current;
49
+ if (!el) return;
50
+ const measure = () => {
51
+ setSvgWidth(el.getBoundingClientRect().width);
52
+ };
53
+ measure();
54
+ const ro = new ResizeObserver(measure);
55
+ ro.observe(el);
56
+ return () => {
57
+ ro.disconnect();
58
+ };
59
+ }, []);
60
+ const trackWidth = svgWidth !== null ? svgWidth - PADDING_LEFT - PADDING_RIGHT : 0;
61
+ const timeScale = useMemo(() => {
62
+ if (!trackWidth || dates.length < 2) return null;
63
+ const periodMs = dates[1].getTime() - dates[0].getTime();
64
+ const domainEnd = new Date(dates[dates.length - 1].getTime() + periodMs);
65
+ return scaleTime().domain([dates[0], domainEnd]).range([0, trackWidth]);
66
+ }, [dates, trackWidth]);
67
+ useEffect(() => {
68
+ if (!timeScale || !axisRef.current || !trackWidth) return;
69
+ const maxTicks = Math.round(trackWidth / LABEL_WIDTH);
70
+ const axis = axisBottom(timeScale).ticks(maxTicks);
71
+ select(axisRef.current).call(axis);
72
+ select(axisRef.current).select(".domain").attr("stroke", "#ccc");
73
+ select(axisRef.current).selectAll(".tick line").attr("stroke", "#ccc");
74
+ select(axisRef.current).selectAll(".tick text").attr("fill", "#666").attr("font-size", "10px").attr("font-family", "sans-serif");
75
+ }, [timeScale, trackWidth]);
76
+ const stepTo = useCallback(
77
+ (idx) => {
78
+ setIndex(idx);
79
+ onStep(dates[idx]);
80
+ },
81
+ [dates, onStep]
82
+ );
83
+ useEffect(() => {
84
+ if (intervalRef.current) clearInterval(intervalRef.current);
85
+ if (!playing) return;
86
+ intervalRef.current = setInterval(() => {
87
+ setIndex((prev) => {
88
+ const next = prev + 1 >= dates.length ? 0 : prev + 1;
89
+ onStep(dates[next]);
90
+ return next;
91
+ });
92
+ }, interval);
93
+ return () => {
94
+ if (intervalRef.current) clearInterval(intervalRef.current);
95
+ };
96
+ }, [playing, interval, dates, onStep]);
97
+ const periodRects = useMemo(() => {
98
+ if (!timeScale || dates.length < 2) return null;
99
+ const unitWidth = timeScale(dates[1]) - timeScale(dates[0]);
100
+ return dates.map((date, i) => {
101
+ const x = timeScale(date);
102
+ const spanWidth = i < dates.length - 1 ? timeScale(dates[i + 1]) - x : unitWidth;
103
+ const w = Math.max(spanWidth - PERIOD_RECT_GAP, 1);
104
+ const capturedIndex = i;
105
+ return /* @__PURE__ */ jsx(
106
+ "rect",
107
+ {
108
+ fill: capturedIndex === index ? ACCENT : "#b0c4d8",
109
+ height: PERIOD_RECT_HEIGHT,
110
+ onClick: () => {
111
+ setPlaying(false);
112
+ stepTo(capturedIndex);
113
+ },
114
+ rx: 1,
115
+ style: { cursor: "pointer" },
116
+ width: w,
117
+ x,
118
+ y: 0
119
+ },
120
+ date.toISOString()
121
+ );
122
+ });
123
+ }, [timeScale, dates, index, stepTo]);
124
+ const RECT_ROW_Y = 6;
125
+ const svgHeight = RECT_ROW_Y + PERIOD_RECT_HEIGHT + AXIS_OFFSET + 24;
126
+ const playLabel = playing ? button?.playingText ?? "Pause" : button?.pausedText ?? "Play";
127
+ return /* @__PURE__ */ jsxs(
128
+ "svg",
129
+ {
130
+ "aria-label": "Timeline",
131
+ height: svgHeight,
132
+ ref: svgRef,
133
+ style: {
134
+ background: "white",
135
+ boxShadow: "0 -1px 4px rgba(0,0,0,0.15)",
136
+ display: "block",
137
+ width: "100%"
138
+ },
139
+ children: [
140
+ /* @__PURE__ */ jsxs(
141
+ "g",
142
+ {
143
+ "aria-label": playLabel,
144
+ onClick: () => {
145
+ setPlaying((p) => !p);
146
+ },
147
+ role: "button",
148
+ style: { cursor: "pointer" },
149
+ transform: `translate(7, ${RECT_ROW_Y - 8})`,
150
+ children: [
151
+ /* @__PURE__ */ jsx("path", { d: "M0 0h24v24H0z", fillOpacity: 0 }),
152
+ playing ? /* @__PURE__ */ jsx("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z", fill: ACCENT }) : /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7z", fill: ACCENT })
153
+ ]
154
+ }
155
+ ),
156
+ timeScale !== null && /* @__PURE__ */ jsx("g", { transform: `translate(${PADDING_LEFT}, ${RECT_ROW_Y})`, children: periodRects }),
157
+ timeScale !== null && /* @__PURE__ */ jsx(
158
+ "g",
159
+ {
160
+ ref: axisRef,
161
+ transform: `translate(${PADDING_LEFT}, ${RECT_ROW_Y + PERIOD_RECT_HEIGHT + AXIS_OFFSET})`
162
+ }
163
+ )
164
+ ]
165
+ }
166
+ );
167
+ }
168
+ function TimelineControl({
169
+ autoplay = false,
170
+ button,
171
+ interval = 1e3,
172
+ periodType,
173
+ position = "bottom",
174
+ timeline
175
+ }) {
176
+ const map = useMap();
177
+ const {
178
+ periodType: contextPeriodType,
179
+ setActivePeriod,
180
+ setPeriodType
181
+ } = useContext(MapPeriodFilterContext);
182
+ const [container, setContainer] = useState(null);
183
+ useEffect(() => {
184
+ if (periodType) setPeriodType(periodType);
185
+ }, [periodType, setPeriodType]);
186
+ const resolvedPeriodType = periodType ?? contextPeriodType ?? "Monthly";
187
+ const dates = useMemo(() => resolveDates(timeline), [timeline]);
188
+ useEffect(() => {
189
+ const mapEl = map.getContainer();
190
+ const div = DomUtil.create("div", "", mapEl);
191
+ DomEvent.disableClickPropagation(div);
192
+ DomEvent.disableScrollPropagation(div);
193
+ const isTop = position.startsWith("top");
194
+ Object.assign(div.style, {
195
+ bottom: isTop ? "auto" : "18px",
196
+ boxSizing: "border-box",
197
+ left: "0",
198
+ padding: isTop ? "8px 8px 0" : "0 8px 8px",
199
+ position: "absolute",
200
+ right: "0",
201
+ top: isTop ? "0" : "auto",
202
+ zIndex: "1000"
203
+ });
204
+ setContainer(div);
205
+ return () => {
206
+ div.remove();
207
+ setContainer(null);
208
+ };
209
+ }, [map, position]);
210
+ const handleStep = useCallback(
211
+ (date) => {
212
+ setActivePeriod(dateToDHIS2PeriodId(date, resolvedPeriodType));
213
+ },
214
+ [resolvedPeriodType, setActivePeriod]
215
+ );
216
+ if (!container) return null;
217
+ return createPortal(
218
+ /* @__PURE__ */ jsx(
219
+ TimelineUI,
220
+ {
221
+ autoplay,
222
+ button,
223
+ dates,
224
+ interval,
225
+ onStep: handleStep
226
+ }
227
+ ),
228
+ container
229
+ );
230
+ }
231
+
232
+ export { TimelineControl };
233
+ //# sourceMappingURL=index.js.map
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../src/components/Map/components/MapControls/components/TimelineControl/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { axisBottom } from \"d3-axis\";\nimport { scaleTime } from \"d3-scale\";\nimport { select } from \"d3-selection\";\nimport { DomEvent, DomUtil } from \"leaflet\";\nimport { DateTime, type DurationObjectUnits } from \"luxon\";\nimport { createPortal } from \"react-dom\";\nimport {\n\ttype ReactElement,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { useMap } from \"react-leaflet\";\nimport { MapPeriodFilterContext } from \"../../../../state/index.js\";\nimport { dateToDHIS2PeriodId, type DHIS2PeriodType } from \"../../../../utils/helpers.js\";\n\nexport type { DHIS2PeriodType };\n\nexport type TimelineRange =\n\t| { range: [Date, Date]; step: DurationObjectUnits }\n\t| { range: [Date, Date, ...Date[]] };\n\nexport type TimelinePosition =\n\t| \"bottom\"\n\t| \"bottomleft\"\n\t| \"bottomright\"\n\t| \"bottomcenter\"\n\t| \"top\"\n\t| \"topleft\"\n\t| \"topright\"\n\t| \"topcenter\";\n\nexport interface TimelineControlOptions {\n\tautoplay?: boolean;\n\tbutton?: {\n\t\tpausedText?: string;\n\t\tplayingText?: string;\n\t};\n\tinterval?: number;\n\tperiodType?: DHIS2PeriodType;\n\tposition?: TimelinePosition;\n\ttimeline: {\n\t\tdateFormat: string;\n\t} & TimelineRange;\n}\n\nfunction resolveDates(tl: TimelineControlOptions[\"timeline\"]): Date[] {\n\tif (\"step\" in tl) {\n\t\tconst [start, end] = tl.range;\n\t\tconst dates: Date[] = [];\n\t\tlet cur = DateTime.fromJSDate(start);\n\t\tconst endDt = DateTime.fromJSDate(end);\n\t\twhile (cur <= endDt) {\n\t\t\tdates.push(cur.toJSDate());\n\t\t\tcur = cur.plus(tl.step);\n\t\t}\n\t\treturn dates;\n\t}\n\treturn [...tl.range];\n}\n\nconst ACCENT = \"#2c6693\";\nconst PADDING_LEFT = 40; // space for play/pause icon\nconst PADDING_RIGHT = 20;\nconst LABEL_WIDTH = 80; // min pixels per axis tick label\nconst PERIOD_RECT_HEIGHT = 8;\nconst PERIOD_RECT_GAP = 1; // gap between adjacent period rects\nconst AXIS_OFFSET = 4; // pixels between rect bottom and axis line\n\n\nfunction TimelineUI({\n\tautoplay: initialAutoplay,\n\tbutton,\n\tdates,\n\tinterval,\n\tonStep,\n}: {\n\tautoplay: boolean;\n\tbutton?: TimelineControlOptions[\"button\"];\n\tdates: Date[];\n\tinterval: number;\n\tonStep: (date: Date) => void;\n}): ReactElement {\n\tconst [index, setIndex] = useState(0);\n\tconst [playing, setPlaying] = useState(initialAutoplay);\n\tconst [svgWidth, setSvgWidth] = useState<number | null>(null);\n\tconst svgRef = useRef<SVGSVGElement>(null);\n\tconst axisRef = useRef<SVGGElement>(null);\n\tconst intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n\tuseEffect(() => {\n\t\tconst el = svgRef.current;\n\t\tif (!el) return;\n\t\tconst measure = (): void => { setSvgWidth(el.getBoundingClientRect().width); };\n\t\tmeasure();\n\t\tconst ro = new ResizeObserver(measure);\n\t\tro.observe(el);\n\t\treturn () => { ro.disconnect(); };\n\t}, []);\n\n\tconst trackWidth = svgWidth !== null ? svgWidth - PADDING_LEFT - PADDING_RIGHT : 0;\n\n\tconst timeScale = useMemo(() => {\n\t\tif (!trackWidth || dates.length < 2) return null;\n\t\tconst periodMs = dates[1].getTime() - dates[0].getTime();\n\t\tconst domainEnd = new Date(dates[dates.length - 1].getTime() + periodMs);\n\t\treturn scaleTime()\n\t\t\t.domain([dates[0], domainEnd])\n\t\t\t.range([0, trackWidth]);\n\t}, [dates, trackWidth]);\n\n\tuseEffect(() => {\n\t\tif (!timeScale || !axisRef.current || !trackWidth) return;\n\t\tconst maxTicks = Math.round(trackWidth / LABEL_WIDTH);\n\t\tconst axis = axisBottom<Date>(timeScale).ticks(maxTicks);\n\t\tselect<SVGGElement, unknown>(axisRef.current).call(axis);\n\t\tselect(axisRef.current).select(\".domain\").attr(\"stroke\", \"#ccc\");\n\t\tselect(axisRef.current).selectAll(\".tick line\").attr(\"stroke\", \"#ccc\");\n\t\tselect(axisRef.current)\n\t\t\t.selectAll<SVGTextElement, unknown>(\".tick text\")\n\t\t\t.attr(\"fill\", \"#666\")\n\t\t\t.attr(\"font-size\", \"10px\")\n\t\t\t.attr(\"font-family\", \"sans-serif\");\n\t}, [timeScale, trackWidth]);\n\n\tconst stepTo = useCallback(\n\t\t(idx: number) => {\n\t\t\tsetIndex(idx);\n\t\t\tonStep(dates[idx]);\n\t\t},\n\t\t[dates, onStep],\n\t);\n\n\t// Autoplay interval\n\tuseEffect(() => {\n\t\tif (intervalRef.current) clearInterval(intervalRef.current);\n\t\tif (!playing) return;\n\t\tintervalRef.current = setInterval(() => {\n\t\t\tsetIndex((prev) => {\n\t\t\t\tconst next = prev + 1 >= dates.length ? 0 : prev + 1;\n\t\t\t\tonStep(dates[next]);\n\t\t\t\treturn next;\n\t\t\t});\n\t\t}, interval);\n\t\treturn () => {\n\t\t\tif (intervalRef.current) clearInterval(intervalRef.current);\n\t\t};\n\t}, [playing, interval, dates, onStep]);\n\n\tconst periodRects = useMemo(() => {\n\t\tif (!timeScale || dates.length < 2) return null;\n\t\tconst unitWidth = timeScale(dates[1]) - timeScale(dates[0]);\n\t\treturn dates.map((date, i) => {\n\t\t\tconst x = timeScale(date);\n\t\t\tconst spanWidth = i < dates.length - 1\n\t\t\t\t? timeScale(dates[i + 1]) - x\n\t\t\t\t: unitWidth;\n\t\t\tconst w = Math.max(spanWidth - PERIOD_RECT_GAP, 1);\n\t\t\tconst capturedIndex = i;\n\t\t\treturn (\n\t\t\t\t<rect\n\t\t\t\t\tkey={date.toISOString()}\n\t\t\t\t\tfill={capturedIndex === index ? ACCENT : \"#b0c4d8\"}\n\t\t\t\t\theight={PERIOD_RECT_HEIGHT}\n\t\t\t\t\tonClick={() => { setPlaying(false); stepTo(capturedIndex); }}\n\t\t\t\t\trx={1}\n\t\t\t\t\tstyle={{ cursor: \"pointer\" }}\n\t\t\t\t\twidth={w}\n\t\t\t\t\tx={x}\n\t\t\t\t\ty={0}\n\t\t\t\t/>\n\t\t\t);\n\t\t});\n\t}, [timeScale, dates, index, stepTo]);\n\n\n\tconst RECT_ROW_Y = 6;\n\tconst svgHeight = RECT_ROW_Y + PERIOD_RECT_HEIGHT + AXIS_OFFSET + 24\n\n\tconst playLabel = playing\n\t\t? (button?.playingText ?? \"Pause\")\n\t\t: (button?.pausedText ?? \"Play\");\n\n\treturn (\n\t\t<svg\n\t\t\taria-label=\"Timeline\"\n\t\t\theight={svgHeight}\n\t\t\tref={svgRef}\n\t\t\tstyle={{\n\t\t\t\tbackground: \"white\",\n\t\t\t\tboxShadow: \"0 -1px 4px rgba(0,0,0,0.15)\",\n\t\t\t\tdisplay: \"block\",\n\t\t\t\twidth: \"100%\",\n\t\t\t}}\n\t\t>\n\t\t\t{/* Play / Pause — Material Design SVG icon */}\n\t\t\t<g\n\t\t\t\taria-label={playLabel}\n\t\t\t\tonClick={() => { setPlaying((p) => !p); }}\n\t\t\t\trole=\"button\"\n\t\t\t\tstyle={{ cursor: \"pointer\" }}\n\t\t\t\ttransform={`translate(7, ${RECT_ROW_Y - 8})`}\n\t\t\t>\n\t\t\t\t<path d=\"M0 0h24v24H0z\" fillOpacity={0} />\n\t\t\t\t{playing\n\t\t\t\t\t? <path d=\"M6 19h4V5H6v14zm8-14v14h4V5h-4z\" fill={ACCENT} />\n\t\t\t\t\t: <path d=\"M8 5v14l11-7z\" fill={ACCENT} />\n\t\t\t\t}\n\t\t\t</g>\n\n\t\t\t{/* Period rectangles */}\n\t\t\t{timeScale !== null && (\n\t\t\t\t<g transform={`translate(${PADDING_LEFT}, ${RECT_ROW_Y})`}>\n\t\t\t\t\t{periodRects}\n\t\t\t\t</g>\n\t\t\t)}\n\n\t\t\t{/* D3 x-axis */}\n\t\t\t{timeScale !== null && (\n\t\t\t\t<g\n\t\t\t\t\tref={axisRef}\n\t\t\t\t\ttransform={`translate(${PADDING_LEFT}, ${RECT_ROW_Y + PERIOD_RECT_HEIGHT + AXIS_OFFSET})`}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</svg>\n\t);\n}\n\n\nexport function TimelineControl({\n\tautoplay = false,\n\tbutton,\n\tinterval = 1000,\n\tperiodType,\n\tposition = \"bottom\",\n\ttimeline,\n}: TimelineControlOptions): ReactElement | null {\n\tconst map = useMap();\n\tconst {\n\t\tperiodType: contextPeriodType,\n\t\tsetActivePeriod,\n\t\tsetPeriodType,\n\t} = useContext(MapPeriodFilterContext);\n\tconst [container, setContainer] = useState<HTMLElement | null>(null);\n\n\tuseEffect(() => {\n\t\tif (periodType) setPeriodType(periodType);\n\t}, [periodType, setPeriodType]);\n\n\tconst resolvedPeriodType = periodType ?? contextPeriodType ?? \"Monthly\";\n\tconst dates = useMemo(() => resolveDates(timeline), [timeline]);\n\n\tuseEffect(() => {\n\t\tconst mapEl = map.getContainer();\n\t\tconst div = DomUtil.create(\"div\", \"\", mapEl);\n\t\tDomEvent.disableClickPropagation(div);\n\t\tDomEvent.disableScrollPropagation(div);\n\n\t\tconst isTop = position.startsWith(\"top\");\n\t\tObject.assign(div.style, {\n\t\t\tbottom: isTop ? \"auto\" : \"18px\",\n\t\t\tboxSizing: \"border-box\",\n\t\t\tleft: \"0\",\n\t\t\tpadding: isTop ? \"8px 8px 0\" : \"0 8px 8px\",\n\t\t\tposition: \"absolute\",\n\t\t\tright: \"0\",\n\t\t\ttop: isTop ? \"0\" : \"auto\",\n\t\t\tzIndex: \"1000\",\n\t\t});\n\n\t\tsetContainer(div);\n\t\treturn () => {\n\t\t\tdiv.remove();\n\t\t\tsetContainer(null);\n\t\t};\n\t}, [map, position]);\n\n\tconst handleStep = useCallback(\n\t\t(date: Date) => {\n\t\t\tsetActivePeriod(dateToDHIS2PeriodId(date, resolvedPeriodType));\n\t\t},\n\t\t[resolvedPeriodType, setActivePeriod],\n\t);\n\n\tif (!container) return null;\n\n\treturn createPortal(\n\t\t<TimelineUI\n\t\t\tautoplay={autoplay}\n\t\t\tbutton={button}\n\t\t\tdates={dates}\n\t\t\tinterval={interval}\n\t\t\tonStep={handleStep}\n\t\t/>,\n\t\tcontainer,\n\t);\n}\n"]}
@@ -2,29 +2,34 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { ScaleControl, ZoomControl } from 'react-leaflet';
3
3
  import FullscreenControl from './components/FullscreenControl/index.js';
4
4
  import DownloadControl from './components/DownloadControl/index.js';
5
+ import { TimelineControl } from './components/TimelineControl';
5
6
 
6
- function MapControl({
7
- type,
8
- options,
9
- position,
10
- mapId
11
- }) {
7
+ function MapControl(props) {
8
+ const { type } = props;
12
9
  switch (type) {
13
10
  case "zoom":
14
- return /* @__PURE__ */ jsx(ZoomControl, { position, ...options });
11
+ return /* @__PURE__ */ jsx(ZoomControl, { position: props.position, ...props.options });
15
12
  case "scale":
16
- return /* @__PURE__ */ jsx(ScaleControl, { position, ...options });
13
+ return /* @__PURE__ */ jsx(ScaleControl, { position: props.position, ...props.options });
17
14
  case "fullscreen":
18
- return /* @__PURE__ */ jsx(FullscreenControl, { position, ...options });
15
+ return /* @__PURE__ */ jsx(
16
+ FullscreenControl,
17
+ {
18
+ position: props.position,
19
+ ...props.options
20
+ }
21
+ );
19
22
  case "print":
20
23
  return /* @__PURE__ */ jsx(
21
24
  DownloadControl,
22
25
  {
23
- mapId,
24
- position,
25
- options
26
+ mapId: props.mapId,
27
+ options: props.options,
28
+ position: props.position
26
29
  }
27
30
  );
31
+ case "timeline":
32
+ return /* @__PURE__ */ jsx(TimelineControl, { ...props });
28
33
  default:
29
34
  return null;
30
35
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../src/components/Map/components/MapControls/index.tsx"],"names":[],"mappings":";;;;;AAUe,SAAR,UAAA,CAA4B;AAAA,EAClC,IAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA;AACD,CAAA,EAAoB;AACnB,EAAA,QAAQ,IAAA;AAAM,IACb,KAAK,MAAA;AACJ,MAAA,uBAAO,GAAA,CAAC,WAAA,EAAA,EAAY,QAAA,EAAqB,GAAG,OAAA,EAAS,CAAA;AAAA,IACtD,KAAK,OAAA;AACJ,MAAA,uBAAO,GAAA,CAAC,YAAA,EAAA,EAAa,QAAA,EAAqB,GAAG,OAAA,EAAS,CAAA;AAAA,IACvD,KAAK,YAAA;AACJ,MAAA,uBAAO,GAAA,CAAC,iBAAA,EAAA,EAAkB,QAAA,EAAqB,GAAG,OAAA,EAAS,CAAA;AAAA,IAC5D,KAAK,OAAA;AACJ,MAAA,uBACC,GAAA;AAAA,QAAC,eAAA;AAAA,QAAA;AAAA,UACA,KAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA;AAAA,OACD;AAAA,IAEF;AACC,MAAA,OAAO,IAAA;AAAA;AAEV","file":"index.js","sourcesContent":["import React from \"react\";\nimport { ScaleControl, ZoomControl } from \"react-leaflet\";\nimport { MapControls } from \"../MapArea/interfaces/index.js\";\nimport FullscreenControl from \"./components/FullscreenControl/index.js\";\nimport DownloadControl from \"./components/DownloadControl/index.js\";\n\nexport interface MapControlProps extends MapControls {\n\tmapId: string;\n}\n\nexport default function MapControl({\n\ttype,\n\toptions,\n\tposition,\n\tmapId,\n}: MapControlProps) {\n\tswitch (type) {\n\t\tcase \"zoom\":\n\t\t\treturn <ZoomControl position={position} {...options} />;\n\t\tcase \"scale\":\n\t\t\treturn <ScaleControl position={position} {...options} />;\n\t\tcase \"fullscreen\":\n\t\t\treturn <FullscreenControl position={position} {...options} />;\n\t\tcase \"print\":\n\t\t\treturn (\n\t\t\t\t<DownloadControl\n\t\t\t\t\tmapId={mapId}\n\t\t\t\t\tposition={position}\n\t\t\t\t\toptions={options}\n\t\t\t\t/>\n\t\t\t);\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n"]}
1
+ {"version":3,"sources":["../../../../../../src/components/Map/components/MapControls/index.tsx"],"names":[],"mappings":";;;;;;AAOe,SAAR,WAA4B,KAAA,EAAwC;AAC1E,EAAA,MAAM,EAAE,MAAK,GAAI,KAAA;AACjB,EAAA,QAAQ,IAAA;AAAM,IACb,KAAK,MAAA;AACJ,MAAA,2BAAQ,WAAA,EAAA,EAAY,QAAA,EAAU,MAAM,QAAA,EAAW,GAAG,MAAM,OAAA,EAAS,CAAA;AAAA,IAClE,KAAK,OAAA;AACJ,MAAA,2BACE,YAAA,EAAA,EAAa,QAAA,EAAU,MAAM,QAAA,EAAW,GAAG,MAAM,OAAA,EAAS,CAAA;AAAA,IAE7D,KAAK,YAAA;AACJ,MAAA,uBACC,GAAA;AAAA,QAAC,iBAAA;AAAA,QAAA;AAAA,UACA,UAAU,KAAA,CAAM,QAAA;AAAA,UACf,GAAG,KAAA,CAAM;AAAA;AAAA,OACX;AAAA,IAEF,KAAK,OAAA;AACJ,MAAA,uBACC,GAAA;AAAA,QAAC,eAAA;AAAA,QAAA;AAAA,UACA,OAAO,KAAA,CAAM,KAAA;AAAA,UACb,SAAS,KAAA,CAAM,OAAA;AAAA,UACf,UAAU,KAAA,CAAM;AAAA;AAAA,OACjB;AAAA,IAEF,KAAK,UAAA;AACJ,MAAA,uBAAO,GAAA,CAAC,eAAA,EAAA,EAAiB,GAAG,KAAA,EAAO,CAAA;AAAA,IACpC;AACC,MAAA,OAAO,IAAA;AAAA;AAEV","file":"index.js","sourcesContent":["import React from \"react\";\nimport { ScaleControl, ZoomControl } from \"react-leaflet\";\nimport { MapControls } from \"../MapArea/interfaces\";\nimport FullscreenControl from \"./components/FullscreenControl/index.js\";\nimport DownloadControl from \"./components/DownloadControl/index.js\";\nimport { TimelineControl } from \"./components/TimelineControl\";\n\nexport default function MapControl(props: MapControls & { mapId: string }) {\n\tconst { type } = props;\n\tswitch (type) {\n\t\tcase \"zoom\":\n\t\t\treturn <ZoomControl position={props.position} {...props.options} />;\n\t\tcase \"scale\":\n\t\t\treturn (\n\t\t\t\t<ScaleControl position={props.position} {...props.options} />\n\t\t\t);\n\t\tcase \"fullscreen\":\n\t\t\treturn (\n\t\t\t\t<FullscreenControl\n\t\t\t\t\tposition={props.position}\n\t\t\t\t\t{...props.options}\n\t\t\t\t/>\n\t\t\t);\n\t\tcase \"print\":\n\t\t\treturn (\n\t\t\t\t<DownloadControl\n\t\t\t\t\tmapId={props.mapId}\n\t\t\t\t\toptions={props.options}\n\t\t\t\t\tposition={props.position}\n\t\t\t\t/>\n\t\t\t);\n\t\tcase \"timeline\":\n\t\t\treturn <TimelineControl {...props} />;\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n"]}
@@ -1,9 +1,10 @@
1
- import { compact, find, isEmpty, differenceBy, sortBy, last, head } from 'lodash';
2
- import { useMapOrganisationUnit, useMapPeriods } from '../../../hooks';
3
- import { useState, useMemo, useCallback } from 'react';
4
- import { getOrgUnitsSelection, sanitizeDate, sanitizeOrgUnits, toGeoJson, generateLegends } from '../../../../../utils';
1
+ import { compact, find, isEmpty, sortBy, last, head, differenceBy } from 'lodash';
2
+ import { useMapOrganisationUnit, useMapPeriods, useMapPeriodFilter } from '../../../hooks';
3
+ import { useState, useMemo, useRef, useCallback } from 'react';
4
+ import { getOrgUnitsSelection, sanitizeOrgUnits, toGeoJson, generateLegends } from '../../../../../utils';
5
5
  import { useDataEngine } from '@dhis2/app-runtime';
6
6
  import { map, asyncify } from 'async-es';
7
+ import { computeTimelinePeriods } from '../../../../../utils/helpers.js';
7
8
  import { defaultColorScaleName, defaultClasses } from '../../../../../utils/colors.js';
8
9
  import { useGoogleEngineToken } from '../../../../MapLayer/components/GoogleEngineLayer/hooks/index.js';
9
10
  import { useBoundaryData } from '../../../../MapLayer/components/BoundaryLayer/hooks/useBoundaryData.js';
@@ -14,13 +15,13 @@ const analyticsQuery = {
14
15
  analytics: {
15
16
  resource: "analytics",
16
17
  params: ({ ou, pe, dx, startDate, endDate, analyticsOptions }) => {
17
- const peDimension = !isEmpty(pe) ? `pe:${pe?.join(";")}` : void 0;
18
+ const usingDateRange = !isEmpty(startDate) && !isEmpty(endDate);
19
+ const peDimension = !usingDateRange && !isEmpty(pe) ? `pe:${pe?.join(";")}` : void 0;
18
20
  const ouDimension = !isEmpty(ou) ? `ou:${ou?.join(";")}` : void 0;
19
21
  const dxDimension = !isEmpty(dx) ? `dx:${dx?.join(";")}` : void 0;
20
22
  return {
21
23
  dimension: compact([dxDimension, peDimension, ouDimension]),
22
- startDate,
23
- endDate,
24
+ ...usingDateRange ? { startDate, endDate } : {},
24
25
  displayProperty: "NAME",
25
26
  ...analyticsOptions ?? {}
26
27
  };
@@ -77,24 +78,32 @@ function useThematicLayers({
77
78
  const [loading, setLoading] = useState(false);
78
79
  const { orgUnits, orgUnitSelection } = useMapOrganisationUnit();
79
80
  const { periods, range } = useMapPeriods() ?? {};
81
+ const { activePeriod, periodType } = useMapPeriodFilter();
80
82
  const ou = useMemo(
81
83
  () => getOrgUnitsSelection(orgUnitSelection),
82
84
  [orgUnitSelection]
83
85
  );
84
- const pe = useMemo(() => periods?.map((pe2) => pe2.id), [periods]);
86
+ const timelinePeriods = useMemo(() => {
87
+ if (!range || !periodType) return null;
88
+ return computeTimelinePeriods(range, periodType);
89
+ }, [range, periodType]);
90
+ const pe = useMemo(() => {
91
+ if (timelinePeriods) return timelinePeriods;
92
+ return periods?.map((pe2) => pe2.id);
93
+ }, [periods, timelinePeriods]);
94
+ const toISODate = (date) => date.toISOString().slice(0, 10);
85
95
  const { startDate, endDate } = useMemo(() => {
86
- if (!range) {
87
- return {
88
- startDate: void 0,
89
- endDate: void 0
90
- };
96
+ if (timelinePeriods || !range) {
97
+ return { startDate: void 0, endDate: void 0 };
91
98
  }
92
99
  return {
93
- startDate: sanitizeDate(range.start.toDateString()),
94
- endDate: sanitizeDate(range.end.toDateString())
100
+ startDate: toISODate(range.start),
101
+ endDate: toISODate(range.end)
95
102
  };
96
- }, [range]);
97
- const sanitizeData = (data, layer) => {
103
+ }, [range, timelinePeriods]);
104
+ const analyticsDataRef = useRef(null);
105
+ const legendSetsRef = useRef(/* @__PURE__ */ new Map());
106
+ const sanitizeData = (data, layer, currentPeriod) => {
98
107
  if (data) {
99
108
  const { analytics } = data;
100
109
  const rows = analytics?.rows;
@@ -107,18 +116,20 @@ function useThematicLayers({
107
116
  const valueIndex = analytics.headers.findIndex(
108
117
  (header) => header.name === "value"
109
118
  );
119
+ const peIndex = analytics.headers.findIndex(
120
+ (header) => header.name === "pe"
121
+ );
110
122
  if (!isEmpty(rows)) {
111
123
  return sortBy(
112
- orgUnits?.map((ou2) => {
113
- const row = rows.find(
114
- (row2) => row2[ouIndex] === ou2.id && row2[dxIndex] === layer.dataItem.id
124
+ orgUnits?.map((orgUnit) => {
125
+ const matchingRows = rows.filter(
126
+ (row) => row[ouIndex] === orgUnit.id && row[dxIndex] === layer.dataItem.id && (currentPeriod && peIndex >= 0 ? row[peIndex] === currentPeriod : true)
115
127
  );
128
+ const values = matchingRows.map((row) => parseFloat(row[valueIndex])).filter((v) => !isNaN(v));
116
129
  return {
117
- orgUnit: ou2,
118
- data: row ? parseFloat(row[valueIndex]) : void 0,
119
- dataItem: {
120
- ...layer.dataItem
121
- }
130
+ orgUnit,
131
+ data: values.length > 0 ? values.reduce((sum, v) => sum + v, 0) / values.length : void 0,
132
+ dataItem: { ...layer.dataItem }
122
133
  };
123
134
  }),
124
135
  ["data"]
@@ -135,15 +146,17 @@ function useThematicLayers({
135
146
  try {
136
147
  const legends = [];
137
148
  if (layer.dataItem.legendSet) {
138
- const legendSetData = await engine.query(
139
- legendSetsQuery,
140
- {
141
- variables: {
142
- id: layer.dataItem.legendSet
143
- }
149
+ let legendSet = legendSetsRef.current.get(layer.dataItem.legendSet);
150
+ if (!legendSet) {
151
+ const legendSetData = await engine.query(
152
+ legendSetsQuery,
153
+ { variables: { id: layer.dataItem.legendSet } }
154
+ );
155
+ legendSet = legendSetData?.legendSets;
156
+ if (legendSet) {
157
+ legendSetsRef.current.set(layer.dataItem.legendSet, legendSet);
144
158
  }
145
- );
146
- const legendSet = legendSetData?.legendSets;
159
+ }
147
160
  if (legendSet) {
148
161
  legends.push(...legendSet.legends);
149
162
  }
@@ -159,17 +172,11 @@ function useThematicLayers({
159
172
  const autoLegends = generateLegends(
160
173
  last(sortedData)?.data ?? 0,
161
174
  head(sortedData)?.data ?? 0,
162
- {
163
- classesCount: scale,
164
- colorClass
165
- }
175
+ { classesCount: scale, colorClass }
166
176
  );
167
177
  legends.push(...autoLegends);
168
178
  }
169
- return {
170
- ...layer,
171
- legends
172
- };
179
+ return { ...layer, legends };
173
180
  } catch (e) {
174
181
  return layer;
175
182
  }
@@ -180,38 +187,25 @@ function useThematicLayers({
180
187
  try {
181
188
  setLoading(true);
182
189
  const layersWithoutData = layers?.filter((layer) => !layer.data);
183
- const layersWithData = differenceBy(
184
- layers,
185
- layersWithoutData,
186
- "id"
187
- );
190
+ const layersWithData = differenceBy(layers, layersWithoutData, "id");
188
191
  const dx = layersWithoutData.map((layer) => layer.dataItem.id);
189
192
  let sanitizedLayersWithData = [];
190
193
  if (!isEmpty(dx)) {
191
194
  const data = await engine.query(analyticsQuery, {
192
- variables: {
193
- dx,
194
- ou,
195
- pe,
196
- startDate,
197
- endDate,
198
- analyticsOptions
199
- }
195
+ variables: { dx, ou, pe, startDate, endDate, analyticsOptions }
200
196
  });
197
+ analyticsDataRef.current = data;
201
198
  sanitizedLayersWithData = layersWithoutData.map((layer) => ({
202
199
  ...layer,
203
200
  name: layer?.name ?? layer?.dataItem?.displayName ?? layer.id,
204
- data: sanitizeData(data, layer)
201
+ data: sanitizeData(data, layer, activePeriod)
205
202
  }));
206
203
  }
207
204
  const sanitizedLayersWithOrgUnits = layersWithData.map((layer) => ({
208
205
  ...layer,
209
206
  data: layer.data?.map((datum) => ({
210
207
  ...datum,
211
- orgUnit: find(orgUnits, [
212
- "id",
213
- datum.orgUnit
214
- ]),
208
+ orgUnit: find(orgUnits, ["id", datum.orgUnit]),
215
209
  dataItem: layer.dataItem,
216
210
  name: layer?.name ?? layer?.dataItem?.displayName ?? layer.id
217
211
  }))
@@ -227,8 +221,21 @@ function useThematicLayers({
227
221
  return [];
228
222
  }
229
223
  };
224
+ const refilterLayers = useCallback(
225
+ async (layers) => {
226
+ if (!analyticsDataRef.current) return [];
227
+ const processed = layers.filter((layer) => !layer.data).map((layer) => ({
228
+ ...layer,
229
+ name: layer?.name ?? layer?.dataItem?.displayName ?? layer.id,
230
+ data: sanitizeData(analyticsDataRef.current, layer, activePeriod)
231
+ }));
232
+ return sanitizeLegends(processed);
233
+ },
234
+ [activePeriod, orgUnits]
235
+ );
230
236
  return {
231
237
  sanitizeLayers,
238
+ refilterLayers,
232
239
  loading
233
240
  };
234
241
  }