knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
We wanted to create a unified way to add filters and controls for all our plots in the front end. Previously there were different approaches to this for each plot but we wanted to create a unified way to reduce code duplication, make the interface consistent and mean enhancements to one plot can easily be ported over to all others. The drop downs in the side bar are split into two sections, plot controls and filters.
controls
- dropdowns that influence other dropdownsfilters
- dropdowns that act as filters for slicing the dataThe controls
can run effects
to modify the filters such as:
X Axis
and Disaggregate by
controls which set whatever is selected into a multi-select dropdown, while keeping the rest as single selects. See barchart screenshotBelow are some examples of the plot control and filter UIs that the metadata allow us to build
{width="100%"}
X Axis
& Disaggregate by
set the selected filters as multiselects{width="100%"}
{width="100%"}
{width="100%"}
{width="100%"}
The backend (this repository) provides metadata JSON endpoints that tell the frontend how to construct the UI and also how to dynamically update it based on the value of the dropdowns.
For details on where metadata is currently served from see Endpoints section.
Reminder that there are two types of dropdowns that can be created via metadata:
controls
and call their values settings
filters
and their values selections
Here is a useful visualisation of the schema that I'll keep referring to so that we don't get lost in the JSON nesting (note that indicators
is not expanded as this just contains metadata about the indicator values such as their format which isn't relevant to the metadata structure):
├── filterTypes[] │ ├── id │ ├── column_id │ ├── use_shape_regions │ ├── options[] │ │ ├── label │ │ ├── id │ │ ├── description ├── indicators ├── plotSettingsControl{} │ ├── defaultEffect │ │ ├── setFilters │ │ ├── ... (see effect below) │ ├── plotSettings[] │ │ ├── label │ │ ├── id │ │ ├── hidden │ │ ├── value │ │ ├── options[] │ │ │ │ ├── id │ │ │ │ ├── label │ │ │ │ ├── effect │ │ │ │ │ ├── setFilters │ │ │ │ │ ├── setMultiple │ │ │ │ │ ├── setFilterValues │ │ │ │ │ ├── setHidden │ │ │ │ │ ├── customPlotEffect
This is just a full version of the json schema.
├── filterTypes[] # here │ ├── id # 1 │ ├── column_id # 2 │ ├── use_shape_regions # 3 │ ├── options[] # 4 │ │ ├── label │ │ ├── id │ │ ├── description ├── indicators ├── plotSettingsControl{} │ ├── defaultEffect │ │ ├── setFilters │ │ ├── ... (see effect below) │ ├── plotSettings[] │ │ ├── label │ │ ├── id │ │ ├── hidden │ │ ├── value │ │ ├── options[] │ │ │ │ ├── id │ │ │ │ ├── label │ │ │ │ ├── effect │ │ │ │ │ ├── setFilters │ │ │ │ │ ├── setMultiple │ │ │ │ │ ├── setFilterValues │ │ │ │ │ ├── setHidden │ │ │ │ │ ├── customPlotEffect
The filterTypes
key is the simplest. It is an array of all the filters that can be displayed in the UI with their options. Explanations for each number in the visualisation:
id
is just an id we can assign to the filter to refer to it latercolumn_id
this is the column id of the data which we have to filteruse_shape_regions
is a boolean that should largely be ignored, it is a one off trick for geographical area filters only to reduce the amount of payload we send to the frontend by telling it to derive the options of a filter select with this boolean from the shape file that the user has uploadedoptions
are the options visible to the user in the dropdown. Each option has an id
by which filter selections can refer to the option, a label
to display in the dropdown menu and an optional description
prop for contextual information (this doesn't get displayed to the user at all)├── filterTypes[] │ ├── id │ ├── column_id │ ├── use_shape_regions │ ├── options[] │ │ ├── label │ │ ├── id │ │ ├── description ├── indicators ├── plotSettingsControl{} # here │ ├── defaultEffect # 1 │ │ ├── setFilters │ │ ├── ... (see effect below) │ ├── plotSettings[] # 2 │ │ ├── label # 3 │ │ ├── id # 4 │ │ ├── hidden # 5 │ │ ├── value # 6 │ │ ├── options[] │ │ │ │ ├── id # a │ │ │ │ ├── label │ │ │ │ ├── effect │ │ │ │ │ ├── setFilters │ │ │ │ │ ├── setMultiple │ │ │ │ │ ├── setFilterValues │ │ │ │ │ ├── setHidden │ │ │ │ │ ├── customPlotEffect
The plot settings controls is an object with keys of the relevant plot name (e.g. choropleth
) and value with the structure shown in the visualisation. Explanation of keys:
defaultEffect
is the base effect applied to all settings
(remember these are the values of the control
dropdown) - more on this laterplotSettings
is an array of all the controls
a tab will havelabel
is the title of the dropdown the user will see in the UIid
is the id of the plot settings (this id is how the store keeps track of which setting
of the control
is selected)hidden
boolean - there are times we want the control
to be invisible. The calibrate plot reuses the barchart component, however they expect an X Axis
and Disaggregate by
control - these controls are fixed in the calibrate plot so we set them to a constant value and hide them from the user allowing us to reuse the barchart component without any special codevalue
is an optional key to set the default value of the control
as something other than the default (default in frontend for single select is the first value and for a multi-select is all the values). This is the id
of the option (a
in the visualisation)├── filterTypes[] │ ├── id │ ├── column_id │ ├── use_shape_regions │ ├── options[] │ │ ├── label │ │ ├── id │ │ ├── description ├── indicators ├── plotSettingsControl{} │ ├── defaultEffect │ │ ├── setFilters │ │ ├── ... (see effect below) │ ├── plotSettings[] │ │ ├── label │ │ ├── id │ │ ├── hidden │ │ ├── value │ │ ├── options[] # here │ │ │ │ ├── id # 1 │ │ │ │ ├── label # 2 │ │ │ │ ├── effect # 3 │ │ │ │ │ ├── setFilters │ │ │ │ │ ├── setMultiple │ │ │ │ │ ├── setFilterValues │ │ │ │ │ ├── setHidden │ │ │ │ │ ├── customPlotEffect
The settings
completely determine the filters
and their selections
. The next section will go through each effect and how the frontend resolves it but for now, explanation of keys:
id
for the store to record what setting
is chosenlabel
displayed to the user in the dropdown menueffect
key is probably the most important idea here. Every setting
has an effect associated with it and aggregating all the effects along all the settings the user has chosen completely determines the filters
and their initial selections
that are shown to the user. So everything can be derived from just the settings
that are chosenEffects are the actions that can be undertaken when a plot control is selected. They can be used to modify the filters and set custom effects which can be used by the plot.
├── filterTypes[] │ ├── id # a │ ├── column_id │ ├── use_shape_regions │ ├── options[] │ │ ├── label │ │ ├── id # b │ │ ├── description ├── indicators ├── plotSettingsControl{} │ ├── defaultEffect # (here as well) │ │ ├── setFilters │ │ ├── ... (see effect below) │ ├── plotSettings[] │ │ ├── label │ │ ├── id │ │ ├── hidden │ │ ├── value │ │ ├── options[] │ │ │ │ ├── id │ │ │ │ ├── label │ │ │ │ ├── effect # here │ │ │ │ │ ├── setFilters │ │ │ │ │ ├── setMultiple │ │ │ │ │ ├── setFilterValues │ │ │ │ │ ├── setHidden │ │ │ │ │ ├── customPlotEffect
For this section we will need a more detailed view of the effects structure:
├── setFilters[] # 1 │ ├── filterId # 1.1 │ ├── label # 1.2 │ ├── stateFilterId # 1.3 ├── setMultiple[] # 2 ├── setFilterValues{} # 3 ├── setHidden[] # 4 ├── customPlotEffect{} # 5
Explanation of each effect and how the frontend aggregates this:
setFilters
is how the effect specifies what filters
the user sees from the filterTypes
. Only one setFilters
effect is used, they are not combined. So if two settings
set filters, the last setFilters
in the for loop would be used when going through all the settings
. Keys:filterId
is the id
key in a filterTypes
element (a
in the schema above)label
is the title of the filter
displayed to the user in the UIstateFilterId
is the id the state uses to identify this filter. This is distinct to the filterId
because in the bubble plot we have two indicators filters that are the same filter type (so have the same options
and the same column_id
) however they are distinct because one controls the color of the bubbles and one controls the size of the bubbles so while both their filterId
s are indicator
, the stateFilterId
s for both are different, colorIndicator
and sizeIndicator
. For most cases this can be set as the same thing as the filterId
. All other effects will now identify filters by stateFilterId
as these uniquely correspond to what the user seessetMultiple
is an effect that converts a filter
to a multi-select dropdown (they are all single select by default). It is an array of stateFilterId
s (1.3
in the schema) This is aggregated via concatenation in the frontend so if two settings have setMultiple
properties ["state_id_1", "state_id_2"]
and ["state_id_3"]
respectively then all three filters with those state ids will be set to multi-selectssetFilterValues
is an effect that sets the selection of the filters in setFilters
. It is an object with keys equal to stateFilterId
and value an array of ids of the selections
(b
in the schema). Note this is still an array of length one if you want to specify a value for a single select filter. This is aggregated by the spread operator in the frontendsetHidden
is an effect that hides filters
and is usually used when we have a fixed filter that we don't want the user to change but is still required to slice the data. This is aggregated via concatenation in the frontendcustomPlotEffect
is an effect that plots can use to build reactive UI from for a specific plot type. At the moment we are only using this in the table, so set the row and header groups. But it could be expanded to general metadata which we want a plot control to update, but does not have an effect on the filters themselves.To hammer it home for Rob because he can't remember this no matter how many times he reads it. FilterRef
s have a filterId
and stateFilterId
filterId
- used to identify the id
in the full set of filters
i.e. to get the label and options for this filterstateFilterId
- reference to this filter within the rest of the metadata and the effects i.e. to identify which drop down to update when a plot control changesThis is because on the bubble plot, there are two drop downs which have indicator
as their options, but run different effects.
I have used the directory-structure-esque visualisation for most of this because it is simple and obscures a lot of the types and other information as that would be too much to throw. But now that you know how the metadata works, here is the full typescript definition of it with contextual types (instead of just string
, it shows which type of string it refers to, e.g. whether it is stateFilterId
or filterId
):
type FilterId = string type StateFilterId = string type ColumnId = string type SelectionId = string type PlotControlId = string type PlotSettingId = string type PlotName = string type FilterOption = { id: SelectionId, label: string, description: string } type FilterType = { id: FilterId, column_id: ColumnId, options: FilterOption[] } type FilterRef = { filterId: FilterId, label: string, stateFilterId: StateFilterId } type PlotSettingEffect = { setFilters?: FilterRef[], setMultiple?: StateFilterId[], setFilterValues?: Record<StateFilterId, SelectionId[]>, setHidden?: StateFilterId[], customPlotEffect?: any } type PlotSettingOption = { id: PlotSettingId, label: string, effect: PlotSettingEffect } type PlotSetting = { id: PlotControlId, label: string, hidden?: boolean, value?: PlotSettingId, options: PlotSettingOption[] } type PlotSettingsControl = { defaultEffect: PlotSettingEffect, plotSettings: PlotSetting[] } type MetadataStructure = { filterTypes: FilterType[], indicators: any, // irrelevant plotSettingsControl: { // example of plotName can be choropleth or table, etc [plotName: PlotName]: PlotSettingsControl } }
Remember the frontend store is also fully derived from this metadata. Nothing is hardcoded in the store expect for the plotNames
so the derived store takes the type:
type FilterSelection = { multiple: boolean selection: FilterOption[], hidden: boolean } & FilterRef type ControlSelection = { id: PlotControlId, label: string, selection: FilterOption[] } type FrontendSelectionState = { // these plotNames keys in the type are the generated keys from // the json schema in Hintr [plotName: PlotName]: { controls: ControlSelection[] filters: FilterSelection[] } } & { error: Error }
Note that the state stores some extra information other than the selections of each of the dropdowns, this is to reduce the need to parse the metadata all the time. However, every time an update to the controls
occurs. the frontend has to parse the metadata, aggregate all the effects and dynamically render all the filters again. In practice this is very fast.
The data state is quite simple. It takes the form:
type FrontendDataState = { // just an array of filtered data [plotName: PlotName]: any[] }
For each data source in the frontend UI there is a different endpoint and associated JSON schema that specifies the metadata. Note this will probably go stale but should give a starting point
| Step | Plot | Data source | Metadata source | Schema |
|------|------|-------------|-----------------|--------|
| Input review | Time series | /chart-data/input-time-series/<anc/art>
| /review-input/metadata
| Review inputs |
| Input review | Choropleth | /validate/survey-and-programme
|/review-input/metadata
| Review inputs |
| Input review | Comparison table | /chart-data/input-comparison
| /chart-data/input-comparison
| Input comparison |
| Input review | Comparison barchart |/chart-data/input-comparison
| /chart-data/input-comparison
| Input comparison |
| Input review | Population | /validate/baseline-individual
only for population data |/chart-data/input-population
| Input population |
| Calibrate | Calibration barchart | /calibrate/plot/<id>
| /calibrate/plot/<id>
| Calibrate plot|
| Output review | Choropleth | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
| Output review | Barchart | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
| Output review | Choropleth | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
| Output review | Table | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
| Output review | Comparison | /comparison/plot/<id>
| /comparison/plot/<id>
| Output comparison|
| Output review | Bubble | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
| Output review | Cascade | /calibrate/result/path/<id>
()| /calibrate/result/metadata/<id>
| Review output|
(*) Kotlin backend gets the path to the duckdb output file using hintr endpoint at /calibrate/result/path/<id>
. It then slices this using SQL based on the filters the user set in the front end. So we only return the slice of data the user is looking at.
Note that data and metadata can be in the same endpoint or different ones. Favour returning them from the same endpoint unless splitting them up will significantly speed up the UI e.g. returning metadata for output plots all together saves reading the output and building the filters from this multiple times. In general, I think having the data & metadata returned together is simpler.
This section gives an overview of how the front end handles the metadata and where any custom logic for a specific plot should be injected. The aim of the front end impl is that the wiring up of state will be as straight forward as possible, and we get as much code use as possible.
For output plots data is fetched on demand when users set filters, therefore there is no big blob of data in state
For all other plots data is pre-fetched via a store action. This might be when an input file is uploaded (e.g. population, survey, ART, ANC), or when a plot is opened e.g. input comparison, input time series. Data is fetched via an action and saved in the store state somewhere, this varies from data source to data source but will be in baseline
, surveyAndProgramme
, reviewInput
Metadata is fetched via a store action which calls to endpoints in the table above. It can be fetched as part of the same endpoint as the data, or separately. When metadata is fetched separately we typically do a couple of modifications before saving it into the store. Firstly, if the data has use_shape_regions
or use_shape_area_level
set we fill in these filter options from those already in state. We also commit the initial selections, this runs any default effects and saves them resulting controls and filters into plotSelections
state, it also runs getPlotData
which manges running the plot-specific filtering logic and saves this into the plotData
state.
The plot should have a prop which is of type PlotName
. It should then read the metadata via the getMetadataFromPlotName
helper and use the data from plotData
state. For each specific plot, it should then modify the plotData
into a structure which can be used by plot component. Potentially using metadata if it needs, including any customPlotEffect
s for the selection.
This dispatches an action plotSelections/updateSelections
which runs any effects to get the new state, it then runs getPlotData
updating the plotData
in state with the filtered data.
If you are using an existing data source or metadata source, you will need to
filtersAfterUseShapeRegions
and commitPlotDefaultSelections
.PlotName
getMetadataFromPlotName
, it should just map to the location of the metadatagetPlotData
, if this is using a new data source, write a function to do the filtering for this data source, and run any specific logic for this data source type.plotData
state and metadata via getMetadataFromPlotName
.Modify the metadata before it is ever used, write it in the action after the data is fetched and before it saved to state. For example, setting options for a filter to those from the shape file.
Customise the filter logic, write it in one of the function getPlotData
calls to. For example can use this to switch between different data sources in one plot (time series), derive some data from selections (population data gets aggregated to selected level)
Modify selections when a plot control changes, hook into the filtering logic, you can do this in handlePlotControlOverrides
. We have used this to set x-axis for the comparison barchart. Plot selections cannot by default update other plot selections, but we wanted this for the comparison barchart. We wanted the UI to show a "plot type" control but not let users modify the x-axis because they could easily break the display. Instead we have a hidden x-axis and then hook into the filtering logic to set the x-axis control if the "plot control" is changed. This one I think we should be careful with, if we spot common overrides we should think about refactoring this into an effect.
Modify the plot itself based on a plot control, use a customPlotEffect
to set metadata for this specific plot selection e.g. for the table we specify in customPlotEffect
what category in the data should be a column group or shown in the rows.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.