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
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ web-common/src/features/dashboards/url-state/filters/expression.cjs
# generated by paraglidejs
web-common/src/lib/i18n/gen
web-common/src/lib/i18n/project.inlang

# generated by web-common/scripts/generate-flint-catalog.ts
web-common/src/features/custom-viz/examples/flint-charts-index.json
177 changes: 177 additions & 0 deletions docs/docs/developers/build/dashboards/canvas-widgets/custom-viz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
title: "Custom Viz Components"
sidebar_label: "Custom Viz"
sidebar_position: 15
---

Custom viz components are reusable visualizations defined as standalone `type: component` files. A component declares typed **params** and renders a [custom chart](/reference/project-files/component) built from a Metrics SQL query and a chart spec. Canvas dashboards reference the component by name and bind values to its params, so one visualization can be reused across many dashboards with different metrics views, measures, and dimensions.

Charts are described declaratively: you state the chart type and which field goes on which channel, and Rill derives the scales, axes, number formats, sorting, stacking, colors, and layout. Rill supplies the semantics of each field from your metrics view — a currency measure formats as currency, the time dimension is treated as temporal at the dashboard's grain — so the spec stays short and the same component adapts when it is bound to a different metrics view.

Unlike an inline `custom_chart` widget, a custom viz lives in its own file (conventionally under `viz_library/`), has a declared parameter contract that Rill validates, and can be developed in a dedicated editor with a live preview.

## Creating a custom viz

In Rill Developer, click **Add → Custom viz → Blank** (or create a file under `viz_library/` by hand):

```yaml
# viz_library/measure_trend.yaml
type: component
display_name: Measure trend

params:
- name: metrics_view
type: metrics_view
required: true
- name: measure
type: measure
required: true
- name: time_dim
type: time_dimension
required: true

custom_chart:
metrics_sql: |
SELECT {{ .params.time_dim }}, {{ .params.measure }}
FROM {{ .params.metrics_view }}
ORDER BY {{ .params.time_dim }}
spec:
chartType: Line Chart
encodings:
x: { field: "{{ .params.time_dim }}" }
y: { field: "{{ .params.measure }}" }
```

There is no schema, sizing, mark, or encoding type to write: `chartType` and `encodings` are the whole spec. An encoding accepts only `field`, `type`, `aggregate`, `sortOrder`, `sortBy`, and `scheme` — and `type` and `aggregate` are rarely needed, since Rill already knows that measures are quantitative and pre-aggregated. Presentation tuning that a chart type exposes (`innerRadius`, `stackMode`, `interpolate`, …) goes under an optional `chartProperties` mapping.

The component editor shows a live preview. Use the **Test values** panel to bind preview-only values to the params; dashboards set their own values. The **Used by** panel lists every dashboard referencing the component.

## Params

Params are the component's contract with dashboards. Each param has a `name`, a `type`, and optionally `required`, `default`, `description`, and (for scalars) `options`:

| Type | Bound value | Notes |
|---|---|---|
| `metrics_view` | A metrics view name | Must be named `metrics_view` or end with `_metrics_view` |
| `measure` | A measure of the bound metrics view | Validated at reconcile time |
| `dimension` | A dimension of the bound metrics view | Validated at reconcile time |
| `time_dimension` | A time dimension of the bound metrics view | The primary time dimension or a time-typed dimension |
| `string`, `number`, `boolean` | A scalar value | Useful for row limits and `chartProperties` |

Params are substituted into `metrics_sql` and `spec` through templating: `{{ .params.measure }}`. A value that is exactly one placeholder keeps the param's type, so a `number` param bound to `innerRadius: "{{ .params.inner_radius }}"` arrives as a number rather than a string. Partial interpolations such as `LIMIT {{ .params.limit }}` are ordinary strings.

When evolving a component that dashboards already use, prefer adding params with a `default` (or `optional`) so existing references keep working. Renaming or removing a param surfaces a validation error on every dashboard that binds it.

## Referencing from a canvas

In the canvas visual editor, open **Add widget → Your components** and pick the component: Rill pre-selects sensible bindings (a metrics view with a time dimension, its first measure, and so on), and the inspector shows a form generated from the declared params. In YAML, a reference looks like:

```yaml
# dashboards/overview.yaml
type: canvas
rows:
- items:
- component: measure_trend
params:
metrics_view: bids_metrics
measure: total_bids
time_dim: __time
width: 8
- component: measure_trend
params:
metrics_view: bids_metrics
measure: avg_bid_price
time_dim: __time
width: 4
```

