Skip to content

feat: no data / unclassified / isolated classes + thematicLoader refactor [PR5] [DHIS2-18242]#3649

Open
BRaimbault wants to merge 11 commits into
feat/DHIS2-18242-PR4from
feat/DHIS2-18242-PR5
Open

feat: no data / unclassified / isolated classes + thematicLoader refactor [PR5] [DHIS2-18242]#3649
BRaimbault wants to merge 11 commits into
feat/DHIS2-18242-PR4from
feat/DHIS2-18242-PR5

Conversation

@BRaimbault
Copy link
Copy Markdown
Collaborator

@BRaimbault BRaimbault commented Apr 30, 2026

Parent

Implements

  • DHIS2-19812: No data and unclassified classes for thematic layers
  • DHIS2-15514: Isolated class for layer classification
  • DHIS2-10823: Improve predefined legend display by detecting range-encoded names
  • DHIS2-3156: Move decimalPlaces from legend items to the legend object (follow-up from PR4)

Overview

Fifth PR in the series extracted from the parent epic. Adds configurable no data and unclassified classes that let users assign a color and label to org units with missing values or values outside the legend range. Adds a configurable isolated class that pins a specific value range to a fixed color, independent of the automatic classification. Improves predefined legend display by detecting when item names already encode the range. Fixes logarithmic and standard deviation classification to route non-classifiable values to the unclassified bucket rather than silently degrading them. Includes a significant thematicLoader refactor to support all of the above cleanly.

Changes

No data and unclassified classes DHIS2-19812

Two new optional legend classes are added to the thematic layer dialog:

  • No data — shown for org units that have no value in the analytics response. Previously only a color could be set (noDataColor); now both color and name are configurable via a noDataLegend object.
  • Unclassified — shown for org units whose value exists but does not map to any legend class (predefined legend: value outside all ranges; logarithmic: value ≤ 0; standard deviation: value outside the σ-aligned outer bounds).

Both are rendered as optional rows in the dialog using a new shared OptionalLegendItem component. The values are stored as noDataLegend and unclassifiedLegend on the layer config, serialized into config JSON on save (with noDataColor also written for backward compatibility), and parsed back on load via parseJsonConfig.

In thematicLoader, the two items are appended to legend.items with isNoData: true / isUnclassified: true flags. Feature styling helpers (getFeatureColor, getFeatureLegend, getFeatureRadius, countLegendItem) use these flags to assign color, legend text, radius, and count. Features with no matching class are either routed to the configured class or filtered out of the data array.

The same routing is applied in styleByDataItem.js for event layer style-by-data-item: isUnclassified is now set for any valued feature that returns no legend item, not only for predefined classification.

For the logarithmic method, non-positive values are filtered from the classification input (instead of falling back to equal intervals), and clamping is disabled so they return undefinedisUnclassified. For the standard deviation method, breaks are no longer clipped to [minValue, maxValue]; instead σ-aligned outer bounds are computed at μ ± (maxOffset + 1)σ, and values outside those bounds return undefinedisUnclassified.

The unclassified class is also supported for facilities and org units layers when styled by a group set. In getStyledOrgUnits (orgUnits.js), features with no matching group in the active group set receive the unclassifiedLegend color and are retained in the feature array — previously, facilities with no group membership were always filtered out.

Files: src/loaders/thematicLoader.js, src/loaders/facilityLoader.js, src/loaders/orgUnitLoader.js, src/util/classify.js, src/util/legend.js, src/util/orgUnits.js, src/util/styleByDataItem.js, src/util/favorites.js, src/components/edit/shared/NoDataLegend.jsx, src/components/edit/shared/UnclassifiedLegend.jsx, src/components/edit/shared/OptionalLegendItem.jsx, src/components/edit/thematic/ThematicDialog.jsx, src/components/edit/event/EventDialog.jsx, src/components/edit/FacilityDialog.jsx, src/components/edit/orgUnit/OrgUnitDialog.jsx, src/components/legend/Bubbles.jsx, src/components/legend/Legend.jsx, src/components/map/layers/ThematicLayer.jsx, src/constants/actionTypes.js, src/actions/layerEdit.js, src/reducers/layerEdit.js, src/util/__tests__/orgUnits.spec.js

Isolated class DHIS2-15514

A configurable isolated class pins a specific value range [min, max] to a fixed color. Values inside the range are assigned the isolated item and excluded from the automatic classification; values outside are classified normally. The isolated item is prepended to legend.items with isIsolated: true and is checked first in getLegendItemForValue (before the regular range items). Clamping and range-find operations skip isolated, no-data, and unclassified items via the new isRegularLegendItem predicate.

