Once data is imported, mapped, stored, and processed, someone has to see it.
This is hop ⑤ — the display side of FEWS. Two config
files own this job, and they answer two different questions:
Filters (Filters) build the browsable
tree in the FEWS Explorer — what data can a user reach, and how is it
grouped?
Display groups (DisplayGroups) bundle
related series into predefined chart and table views — once you’ve picked
something, what does the plot look like?
Both are just queries over the data store. Neither creates data; they surface
what imports and transformations
already wrote. That single fact drives every gotcha on this page.
The distinction trips up newcomers because both contain timeSeriesSet blocks
and both end up on screen. Keep them straight:
Filter
Display group
Governs
Browsability — the tree on the left of the Explorer
Presentation — a specific chart/table layout
User sees
A clickable node (“Discharge → Observed”)
A named plot with axes, colors, subplots
File
Config/RegionConfigFiles/Filters …
Config/DisplayConfigFiles/DisplayGroups …
Root element
<filters> (version 1.1)
<displayGroups> (version 1.0)
Mental model
“What’s reachable.”
“How the reachable thing is drawn.”
You can use FEWS with filters alone (the default plot is fine); display groups
are what you add when you want curated views — this station’s stage and
discharge stacked on one figure, say.
└─ child+ ← a BRANCH: <child foreignKey="otherFilterId"/>
Every <filter> is a node. A node is either a leaf (it carries one or more
timeSeriesSet queries that define the data under it) or a branch (it
carries <child> references to other filters). That either/or is a schema
<choice> — you don’t mix timeSeriesSet and child in the same filter.
A parent filter lists its children by id, using <child> — whose only
attribute is foreignKey, pointing at another filter’s id:
<filterid="Discharge"name="Discharge">
<childforeignKey="Discharge.Observed"/>
<childforeignKey="Discharge.Forecast"/>
</filter>
<filterid="Discharge.Observed"name="Observed">
<timeSeriesSet>…</timeSeriesSet>
</filter>
The parent is a pure branch (no timeSeriesSet of its own); the leaves it names
carry the actual queries. This is how you hand-build a tidy, hand-labelled tree.
When a leaf’s data has meaningful attributes (a basin, a region, a parameter
group), let FEWS split it for you. <groupBy> builds sub-nodes on the fly from
attribute values — no child filters to hand-maintain:
<filterid="Discharge"name="Discharge">
<timeSeriesSet>…</timeSeriesSet>
<groupBy>
<locationAttributeId>BASIN</locationAttributeId>
</groupBy>
</filter>
groupBy accepts locationAttributeId, parameterAttributeId, and
qualifierAttributeId (repeatable, so you can nest “by basin, then by
parameter”). Reach for this once a hand-listed tree becomes unwieldy.
A filter’s timeSeriesSet is a query — it must match the stored header
This is the crux of the whole page. A leaf filter’s timeSeriesSet is not
where data is defined; it’s a lookup key. FEWS takes its fields, assembles a
time series header, and asks the data
store “what matches?” If every field matches what the import stored, the
series appears. If one field differs, the node is empty — with no error.
So the filter below is deliberately the mirror image of the
import guide’stimeSeriesSet. Line them up field
by field:
Every one of those first six fields has to equal what the import wrote —
moduleInstanceIdImportObservations, parameterIdQ.obs,
timeSeriesTypeexternal historical, the same timeStep. This is the “write
here, read there” contract: the import writes the header, the filter reads
it back. Get one wrong and it’s the classic “my data imported but won’t show up”
bug (see Gotchas).
Part 2 — Display groups: predefined chart & table views
Where a filter decides reachability, a display group decides presentation.
It’s the answer to “when the user opens this, show discharge and stage on the
same figure, discharge on the left axis, in blue.”
├─ plot *(optional — reusable plot definitions, by id)
│ └─ subplot +(one or more stacked panels)
│ └─ line (a styled series → its timeSeriesSet)
└─ displayGroup +(one or more — a node in the display tree)
├─ @name (required)
├─ @id (optional)
└─ display / displayGroup / displayGroupId …
└─ display
├─ locationId | locationSetId (which locations to draw)
└─ plotId OR inline subplot+ (which plot layout to use)
The reusable-plot pattern is the idiomatic one: define the layout once
(subplots, lines, axes) with an id, then a display points at it with
plotId and constrains it to specific locations. That lets one plot definition
serve many stations.
Notice the timeSeriesSet inside the line is the same kind of query as in
the filter — same header-matching rules apply. A display group with a mistyped
parameterId draws an empty chart just as silently as a mismatched filter shows
an empty node.
One plot, many gauges. Define the plot once and give the display a
locationSetId (e.g. AllDischargeGauges) instead of a single locationId —
every station in the set gets the same layout. Combine with locationLoop to
spin up one display per location automatically.
Observed and forecast on one figure. Put two lines (or two
timeSeriesSets) in a subplot — one external historical, one
external forecasting / simulated forecasting — to see history and forecast
meet at T0.
Stacked panels. Multiple <subplot>s in one plot stack vertically on a
shared time axis — discharge on top, rainfall below.
A tidy filter tree. Use a branch filter with <child> references for the
top levels (“Discharge”, “Water level”) and groupBy on the leaves to split
by basin — hand-labelled where it matters, automatic where it scales.
A default landing node. Set <defaultFilterId> so the Explorer opens on a
sensible filter instead of the first one alphabetically.
Imported but won’t display → header mismatch. The single most common
problem. The filter/display timeSeriesSet must reconstruct the exact header
the import wrote — same moduleInstanceId,
parameterId, locationId/locationSetId, timeSeriesType, and timeStep.
A different moduleInstanceId (say the import uses ImportObservations but the
filter asks for Import) yields an empty node with no error. Compare field by
field.
external historical vs external forecasting counts.timeSeriesType
is part of the header. A filter asking for external forecasting will not find
observed data stored as external historical, even if everything else matches.
child/foreignKey typo → empty branch. The foreignKey must equal an
existing <filter>id. Mismatched case or punctuation gives a silent, empty
node — same failure mode as ID mapping.
Referencing an undeclared location/parameter. A timeSeriesSet naming a
locationSetId or parameterId that isn’t declared won’t resolve — declare
your locations and parameters first.
A change has no effect → a higher-versioned file wins. Check for a sibling
like Filters 1.01 default.xml or DisplayGroups 1.01 default.xml. FEWS loads
the highest version and ignores the rest. See how FEWS finds a
file.
Open the FEWS Explorer and confirm your filter appears in the tree with
the name you gave it, nested where you expect. defaultFilterId should be
selected on start-up.
Click the leaf node. The map/location panel should populate with the
locations in your timeSeriesSet (e.g. every gauge in AllDischargeGauges).
An empty node almost always means a header field doesn’t match — revisit the
import’stimeSeriesSet.
Select a location and open the display. Your display group’s plot should
draw the series with the styling you configured. A blank chart with a
populated node points at the display’stimeSeriesSet, not the filter’s.
Cross-check the header. If it’s still empty, open the same series through a
plain default filter (just the timeSeriesSet, no styling). If that works,
the problem is in the display config, not the stored data.