mockup: spatial hotspot analysis for thematic layers#3684
Draft
BRaimbault wants to merge 11 commits into
Draft
Conversation
Add SPATIAL_NONE/GI/LISA method constants, WEIGHTS_CONTIGUITY/DISTANCE_BAND/KNN weight type constants, and four guardrail alert codes (WARNING_RATE_DENOMINATOR, WARNING_LOW_N, WARNING_NO_NEIGHBORS, WARNING_SPARSE_COORDS). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Needed for distance-band and kNN spatial weight construction in the upcoming spatial analysis feature. @turf/centroid is already present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… with tests - buildSpatialWeights: queen/rook contiguity (coordinate-set comparison), distance-band and kNN (via @turf/centroid + @turf/distance), row-standardized; islands go into noNeighborIds with empty rows rather than zero-rows. - getGiStar (Ord & Getis 1995): includes self in extended neighbor set (Gi* variant), two-sided normal p-value, FDR (BH) or Bonferroni correction. - getLisa (Anselin 1995): conditional permutation (999 default) with mulberry32 seeded RNG for reproducibility; self EXCLUDED from permutation pool (esda issue #86); two-sided pseudo-p capped at 1; BH/Bonferroni correction; GeoDa/PySAL quadrant schemes. Tests cover: all weight types, island detection, row-standardization, z-score direction, p-value bounds, reproducibility, quadrant scheme agreement on HH/LL, FDR monotonicity, and degenerate inputs (all-missing, all-same-value). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- colors.js: SPATIAL_GI_COLOR_SCALE = 'RdBu_reverse' — diverging palette for Gi*
z-scores (blue=cold/negative, red=hot/positive). The loader uses this with the
existing getColorPalette + getAutomaticLegendItems + CLASSIFICATION_STANDARD_DEVIATION,
so no new classification path is needed.
- legend.js: buildLisaLegendItems() — returns 5 fixed items (HH/LL/HL/LH/NS) in
the standard { name, color, count } shape using GeoDa colors. Lookup at render
time is legendItems.find(item => item.cluster === cluster).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rdrails Gated by config.spatialAnalysis.method (SPATIAL_GI or SPATIAL_LISA); no-op when absent or set to SPATIAL_NONE, so existing maps are unaffected. Spatial step (single-map only): - buildSpatialWeights → getGiStar or getLisa after valueById is built. - Gi*: diverging choropleth via getAutomaticLegendItems + CLASSIFICATION_STANDARD_DEVIATION over z-scores with the RdBu_reverse palette. - LISA: categorical 5-item legend (buildLisaLegendItems) with cluster-key lookup. - Feature properties carry spatialZ/spatialI/spatialP/spatialSignificant alongside raw value and formatted value for popup and data table display. - No-neighbor units rendered in grey with "No neighbors" label, excluded from counts. Guardrails (push onto alerts array when spatial is active): - WARNING_LOW_N: fewer than 30 units. - WARNING_NO_NEIGHBORS: any island units detected by buildSpatialWeights. - WARNING_SPARSE_COORDS: >10% of org units lack geometry. - WARNING_RATE_DENOMINATOR: DATA_ELEMENT data item whose name does not contain rate/coverage/proportion/per/ratio/prevalence keywords. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New "Analysis" tab (single-map only — hidden for Timeline/Split) with:
Primary controls: method (Off / Gi* / LISA), spatial weights type, distance
threshold (distance band), k (kNN), significance α.
Advanced section (collapsed by default): permutations (LISA), multiple-comparison
correction, quadrant scheme (LISA), row-standardize toggle, random seed.
Denominator warning: NoticeBox shown when method is active and the selected
data item is a DATA_ELEMENT whose name lacks rate/coverage/proportion/ratio/
prevalence keywords — nudges users toward per-capita indicators.
Redux: LAYER_EDIT_SPATIAL_ANALYSIS_SET action + reducer merge-patch on
state.spatialAnalysis. Config round-trips via LayerEdit's {...layer} spread
so saved maps reproduce exactly. Seed is auto-generated on first activation
and persisted so results are reproducible when the map is shared.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename centroid/distance imports to turfCentroid/turfDistance to avoid name collision with local variables - Make WEIGHTS_KNN branch explicit (else if) to satisfy no-else-return - Refactor edgeKey to take two array params (max-params lint rule) - Remove unused `weights` destructuring from getGiStar - Remove unused `id` binding in spatialStats.spec.js loop - Remove unused SPATIAL_NONE import from ThematicDialog - Remove unused dataItem prop from SpatialAnalysisSection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
colorbrewer.js only auto-generates '_reverse' variants for sequential
scales, not diverging ones like RdBu, so getColorPalette('RdBu_reverse', n)
returned undefined and crashed on classes lookup. Use the native 'RdBu'
scale and reverse the returned array in the loader so cold (low z) stays
blue and hot (high z) stays red.
Also regenerate i18n/en.pot to pick up translatable strings added by the
spatial analysis UI in earlier commits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gi* previously labeled bins with raw sigma-of-z-score ranges (e.g. "-1.5 – 1.5"), which is meaningless without statistics background and was also computed from the sample distribution of z-scores rather than the calibrated standard normal — the displayed bins didn't actually correspond to any significance threshold. Replace it with the conventional "Hot Spot Analysis" labeling: fixed Cold/Hot spot bins at 90/95/99% confidence, derived directly from each unit's corrected (FDR/Bonferroni) p-value via a new `tier` field on the Gi* result. Only tiers reachable under the chosen alpha are shown, so e.g. alpha=0.01 never displays an unreachable 90%/95% bin. LISA quadrant names (High-High etc.) get a parenthetical explaining what each one means. Also fixes a latent bug where Gi* no-neighbor (island) units never showed "No neighbors" in the legend: the check required both `stat.z` and `stat.I` to be null, but those fields belong to different methods (Gi* only sets z, LISA only sets I), so the Gi* branch could never match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ALPHA_OPTIONS used value: '0.10', but the selected option is computed as
String(alpha) where alpha is stored as a Number — Number('0.10') stringifies
back to '0.1', not '0.10', so the SingleSelect could no longer find a
matching option and crashed with "There is no option with the value: 0.1".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SpatialAnalysisSection used raw @dhis2/ui components (SingleSelectField,
InputField, Checkbox, Button) instead of this app's core/ wrappers
(SelectField, NumberField, Checkbox) that every other tab in this dialog
is built from. That meant manual value<->string conversions the rest of
the codebase doesn't need to do — and was the direct cause of the
"There is no option with the value" crash from hand-written option
strings like '0.10' silently drifting from Number('0.10').toString().
Switching to SelectField (which takes {id, name} items and compares via
a single consistent String(id)) removes that whole class of bug.
Also:
- Replace the "Show/Hide advanced options" Button toggle — not a pattern
used anywhere else in this dialog — with a second always-visible
column ("Advanced settings"), matching the Style tab's two-column
layout and TrackedEntityDialog's section-header convention.
- Fix the data-quality NoticeBox overflowing the dialog: it was using
the `.notice` class (vertical margin only), so it inherited the
`.tabContent` bleed margin with no horizontal inset to compensate,
unlike `.flexColumn` siblings. New `.fullWidthNotice` composes the
existing `.flexRowFlow` (already used for full-width rows elsewhere
in this dialog) instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Contributor
|
🚀 Deployed on https://pr-3684.maps.netlify.dhis2.org |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Mockup for DHIS2-21461
DO NOT MERGE
Description
What: Adds inferential spatial statistics — Getis-Ord Gi* and Local Moran's I (LISA) — as an optional "Analysis" tab on the thematic layer edit dialog. Gated entirely behind
config.spatialAnalysis, so existing saved maps are unaffected.core/field wrappers, two-column style)This is a proof of concept for the first slice of a larger epic — heatmap/DBSCAN clustering for events and multidimensional thematic maps are planned as follow-ups, not included here.
Screenshots