Extending chart templates

June 23, 2026 · View on GitHub

Use this guide when a backend already exists and you want to add another chart type to it. For a new rendering target, start with Extending backends.


Table of Contents


§1 Choose the chart name and channels

The public identity is the chart string on ChartTemplateDef. It must match chart_spec.chartType exactly, for example "Scatter Plot".

Pick only the channels the mark actually uses. Copy a similar template as a starting point:

FamilyVega-Lite reference
Scatter / pointvegalite/templates/scatter.ts
Bar / columnvegalite/templates/bar.ts
Line / areavegalite/templates/line.ts
Radialvegalite/templates/pie.ts

ECharts and Chart.js use the same ChartTemplateDef interface under their own templates/ folders.


§2 Author the template

ChartTemplateDef lives in packages/flint-js/src/core/types.ts.

import { ChartTemplateDef } from '../../core/types';
import { defaultBuildEncodings } from './utils';

export const dotPlotDef: ChartTemplateDef = {
    chart: 'Dot Plot',
    template: { mark: 'circle', encoding: {} },
    channels: ['x', 'y', 'color', 'size', 'column', 'row'],
    markCognitiveChannel: 'position',

    declareLayoutMode: (channelSemantics, table, chartProperties) => {
        // optional: banded axes, σ overrides, Q→O conversion
        return { /* LayoutDeclaration */ };
    },

    instantiate: (spec, ctx) => {
        defaultBuildEncodings(spec, ctx.resolvedEncodings);
        // ctx.channelSemantics, ctx.layout, ctx.table, ctx.chartProperties, …
    },

    properties: [
        { key: 'opacity', label: 'Opacity', type: 'continuous',
          min: 0.1, max: 1, step: 0.05, defaultValue: 1 },
    ],
};

Key rules

  1. template — minimal native skeleton; instantiate fills encodings and mark properties.
  2. markCognitiveChannel — tells the compiler how readers decode value (affects zero baseline and Auto Layout Algorithm compression).
  3. instantiate — receives a deep clone of template plus InstantiateContext (resolved encodings, ChannelSemantics, LayoutResult, data table, canvas size).
  4. No semantic branching — read ctx.channelSemantics[channel].format, .type, .zero, etc.; do not switch on raw field names or storage types.

Optional hooks: postProcess (after layout), encodingActions (shelf quick actions).


§3 Register the template

In packages/flint-js/src/<backend>/templates/index.ts:

  1. Import the new *Def constant.
  2. Add it to the appropriate category array inside *TemplateDefs (e.g. scatterTemplates).
  3. Ensure *GetTemplateDef(chartType) can find it: defs.find(t => t.chart === chartType).

Vega-Lite also runs withInjectedProperties() to attach shared facet and log-scale properties across templates. Follow existing entries in that file when your chart needs the same hooks.


§4 Add test data and gallery coverage

Generator pattern

The TestCase interface lives in packages/flint-js/src/test-data/types.ts.

Typical flow (see scatter-tests.ts, bar-tests.ts):

  1. Define a small parameter matrix (cardinality, color on/off, facet on/off).
  2. Export gen<Chart>Tests(): TestCase[] with chartType matching ChartTemplateDef.chart.
  3. Register in packages/flint-js/src/test-data/index.ts:
TEST_GENERATORS['Dot Plot'] = genDotPlotTests;
  1. Optionally add a page in gallery-tree.ts listing the generator key.

Verify

npm run typecheck
npm run test
npm run site    # Gallery → find your chart type

Review formatting, layout stretch, legends, and facet behavior across 3–6 representative cases.


§5 Cross-backend parity

The user-facing contract is that the same chartType string should work across assembleVegaLite, assembleECharts, and assembleChartjs when templates exist. In practice:

  • Port to backends you need immediately; file follow-ups for the rest.
  • site/src/shared/supported-backends.ts filters chart types per backend registry. A Vega-Lite-only template will not appear in ECharts until it is registered there too.

§6 Related