Bindings are validated when the canvas reconciles: unknown params, missing required params, and fields that don't exist in the bound metrics view are reported as errors on the dashboard. Canvas-level time and dimension filters apply to custom viz like any other widget.

A time dimension selected by the `metrics_sql` is bucketed at the dashboard's time grain, so a time series re-buckets when the time controls change, just like a native chart. Write `date_trunc('<grain>', <field>) AS <field>` in the query only when a chart should stay at one fixed bucket regardless of the dashboard; the alias is required, or the column comes out named after the expression and the encodings can no longer reference it. The component editor's preview has no time controls to inherit from and renders at day grain.

## Starting from a chart type

**Add → Custom viz** opens a gallery of every chart type Rill can render, grouped by family (bars, lines and areas, distributions, circular, points, tables and maps) and labelled with the fields each one needs — for example a heatmap needs two categorical fields and a measure. Picking one writes a component with a required `metrics_view` param, one typed param per channel named after its chart role (`x_axis`, `color`, …), a templated `metrics_sql`, and a row limit sized to the chart's visual density.

When the AI assistant is enabled, the chart type is handed to it instead, and it authors the same component against your project's own metrics views so the params come pre-bound to real fields. You're taken to the component immediately and watch an "Importing chart using AI…" screen while it generates; the editor appears once the component reconciles.

## Ejecting to Vega-Lite

When a chart needs something the chart spec cannot express, **Eject to Vega-Lite** in the component
editor's header replaces `spec` with the Vega-Lite it currently compiles to, under `vega_spec`. From
there the spec is yours to edit: marks, layers, transforms, interactions, anything Vega-Lite can do.

Ejecting keeps the component parameterized. The compiled spec has the bound values resolved into it,
so before writing the file Rill turns them back into template references:

| Resolved value | Written as |
|---|---|
| A field name, wherever it appears (encodings, transforms, expressions) | `{{ .params.<param> }}` |
| A field's display name, in an axis or legend title | `{{ .fields.<param>.display_name }}` |
| A measure's number formatter, in `formatType` | `{{ .fields.<param>.format_type }}` |

Nothing about the component's contract changes: the params, the `metrics_sql` query, and every
dashboard that references it keep working, and re-binding a param in a dashboard still re-titles the
axes and re-formats the numbers.

```yaml
# viz_library/measure_trend.yaml, after ejecting
custom_chart:
metrics_sql: |
SELECT {{ .params.time_dim }}, {{ .params.measure }}
FROM {{ .params.metrics_view }}
ORDER BY {{ .params.time_dim }}
vega_spec: |
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"width": "container",
"height": "container",
"autosize": { "type": "fit" },
"data": { "name": "query1" },
"mark": "line",
"encoding": {
"x": {
"field": "{{ .params.time_dim }}",
"type": "temporal",
"title": "{{ .fields.time_dim.display_name }}"
},
"y": {
"field": "{{ .params.measure }}",
"type": "quantitative",
"title": "{{ .fields.measure.display_name }}",
"formatType": "{{ .fields.measure.format_type }}"
}
}
}
```

The query result arrives as the `query1` dataset, which is why the spec reads its data from
`{"name": "query1"}` rather than declaring values inline. The container sizing is the same as the
native charts use: `autosize: fit` makes the rendered dimensions the chart's total size, so axes and
legends stay inside the widget instead of growing past it.

Alongside `display_name` and `format_type`, a field-typed param also exposes `name` (the same value
as `{{ .params.<param> }}`), `format_d3` and `format_preset`, which are useful when you would rather
format with Vega-Lite's own `format` than route through Rill's formatter.

Ejecting is one-way, and it is a snapshot: the Vega-Lite it writes reflects the data the chart was
compiled against, and Rill stops deriving scales, axes, formats, sorting, stacking and layout, so
the chart no longer adapts when the data changes shape. Eject when you need control that the chart
spec does not offer, not as a matter of course. To get back, undo the edit or revert the file.

## Limits

A component issues exactly one Metrics SQL query, so charts that layer several datasets cannot be expressed. Cross-row derivations (running totals, rankings, per-group offsets) are not available in Metrics SQL either — use a chart type that computes them, such as `Waterfall Chart`, `Bump Chart`, or `ECDF Plot`.

Rows that overflow what an axis can legibly fit are dropped and reported above the chart, so a row limit set far above a chart's density silently truncates rather than crowds.

