Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/docs/developers/build/dashboards/canvas-widgets/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ The aggregation grain works the same way with `grain`. Time-series charts and KP
time_filters: grain=week
```

To compare a measure against a target instead of against an earlier period, add
`measure_comparisons`. The comparison is made over the selected time range.
`compare_to` can be a measure of the same metrics view or one of the grid's
`adhoc_measures`. If it has no single total, because it has
`required_dimensions`, the card shows no comparison:

```yaml
- kpi_grid:
metrics_view: auction_metrics
measures:
- requests
measure_comparisons:
- measure: requests
compare_to: target_requests
comparison:
- previous
- percent_change
```

## Leaderboard

Leaderboards show ranked data with the top performers highlighted.
Expand Down
29 changes: 29 additions & 0 deletions runtime/ai/instructions/data/resources/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,35 @@ kpi_grid:
hide_time_range: true
```

**Against a target instead of the past:**

Use `measure_comparisons` when the thing to compare against is another measure
of the same metrics view, such as a budget or a forecast. The comparison is
then made over the selected time range rather than an earlier one, and the
time comparison toggle no longer applies to that measure.

```yaml
kpi_grid:
metrics_view: sales_metrics
measures:
- total_revenue
- gross_margin_pct
measure_comparisons:
- measure: total_revenue
compare_to: target_revenue
- measure: gross_margin_pct
compare_to: target_margin_pct
comparison:
- previous # here, the target's value
- percent_change
```

`compare_to` can name a measure of the same metrics view or an entry in the
grid's `adhoc_measures`. A target with `required_dimensions` has no single
total, so the card shows no comparison for it. For a percentage measure use
`delta` rather than `percent_change`: the relative change of a percentage is
misleading, and Rill omits it, so `delta` gives the difference in points.

### Leaderboard

Display ranked dimension values by measures:
Expand Down
27 changes: 27 additions & 0 deletions runtime/canvas/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,33 @@ func (v rendererValidator) validateKPIGrid(props map[string]any) error {
}
}

comparisons, ok := props["measure_comparisons"].([]any)
if !ok && props["measure_comparisons"] != nil {
return errors.New("renderer properties for kpi_grid must have 'measure_comparisons' as an array")
}
for _, c := range comparisons {
entry, ok := c.(map[string]any)
if !ok {
return errors.New("each entry in 'measure_comparisons' must be an object with 'measure' and 'compare_to'")
}
measure, ok := pathutil.GetPathString(entry, "measure")
if !ok {
return errors.New("each entry in 'measure_comparisons' must include a 'measure' string")
}
// Checked against the metrics view, not against 'measures': an entry for a
// measure the grid no longer shows is inert, not an error.
if !v.hasMeasure(mv, measure, ephemeralNames) {
return fmt.Errorf("referenced measure_comparisons measure %q is not a measure in metrics view %q", measure, mvn)
}
compareTo, ok := pathutil.GetPathString(entry, "compare_to")
if !ok {
return errors.New("each entry in 'measure_comparisons' must include a 'compare_to' string")
}
if !v.hasMeasure(mv, compareTo, ephemeralNames) {
return fmt.Errorf("referenced compare_to value %q is not a measure in metrics view %q", compareTo, mvn)
}
}

return nil
}

Expand Down
88 changes: 88 additions & 0 deletions runtime/canvas/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,70 @@ kpi_grid:
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 1, 0)
testruntime.RequireReconcileErrorContains(t, rt, id, runtime.ResourceKindComponent, "c1", "is not a measure")

// Valid: a measure compared against another measure.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
type: component
kpi_grid:
metrics_view: mv1
measures:
- y
measure_comparisons:
- measure: y
compare_to: z
`})
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 0, 0)

// Invalid: compare_to isn't a measure of the metrics view.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
type: component
kpi_grid:
metrics_view: mv1
measures:
- y
measure_comparisons:
- measure: y
compare_to: nonexistent
`})
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 1, 0)
testruntime.RequireReconcileErrorContains(t, rt, id, runtime.ResourceKindComponent, "c1", "compare_to")

