Skip to content

feat: implement grouping for numeric data items with legend set [DHIS2-21699] - #340

Open
edoardo wants to merge 16 commits into
masterfrom
feat/grouping-DHIS2-21699
Open

feat: implement grouping for numeric data items with legend set [DHIS2-21699]#340
edoardo wants to merge 16 commits into
masterfrom
feat/grouping-DHIS2-21699

Conversation

@edoardo

@edoardo edoardo commented Aug 13, 2026

Copy link
Copy Markdown
Member

Implements DHIS2-21699

Description

Implements the grouping functionality for numeric data items that are configured to use a legend set.
The dimension modal shows additional UI where the grouping can be configured.
"No grouping" means a legend set is not used and the filtering works as for any other dimension.
The selectable legend sets are shown as radio cards and when selecting one the legend set's groups (bands) can be filtered using a Transfer component where the legend set's groups (bands) are listed.

Notes for the PR review

  1. legend-sets-api.ts has been moved from /components/conditions-modal-content/numeric-condition/ to /api/ but because it has been heavily changed it doesn't show up as a moved file.
    It now lives in the shared location because it's used in both the hook in /components/dimension-modal/grouping-radio/ and the thunk that is called when a dimension with legend set is added/dragged from the sidebar to the layout without opening the dimension modal.

  2. The ConditionsTabContent component is much smaller now because with the addition of the Grouping section 2 new components have been added, GroupingSection and FilteringSection.
    Previously the filtering UI was implemented in the ConditionsTabContent component.

  3. The LegendSetCondition has been implemented similarly to the OptionSetCondition, where we don't look at the valueType but render the component when conditions.legendSet is present. This means that the NumericCondition that previously was implementing the legendSet case via the Is one of preset options is now only handling numeric types and it's much simpler.

Doubts and open questions

  1. conditions-context.ts has been added to provide the context and the useConditions hook.
    Previously was part of ConditionsTabContent and after the refactor of point 2 above ended up unchanged in FilteringSection.
    It felt wrong that useConditions was exported from a UI component where it's not even used.
    I'm still unsure about this file tho, the hook needs the context so it can be in the same file, but so far we only have hooks in files that carry their name. This context + hook is a new pattern. We could separate further context provider and hook to keep consistency with the naming and the way hooks are used elsewhere.

  2. seed-default-grouping.ts is a bit particular because it defines a thunk and a startAppListener.
    We don't have a similar precedent.
    I'm wondering if this can be structured differently, ie. put the thunk in thunks (or even use something else than the thunk) and the listener in the visUiConfig slice.
    I did ask Claude about this but the thunk needs to access the store and the middleware does not currently handle the extra argument. We need to decide if we want to change this.


Quality checklist

Add N/A to items that are not applicable and check them.


Screenshots

Dimension modal showing the grouping radio cards, default for LL is "No grouping" and for PT is the first available legend set:

Screenshot 2026-08-14 at 15 53 29 Screenshot 2026-08-14 at 15 53 42

"No grouping" filtering options are "Show all values" and the filters work as for other dimensions (notice the lack of "Is one of preset options" in the filter type dropdown):

Screenshot 2026-08-14 at 15 54 02

"Grouping" filtering options are "Show all groups" and the filter shows a Transfer with the legend set's available groups (bands):

Screenshot 2026-08-14 at 15 53 13

@dhis2-bot

Copy link
Copy Markdown
Contributor

🚀 Deployed on https://pr-340.event-visualizer.netlify.dhis2.org

@dhis2-bot
dhis2-bot temporarily deployed to netlify August 14, 2026 14:04 Inactive
@dhis2-bot
dhis2-bot temporarily deployed to netlify August 14, 2026 17:08 Inactive

@HendrikThePendric HendrikThePendric left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This wasn't the easiest to review because this is a combination of new code and reorganised code, which made it a bit hard to tell which parts to focus.

In general I think this is looking very well organised, but I do see two parts that could maybe be aligned a bit more with pre-established patterns we have in the codebase. These two suggestions actually address the "doubts and open questions" you had:

Conditions context

I think we can align this with the other context providers we have in the app, InterpretationsProvider, MetadataProvider, ScrollBox:

  • Instead of having a conditions-context.ts you could have a conditions-provider. In this file you then define the type(s), the context, a named provider that populates the context-provider value and the hook
  • Then filtering-section becomes much smaller and does not have mixed responsibilities
  • Instead of trying to capture all of this in review comments I created #348

Seeding the default grouping

Technically the implementation looks perfect to me. I also see why you say "We don't have a similar precedent". We do have thunks and we do have a listener, but what we have already is a listener that is specific to a single slice and it lives there, while this new listener is triggered by actions from different slices. This means we can't add it to a slice.

What I would suggest doing is the following:

  • Move the thunk to src/store/thunks.ts
  • Create a new file src/store/listeners.ts and add the listeners there. Not just the one you added in the current PR, but the one currently in the navigation slice too.

Doing this reorganisation is perhaps better done in a separate PR. If you agree it's needed, please create a JIRA ticket.

Comment on lines +42 to +47
const options = useMemo(() => {
const legends =
legendSets.find(({ id }) => id === legendSetId)?.legends ?? []

return legends.map(({ id, name }) => ({ value: id, label: name }))
}, [legendSets, legendSetId])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MINOR:

Instead of ?? [] on L44 you can just do legends?.map on L46.

In fact, that means, you can just do an implicit return with everything conditionally chained:

() => legendSets.find(({ id }) => id === legendSetId)?.legends?.map(({ id, name }) => ({ value: id, label: name })) ?? []

dimensionName: dimension.name,
suffix: dimension.suffix,
itemsText: chipItemsText,
isGrouped: Boolean(conditions?.legendSet),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MINOR: To align isGrouped with groupingName maybe both should be based on legendSet?

So then isGrouped: Boolean(legendSet) and also add legendSet to the dependency array of the useCalback hook...

export const canDimensionHaveLegendSets = (
dimension: Pick<DimensionMetadataItem, 'dimensionType' | 'valueType'>
): boolean =>
dimension.dimensionType === 'PROGRAM_INDICATOR' ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a question... Are PIs numeric by definition? I saw some (I think pre-existing) sort logic that assumes we are always sorting numbers.

@dhis2-bot
dhis2-bot temporarily deployed to netlify August 20, 2026 10:45 Inactive
@sonarqubecloud

Copy link
Copy Markdown

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