{
"key": "durban",
"label": "Durban",
"description": "Port of Durban, KwaZulu-Natal",
"group": "East coast",
"values": {
"name": "Durban",
"bounds": {"longitude": 30.95, "latitude": -29.93, "width": 0.12, "height": 0.08}
}
}
|
Beta feature
Selector widgets and template widgets are in beta. The request formats described in this article may still change. |
A project dashboard brings together widgets from the analytics pages: maps from Visual Analytics, charts from Trend and Distribution Analytics, origin-destination and dwell-time results, and documentation. Each widget stores the complete page state it was created from, so it always opens with the area, filters, styling and time range its author chose.
Those widgets are already dynamic in several ways: they show new data as soon as a dataset is updated, and with a relative time range (for example the last 30 days) they move along with the data automatically. What a regular widget does not do is let the viewer choose: which area to look at, which period, which segment or which view. Each of those choices meant another widget, another section or another dashboard, prepared by the author in advance.
Selectors and templates add that choice to the dashboard itself.
With selectors and templates you build one dashboard in which the viewer makes the choices:
a selector widget shows one or more drop-down lists on the dashboard, for example Port and Period;
a template widget is a normal widget (map, chart, distribution, documentation, …) in which some values are placeholders such as {{port.name}};
when the viewer picks another option, every template that uses that selector is filled in again with the values of the new option and redrawn.
Instead of one dashboard per port, you create one dashboard with a port selector, and every chart, map and text on it follows the selection. Selectors can also show and hide whole dashboard sections (widget groups), for example to switch between a detailed view and a summary view. Templates keep everything regular widgets do: they still show updated data and can still use relative time ranges.
One dashboard instead of many. A dashboard for 40 locations is one project, not 40 copies to keep in sync.
Viewers answer their own questions. They pick the location, period or view they need, without asking for a new dashboard.
The same definitions everywhere. Every option uses the same charts, filters and colours, so results are comparable by construction.
Change once, update everywhere. Fix a chart or a colour in one template and every selection shows the change.
One link to share. Bookmarks and links point to one dashboard that covers every case.
Anything in a widget that is a value can follow a selection. Some typical uses, and what they bring:
| When the viewer picks … | the dashboard … | which helps to … |
|---|---|---|
another location |
fits every map on that area and filters every chart to it |
let each regional team or customer find their own site on the same dashboard |
another period |
switches the time range, or the dataset of that month or year |
compare seasons, follow up a change before and after, or check the latest month first |
another segment (vessel type, vehicle class, product) |
applies a different property filter to every chart |
see whether a pattern holds for all segments or only for one |
another view on the map |
colours the same data by another property, switches between density and trajectories, or shows other layers |
answer different questions with one map: "where is it busy?", "where is it fast?", "which types are there?" |
another measure |
shows another property, aggregation or time axis (hour of day, day of week, months) |
go from volumes to speeds or dwell times without leaving the dashboard |
another scale or radius |
uses another area layer (for example a 100 m, 1 km or 10 km circle) and a matching chart axis |
zoom from the site itself to its catchment area with one click |
another level of detail |
shows or hides whole sections |
keep the dashboard short for a quick look and offer the details on demand |
any of the above |
updates titles, headers and texts, which can carry extra context per option (address, capacity, contact) |
make every screenshot and export self-explanatory |
A Port and a Period selector at the top. The map fits on the selected port; charts show arrivals per day, dwell time and the vessel types in port.
Benefit: one dashboard serves every port and every port manager. New ports are a new option in the selector, not a new project.
Four columns, each with its own site selector: a candidate location next to three existing ones. Every column shows the same map, traffic by hour of the day, trip lengths and nearby points of interest, with the same chart axes in every column.
Benefit: any candidate can be compared with any benchmark in seconds, side by side and on the same scale, instead of preparing a new comparison for every question.
A site selector and one chart per month next to each other, all with the same axis.
Benefit: seasonal patterns and unusual months stand out at a glance, for whichever site the viewer picks.
A View selector with the options Volume, Speed and Vehicle type. The map keeps its area and period, but colours the same movements by number of assets, by average speed or by type, and the legend and title follow.
Benefit: analysts and stakeholders explore the data themselves without needing to know how to configure the map styling.
A Level selector that shows a short summary section by default and the detailed sections on request.
Benefit: a dashboard that works for a management overview and for an in-depth analysis, without overwhelming either audience.
Throughout this article we use one example: a project with AIS vessel data and an area-of-interest layer Ports with one area per port. The dashboard gets a Port selector, a Period selector, a header, a map and a chart that follow both.
Every selector defines one variable (for example port) and a fixed list of options.
Each option has
a key: a stable identifier, used to set the default;
a label: what the viewer sees in the drop-down list;
optionally a description (shown under the field), a group (options are grouped under headers in the list) and disabled;
values: any number of fields with any JSON value (text, number, boolean, object or array). These are the values the templates use.
{
"key": "durban",
"label": "Durban",
"description": "Port of Durban, KwaZulu-Natal",
"group": "East coast",
"values": {
"name": "Durban",
"bounds": {"longitude": 30.95, "latitude": -29.93, "width": 0.12, "height": 0.08}
}
}
Put everything a widget needs into the option values, even values you could derive from others (map bounds, colours, axis ranges, dataset ids). The templates cannot calculate; they only copy values.
A template widget has the same title, description, properties and state as the regular widget of that type.
Anywhere in those values you can write {{variable.field}}:
a placeholder that is the whole JSON string keeps the type of the value: "bounds": "{{port.bounds}}" becomes the bounds object of the selected option, and "maxVerticalAxisValue": "{{period.max}}" becomes a number;
a placeholder inside a longer string is inserted as text: "filterPipeline": "INSIDE(AREA(area_of_interest_id=\"<PORTS_AOI_ID>\", id=\"{{port.name}}\"))".
Every option also has these built-in fields:
| Placeholder | Value |
|---|---|
|
key of the selected option |
|
label of the selected option |
|
description of the selected option |
|
group of the selected option |
|
label of the selector |
|
description of the selector |
A good habit: use {{port._label}} in titles, so every widget says which option it shows.
An option can list showGroups: the ids of the widget groups (dashboard sections) that are visible while that option is selected.
A group that no option of any selector lists is always visible.
A group that is listed is visible only while the selected option of every selector that lists it includes it.
A selector cannot show or hide the group it is placed in (the viewer could never switch it back). Put selectors that toggle sections in a section that stays visible.
Templates are checked against the project like regular widgets, so first
attach every dataset and every area-of-interest or background layer the widgets use;
create the widget groups (dashboard sections), because showGroups refers to their ids.
POST /public/api/projects/{projectId}/datasets
POST /public/api/projects/{projectId}/mapdata
POST /public/api/projects/{projectId}/dashboard/widgetgroups
Create selectors before the templates that use them: a template is rejected when one of its variables has no selector yet.
POST /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/selectorwidgets
{
"title": "Filters",
"description": "Choose the port and the period.",
"width": 6, "height": 1, "gridColumnStart": 0, "gridRowStart": 0,
"properties": {
"schemaVersion": 1,
"selectors": [
{
"variable": "port",
"label": "Port",
"description": "The port shown on this dashboard.",
"placeholderText": "Choose a port",
"source": {"type": "INLINE", "options": [
{"key": "durban", "label": "Durban", "group": "East coast",
"values": {"name": "Durban",
"bounds": {"longitude": 30.95, "latitude": -29.93, "width": 0.12, "height": 0.08}}},
{"key": "cape-town", "label": "Cape Town", "group": "West coast",
"values": {"name": "Cape Town",
"bounds": {"longitude": 18.38, "latitude": -33.95, "width": 0.12, "height": 0.08}}}
]},
"selection": {"optional": false, "default": "durban"}
},
{
"variable": "period",
"label": "Period",
"source": {"type": "INLINE", "options": [
{"key": "2025-q4", "label": "Q4 2025",
"values": {"range": {"startTimeType": "ABSOLUTE_TIME", "startTimeInSeconds": 1759276800,
"startTimeDeltaInSeconds": 0, "endTimeType": "ABSOLUTE_TIME",
"endTimeInSeconds": 1767225599, "endTimeDeltaInSeconds": 0}}},
{"key": "2026-q1", "label": "Q1 2026",
"values": {"range": {"startTimeType": "ABSOLUTE_TIME", "startTimeInSeconds": 1767225600,
"startTimeDeltaInSeconds": 0, "endTimeType": "ABSOLUTE_TIME",
"endTimeInSeconds": 1775001599, "endTimeDeltaInSeconds": 0}}}
]},
"selection": {"optional": false, "default": "2026-q1"}
}
]
}
}
Several selectors in one widget appear side by side, like a filter bar. You can also give every selector its own widget, for example one selector above each column of a comparison.
The rules for selectors:
variable names are unique on the dashboard (across all selector widgets) and contain only letters, digits and _, starting with a letter;
option keys are unique within a selector, and the default is the key of an enabled option;
a field has the same JSON type in every option (for example bounds is an object in all of them);
do not put double quotes (") in text values; write the quotes in the template instead (see the filter example below);
a selector has a limited number of options (currently up to 1000).
With "optional": true the viewer may clear the selection; the widgets that use that variable are then hidden.
POST /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/templatewidgets
The body is the request of the regular widget plus widgetType, which tells the platform which widget it is:
VISUAL_ANALYTICS, TREND_ANALYTICS, CATEGORIZED_TREND_ANALYTICS, DISTRIBUTION_ANALYTICS, ORIGIN_DESTINATION, DWELL_TIME or DOCUMENTATION.
A header that names the selection (documentation widget, no state):
{
"widgetType": "DOCUMENTATION",
"title": "{{port._label}}",
"description": "",
"properties": {"contents": "<h3>{{port._label}}</h3><p>{{port._group}} · {{period._label}}</p>"},
"width": 6, "height": 1, "gridColumnStart": 6, "gridRowStart": 0
}
A chart of the vessels in the selected port during the selected period (categorized trend):
{
"widgetType": "CATEGORIZED_TREND_ANALYTICS",
"title": "{{port._label}} — vessels per day",
"description": "Number of vessels in {{port._label}} per day, {{period._label}}.",
"properties": {"widgetType": "CHART"},
"state": {
"activeDataSetId": "<AIS_DATASET_ID>",
"mode": "DISPLAY_NUMBER_OF_ASSETS",
"chartType": "BAR_CHART",
"chartMode": "REGULAR",
"timeRange": "{{period.range}}",
"timeZoneIdentifier": "Africa/Johannesburg",
"timeAxisConfiguration": {"mode": "DAYS"},
"filterPipeline": "INSIDE(AREA(area_of_interest_id=\"<PORTS_AOI_ID>\", id=\"{{port.name}}\"))",
"categories": [{"items": [{"label": "{{port._label}}", "filterPipeline": ""}]}]
},
"width": 6, "height": 2, "gridColumnStart": 6, "gridRowStart": 1
}
A map fitted on the selected port (Visual Analytics).
For Visual Analytics templates, state is the complete page state (individualMapStates, with bounds, time range, dataset and layer styling), exactly as for a regular Visual Analytics widget:
{
"widgetType": "VISUAL_ANALYTICS",
"title": "{{port._label}}",
"description": "Vessel density in {{port._label}}, {{period._label}}.",
"properties": {"widgetType": "SPATIAL_MAP", "mapIndex": 0},
"state": {"individualMapStates": [{
"temporalRangeFilter": "{{period.range}}",
"timeZoneIdentifier": "Africa/Johannesburg",
"spatialMapProperties": {"bounds": "{{port.bounds}}", "showLegend": true, "showScale": true},
"dataSetProperties": {"<AIS_DATASET_ID>": {"visible": true, "visualizationMode": "GRIDDED"}},
"mapDataStyling": {"<PORTS_AOI_ID>": {"visible": true}}
}]},
"width": 6, "height": 3, "gridColumnStart": 0, "gridRowStart": 1
}
The stored widget keeps the placeholders. Read it back in that form with GET …/templatewidgets/{widgetId}.
Add showGroups to the options of a selector, with the ids of the widget groups that option shows.
Example: a View selector, placed in an always-visible Filters section, that switches between a Details and a Summary section:
{
"variable": "view",
"label": "View",
"source": {"type": "INLINE", "options": [
{"key": "details", "label": "Details", "showGroups": ["<DETAILS_GROUP_ID>"]},
{"key": "summary", "label": "Summary", "showGroups": ["<SUMMARY_GROUP_ID>"]}
]},
"selection": {"optional": false, "default": "details"}
}
GET /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/selectorwidgets/{widgetId}
PUT /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/selectorwidgets/{widgetId}
GET /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/templatewidgets/{widgetId}
PUT /public/api/projects/{projectId}/dashboard/widgetgroups/{widgetGroupId}/templatewidgets/{widgetId}
A PUT replaces the whole widget: send all selectors of the selector widget, or the complete template.
When you change or remove a selector, the platform checks again every template that uses it, with the old and the new options. The change is refused when it would break a template that was valid, for example when you remove a field that a template uses.
A template keeps its widget type; to change the type, delete it and create a new one.
When you create or change a template, the platform
fills in the placeholders with the default of every selector and checks the result;
checks the other options too:
all combinations of the options of the selectors the template uses, when there are at most 1000;
above that, combinations of two selectors at a time (the others at their default);
when that is still too many, every option of every selector once;
checks each filled-in result like a regular widget of that type: required fields, field types, allowed values, and the references to datasets, areas and layers of the project.
So an option with a missing field, a wrong type or an area that does not exist is reported when you create the template, not when a viewer selects it. The check covers the widget configuration, not the analytical result: a template can be valid and still show an empty chart for an option without data.
|
Big selectors make templates slow to create
A template is checked for many options, so a selector with hundreds of options makes every template that uses it take longer to create. Create the selectors first and the templates in a script, and allow for a longer request timeout. |
{{site.{{range.key}}}} does not exist: a placeholder takes its value from one option of one selector.
When a value depends on two choices, for example the map bounds of a site and a zoom level, use one of these patterns:
store the combination in one option: give every site option one field per zoom level (bounds_1km, bounds_10km) and create one template per zoom level;
one section per choice: put the templates for each zoom level in their own section and let the zoom selector show one of these sections with showGroups.
When several templates show the same measure for different selections (one column per site), give them a fixed vertical axis so the columns compare at a glance:
"verticalAxisConfiguration": {"verticalAxisRangeMode": "FIXED_RANGE", "minVerticalAxisValue": 0, "maxVerticalAxisValue": 500}
If the right range depends on the selection, put the whole axis object in the option values and write "verticalAxisConfiguration": "{{range.axis}}".
Object keys, such as the dataset ids in dataSetProperties or the layer ids in mapDataStyling, cannot contain placeholders.
List every dataset and layer the widget may need, and switch what is shown with placeholders in the values, for example "activeDataSetId": "{{period.dataset}}".
For a side-by-side comparison, create one selector widget per column (variables site1, site2, …) above the column, and build the templates of each column with that column’s variable.
The same option list can be used for every column.
Every placeholder refers to a field that exists in every option of its selector.
Every option of a selector uses the same type per field.
Every area name, dataset id and layer id that an option fills in exists in the project.