For bubble maps, non-isolated values drive minValue / maxValue so the radius scale is not distorted by isolated values.

Files: src/util/classify.js, src/util/legend.js, src/loaders/thematicLoader.js, src/util/favorites.js, src/components/classification/IsolatedClass.jsx, src/components/classification/Classification.jsx, src/components/legend/Bubbles.jsx, src/constants/actionTypes.js, src/actions/layerEdit.js, src/reducers/layerEdit.js

Predefined legend range detection DHIS2-10823

legendNamesContainRange(items) returns true when ≥ 50 % of items have their startValue or endValue embedded in the item name. Legend.jsx uses this to decide whether to hide the range column when it would duplicate the name. Previously, item names equal to "startValue - endValue" were silently cleared in getPredefinedLegendItems; that clearing is removed — the detection is now done at render time so names are preserved in the data model.

Files: src/util/legend.js, src/components/legend/Legend.jsx, src/components/legend/LegendItem.jsx, src/components/legend/LegendItemRange.jsx

Move decimalPlaces from legend items to legend object DHIS2-3156

Follow-up to PR4: decimalPlaces is no longer stamped onto every individual legend item by getAutomaticLegendItems. It is stored once on legend.decimalPlaces and read from there wherever formatting is needed. This avoids the redundancy and keeps item objects clean.

Files: src/util/legend.js, src/loaders/thematicLoader.js

Tests update

  • src/util/__tests__/classify.spec.jsgetLegendItemForValue with isolated / noData / unclassified items; log and SD new behavior
  • src/util/__tests__/legend.spec.jsisRegularLegendItem, buildIsolatedLegendItem, sortLegendItems with isolated items, getAutomaticLegendItems with legendIsolated, legendNamesContainRange
  • src/util/__tests__/favorites.spec.jsnoDataLegend, unclassifiedLegend, legendIsolated serialization

Manual testing

Netlify: https://pr-3649.maps.netlify.dhis2.org/ + Instance: https://dev.im.dhis2.org/maps-app-42-3

Quality checklist

  • Jest tests added/updated
  • Docs added N/A
  • d2-ci dependencies replaced N/A
  • Include plugin in testing
  • Tester approved (@edoardo)

@BRaimbault BRaimbault changed the base branch from master to feat/DHIS2-18242-PR4 April 30, 2026 15:34
@dhis2-bot
Copy link
Copy Markdown
Contributor

dhis2-bot commented Apr 30, 2026

🚀 Deployed on https://pr-3649.maps.netlify.dhis2.org

@dhis2-bot dhis2-bot temporarily deployed to netlify April 30, 2026 15:35 Inactive
@dhis2-bot dhis2-bot temporarily deployed to netlify April 30, 2026 15:53 Inactive
@dhis2-bot dhis2-bot temporarily deployed to netlify April 30, 2026 16:31 Inactive
@BRaimbault BRaimbault requested a review from a team April 30, 2026 16:31
Copy link
Copy Markdown
Collaborator Author

@BRaimbault BRaimbault left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready for review

@dhis2-bot dhis2-bot temporarily deployed to netlify April 30, 2026 17:27 Inactive
@BRaimbault BRaimbault marked this pull request as ready for review May 1, 2026 09:34
@BRaimbault BRaimbault force-pushed the feat/DHIS2-18242-PR5 branch from 2e014f4 to eefbdfd Compare May 1, 2026 09:35
@dhis2-bot dhis2-bot temporarily deployed to netlify May 1, 2026 09:37 Inactive
@BRaimbault BRaimbault changed the title feat: no data / unclassified / isolated classes + thematicLoader refactor [DHIS2-18242] feat: no data / unclassified / isolated classes + thematicLoader refactor [PR5] [DHIS2-18242] May 1, 2026
Copy link
Copy Markdown
Member

@edoardo edoardo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a big PR, but I tried poking around in the examples and it looks good.
I guess a proper KFMT would be good.

@dhis2-bot dhis2-bot temporarily deployed to netlify May 14, 2026 11:16 Inactive
@dhis2-bot dhis2-bot temporarily deployed to netlify May 14, 2026 15:58 Inactive
@BRaimbault BRaimbault force-pushed the feat/DHIS2-18242-PR5 branch from 7eaf47d to bd277ac Compare May 14, 2026 16:28
@sonarqubecloud
Copy link
Copy Markdown

@dhis2-bot dhis2-bot temporarily deployed to netlify May 14, 2026 16:30 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants