Data and states
A KPI card has a card-level data source and per-component states. This article explains how the query result is mapped to components and how each component's states drive its appearance.
The data source
A card has a card-level DataSource — a SQL query that runs once per card and returns one row —
which is the default result for every component. (If the query returns more than one row, only
the first row is used.)
SELECT SUM(Amount) AS NumericValue, 'Revenue YTD' AS TextValue FROM FactSales
A component can override this by declaring its own DataSource child element; when it does, that
component resolves its value, text, and states from its own result instead of the card's. When a
component leaves DataSource blank, it falls back to the card-level result. (The Chart component
is the exception: its DataSource returns the whole series and has no card-level fallback — see
Components.)
The query runs with the current Workbook filter and parameter context applied.
The card's value and text
From the result row the card resolves a single value (the primary number) and an optional text label, which the components display:
- Value — if a component sets
ValueColumn, that named column is used; otherwise, if the result has exactly one numeric column that column is used, and failing that the first column is used. - Text — if a component sets
TextColumn, that named column is used; otherwise, if the result has exactly one non-numeric column (other than the value column) that column is used, and failing that the first column that is not the value column is used.
See Mapping columns to components below for the per-component overrides.
Reserved columns in conditions: NumericValue and TextValue
State conditions can reference two reserved columns in addition to the result-set columns:
NumericValue— the resolved value, as a number.TextValue— the resolved text.
These let a condition test the card's value/text without knowing the underlying column name. If your
result set already contains a column named NumericValue or TextValue, that column is used
directly instead of the resolved value/text.
Mapping columns to components: ValueColumn and TextColumn
Components that display card data can override which result column they read. The components that
show a number read ValueColumn (Metric, TrendDirection, TrendBadge); the components that show text
read TextColumn (Text, TrendText). The purely state-driven components — StatusBlock, Image, and
StatusIndicator — are driven by their own states and ignore both.
ValueColumn
The name of the column this component should read as its value, instead of the resolved value. If the named column does not exist in the result set, the card reports an error.
TextColumn
The name of the column this component should read as its text, instead of the resolved text. If the named column does not exist in the result set, the card reports an error.
This lets a single query feed several components from different columns. For example, a query
returning Revenue, Margin, and Region can drive one Metric bound to Revenue and another
bound to Margin:
SELECT 1200000 AS Revenue, 0.18 AS Margin, 'North' AS Region
<Row>
<Metric ValueColumn="Revenue" FormatString="0,0" />
<Metric ValueColumn="Margin" FormatString="0%" />
<Text ValueColumn="Region" />
</Row>
States: conditional color, image, and status
Each component can carry its own States — a list nested inside the component element, which is
optional; a component with no states simply renders without a state-driven color, image, or status.
A state is a condition plus the visual outcome to apply when that condition matches. The outcome
values are written as Property elements (<Property Name="Color" Value="green" />) inside a
Properties element, a sibling of Condition:
Condition
A boolean JavaScript expression evaluated against the component's single result row. The result columns are available by name, including the reserved
NumericValueandTextValuevalues, plus any other column in the result set. Use JavaScript operators —>,>=,===,!==,&&,||,!— for exampleNumericValue > 25. A state with no (or an empty)Conditionalways applies as a base (see evaluation below).The
<Condition>element accepts an optionalAsyncattribute. WithAsync="true"the condition runs as an asynchronous function and mayawaitHTTP calls —HttpGet(url),HttpPost(url, { data }),HttpPut(url, { data }),HttpDelete(url)— to derive its result, for exampleAsync="true"withNumericValue > (await HttpGet('/api/target')).value. Without the attribute (the default) the condition runs synchronously and has no network access. Either way the condition must still produce a boolean — it decides which state matches; it cannot set the displayed value, color, image, or status (those remain thePropertyvalues).
Color (<Property Name="Color" Value="…" />)
The color applied when this state matches. Any CSS color works — a named color (
green,red,yellow) or a hex value (#1a9c4f). It colors the component that owns the state (for example Metric, StatusBlock, or TrendText); on the card-levelCardBorderelement it drives the card's border color.
Image (<Property Name="Image" Value="…" />)
An image applied when this state matches, used by the Image component. The value is either an Image Library reference of the form
@images/<image-name>.pngor a raw image URL (see Images and the Image Library below).
Status (<Property Name="Status" Value="…" />)
A status token applied when this state matches, consumed by the StatusIndicator component to choose its status icon. Allowed values are
Complete,HalfWay, andInitial.
Two-layered evaluation
Each component resolves its own states on the client, against its own result row, in two layers:
- Base layer — every state with no (or an empty)
Conditionalways applies. TheirColor,Image, andStatusform a base; when several condition-less states exist, the first-declared value wins for each property independently. - Conditional overlay — the first state whose condition is truthy overlays the base: each property it sets overrides the base, and any property it leaves unset falls through to the base.
If no conditional state matches, the base alone applies; if there is no condition-less state either, the component renders without a state-driven color, image, or status.
Conditional states are evaluated in declared order and short-circuit at the first match — so an
Async="true" condition only issues its HTTP request if no earlier conditional state has
already matched. If a condition fails — a malformed expression, an invalid column name, or a
failed/rejected async request — the error surfaces to the user and halts the card's state
evaluation, rather than passing unnoticed. Write conditions that reference valid columns and
produce a boolean.
Because an Async="true" condition's HTTP request runs as part of the card's first load, the card's
loading skeleton stays visible until that request resolves (see
Execution).
<States>
<State>
<Condition><![CDATA[NumericValue > 25]]></Condition>
<Properties>
<Property Name="Color" Value="green" />
<Property Name="Status" Value="Complete" />
<Property Name="Image" Value="@images/trend-up.png" />
</Properties>
</State>
<State>
<Condition><![CDATA[NumericValue <= 25]]></Condition>
<Properties>
<Property Name="Color" Value="red" />
<Property Name="Status" Value="Initial" />
<Property Name="Image" Value="@images/trend-down.png" />
</Properties>
</State>
</States>
A condition-less state can supply defaults that a matching state then overrides — here the base color is grey, and the conditional state turns it orange when it matches:
<States>
<State>
<Properties>
<Property Name="Color" Value="grey" />
</Properties>
</State>
<State>
<Condition><![CDATA[NumericValue < Target && Region === 'North']]></Condition>
<Properties>
<Property Name="Color" Value="orange" />
</Properties>
</State>
</States>
An Async="true" condition can compare the value against data fetched from an API at render time:
<States>
<State>
<Condition Async="true"><![CDATA[NumericValue >= (await HttpGet('/api/targets/current')).target]]></Condition>
<Properties>
<Property Name="Color" Value="green" />
</Properties>
</State>
</States>
Images and the Image Library
The state Image value (rendered by the Image component) is most often an Image Library
reference of the form:
@images/<image-name>.png
@images/ points at images stored in your solution's Image Library (web assets). At render
time the @images prefix is resolved to the deployed image path, so the named image must exist in
the Image Library for it to display. For example, @images/trend-up.png resolves to the
trend-up.png image in the library.
To use a library image: add it to the Image Library, then reference it by name as
@images/<image-name>.png in the state's Image property.
A raw image URL is also accepted. Any value that does not start with
@imagesis used as-is, so you can point at an external or absolute image URL directly. Using an@images/reference is recommended where possible, since it keeps the image inside the solution and travels with it on import/export. (The@images/convention matches the Button control'sImageproperty.)