Legend sets¶
LegendSetsAccessor on Dhis2Client.legend_sets — create / read / clone / delete LegendSets that visualizations and maps reference by UID to colour their data.
A DHIS2 LegendSet is an ordered list of Legend entries, each assigning a colour (#RRGGBB hex) + display name to a half-open numeric range [startValue, endValue). At render time DHIS2 buckets each cell by which legend its value falls into and paints it with that legend's colour. Visualizations reference a legend set via VisualizationSpec(legend_set=<uid>, ...); maps via MapSpec(legend_set=<uid>, ...) on the thematic layer.
LegendSetSpec + LegendSpec — the builder pattern¶
LegendSet and Legend are generated models — pydantic classes emitted from DHIS2's OpenAPI schema. They carry every field the API can return (roughly 20 on LegendSet, 18 on Legend) including DHIS2-maintained bookkeeping (created, lastUpdated, href, access, createdBy, favorites, translations). When authoring a new legend set, you only care about a handful of those: the name, optional code, the ordered legends, and per-legend (startValue, endValue, color, name). Populating the full model by hand for each new set is tedious and error-prone.
LegendSetSpec and LegendSpec are the authoring shapes — small frozen pydantic models whose fields are the tiny subset the caller actually supplies:
| Spec field | Generated equivalent | Notes |
|---|---|---|
LegendSetSpec.uid |
LegendSet.id |
Optional; build() auto-generates an 11-char UID if omitted. |
LegendSetSpec.name |
LegendSet.name |
Required. |
LegendSetSpec.code |
LegendSet.code |
Optional business code. |
LegendSetSpec.legends |
LegendSet.legends[] |
Ordered list of LegendSpecs — one per colour range. |
LegendSpec.start |
Legend.startValue |
Inclusive range start. Must be < end. |
LegendSpec.end |
Legend.endValue |
Exclusive range end. |
LegendSpec.color |
Legend.color |
Hex #RRGGBB or #RRGGBBAA. |
LegendSpec.name |
Legend.name |
Optional; auto-named from the numeric range when omitted. |
LegendSetSpec.build() materialises the spec into the full typed LegendSet with every child Legend inlined under legends[] (DHIS2's metadata importer requires full objects, not sibling references — see the "Why POST through /api/metadata" note below). The builder also generates stable per-legend UIDs, validates that end > start, and sets sensible defaults on the Legend fields the spec didn't specify.
This is the same pattern the workspace uses for every non-trivial authoring flow:
| Spec | Generated model | Builder method |
|---|---|---|
VisualizationSpec |
Visualization |
VisualizationsAccessor.create_from_spec |
MapSpec + MapLayerSpec |
Map + MapView |
MapsAccessor.create_from_spec |
LegendSetSpec + LegendSpec |
LegendSet + Legend |
LegendSetsAccessor.create_from_spec |
OptionSpec |
Option |
OptionSetsAccessor.sync |
Rule of thumb: a *Spec exists only where the accessor needs to transform caller intent into a wire shape the generated model can't represent directly. Three concrete reasons drive every spec in the workspace:
- A typed enum on the spec fans out into many boolean flags on the wire —
VisualizationSpec.relative_periods: frozenset[RelativePeriod]materialises into 45 individual booleans onVisualization.relativePeriods. - One field selects the population rule for others —
VisualizationSpec.viz_typepicks chart-type-awarerows/columns/filtersdefaults;MapLayerSpec.layer_kinddecides whether dimension selectors are populated at all. - A parent's children need server-correct UIDs / defaults the caller shouldn't have to invent —
LegendSetSpec.build()mints per-LegendUIDs so re-runs are well-formed.
Every other write accessor — organisation_units, data_elements, indicators, categories, category_options, category_combos, programs, program_stages, tracked_entity_attributes, validation_rules, predictors, data_sets, and the rest — ships without a spec. The generated model already mirrors what DHIS2 accepts on POST, so the accessor takes plain keyword args and posts them. Reach for a spec only when the accessor needs to do transformation work like the three above; reach for kwargs in every other case.
Why POST through /api/metadata¶
A direct PUT /api/legendSets/{uid} with a sibling legends collection is rejected by DHIS2's importer — the importer doesn't cross-link legends references back into the parent LegendSet, so every child needs to be inlined under legendSets[*].legends[] as a full object. LegendSetSpec.build() handles this by generating a LegendSet with inline Legend children already dumped; LegendSetsAccessor.create_from_spec POSTs the whole object atomically via /api/metadata?importStrategy=CREATE_AND_UPDATE&atomicMode=OBJECT.
Typed builder¶
from dhis2w_client import LegendSpec, LegendSetSpec
spec = LegendSetSpec(
name="Dose coverage",
code="DOSE_COVERAGE",
legends=[
LegendSpec(start=0, end=50, color="#d73027", name="Low"),
LegendSpec(start=50, end=80, color="#fdae61", name="Medium"),
LegendSpec(start=80, end=120, color="#1a9850", name="High"),
],
)
async with Dhis2Client(...) as client:
legend_set = await client.legend_sets.create_from_spec(spec)
LegendSpec.end must be strictly greater than start — the builder rejects inverted / zero-width ranges. Each child Legend gets a fresh UID on build() so the spec is idempotent across re-runs only when the caller supplies a fixed uid on the spec itself; otherwise each run produces a new set.
Attach to a visualization¶
from dhis2w_client import VisualizationSpec
from dhis2w_client.generated.v42.enums import VisualizationType
viz_spec = VisualizationSpec(
name="BCG doses 2024 monthly",
viz_type=VisualizationType.COLUMN,
data_elements=["s46m5MS0hxu"],
periods=[f"2024{m:02d}" for m in range(1, 13)],
organisation_units=["ImspTQPwCqd"],
legend_set=legend_set.id, # <- threshold colouring on render
)
The workspace seed (infra/scripts/seed/workspace_fixtures.py) ships LsDoseBand1 — four colour ranges tuned to 2024 monthly dose-count totals (red < 2k, amber 2–5k, yellow 5–10k, green 10k+) — and attaches it to uwtuVAnbt6E (Measles monthly) and D3oOqWAM0az (Penta-1 monthly) on the Immunization dashboard.
legend_sets
¶
Legend set authoring helpers — Dhis2Client.legend_sets.
A DHIS2 LegendSet is an ordered list of Legend objects, each
assigning a colour (hex string) + display name to a half-open numeric
band [startValue, endValue). Visualizations and maps reference a
legend set by UID; at render time DHIS2 buckets each data-value cell
by band and paints it with the matching colour.
Hand-rolling this via POST /api/metadata is tedious — ten legends
means ten Legend dicts with precomputed UIDs, correct sort order,
and colour validation. LegendSetSpec + LegendSpec cover the
common case with sensible defaults, generating stable UIDs for each
legend so re-runs of the same spec are idempotent.
Why POST through /api/metadata¶
Same reason as Visualization and Map: a direct PUT
/api/legendSets/{uid} with nested legends works for simple cases
but doesn't round-trip the whole object faithfully (DHIS2 computes
derived fields on the full-metadata path). Route creates + updates
through POST /api/metadata?importStrategy=CREATE_AND_UPDATE so both
the LegendSet + its child Legends import atomically.
Classes¶
Legend
¶
Bases: BaseModel
Generated model for DHIS2 Legend.
DHIS2 Legend - DHIS2 resource (generated from /api/schemas at DHIS2 v42).
Field Field(description=...) entries flag DHIS2 semantics the bare
type can't capture: which side of a relationship owns the link
(writable) vs the inverse side (ignored by the API), uniqueness
constraints, and length bounds.
Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/schemas/legend.py
LegendSet
¶
Bases: BaseModel
Generated model for DHIS2 LegendSet.
DHIS2 Legend Set - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/legendSets.
Field Field(description=...) entries flag DHIS2 semantics the bare
type can't capture: which side of a relationship owns the link
(writable) vs the inverse side (ignored by the API), uniqueness
constraints, and length bounds.
Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/schemas/legend_set.py
LegendSpec
¶
Bases: BaseModel
Typed builder for one Legend inside a LegendSet — a [start, end) colour range.
start must be strictly less than end; the validator enforces
this. color is a CSS-style hex string (#RRGGBB or #RRGGBBAA);
anything else DHIS2 renders as the default greyscale. name
defaults to the legend's numeric range when omitted, matching
DHIS2's own auto-naming in the legend editor.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
LegendSetSpec
¶
Bases: BaseModel
Typed builder for a LegendSet — name + ordered list of Legend entries.
uid is optional; an 11-char UID is generated when omitted. Each
child legend also gets a stable auto-generated UID so re-runs of
the same spec (same uid, same legend order) upsert without
creating duplicates.
Legends are expected in ascending order of start; they're kept
as-given (no implicit sort) so a spec with overlapping or
non-monotonic legends lands on DHIS2 verbatim and fails at render
time — that's a user-authored mistake the builder doesn't mask.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
Functions¶
build()
¶
Materialise the spec into a typed LegendSet with inline Legend children.
DHIS2's /api/metadata importer for LegendSets requires every
Legend to be inlined under legendSets[*].legends[] as a full
object with startValue / endValue / name / color —
passing legends as a sibling collection of references is
rejected with E4000 ("Missing required property") because
the server-side importer doesn't cross-link references back
into the parent.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
LegendSetsAccessor
¶
Dhis2Client.legend_sets — list / get / create / clone / delete legend sets.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | |
Functions¶
__init__(client)
¶
list_all()
async
¶
Return every LegendSet with its legends resolved inline.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
get(uid)
async
¶
Fetch one LegendSet by UID with its legends child list resolved inline.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
create_from_spec(spec)
async
¶
Build + POST a LegendSet via /api/metadata with inline Legend children.
Returns the freshly-fetched LegendSet so the caller sees
DHIS2's computed fields (href, displayName, …) populated.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
clone(source_uid, *, new_uid=None, new_name=None, new_code=None)
async
¶
Duplicate an existing LegendSet — same legends, fresh UIDs on the set and each legend.
Useful for forking a base set ("Coverage 0-100") into a variant with tweaked colours ("Coverage 0-100 monochrome") without rebuilding the legends by hand.
Source code in packages/dhis2w-client/src/dhis2w_client/legend_sets.py
delete(uid)
async
¶
Delete a LegendSet — DELETE /api/legendSets/{uid}.