# Visualisation (/docs/analytical-apps/visualisation)



The **Visualisation** is where your team works with a finished analytic app. It is
built from **widgets** on a grid, tables, charts, text, and inputs, that turn the
results of your pipeline into something people can actually use. It is more than a
report to read: from the Visualisation your team can run the app, see the latest
run results, filter the data, type values back into a table, and download what they
need. An app can hold more than one page of widgets, so a single app can cover
several related topics.

## How the Visualisation is built [#how-the-visualisation-is-built]

You build it in the editor, on the **Visualisation** tab. Add widgets, arrange them
on the grid by dragging and resizing, and give each data widget a **data source**:
the pipeline step (called a node) whose results it should show.

A Table, Chart, Pivot or Figure widget can read any node that holds data, and so can
a parameter whose dropdown options load from a node: a
[dataset](./pipeline/datasets.mdx), a [SQL node](./pipeline/sql-nodes.mdx), and the
two nodes that import data from elsewhere, [indicator data](./pipeline/indicator-data.mdx)
from one of your indicator apps and [analytic app data](./pipeline/analytic-data.mdx)
from another analytic app. You can show imported figures directly, without a SQL node
in between. An imported node gets its columns from the pipeline's last successful
run, so run the pipeline once after adding one: until then the widget's column
settings ask you to run it first.

For data to appear, the pipeline must have run at least once. If a widget prompts
you to run the pipeline, switch to the **Data Pipeline**, click **Run**, and the
widget fills with fresh results. See the [Pipeline overview](./pipeline/overview.mdx)
for how running works.