Inline `custom_chart` widgets inside a canvas are a separate, Vega-Lite-based widget and continue to use `vega_spec`. A component file authors a chart spec under `spec`, and only carries a `vega_spec` once it has been ejected.

:::note Feature flag
Custom viz is currently gated behind the `customComponents` feature flag. Enable it in `rill.yaml`:

```yaml
features:
- customComponents
```
:::
2 changes: 2 additions & 0 deletions docs/docs/reference/project-files/canvas-dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ _[array of object]_ - Refers to all of the rows displayed on the Canvas. Each en
- **donut_chart** - Donut or Pie chart to display sums of total


- **`params`** - _[object]_ - Values bound to the referenced component's declared params. Only valid together with `component`. Values must be scalars.

- **`width`** - _[string, integer]_ - Width of the component (can be a number or string with unit)

- **`name`** - _[string]_ - Stable identifier for a tab group, used as its deep-link URL key. Defaults to `group-<index>` if omitted. Only used for tab-group entries.
Expand Down
48 changes: 48 additions & 0 deletions docs/docs/reference/project-files/component.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ _[string]_ - Refers to the display name for the component

_[string]_ - Detailed description of the component's purpose and functionality

### `params`

_[array of object]_ - List of typed parameters that canvases can bind values to when referencing this component. Bound values are available in the renderer properties' templating as `{{ .params.<name> }}`, and the metrics view metadata of a field-typed param as `{{ .fields.<name>.display_name }}`, `{{ .fields.<name>.format_d3 }}`, `{{ .fields.<name>.format_preset }}` and `{{ .fields.<name>.format_type }}`.

- **`name`** - _[string]_ - Param name. Must be a valid identifier; referenced in the renderer properties' templating as `{{ .params.<name> }}`. _(required)_

- **`type`** - _[string]_ - Param type. Params of type `metrics_view` must be named `metrics_view` or end with `_metrics_view`. _(required)_

- **`description`** - _[string]_ - Human-facing description of the param

- **`required`** - _[boolean]_ - If true, a canvas item referencing this component must bind a value for the param. Mutually exclusive with `default`.

- **`default`** - _[string, number, boolean]_ - Default value used when the param is not bound

- **`metrics_view`** - _[string]_ - For `measure`, `dimension` and `time_dimension` params, the name of a sibling param of type `metrics_view` whose bound metrics view the field must belong to. May be omitted when exactly one `metrics_view` param is declared.

- **`options`** - _[array]_ - For scalar params, the allowed values. Renders as a select input in visual editors.

### `input`

_[array of object]_ - List of input variables that can be passed to the component
Expand Down Expand Up @@ -301,3 +319,33 @@ _[object]_ - (no description)
- **`title`** - _[string]_ - Image title

- **`description`** - _[string]_ - Image description

### `custom_chart`

_[object]_ - A custom visualization: a Metrics SQL query plus a chart spec describing how to draw it.
Rill derives scales, axes, formats, sorting, stacking and layout from the data and from the
metrics view's semantics, so the spec only states the chart type and which field goes on which channel.


- **`metrics_sql`** - _[string]_ - Metrics SQL query selecting the dimensions and measures to plot, e.g.
`SELECT {{ .params.dim }}, {{ .params.measure }} FROM {{ .params.metrics_view }} LIMIT 10`.
Measures arrive pre-aggregated; do not wrap them in aggregate functions.


- **`spec`** - _[object]_ - (no description)

- **`chartType`** - _[string]_ - The chart type, e.g. `Line Chart`, `Bar Chart`, `Heatmap`, `Waterfall Chart`.

- **`encodings`** - _[object]_ - Maps a visual channel (`x`, `y`, `color`, `size`, `column`, `row`, `group`, `angle`, `goal`, ...)
to a field. Each value is either a field name or an encoding object.


- **`chartProperties`** - _[object]_ - Per-chart-type presentation tuning, e.g. `innerRadius`, `stackMode`, `interpolate`.

- **`vega_spec`** - _[string]_ - A Vega-Lite spec, as an alternative to `spec` for a chart Rill cannot describe.
This is what ejecting a chart spec in the component editor produces: the Vega-Lite it
compiled to, with the param bindings turned back into template references. Rill no longer
derives scales, axes, formats, sorting or layout, so the chart stops adapting on its own.
Read the query result from the `query1` dataset, i.e. `"data": {"name": "query1"}`,
and size the chart to its container with `"width": "container"`, `"height": "container"`
and `"autosize": {"type": "fit"}`. Mutually exclusive with `spec`.
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading