Skip to content

Filters & display groups

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:

FilterDisplay group
GovernsBrowsability — the tree on the left of the ExplorerPresentation — a specific chart/table layout
User seesA clickable node (“Discharge → Observed”)A named plot with axes, colors, subplots
FileConfig/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.

filters (version="1.1", required)
├─ description (optional)
├─ defaultFilterId (optional — which node is selected first)
├─ timeSeriesSets *(optional — reusable named set collections)
└─ filter +(one or more — each is a node in the tree)
├─ @id (required — referenced by defaultFilterId / child)
├─ @name (optional — the label shown in the Explorer)
└─ ONE of:
├─ timeSeriesSet+ ← a LEAF: the query that fills this node
│ └─ (+ optional groupBy to auto-split by attribute)
├─ timeSeries+ / constraints ← criteria-based leaf (2014.02+)
└─ 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.

There are two ways to build depth, and they solve different problems.

A parent filter lists its children by id, using <child> — whose only attribute is foreignKey, pointing at another filter’s id:

<filter id="Discharge" name="Discharge">
<child foreignKey="Discharge.Observed"/>
<child foreignKey="Discharge.Forecast"/>
</filter>
<filter id="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.

A filter’s timeSeriesSet is a query — it must match the stored header

Section titled “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’s timeSeriesSet. Line them up field by field:

Config/RegionConfigFiles/Filters 1.00 default.xml
<?xml version="1.0" encoding="UTF-8"?>
<filters xmlns="http://www.wldelft.nl/fews" version="1.1">
<defaultFilterId>Discharge</defaultFilterId>
<filter id="Discharge" name="Discharge observations">
<timeSeriesSet>
<moduleInstanceId>ImportObservations</moduleInstanceId>
<valueType>scalar</valueType>
<parameterId>Q.obs</parameterId>
<locationSetId>AllDischargeGauges</locationSetId>
<timeSeriesType>external historical</timeSeriesType>
<timeStep unit="hour" multiplier="1"/>
<readWriteMode>read only</readWriteMode>
</timeSeriesSet>
</filter>
</filters>

Every one of those first six fields has to equal what the import wrote — moduleInstanceId ImportObservations, parameterId Q.obs, timeSeriesType external 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

Section titled “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.”

displayGroups (version="1.0", optional attribute)
├─ description (optional)
├─ 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.

Config/DisplayConfigFiles/DisplayGroups 1.00 default.xml
<?xml version="1.0" encoding="UTF-8"?>
<displayGroups xmlns="http://www.wldelft.nl/fews" version="1.0">
<plot id="DischargePlot">
<subplot>
<line>
<color>0000FF</color>
<lineStyle>solid</lineStyle>
<timeSeriesSet>
<moduleInstanceId>ImportObservations</moduleInstanceId>
<valueType>scalar</valueType>
<parameterId>Q.obs</parameterId>
<locationId>RiverGauge_01</locationId>
<timeSeriesType>external historical</timeSeriesType>
<timeStep unit="hour" multiplier="1"/>
<readWriteMode>read only</readWriteMode>
</timeSeriesSet>
</line>
</subplot>
</plot>
<displayGroup name="Discharge gauges">
<display name="River Gauge 01">
<locationId>RiverGauge_01</locationId>
<plotId>DischargePlot</plotId>
</display>
</displayGroup>
</displayGroups>

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.

ElementRole
plot (+ id)A reusable layout: one or more subplots, referenced by plotId.
subplotA stacked panel within a plot. Multiple subplots share the time axis.
line / area / clusteredBarsA styled series inside a subplot; each wraps a timeSeriesSet.
displayGroup (+ name)A node in the display tree; can nest further displayGroups.
displayOne entry under a group — a plotId (or inline subplots) plus location constraints.
locationLoopAuto-generate one display per location in a set (2015.01+).
  • 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.

Filter (filters.xsd)

FieldMeaning
filter @idNode id; targeted by defaultFilterId and child/@foreignKey.
filter @nameLabel shown in the Explorer tree.
timeSeriesSetThe query that fills a leaf node (must match a stored header).
child @foreignKeyReference to a child filter’s id — builds a branch.
groupByAuto-split a node by locationAttributeId / parameterAttributeId / qualifierAttributeId.
defaultFilterIdWhich filter is selected when the Explorer opens.

Display group (displayGroups.xsd)

FieldMeaning
plot @idReusable plot layout, referenced by plotId.
subplotA stacked panel; holds line/area/clusteredBars.
linetimeSeriesSetA styled series and the query that supplies it.
displayGroup @nameA node in the display tree (required attribute).
displayplotIdBinds a plot layout to location constraints.
locationId / locationSetIdWhich locations a display draws.
  • 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.
  1. 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.
  2. 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’s timeSeriesSet.
  3. 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’s timeSeriesSet, not the filter’s.
  4. 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.