While you work, all of this lives in your private draft. Click **Publish** to hand
the new layout to your team; until you do, they keep seeing the previous one. See
[Draft and publish](../overview/apps.mdx#draft-and-publish).

## When the Visualisation shows nothing [#empty-visualisation]

An app your team opens shows results from the latest **published** run. When there
is no such run, the widgets are empty and a notice at the top of the page says
which of three things is missing, and what fills it:

| The notice says                 | What it means                                                                                                                                                                                      | What fills it                                                                                         |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Not published yet               | The app has no published version, so your team has nothing to see. Running it in this state only runs your draft.                                                                                  | **Publish** the app from the editor (a member with the **App builder** or **Admin** role on the app). |
| Not calculated yet              | The app is published but its pipeline has never run.                                                                                                                                               | Click **Run** on the Visualisation, or run it from the Data Pipeline.                                 |
| No run has produced results yet | The pipeline has run, but no run has produced results for your team. A preview run of the draft keeps its results private to the editor; a run that failed or is still going has none to show yet. | Click **Run** on the Visualisation - it runs the published version.                                   |

If there is no notice and a widget is still empty, the app IS populated and the
question is the data itself: the filter or query matched no rows.

The **Run** button appears only for people who may run apps. Everyone else sees the
same notice ending in "Ask a workspace administrator...", because the run would be
refused for them.

## When a widget names a column that no longer exists [#missing-column]

A chart or pivot that groups its data names the columns it groups by and the
columns it adds up, and how it adds each one up. If one of those columns is not in the
widget's data source any more - it was renamed or removed in the pipeline, or Vera or
an import created the widget with a column that was never there - or a summary names a
function other than sum, count, avg, min or max, the widget does not guess. It shows a
short message instead of data:

| Where  | What you see                                                                                                                                                                                                                                          |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pivot  | "This widget's grouping can't be read: it names a column its data source doesn't have, an aggregate with no column, or a function other than sum, count, avg, min or max. An editor can fix it in the widget's settings.", in the pivot's grid.       |
| Chart  | "This chart's grouping can't be read: it uses a column its data source doesn't have, or a function other than sum, count, avg, min or max. An editor can fix it in the chart's settings.", in place of the bars or slices.                            |
| Report | A pivot block keeps its column headings, and the line under it says "The grouping can't be read: it names a column its data source doesn't have, or a function it can't apply" where the row count would be. A chart block shows the chart's message. |

There is no **Try again**: asking again gives the same answer until the widget changes.

To fix it, open the widget's settings in the editor. The column that no longer exists
shows as an empty choice: pick one of the data source's columns there, or remove that
row with its **X**. The widget reads its data again and draws as soon as the answer
arrives. People reading the published app see the message until you
[publish](../overview/apps.mdx#draft-and-publish) the corrected version.

## When a widget uses an unsupported feature [#unsupported-feature]

A widget whose saved settings use a feature Ovidens no longer supports shows
"This widget uses a feature that is no longer supported" in its own space. The
other widgets on the page keep working. The same notice appears wherever a report
or printout places that widget. Vera and connected AI clients receive the notice
as a note, with no data for that widget.

An unsupported widget does not stop you editing the rest of the Visualisation:
the editors keep saving your other changes and leave that widget exactly as it was
saved. To clear the notice, remove the widget or replace it with a new one. If an
edit leaves a widget with a setting its type does not accept, a persistent banner
names the widget and its page, and changes are not saved until you correct or
remove it.

Each widget accepts only the settings for its own type. When Vera, a connected AI
client or an app import supplies an invalid setting, Ovidens refuses the write and
names the widget and the field. Choosing a different type in the editor starts
with that type's settings.

## Widget settings for AI clients [#widget-settings]

The tool schema describes nine authored widget types selected by `type`. All share `id`,
`type`, `title`, `description`, `descriptionAsTooltip`, `width` and `height`.
Widths are `full`, `half`, `third` or `quarter`; heights are steps 1 through 4 or
`auto`. Each alternative adds only these fields:

| Type             | Settings                                                                                                                                                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `table`          | `nodeId`, `fixedFilters`, `parameterOptOut`, `visibleColumns`, `pinnedColumns`, `linkColumns`, `columnWidths`, `editable`, `contextSources`                                                                                   |
| `chart`          | `nodeId`, `fixedFilters`, `parameterOptOut`, `chartType`, `xColumn`, `chartGrouped`, `chartStacked`, `chartOrientation`, `chartLabelAngle`, `chartCategoryOrder`, `chartShowLegend`, `chartCategoryLabels`, `chartAggregates` |
| `pivot`          | `nodeId`, `fixedFilters`, `parameterOptOut`, `tableGroupBys`, `tableAggregates`, `columnWidths`                                                                                                                               |
| `stat`           | `nodeId`, `fixedFilters`, `parameterOptOut`, `statColumn`, `statAggregation`, `statFormat`, `statComparisonFilters`                                                                                                           |
| `parameter`      | `params`                                                                                                                                                                                                                      |
| `sql`            | `nodeId`, `textContent`                                                                                                                                                                                                       |
| `text`           | `textContent`, `textAlign`                                                                                                                                                                                                    |
| `ai`             | `skillTechnicalId`                                                                                                                                                                                                            |
| `knowledge_base` | `kbItemKey`                                                                                                                                                                                                                   |

Nested entries also have their own schemas. Filters use `id`, `column`, `operator`
and `value`; aggregates use `id`, `column`, `function` and an optional `label`
(a chart aggregate can also have `color`). Aggregate functions are `sum`, `count`,
`avg`, `min` and `max`. A table has no grouping settings: use a pivot to summarise
rows. The tool schema states which fields can be omitted. Omitted settings on an
existing widget are kept; a new widget starts with its type's defaults.

An unsupported widget is a read-only fallback, outside the tool schema. A write that
leaves it as it is - sending it back as exactly its `id` and `type`, or not naming it
in an edit - keeps it exactly as stored, so it never blocks a change to another widget.
Sending it back with all its stored settings is refused.
A write that changes it without making it valid is refused, naming the widget and
its settings: remove it, or change its `type` to rebuild it. Changing a widget's
type keeps its title, description and size and, when the new type reads data, its
node, fixed filters and parameter opt-out; every other setting starts from the new
type's defaults.

An AI write is limited in length for what it adds or changes: a new page or widget id
is at most 64 characters, a widget title 200, a page name 120 and a widget description
2,000. A value sent back unchanged is not limited, so a longer name someone typed in
the editor stays. A text widget's content has no limit.

## The widgets [#the-widgets]

Every widget is added in the editor and positioned on the grid. Table, Chart,
Pivot and Figure widgets use a pipeline node as their data source; the other
types have the settings described below.

### Text [#text]

The **Text** widget puts formatted text on the page: headings, short notes, and
instructions. Use it to give the page structure and to explain what people are
looking at, for example a title, a note on where the data comes from, or how to use
a filter. It shows no data, so it needs no data source.

### Table [#table]

The **Table** widget shows your data as rows and columns, straight from a node. You
choose which columns appear. To summarise rows by group, use a [pivot](#pivot). A table works in one of two ways:

* **Read-only** tables display data and cannot be changed, the usual choice for a
  report.
* **Data-entry** tables let people type values directly into the table from the
  app. Edits are saved when the pipeline runs. This is how a Visualisation becomes
  a place to enter data, not just read it. Only a data-entry table offers
  **Import** for loading a file into it, and in the editor its header carries an
  **Editable** label so you can tell it apart from a read-only table at a glance.
  A table with fixed filters is always read-only:
  its settings show a short note saying why instead of the **Editable** switch.
  Adding fixed filters also switches **Editable** off, and removing them later leaves
  it off, so switch it back on if the table should take entries again.

Anyone whose role lets them enter data in the app - **Contributor**, **App builder** or **Admin** - can edit the rows of a data-entry table: change a
value, add a row, or delete one. Each change is recorded in the audit trail under
the name of the person who made it. **Import**, loading a file into the table, stays
with workspace administrators, so other members do not see it.

Tables respect your [parameters](#parameters): with a region filter set to "North",
the table shows only North rows, and a small label shows which filter is active.

A table keeps the rows it holds in view. With an automatic height it grows with its
rows, and while someone is editing it (**Edit**) it grows a little more to make room
for the **Save** and **Cancel** buttons and the **New row** strip, so a table filtered
to the one or two rows being corrected keeps them on screen. With a fixed height the
card keeps its size; if it is too small for the table, the table scrolls inside the
card, under the widget's title, so every row, the page controls and **Save** and
**Cancel** can still be reached.

With an automatic height, a table is never taller than the window it is read in. It shows
as many whole rows as fit below its title, together with its page controls and its **Show
all** button, and always at least five rows, so in a very small window the page scrolls
instead. A [pivot](#pivot) with an automatic height does the same. Resize the window or
zoom the browser and the table follows, showing more or fewer rows, without reloading
anything. **Show all** shows up to 100 rows at once, and the page scrolls to reach them.

If a table's data cannot be loaded, the table says so in its own space, where the
column names and rows would be, with a **Try again** button that asks again. The message
stays while the request is repeated; then the rows come back, or the message shows again if
the data still cannot be loaded. Search, sort and filter stay available, because
changing any of them asks for the data again. **Edit*&#x2A; and **+ Row** are unavailable
until the data loads, and anything typed but not yet saved is kept.

A table can be downloaded from the widget itself. You choose CSV or Excel - the
Excel file keeps the column types and the percent and currency formatting the table
shows on screen, so it opens ready to sort and total. See
[Download the data](pipeline/datasets.mdx#download-the-data).

Which rows the file holds depends on where the table is. A table in the
[Audit Analytics](../administration/audit-trail.mdx) app, or on a survey's
**Results** page, downloads the rows it shows: its filters (including the ones a
link set when it opened the page already narrowed to one resource), the page's
[parameters](#parameters) and the search all apply, so a table showing 312 rows
downloads 312. The file carries every column of the table's data source, not only
the ones on screen, and its rows come in the table's own default order rather than
one you sorted them into. A table in any other app downloads its whole data source, whatever is
filtered on screen. A spreadsheet holds about a million rows, so Excel is refused
for a bigger table, with a message saying so - choose CSV for it.

#### Files in a table [#table-attachments]

A table over a dataset with an **Attachment** column shows each row's files exactly as the
dataset's own data view does. Click the file icon in a cell to see the files with their names
and sizes, download one (you are asked what to name it first) or see an image in a larger view.
Downloading works on every table, read-only ones included, for anyone who can read the app.

On a data-entry table, administrators can also add and remove files while editing (**Edit**):
choose **Add file** on an empty cell or the paperclip on a filled one, then pick files or drag
them onto the upload dialog. Each file can be up to 10 MB; a bigger one is refused in the
dialog and nothing else you changed is lost. To replace a file, remove it and add the new one.
A file is uploaded when you confirm the dialog, but it becomes part of the row only when you
choose **Save**; **Cancel** drops it with the rest of the unsaved changes. Other readers of the
app can download files but not add or remove them. See
[Files in an attachment column](pipeline/datasets.mdx#attachments) for what happens to a file
that is removed.

#### Context columns [#context-columns]

**Context columns** put facts from other datasets beside each row, so the people
correcting a table see what they need without leaving it. A table over a dataset,
read-only or data-entry, can carry up to 10 of them in total, shown after its own
columns with a lock in the header that names the dataset each one comes from.

Context columns come from **context from a dataset**: you choose a dataset of the same
app, and how its rows match this table's rows, once, and then pick as many of its
columns as you need. Every column of one context reads the same matching rows. Each
column is one of:

* **Value** shows a column of that dataset from the first matching row: each order's
  product category from your products dataset, for example.
* A **function** counts or adds up all the matching rows: how many customers bought
  each product, or the revenue of all its orders, for example. The functions are
  **Count**, **Count distinct**, **Sum**, **Average**, **Minimum** and **Maximum**, and
  that list is complete: a calculation beyond them, such as a ratio or a count of only
  some rows, belongs in a SQL node.

One context can mix both: from your orders dataset, the first customer, the number of
customers and the revenue, all matched on the product.

To add context, open the table's settings and find **Context columns** in the Data
group, under the columns. Click **Add context from a dataset**. Choose
**From dataset**, then **Match rows where** a column of this table equals a column of
that dataset (**Match another column** adds a second or third pair). Then, for each
column, choose its **Column** and its **Function** - **Value** or one of the
functions - and give it a **Name**; **Add column** adds another column from the same
dataset. The editor offers only the functions the dataset has a column for and, per
function, only the columns it can use. Renaming a column, or changing its function,
keeps it the same column. If you later switch a context to another dataset, the columns
that dataset also has stay - found by the same column, or by a column of the same
name - and the others go; removing a context's last column removes the context. One
context is open for editing at a time.

**How values match.** A dropdown is compared by the label you see. A number and a text
are compared as numbers, so `1` matches `"1"`, `"1.0"` and `"1.00"`, while a text that
is not a number matches nothing - that includes a number written with a decimal comma,
such as `1,5`. A date and a timestamp are compared by the calendar day, so the date
5 January 2024 matches every timestamp on that day. A text and a date or a timestamp
are compared as that date or timestamp: `2024-01-05` matches the date 5 January 2024,
and against timestamps it matches only midnight of that day. Two timestamps must be
exactly equal. Rows of the other dataset whose match value is empty, or does not read
as the type it is compared as, are left out.

**What a column shows.** A row with no match shows 0 for a count and stays empty
otherwise. A row whose own match value is empty shows empty in every column, counts
included, because "no customer" is not "no orders". **Count** without a column counts
the matching rows, and with a column counts its non-empty values. **Average** is the
mean of the non-empty values. **Minimum** and **Maximum** of a text compare by
character code, so `Zebra` comes before `apple`, and a dropdown compares by its label,
not by the order of its options. A sum keeps its column's type: a sum of a currency
column shows that currency, a sum of a percent column a percent, and a sum of whole
numbers a whole number; an average keeps a currency or a percent and is otherwise a
decimal. A Value, a minimum and a maximum of a currency or percent column show its
format too.

**What is refused.** **Sum** and **Average** take a number, currency or percent
column, but not a year: the sum of years is not a number anyone reads. A multi-select
column holds several options in one cell, so it cannot be a match column, and
**Count distinct**, **Minimum** and **Maximum** cannot read it; **Count** and **Value**
can.

Context columns sort, filter and search like any column, but they are never edited:
they are not part of **Edit**, the new-row form or the record panel, and the text
filter suggests no values for them. They always show, since hiding and pinning apply to
the table's own columns. Their values come from the same data as the rows - your draft
in the editor, the published version for your team - and follow an edit the next time
the table loads, with no pipeline run. A download carries the table's own columns
only.

When a match value appears more than once in a context's dataset, a **Value*&#x2A; column
shows the first row's value and an amber &#x2A;*!*&#x2A; in its header says how many values
repeat; a function reads every matching row and shows no warning. A column that cannot
be read - its dataset or column was deleted, or it asks for
something refused above - stays empty with a red &#x2A;*!** in the header saying why, while
the rest of the table loads as usual. [Vera](../tips/interact-with-vera.mdx) can add
context columns too; one that names something that does not exist, or asks for
something refused above, is refused with the reason.

### Chart [#chart]

The **Chart** widget draws your data as a **bar**, **line**, **area**, **scatter**,
**pie** or **donut** chart, so a trend or a ranking is obvious at a glance. Bar
compares values across categories, line and area show change over time, pie and donut
show how a whole splits into parts, and scatter plots two number columns against each
other, to show how one measure moves with another. A chart needs a data source plus
the column that labels each point or bar, the column that supplies the values, and
optionally more than one series to compare. You can **group by a category**, and the
chart redraws to match your [parameters](#parameters).

A scatter's horizontal axis and each of its series must be **number** columns: over a
text or date column there is nothing to plot, so the chart says so instead of drawing
an empty plot. A scatter can carry several series, each plotted at its own values. A
chart saved with a type Ovidens does not draw, by another tool for example, names that
type in place of a drawing until an editor picks a chart type in its settings.

A pie, a donut, or a grouped bar, line or area chart whose label or value column is
no longer in its data source shows a message instead of a drawing, and an editor
fixes it in the chart's settings. See
[When a widget names a column that no longer exists](#missing-column).

Each series can also carry its own **label**: open the pencil on the series row to type
one (or clear it back to the column name with Reset), and the legend and the tooltip
both use it - handy when two series aggregate the same column in different ways.

Five settings control how a chart reads:

* **Stacked** (bar and area) places the series on top of one another instead of side
  by side, so each bar shows a total that is split into parts rather than several
  values to compare.
* **Bars** (bar only) chooses vertical columns or horizontal bars. Horizontal is the
  one to reach for when your category names are long: they are read down the side,
  where there is room, instead of packed across the bottom.
* **Label angle** (vertical columns only) turns the category labels flat, at an angle,
  or upright. Turning them fits far more categories across the bottom. When there are
  still more labels than will fit legibly, the chart shows every second or third one
  rather than overlapping them.
* **Category order** decides the order the categories are drawn in, and it changes what
  the chart says: left to right as the data came back, A to Z, highest value first, or
  lowest first. Highest-first turns a chart into a ranking. Line and area charts offer
  only the first two, because their horizontal axis usually runs in time and sorting
  those points by value would destroy the trend they exist to show.
* **Legend** (every chart type) shows or hides the colour key naming each series under
  the chart. It is on by default; switch it off when the tile is tight and the series
  are obvious - pie and donut charts included, whose legend is how their categories
  are read.

### Pivot [#pivot]

The **Pivot** widget groups rows together and shows a summary for each group. An
editor sets it up in the pivot's settings: one or more **group-by** columns (such as
region) and one or more summary columns, each a sum, count, average, minimum or
maximum of a column (such as the sum of revenue). A year column offers count, minimum
and maximum only, because a sum or average of years is not a number anyone reads.
People reading the page see the result, one row per group. Use a [table](#table) when people need the detail, and a
pivot when they need the totals.

A new pivot starts on a default so it shows something straight away: it groups by
the first column that is not a number and adds up the first number column that is not
a year. The
default only fills a part you have left empty. A column you chose is always used as
you chose it and never quietly swapped for another, so if it is no longer in the data
source the pivot shows a message instead of a grid, and an editor fixes it in the
pivot's settings. See [When a widget names a column that no longer exists](#missing-column).

**Formats.** A pivot shows its figures the way the data source defines them. A sum of
a currency column is shown in that currency with its decimal places, and a sum of a
percent column with its decimal places and a % sign. A count is always a whole number,
because it counts rows. An average of whole or decimal numbers is rounded to two
decimal places. The earliest or latest date of a date column is shown as a date, and a
year is shown as a year (2023, never 2,023). A group-by over a dropdown column shows the
option's name and colour.

**Every group, page by page.** A pivot shows every group its data has, a page at a
time, and its pager says how many groups there are.

**Searching, filtering and sorting.** The search box finds groups by the values of the
group-by columns, and a dropdown group-by by the option names you see. A filter on a group-by column narrows the rows before the summaries
are taken, so the totals follow the filter. A filter on a summary column keeps the
groups whose summary passes it, such as the regions whose revenue is above 10,000.
Sorting by a summary column puts the groups in order of their summary. The pivot's
**Fixed filters** (its settings' Data group) and the page's
[parameters](#parameters) narrow the rows before the summaries are taken too.

**Full screen and download.** Open a pivot in full screen from its header (in the
editor, from the buttons over the tile); your search stays as it was. The download
holds every group the pivot shows under its current search, filters and sort, under
the same column headers. Figures are written as plain numbers so a spreadsheet can
calculate with them; an Excel file keeps a currency or percent column's format, and a
date is written as a date (2024-01-01).

If a pivot cannot read its data, for example because the query took too long, it says
so in place of the grid and offers **Try again**. A report block shows a pivot exactly as
the tile does, formats, years and dropdown names included.

### Naming a summary column [#aggregate-labels]

Each summary column of a pivot can have its own
header. In the widget's settings, open the pencil button next to the summary and type
the header into **Column header**, for example "Revenue in EUR". The header is shown
above that column on the page, in full screen, in reports and in a
downloaded file; the small badge with the summary (SUM, AVG, ...) stays beside it.
Leave the field empty, or choose **Reset**, and the column takes its data source
column's name again. A report and a download have no badge, so there an unnamed summary
column is headed by the column's name and its summary, such as "Amount (sum)" beside
"Amount (max)", and a filter on it reads the same way above the page.

### Parameters [#parameters]

A **Parameter** widget adds input controls, a dropdown, text box, number field,
date picker, or range, that let people filter or adjust the page without changing
anything you built. Rather than build one page per region, you add a region
parameter: people pick a region and every table and chart updates to match. One
page serves everyone.

Connect a parameter to the **column** it filters on, and provide a dropdown's
options either as a fixed list or loaded live from a pipeline node. A parameter
filters every widget that uses the matching column, so one control can drive the
whole page. A widget that should always show everything (for example a grand total)
can be set to ignore parameters.

### SQL [#sql]

The **SQL** widget puts a query box on the page so someone comfortable with SQL can
ask ad-hoc questions of the pipeline output. Pick a node as the source and it fills
in a starter query to adjust. A query can name a dataset or a SQL node, but not an
imported node (indicator data or analytic app data): to query imported data, add a
SQL node after the import in the pipeline and query that. If you are not comfortable
with SQL you do not need this widget: describe your question to
[Vera](../tips/interact-with-vera.mdx) instead, or use the other widgets, which cover
most needs without a single query.

A query here reads **only this app's own nodes**, named directly by their id: a name with a prefix, another app's table, a system table, a file path or a
table function in `FROM` is refused with a message saying what to change, and so is a
function that reads settings or the catalog, runs SQL given as text, or changes something. The full rule is in
[What the Run button can read](./pipeline/sql-nodes.mdx#what-the-run-button-can-read).

Writing and running a query of your own is a permission of its own, separate from
reading the app. It comes with the **Contributor**, **Analyst**, **App builder** and **Admin** roles on the whole
workspace - a role on a single space or app does not give it - and you always hold it in your
own personal space. Someone who may only view an
app sees each SQL widget's results - the rows of the query saved on the widget -
without the query box, so they read what the page's author wrote and cannot change
it. In the editor, someone who may change the app but not run queries edits the
saved query with no **Run** button, and a short note says why.

### AI [#ai]

The **AI** widget runs a curated AI [skill](./skills.mdx) and shows the answer right
on the page. Its only setting is which skill it runs; the widget takes the skill's
name as its title. If the skill asks for something, for example a region or a date
range, the widget shows one labelled box per question, so the reader fills those in
and clicks **Run**.

While it works you see what Vera is doing, how long it has been running, and how far
it may go. When it finishes you get the answer, with a foldout showing how Vera
arrived at it. It needs no data source: the skill already knows which data to look
at. A reader whose account is not allowed to use AI sees the widget explain that
instead of a Run button.

### Knowledge Base [#knowledge-base-widget]

The **Knowledge base** widget embeds a single page from the app's
[Knowledge Base](./knowledge-base.mdx) on the Visualisation, rendered read-only.
Use it to keep guidance next to the numbers: what the data means, how the app is
meant to be used, or the rules behind a figure, so people do not have to leave
the page to find it. It shows no pipeline data, so it needs no data source; you
simply pick which page it displays.
