feat: new classification methods and edge-case handling [PR2] [DHIS2-18242]#3646
Open
BRaimbault wants to merge 10 commits into
Open
feat: new classification methods and edge-case handling [PR2] [DHIS2-18242]#3646BRaimbault wants to merge 10 commits into
BRaimbault wants to merge 10 commits into
Conversation
…og, sd) [DHIS2-21142]
Contributor
|
🚀 Deployed on https://pr-3646.maps.netlify.dhis2.org |
BRaimbault
commented
Apr 24, 2026
Collaborator
Author
BRaimbault
left a comment
There was a problem hiding this comment.
Ready for review.
…reducer [DHIS2-21142]
edoardo
approved these changes
Apr 27, 2026
60a3f6b to
9b7bc5b
Compare
9b7bc5b to
60caa87
Compare
|
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.



Parent
Implements
Overview
Second PR in the series extracted from the parent epic. Expands the classification engine with five new methods and hardens edge-case handling across all methods. Builds on the
valueFormatplumbing from PR 1.Changes
Five new classification methods DHIS2-21142
Added to the "Classification" dropdown, in addition to the existing Equal intervals and Equal counts:
Natural breaks (clusters) is the only method where non-last bin
endValues are inclusive, since clusters can have gaps.getLegendItemForValuenow accepts amethodparameter to handle this correctly.New dependency:
simple-statistics@^7.8.9forckmeans,mean,standardDeviation.Also fixes the "Automatic" radio button highlight in
LegendTypeSelect. Master only mappedEQUAL_COUNTSback to theEQUAL_INTERVALSid, so when a user picked any of the new auto methods (natural breaks, pretty breaks, logarithmic, std-dev) the radio appeared deselected. The fix usesgetClassificationTypes()membership to detect any auto method and maps all of them to a singleCLASSIFICATION_AUTO_DEFAULTconstant — also serving as the default classification method when no method is set, so changing the default updates both consistently.Also fixes
LAYER_EDIT_CLASSIFICATION_SETin the reducer, which previously preservedcolorScale/classesonly when switching betweenEQUAL_INTERVALSandEQUAL_COUNTS; it now usesgetClassificationTypes()so all auto methods are treated consistently.Files:
src/util/classify.js,src/constants/layers.js,src/loaders/thematicLoader.js,src/util/styleByDataItem.js,src/components/classification/LegendTypeSelect.jsx,src/reducers/layerEdit.js,package.jsonSingle-value class matching and duplicate bin removal DHIS2-12860
Two changes in
classify.jsto handle duplicate-heavy data:getLegendItemForValuenow matches classes wherestartValue === endValuevia equality rather than range comparison, so single-value classes (e.g.{startValue: 100, endValue: 100}) are actually reachable.getLegendItemsfilters consecutive duplicate bins from the dispatcher output, preventing equal-counts classification from producing unreachable[n, n]bins when many values are identical.Files:
src/util/classify.jsHandle classifications with few distinct values DHIS2-8478
Two guards added to the
getLegendItemsdispatcher:[v, v]regardless of method. Protects downstream methods (notablygetPrettyBreaks, whereMath.log10(0) = -Infinitywould otherwise propagate NaN) and guarantees a sensible legend when a layer filters to a uniform-value dataset.k = min(numClasses, distinctValues.length)) for methods that partition the data (equal counts, natural breaks variants). Methods that derive bins from the data range (equal intervals, logarithmic, std-dev, pretty breaks) are unaffected.No silent method switching — user-selected method is preserved, result size simply adapts to the data.
Files:
src/util/classify.jsFix NaN in bubble legend for equal values DHIS2-20818
When a thematic bubble layer has all values equal, the bubble radius calculation hit
precisionRound(0, maxValue)→NaN, rendering the legend with "NaN" labels.Extracted a
getBubbleValueFormathelper that bypassesprecisionRoundfor the equal-values case. BothcreateBubbleItemsandcreateSingleColorBubblesnow produce a single bubble matching the map's rendered size.Composes with DHIS2-8478's single-bin short-circuit: classification returns one class, bubble layer renders one bubble, legend shows one bubble. Consistent end-to-end.
Also guards
BubblesagainstundefinedminValue/maxValue— a crash path introduced by theminValue === maxValueshortcut ingetBubbleValueFormat(undefined === undefinedis true, returning(n) => n.toString()which throws onundefined). When no data range is present butnoDataColoris set, the component now renders a compact legend with just the no-data circle.Files:
src/util/bubbles.js,src/components/legend/Bubbles.jsxAuto-select legend set when switching to predefined type DHIS2-21356
When a user switched the "Legend type" to "Predefined" without explicitly picking a legend set, the layer would save with
legendSet: nulland break on reload. The pre-existinguseEffectinNumericLegendStyleonly fired when the data item had an associated legend set — leaving the no-association case unhandled.Moved legend-set auto-selection into
LegendSetSelect, where the available list is guaranteed to be loaded. PassdataItem?.legendSetdown asdefaultLegendSet; the component prefers that, falls back to the first available legend set when the association is missing or the associated set isn't in the user's list.Files:
src/components/classification/LegendSetSelect.jsx,src/components/classification/NumericLegendStyle.jsxTests update
src/util/__tests__/bubbles.spec.jssrc/util/__tests__/classify.spec.jsManual testing
Netlify: https://pr-3646.maps.netlify.dhis2.org/ + Instance: https://dev.im.dhis2.org/maps-app-42-3
Quality checklist
Add N/A to items that are not applicable.