Custom Metric Templates
When our provided system templates don't fit, you can author your own Plotly chart with a Liquid template. ReSim renders the template with the columns from your metric's SQL query, and the template must produce a valid Plotly JSON blob, which the app drops onto the page in place of a system chart.
- For Plotly documentation, see https://plotly.com/javascript/
- For the Liquid templating language, see https://shopify.github.io/liquid/
The examples below include a functional custom template along with a metric definition for it.
When to reach for a custom template
System templates cover the common shapes (line, bar, table, scalar, state timeline, image, video) and handle theming and resizing for you. Start with a system template and only drop down to a custom one if none of them produce the chart you're after.
Some reasons to reach for a custom template:
- A Plotly trace type that isn't exposed by a system template (heatmap, box plot, polar plots).
- Bespoke styling — series colors, dash patterns, hover labels, custom axis ranges or tick formatting.
- Multiple trace shapes combined in one figure (e.g. line + scatter, or stacked area with annotations).
If there is a template you'd like us to include in our set of predefined system templates, let us know!
File layout
Custom templates live alongside your config in a templates/ directory:
.resim/
└── metrics/
├── config.resim.yaml
└── templates/
└── speed_over_time.liquid
Reference the template from a metric with template_type: custom and template_file:
metrics:
Speed over time:
type: test
description: Robot speed over time, rendered with a custom Plotly template.
query_string: |
SELECT
'speed' AS group_name,
timestamp,
speeds
FROM drone_speed
template_type: custom # Use `custom` instead of `system`
template_file: speed_over_time.liquid # Reference the liquid template here
resim metrics sync uploads the config and every file in templates/ together — no separate command or extra flag is needed.
How rendering works
For each metric using a custom template, ReSim:
- Runs the metric's
query_stringagainst the data lake. -
Reshapes the result into a map keyed by column name, where each value is an array of that column's values across all rows. A query that returns three columns (
group_name,timestamp,speeds) produces a context like:JSON{ "group_name": ["speed", "speed", "speed"], "timestamp": [0, 1, 2], "speeds": [1.2, 1.4, 1.1] } -
Renders the Liquid template against that context.
- Parses the rendered string as JSON. If parsing fails, the metric errors out and the failure is surfaced in the app.
- Hands the resulting Plotly object to the front-end, where it is rendered with Plotly.js.
Two things to keep in mind:
- The variables available in the template are the column names from your query. Each is an array with one entry per row, in result order. SQL aliases (
AS some_name) rename the variable seen by the template. - The template's output must be valid JSON. Whitespace is free, but a trailing comma or missing brace will fail during rendering.
Authoring a template
Templates are written in Liquid. The full standard tag and filter set is available — {% for %}, {% if %}, {{ value | filter }}, whitespace controls {%- ... -%}, and so on.
Here is a working line-chart template that pairs with the metric above. Save it as .resim/metrics/templates/speed_over_time.liquid:
{
"data": [
{
"x": [
{% for item in timestamp -%}
"{{ item }}"{% unless forloop.last %},{% endunless %}
{%- endfor %}
],
"y": [
{% for item in speeds -%}
"{{ item }}"{% unless forloop.last %},{% endunless %}
{%- endfor %}
],
"type": "scatter",
"mode": "lines",
"line": { "color": "#1f77b4" },
"name": "{{ group_name[0] | default: 'Series' }}"
}
],
"layout": {
"xaxis": { "title": { "text": "timestamp" } },
"yaxis": { "title": { "text": "speed (m/s)" } },
"showlegend": true,
"hovermode": "closest"
},
"config": {
"responsive": true
}
}
A few details worth calling out:
timestamp,speeds, andgroup_namemap directly to the SQL columns. Rename a column in the query and you rename the variable in the template.group_name[0]reads the first row's value — handy when the query returns the same series name on every row.| default: 'Series'covers the case where the column is empty.{%- ... -%}strips surrounding whitespace, and{% unless forloop.last %},{% endunless %}avoids the trailing comma that would otherwise invalidate the JSON array.
You can produce any valid Plotly figure from here — multiple traces, secondary y-axes, annotations, custom modebar buttons. The Plotly JavaScript reference is the source of truth for what is renderable.
Iterating locally
The standard debug workflow works for custom templates:
resim metrics sync --project "my-project" --branch "metrics-dev"
resim metrics debug \
--project "my-project" \
--emissions-file emissions.resim.jsonl \
--metrics-config-path .resim/metrics/config.resim.yaml \
--metrics-set "My Metrics"
Edit the .liquid file, re-run resim metrics sync, then refresh the debug dashboard to see the new output.
Troubleshooting
- JSON parse error after render — the rendered template isn't valid JSON. The usual culprit is a trailing comma inside an array. Make sure loops use the
{% unless forloop.last %},{% endunless %}guard. - Empty chart or
undefinedvalues — the variable name in the template doesn't match a column in the query result. Check the SQL aliases. Column names are case-sensitive and lowercased by default. - Liquid syntax error — the error surfaced in the app includes the offending tag. Verify tag pairs (
{% for %}/{% endfor %},{% if %}/{% endif %}) and that any filters you used are part of the standard Liquid set.