// Invalid: measure isn't a measure of the metrics view, e.g. a typo.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
type: component
kpi_grid:
metrics_view: mv1
measures:
- y
measure_comparisons:
- measure: nonexistent
compare_to: z
`})
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 1, 0)
testruntime.RequireReconcileErrorContains(t, rt, id, runtime.ResourceKindComponent, "c1", `measure_comparisons measure "nonexistent"`)

// Valid: an entry for a measure the grid no longer shows is inert, not an
// error. Removing a measure from the visual editor leaves one behind, and
// failing the resource for it would be a state the editor cannot undo.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
type: component
kpi_grid:
metrics_view: mv1
measures:
- y
measure_comparisons:
- measure: z
compare_to: y
`})
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 0, 0)
}

func TestValidateTable(t *testing.T) {
Expand Down Expand Up @@ -1703,6 +1767,30 @@ kpi_grid:
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 0, 0)

// A kpi_grid comparing a measure against a ephemeral measure, and a ephemeral
// measure against a measure, should be valid.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
type: component
kpi_grid:
metrics_view: mv1
measures: [y, profit]
measure_comparisons:
- measure: y
compare_to: target
- measure: profit
compare_to: z
adhoc_measures:
- name: profit
display_name: Profit
expression: y - z
- name: target
display_name: Target
expression: z * 2
`})
testruntime.ReconcileParserAndWait(t, rt, id)
testruntime.RequireReconcileState(t, rt, id, 4, 0, 0)

// A leaderboard referencing a ephemeral measure should be valid.
testruntime.PutFiles(t, rt, id, map[string]string{
"c1.yaml": `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
time_filters: kpiGridProperties.time_filters,
}));

$: comparisonMeasureNames = kpis.map(
(kpi) =>
kpiGridProperties.measure_comparisons?.find(
(comparison) => comparison?.measure === kpi.measure,
)?.compare_to,
);

$: filters = {
time_filters: kpiGridProperties.time_filters,
dimension_filters: kpiGridProperties.dimension_filters,
Expand Down Expand Up @@ -72,6 +79,7 @@
{#if $timeAndFilterStore}
<KPIProvider
spec={kpi}
comparisonMeasureName={comparisonMeasureNames[i]}
{timeAndFilterStore}
{canvasName}
visible={$visible}
Expand Down
11 changes: 10 additions & 1 deletion web-common/src/features/canvas/components/kpi-grid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export const defaultComparisonOptions: ComponentComparisonOptions[] = [
"percent_change",
];

// Per-measure comparison target persisted in the canvas YAML. A list (not a
// map) mirrors how per-measure config is expressed elsewhere in canvas. A
// measure listed here ignores the time comparison toggle.
export interface KPIMeasureComparisonSpec {
measure: string;
compare_to: string;
}

export interface KPIGridSpec
extends ComponentCommonProperties,
ComponentFilterProperties {
Expand All @@ -47,12 +55,13 @@ export interface KPIGridSpec
hide_time_range?: boolean;
// Defaults to "delta" and "percent_change"
comparison?: ComponentComparisonOptions[];
measure_comparisons?: KPIMeasureComparisonSpec[];
}

export class KPIGridComponent extends BaseCanvasComponent<KPIGridSpec> {
minSize = { width: 2, height: 2 };
defaultSize = { width: 6, height: 4 };
resetParams = ["measures", "adhoc_measures"];
resetParams = ["measures", "adhoc_measures", "measure_comparisons"];
type: CanvasComponentType = "kpi_grid";
component = KPIGrid;

Expand Down
4 changes: 1 addition & 3 deletions web-common/src/features/canvas/components/kpi/KPI.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,7 @@

{#if comparisonLabel}
<p class="text-sm text-fg-secondary break-words">
{m.kpi_vs_comparison({
comparison: comparisonLabel?.toLowerCase() ?? "",
})}
{m.kpi_vs_comparison({ comparison: comparisonLabel ?? "" })}
</p>
{/if}
{/if}
Expand Down
Loading
Loading