Skip to content

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.

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:

Text
.resim/
└── metrics/
    ├── config.resim.yaml
    └── templates/
        └── speed_over_time.liquid

Reference the template from a metric with template_type: custom and template_file:

YAML
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:

  1. Runs the metric's query_string against the data lake.
  2. 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]
    }
    
  3. Renders the Liquid template against that context.

  4. Parses the rendered string as JSON. If parsing fails, the metric errors out and the failure is surfaced in the app.
  5. 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:

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, and group_name map 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:

Bash
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 undefined values — 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.