Custom charts¶
When no built-in type draws the shape you need, type: custom lets you draw it
yourself. A custom chart is an SVG template: a Jinja
file that turns the chart's query rows into SVG shapes. The template reads the
board's style, so a custom chart picks up the theme's colors and fonts and
follows a theme switch like any built-in chart.
Try the built-in types and composed charts first. A built-in chart gets axes, legends, hover, sizing rules, and visual warnings for free; a custom chart draws only what its template draws.
For ready-made templates (Sankey, funnel, gauge, radial bar, XmR, isotype, waffle, and radar), see Custom chart examples.
A first custom chart¶
A template can live inline, under svg:. This one draws one bar per
salesperson against a 100% quota line:
charts:
quota:
type: custom
title: Quota attainment
query:
columns: [rep, attainment]
values:
- [Ana, 1.18]
- [Ben, 0.92]
- [Chloe, 0.64]
- [Dev, 1.03]
svg: |
{% set band = height / (rows | length) %}
{% set scale = (width - 140) / 1.25 %}
<g font-family="{{ style.font.family }}" font-size="{{ style.font.size }}">
{% for r in rows %}
{% set y = loop.index0 * band %}
{% set hit = r.attainment >= 1 %}
<text x="70" y="{{ y + band / 2 }}" dy="0.35em" text-anchor="end"
fill="{{ style.font.color }}">{{ r.rep }}</text>
<rect x="80" y="{{ y + band * 0.2 }}" height="{{ band * 0.6 }}"
width="{{ r.attainment * scale }}"
fill="{{ style.tones.positive if hit else style.charts.color.categorical.single_series_palette[0] }}">
<title>{{ r.rep }}: {{ r.attainment | format('.0%') }}</title>
</rect>
<text x="{{ 86 + r.attainment * scale }}" y="{{ y + band / 2 }}" dy="0.35em"
fill="{{ style.muted }}">{{ r.attainment | format('.0%') }}</text>
{% endfor %}
<line x1="{{ 80 + scale }}" x2="{{ 80 + scale }}" y1="0" y2="{{ height }}"
stroke="{{ style.font.color }}" stroke-dasharray="3 3"/>
</g>
rows:
- quota
The template writes SVG content only. dbt Charts draws the title and
subtitle, wraps the template's output in an <svg> of the chart's size, and
places it on the board.
Template files¶
Inline templates suit a one-off chart. To reuse a chart across boards, put the
template in your project's templates/ folder, next to dbt_charts.yml, and
name it with template::
queries: activation_funnel: columns: [stage, users] values: - [Visited site, 48210] - [Started signup, 12940] - [Upgraded, 740] charts: activation: type: custom template: funnel query: activation_funnel title: Activation funnel x: stage y: users rows: - activation
template: funnel reads templates/funnel.svg.j2. A chart sets exactly one of
template: or svg:.
Finding templates¶
dct describe reads a template's header, so you never open the file to learn
what it takes:
dct describe templates/sankey.svg.j2 # one template
dct describe templates/ # every template in the folder
Each template prints its description, its channels (required or optional, and
whether a list is allowed), and its options (type, default, description).
dct describe --json returns the same under a template key. Describing a
board shows, for each custom chart, the template it uses and that template's
description. A template: name with no file lists the templates the project
does have, and an unsupported type: such as sankey names the matching
template when the project has one.
Channels and options¶
A template file declares what it takes in a header: a Jinja comment that opens
with {#--- and closes with ---#}, holding YAML. It has two parts.
Channels are the chart's data columns, named with the same channel fields
built-in charts use: x, y, color, size, shape, theta, and value.
A chart sets them at its root, exactly as it would on a bar or pie chart. Pick
the channel that matches the role: a category along a funnel is x and its
count is y, the way a bar chart reads; a part of a whole is color and
theta, the way a pie reads.
Options are everything else the template can be told: a format, a maximum,
a symbol. A chart sets them under options:.
{#---
description: Funnel of ordered stages, one row per stage in query order.
channels:
x:
description: Column naming each stage.
y:
description: Column holding the count that reached each stage.
options:
value_format:
type: string
default: ",.0f"
label_width:
type: number
default: 120
---#}
type: custom template: funnel x: stage y: users options: label_width: 140
A channel or option without a default is required; default: null makes it
optional with no value, which a template tests with {% if options.target %}.
A channel declared with multiple: true takes a list of columns
(y: [disease, wounds]) and always reaches the template as a list. A template
with no header, an inline one included, takes no channels and no options.
The chart must match the header. A channel or option the header doesn't
declare, a missing required one, or a value of the wrong type is a validation
error, so a typo fails when the board compiles rather than drawing something
wrong. Every channel, and every column option, is also checked against the
query's result columns when the chart renders.
Option type |
Accepts |
|---|---|
column |
The name of a column in the chart's query, for a secondary role such as a gauge's target. |
columns |
A list of column names. |
string |
Any text. |
number |
Any number. |
integer |
A whole number. |
boolean |
true or false. |
color |
A color: hex, a CSS color name, or a palette token. |
What a template sees¶
| Name | What it holds |
|---|---|
rows |
The query result, one mapping per row: {{ r.revenue }} or {{ r[channels.y] }}. A chart with no query: gets no rows. |
columns |
The result's column names, in order. |
channels |
The columns the chart set for each declared channel: channels.x is a column name, or a list for a multiple channel; an optional channel left unset is none. |
options |
The chart's options, with the header's defaults filled in. |
width, height |
The drawing area in pixels, below the title. |
uid |
A prefix unique to this chart. Use it for id values (gradients, clip paths, symbols) so two charts on one board never share an id. |
style |
The board's style, resolved. See Style. |
math |
math.sin, math.cos, math.tan, math.asin, math.acos, math.atan2, math.sqrt, math.log, math.exp, math.floor, math.ceil, math.pi, math.tau. Angles are in radians. |
And these helpers:
| Helper | Returns |
|---|---|
value \| format(spec) |
The value formatted with a d3-format spec, the same formats format: takes elsewhere: {{ 0.432 \| format('.1%') }} gives 43.2%. |
text_width(text, size, weight) |
The width in pixels of text, a string, in the board's font at size pixels: text_width(r.name, style.font.size). Turn a number into text first, with \| string or \| format(...). weight is optional and defaults to 400. It takes any weight style can carry: a whole number from 1 to 1000 such as 600, or a CSS keyword such as bold or normal, so style.charts.title.font.weight works whatever the theme sets. Use it to fit or wrap labels. |
color_for(value, column) |
The board's pinned color for that column and value, from style.charts.category_colors, so a pinned category matches the built-in charts on the board. A value with no pin gets the next color of the categorical palette, in the order the template asks; the same value always gets the same color within a chart, and running out of palette colors is an error. |
raise_error(message) |
Stops the render and shows message in the chart's place. Use it to reject data the template cannot draw. |
Templates use strict variables: a misspelled column, option, or style key is an error that names the template and line, not an empty string.
Query values arrive as the warehouse returns them. Some warehouses return
decimals, so convert before doing arithmetic with floats:
{{ (r.revenue | float) * scale }}.
Style¶
style has the same structure as the style: block you
author on a board or in a theme, after the theme, the board's style:, and
every inherited value are merged. So a key you can set is a key a template can
read, at the same path:
| In a template | What it is |
|---|---|
style.background |
The board background. |
style.font.family, style.font.color, style.font.size |
Body text. |
style.muted, style.accent |
Secondary text and the accent color. |
style.tones.positive, .negative, .warning, .info |
Status colors. |
style.charts.color.categorical.palette |
The categorical palette built-in charts use, as a list of colors. |
style.charts.color.categorical.single_series_palette[0] |
The color of a single-series chart. |
style.charts.axis.grid.color |
Gridlines and tracks. |
style.charts.axis.labels.font |
Axis label text. |
style.charts.title.font |
Chart title text. |
style.palettes.<role> |
Each palette role, resolved: a list of colors for a categorical, sequential, or diverging palette, and a mapping of named stops (solid, bg, border, text, ...) for a tone. |
Every color is a resolved hex value. The one difference from what you author
is palettes:: you name a palette (category: editorial-10), and the
template sees its colors.
Read colors and fonts from style rather than writing them into the
template, and the chart follows every theme, dark ones included, without
changes. The examples never hard-code a color.
To pin a category's color across every chart on a board, built-in or custom,
use style.charts.category_colors and read it in the template with
color_for(value, column):
style: charts: category_colors: qualification: values: Graduates: "#2b59a8" Certificated: "#d33a32"
What a template may draw¶
A template's output must be well-formed SVG built from drawing elements:
shapes (rect, circle, ellipse, line, polyline, polygon, path),
text (text, tspan, textPath), grouping (g, defs, symbol, use),
paint (linearGradient, radialGradient, stop, pattern, clipPath,
mask, marker), and title and desc.
The output is checked before it reaches the board, and anything outside that set is an error that names the element or attribute. Nothing is silently removed. Not allowed:
<script>,<foreignObject>,<style>,<image>,<a>, and animation elements such as<animate>and<set>- event attributes such as
onclick - links out of the page:
hrefmust be a#idreference (href="#{{ uid }}-person"), andurl(...)must beurl(#id)(fill="url(#{{ uid }}-fade)") - comments, processing instructions, and CDATA sections
- CSS escapes (a backslash in an attribute) and CSS functions other than
url(#id)that load a resource, such asimage-set()
Values written with {{ }} are escaped, so a label containing < or & draws
as text.
A <title> inside a shape becomes its tooltip: hover over any mark in the
examples to see one.
Writing good templates¶
Let the query do the math. Jinja is good at loops and text, and slow and awkward at algorithms. Sorting, ranking, running totals, shares, and binning belong in SQL. The XmR example computes its limits in the template only because they are a few sums; a layout that needs iteration is a sign the shape should be a built-in type.
Validate the data. Check what the template depends on and stop with
raise_error when it is wrong: a gauge with more than one row, a Sankey whose
flows loop back, a negative value in a waffle.
Use uid for every id. A board renders many charts into one document, so
an id like fade would collide when a template is used twice.
Leave room for text. Measure labels with text_width instead of guessing,
and keep a fixed margin for labels that sit outside the marks.
Errors¶
| Code | When |
|---|---|
ERR-CUSTOM-TEMPLATE |
The template file is missing, its header is not valid YAML, or the template has a Jinja syntax error. |
ERR-CUSTOM-FIELDS |
A channel or option the header doesn't declare, a missing required one, or a value of the wrong type. |
ERR-CUSTOM-RENDER |
The template failed while drawing: a missing column, an undefined name, or a raise_error call. |
ERR-CUSTOM-SVG |
The output is not well-formed SVG, or uses an element or attribute that is not allowed. |