Writing Layer Types

This page covers how to define and register custom LayerType instances. For using layer types in a plot see Configuring Plots. For an overview of the data model see the main API doc.


Overview

A LayerType encapsulates everything needed to render one kind of data visualization:

The axis information (quantity kinds, axis positions) is separated from the GPU data:

Either or both can be omitted when static declarations cover the needed information.


Minimal Example (no color or filter axes)

import { LayerType, registerLayerType, AXES } from './src/index.js'

const redDotsType = new LayerType({
  name: "red_dots",
  xAxisQuantityKind: "meters",
  yAxisQuantityKind: "volts",

  getAxisConfig: function(parameters) {
    return {
      xAxis: parameters.xAxis,
      yAxis: parameters.yAxis,
    }
  },

  vert: `
    precision mediump float;
    attribute float x, y;
    uniform vec2 xDomain, yDomain;

    void main() {
      float nx = (x - xDomain.x) / (xDomain.y - xDomain.x);
      float ny = (y - yDomain.x) / (yDomain.y - yDomain.x);
      gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0, 1);
      gl_PointSize = 6.0;
    }
  `,

  frag: `
    precision mediump float;
    void main() {
      gl_FragColor = gladly_apply_color(vec4(1.0, 0.0, 0.0, 1.0));
    }
  `,

  schema: (data) => ({
    $schema: "https://json-schema.org/draft/2020-12/schema",
    type: "object",
    properties: {
      xData: { type: "string" },
      yData: { type: "string" },
      xAxis: { type: "string", enum: AXES.filter(a => a.includes("x")), default: "xaxis_bottom" },
      yAxis: { type: "string", enum: AXES.filter(a => a.includes("y")), default: "yaxis_left" }
    },
    required: ["xData", "yData"]
  }),

  createLayer: function(parameters, data) {
    const { xData, yData } = parameters
    return [{
      attributes: { x: data[xData], y: data[yData] },
      uniforms: {},
    }]
  }
})

registerLayerType("red_dots", redDotsType)

With Color Axes

Color axes map a per-point numeric value to a color via a colorscale.

Colorscale is not specified in createLayer; it comes from:

  1. config.axes[quantityKind].colorscale (per-plot override), or
  2. The quantity kind registry definition (global default)

colorAxisQuantityKinds is a dictionary mapping a GLSL name suffix to the quantity kind for each color axis. The suffix is appended to the base uniform names colorscale, color_range, color_scale_type to form the GLSL uniform names:

import { LayerType, registerLayerType, AXES } from './src/index.js'

const heatDotsType = new LayerType({
  name: "heat_dots",
  xAxisQuantityKind: "meters",
  yAxisQuantityKind: "volts",
  // colorAxisQuantityKinds omitted — resolved dynamically from parameters

  getAxisConfig: function(parameters) {
    return {
      xAxis: parameters.xAxis,
      yAxis: parameters.yAxis,
      // suffix '' → shader uniforms: colorscale, color_range, color_scale_type
      colorAxisQuantityKinds: { '': parameters.vData },
    }
  },

  vert: `
    precision mediump float;
    attribute float x, y;
    attribute float color_data;
    uniform vec2 xDomain, yDomain;
    varying float value;

    void main() {
      float nx = (x - xDomain.x) / (xDomain.y - xDomain.x);
      float ny = (y - yDomain.x) / (yDomain.y - yDomain.x);
      gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0, 1);
      gl_PointSize = 6.0;
      value = color_data;
    }
  `,

  // map_color_s() is injected automatically when color axes are present
  // It calls gladly_apply_color() internally, so no explicit wrap needed.
  frag: `
    precision mediump float;
    uniform int colorscale;
    uniform vec2 color_range;
    uniform float color_scale_type;
    varying float value;

    void main() {
      gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, 0.0);
    }
  `,

  schema: (data) => ({
    $schema: "https://json-schema.org/draft/2020-12/schema",
    type: "object",
    properties: {
      xData: { type: "string" },
      yData: { type: "string" },
      vData: { type: "string", description: "Data key for color values; becomes the color axis quantity kind" },
      xAxis: { type: "string", enum: AXES.filter(a => a.includes("x")), default: "xaxis_bottom" },
      yAxis: { type: "string", enum: AXES.filter(a => a.includes("y")), default: "yaxis_left" }
    },
    required: ["xData", "yData", "vData"]
  }),

  createLayer: function(parameters, data) {
    const { xData, yData, vData } = parameters
    return [{
      attributes: { x: data[xData], y: data[yData], color_data: data[vData] },
      uniforms: {},
    }]
  }
})

registerLayerType("heat_dots", heatDotsType)

Usage:

plot.update({
  data: { x, y, temperature },
  config: {
    layers: [
      { heat_dots: { xData: "x", yData: "y", vData: "temperature" } }
    ],
    axes: {
      temperature: { min: 0, max: 100, colorscale: "plasma" }
    }
  }
})

With Filter Axes

Filter axes discard points whose attribute value falls outside a configured range.

filterAxisQuantityKinds is a dictionary mapping a GLSL name suffix to the quantity kind for each filter axis. The suffix is appended to the base uniform names filter_range and filter_scale_type:

const filteredDotsType = new LayerType({
  name: "filtered_dots",
  xAxisQuantityKind: "meters",
  yAxisQuantityKind: "meters",

  getAxisConfig: function(parameters) {
    return {
      xAxis: parameters.xAxis,
      yAxis: parameters.yAxis,
      // suffix '' → shader uniforms: filter_range, filter_scale_type
      filterAxisQuantityKinds: { '': parameters.zData },
    }
  },

  vert: `
    precision mediump float;
    attribute float x, y;
    attribute float filter_data;
    uniform vec2 xDomain, yDomain;
    uniform vec4 filter_range;

    void main() {
      // filter_in_range() is injected automatically when filter axes are present
      if (!filter_in_range(filter_range, filter_data)) {
        gl_Position = vec4(2.0, 2.0, 2.0, 1.0);  // move outside clip space
        return;
      }
      float nx = (x - xDomain.x) / (xDomain.y - xDomain.x);
      float ny = (y - yDomain.x) / (yDomain.y - yDomain.x);
      gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0, 1);
      gl_PointSize = 4.0;
    }
  `,

  frag: `
    precision mediump float;
    void main() { gl_FragColor = gladly_apply_color(vec4(0.0, 0.5, 1.0, 1.0)); }
  `,

  schema: (data) => ({ /* ... */ }),

  createLayer: function(parameters, data) {
    const { xData, yData, zData } = parameters
    return [{
      attributes: { x: data[xData], y: data[yData], filter_data: data[zData] },
      uniforms: {},
    }]
  }
})

Usage:

plot.update({
  data: { x, y, depth },
  config: {
    layers: [{ filtered_dots: { xData: "x", yData: "y", zData: "depth" } }],
    axes: {
      depth: { min: 100, max: 800 }  // only show points where 100 ≤ depth ≤ 800
    }
  }
})

Combined Color and Filter Axes

const filteredScatterType = new LayerType({
  name: "filtered_scatter",

  getAxisConfig: function(parameters) {
    return {
      xAxis: parameters.xAxis,
      xAxisQuantityKind: parameters.xData,
      yAxis: parameters.yAxis,
      yAxisQuantityKind: parameters.yData,
      colorAxisQuantityKinds:  { '': parameters.vData },
      filterAxisQuantityKinds: { '': parameters.zData },
    }
  },

  vert: `
    precision mediump float;
    attribute float x, y;
    attribute float color_data;
    attribute float filter_data;
    uniform vec2 xDomain, yDomain;
    uniform vec4 filter_range;
    varying float value;

    void main() {
      if (!filter_in_range(filter_range, filter_data)) {
        gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
        return;
      }
      float nx = (x - xDomain.x) / (xDomain.y - xDomain.x);
      float ny = (y - yDomain.x) / (yDomain.y - yDomain.x);
      gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0, 1);
      gl_PointSize = 4.0;
      value = color_data;
    }
  `,

  frag: `
    precision mediump float;
    uniform int colorscale;
    uniform vec2 color_range;
    uniform float color_scale_type;
    varying float value;

    void main() {
      gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, 0.0);
    }
  `,

  schema: (data) => ({ /* ... */ }),

  createLayer: function(parameters, data) {
    const { xData, yData, vData, zData } = parameters
    return [{
      attributes: {
        x: data[xData], y: data[yData],
        color_data: data[vData],
        filter_data: data[zData],
      },
      uniforms: {},
    }]
  }
})

Static Axis Declarations

For layer types whose quantity kinds and axis positions are always the same regardless of parameters, declare them statically on the LayerType. This enables schema-based filtering of compatible layer types without calling any functions.

const fixedScatterType = new LayerType({
  name: "fixed_scatter",
  // Static declarations — readable without parameters or data
  xAxis: "xaxis_bottom",
  xAxisQuantityKind: "distance_m",
  yAxis: "yaxis_left",
  yAxisQuantityKind: "current_A",
  colorAxisQuantityKinds: { '': "temperature_K" },
  filterAxisQuantityKinds: { '': "velocity_ms" },

  getAxisConfig: function(parameters) {
    // Only needed to pass through user-selectable axis positions
    return { xAxis: parameters.xAxis, yAxis: parameters.yAxis }
  },

  vert: `
    precision mediump float;
    attribute float x, y, temperature_K, velocity_ms;
    uniform vec2 xDomain, yDomain;
    uniform vec4 filter_range_velocity_ms;
    varying float value;
    void main() {
      if (!filter_in_range(filter_range_velocity_ms, velocity_ms)) {
        gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
        return;
      }
      float nx = (x - xDomain.x)/(xDomain.y-xDomain.x);
      float ny = (y - yDomain.x)/(yDomain.y-yDomain.x);
      gl_Position = vec4(nx*2.0-1.0, ny*2.0-1.0, 0, 1);
      gl_PointSize = 5.0;
      value = temperature_K;
    }
  `,
  frag: `
    precision mediump float;
    uniform int colorscale_temperature_K;
    uniform vec2 color_range_temperature_K;
    uniform float color_scale_type_temperature_K;
    varying float value;
    void main() {
      gl_FragColor = map_color_s(colorscale_temperature_K, color_range_temperature_K, value, color_scale_type_temperature_K, 0.0);
    }
  `,
  schema: () => ({ /* ... */ }),
  createLayer: function(parameters, data) {
    const { xData, yData, vData, fData } = parameters
    return [{
      attributes: {
        x: data[xData], y: data[yData],
        temperature_K: data[vData],
        velocity_ms: data[fData],
      },
      uniforms: {},
    }]
  }
})

Static declarations and getAxisConfig can be mixed freely. Dynamic values (non-undefined) override statics. Either is sufficient when it covers all needed axis information.


Optional: Using Data.wrap for Multiple Data Formats

This is entirely optional. Nothing in the plotting framework requires it. If your layer type always receives a flat { column: Float32Array } object, just read it directly. Data.wrap is a convenience for layer types that want to support richer data shapes.

The built-in points and lines layers call Data.wrap(data) in both createLayer and getAxisConfig so that they accept plain flat objects, per-column rich objects, and the columnar format (see Data for format details). Custom layer types can do the same.

Pattern: replace data[col] with d.getData(col), and derive quantity kinds from the data rather than hardcoding them:

import { LayerType, registerLayerType, Data, AXES } from './src/index.js'

const myLayerType = new LayerType({
  name: "my_layer",

  getAxisConfig: function(parameters, data) {
    const d = Data.wrap(data)
    const { xData, yData, vData, xAxis, yAxis } = parameters
    return {
      xAxis,
      xAxisQuantityKind: d.getQuantityKind(xData) ?? xData,
      yAxis,
      yAxisQuantityKind: d.getQuantityKind(yData) ?? yData,
      // suffix '' → shader uniforms: colorscale, color_range, color_scale_type
      colorAxisQuantityKinds: { '': d.getQuantityKind(vData) ?? vData },
    }
  },

  // ... vert, frag, schema ...

  createLayer: function(parameters, data) {
    const d = Data.wrap(data)
    const { xData, yData, vData } = parameters

    // Resolve the quantity kind: use data-provided kind if present, else column name
    const vQK = d.getQuantityKind(vData) ?? vData

    const x = d.getData(xData)
    const y = d.getData(yData)
    const v = d.getData(vData)

    // Pass any pre-computed domain from the data, keyed by quantity kind
    const domains = {}
    const vDomain = d.getDomain(vData)
    if (vDomain) domains[vQK] = vDomain

    return [{
      attributes: { x, y, color_data: v },
      uniforms: {},
      domains,
    }]
  }
})

What this enables:

When quantity kinds come from the data, the axis key in config.axes should use the quantity kind string rather than the column name. For example, if vData: "myCol" but getQuantityKind("myCol") returns "temperature_K", the color axis is registered as temperature_K, so the plot config uses axes: { temperature_K: { colorscale: "plasma" } }.


Color Axes — Concepts

Term Description
Quantity kind String identifier for the color axis (e.g. "temperature_K"). Layers sharing a quantity kind share a common range.
GLSL name suffix Key in the colorAxisQuantityKinds dict (e.g. '', '2', '_a'). Appended to base uniform names to form the shader-visible names for that color axis.
Colorscale Named GLSL color function (e.g. "viridis"). Set via config.axes[quantityKind].colorscale or the quantity kind registry.

Uniform Naming

colorAxisQuantityKinds is a Record<suffix, quantityKind>. For each entry, createDrawCommand exposes uniforms named colorscale${suffix}, color_range${suffix}, color_scale_type${suffix}.

Examples:

The render loop passes these via internal prop names colorscale_<quantityKind>, color_range_<quantityKind>, color_scale_type_<quantityKind> and createDrawCommand maps them to the GLSL names automatically. Layer authors use the GLSL names directly in shaders and attributes.

How the Plot Handles Color Axes

  1. Registers each quantity kind with the ColorAxisRegistry
  2. Scans layer.domains for that quantity kind and computes the auto range [min, max] (if no domain exists, falls back to scanning layer.attributes by quantity kind name)
  3. Applies any override from config.axes
  4. Passes uniforms to the draw call

GLSL Integration

When a layer has color axes, createDrawCommand automatically:

  1. Injects all registered colorscale GLSL functions
  2. Injects map_color(int cs, vec2 range, float value) dispatch function
// Using suffix '' — GLSL uniform names are: colorscale, color_range, color_scale_type
uniform int colorscale;
uniform vec2 color_range;
uniform float color_scale_type;
varying float value;

void main() {
  // map_color_s calls gladly_apply_color internally — no explicit wrap needed.
  gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, 0.0);
}

Filter Axes — Concepts

Term Description
Quantity kind String identifier (e.g. "velocity_ms"). Layers sharing a quantity kind share the same filter range.
GLSL name suffix Key in the filterAxisQuantityKinds dict (e.g. '', '2'). Appended to filter_range and filter_scale_type to form the shader-visible uniform names.
Open bound Missing min or max in config.axes means that bound is not enforced.

Uniform Naming

filterAxisQuantityKinds is a Record<suffix, quantityKind>. For each entry, createDrawCommand exposes uniforms filter_range${suffix} and filter_scale_type${suffix}.

Examples:

How the Plot Handles Filter Axes

  1. Registers each quantity kind with the FilterAxisRegistry
  2. Scans layer.domains and layer.attributes (by quantity kind name) and computes the data extent
  3. Applies min/max from config.axes if present; defaults to fully open bounds
  4. Passes uniforms to the draw call

GLSL Integration

When a layer has filter axes, createDrawCommand automatically:

  1. Injects filter_in_range(vec4, float)
// Using suffix '' — GLSL uniform name is: filter_range
uniform vec4 filter_range;
attribute float filter_data;

void main() {
  if (!filter_in_range(filter_range, filter_data)) {
    gl_Position = vec4(2.0, 2.0, 2.0, 1.0);  // vertex shader discard
    return;
  }
  // ...
}

Instanced Rendering

Use instanced rendering when a single data point needs to emit multiple vertices (e.g. a rectangle is 6 vertices) without a CPU loop to expand the geometry.

The pattern uses two types of attributes:

The GPU executes vertexCount vertices × instanceCount instances. Each vertex shader invocation sees the current per-vertex attribute and the current per-instance attributes for its instance.

Neighbor arrays can be built without explicit JS loops using TypedArray.set():

const xPrev = new Float32Array(n)
xPrev.set(x.subarray(0, n - 1), 1)   // xPrev[1..n-1] = x[0..n-2]
xPrev[0] = n > 1 ? 2 * x[0] - x[1] : x[0]  // mirror boundary

const xNext = new Float32Array(n)
xNext.set(x.subarray(1), 0)           // xNext[0..n-2] = x[1..n-1]
xNext[n - 1] = n > 1 ? 2 * x[n - 1] - x[n - 2] : x[n - 1]

Example: rectangle layer

// Per-vertex quad corner coordinates (two CCW triangles: BL-BR-TR, BL-TR-TL)
const QUAD_CX = new Float32Array([0, 1, 1, 0, 1, 0])
const QUAD_CY = new Float32Array([0, 0, 1, 0, 1, 1])

const rectLayerType = new LayerType({
  name: "rects",
  xAxis: "xaxis_bottom",
  yAxis: "yaxis_left",

  getAxisConfig: (params) => ({
    xAxis: params.xAxis,
    xAxisQuantityKind: params.xData,
    yAxis: params.yAxis,
    yAxisQuantityKind: params.yTopData,
  }),

  vert: `
    precision mediump float;
    attribute float cx;    // per-vertex: quad corner x (0 or 1)
    attribute float cy;    // per-vertex: quad corner y (0 or 1)
    attribute float x;     // per-instance: rect center
    attribute float xPrev; // per-instance: previous center (mirror at boundary)
    attribute float xNext; // per-instance: next center (mirror at boundary)
    attribute float top;   // per-instance: top y
    attribute float bot;   // per-instance: bottom y
    uniform float uE;
    uniform vec2 xDomain, yDomain;
    uniform float xScaleType, yScaleType;

    void main() {
      float halfLeft  = (x - xPrev) / 2.0;
      float halfRight = (xNext - x) / 2.0;
      // Cap: if one side exceeds e, use the other side (simultaneous, using originals).
      float hl = halfLeft  > uE ? halfRight : halfLeft;
      float hr = halfRight > uE ? halfLeft  : halfRight;

      float xPos = cx > 0.5 ? x + hr : x - hl;
      float yPos = cy > 0.5 ? top : bot;

      float nx = normalize_axis(xPos, xDomain, xScaleType);
      float ny = normalize_axis(yPos, yDomain, yScaleType);
      gl_Position = vec4(nx * 2.0 - 1.0, ny * 2.0 - 1.0, 0.0, 1.0);
    }
  `,

  frag: `
    precision mediump float;
    void main() { gl_FragColor = gladly_apply_color(vec4(0.2, 0.5, 0.8, 1.0)); }
  `,

  schema: (data) => ({ /* ... */ }),

  createLayer: function(params, data) {
    const { xData, yTopData, yBottomData, e = Infinity } = params
    const x = data[xData], top = data[yTopData], bot = data[yBottomData]
    const n = x.length

    const xPrev = new Float32Array(n)
    xPrev.set(x.subarray(0, n - 1), 1)
    xPrev[0] = n > 1 ? 2 * x[0] - x[1] : x[0]

    const xNext = new Float32Array(n)
    xNext.set(x.subarray(1), 0)
    xNext[n - 1] = n > 1 ? 2 * x[n - 1] - x[n - 2] : x[n - 1]

    const xMin   = x.reduce((a, v) => Math.min(a, v), Infinity)
    const xMax   = x.reduce((a, v) => Math.max(a, v), -Infinity)
    const topMin = top.reduce((a, v) => Math.min(a, v), Infinity)
    const topMax = top.reduce((a, v) => Math.max(a, v), -Infinity)
    const botMin = bot.reduce((a, v) => Math.min(a, v), Infinity)
    const botMax = bot.reduce((a, v) => Math.max(a, v), -Infinity)

    return [{
      attributes: {
        cx: QUAD_CX, cy: QUAD_CY,   // per-vertex
        x, xPrev, xNext, top, bot,  // per-instance
      },
      attributeDivisors: { x: 1, xPrev: 1, xNext: 1, top: 1, bot: 1 },
      uniforms: { uE: e },
      domains: {
        [xData]:    [xMin, xMax],
        [yTopData]: [Math.min(topMin, botMin), Math.max(topMax, botMax)],
      },
      primitive: "triangles",
      vertexCount: 6,
      instanceCount: n,
    }]
  },
})

Key points:


Picking Support

GPU picking lets the application identify which layer and data point is under the mouse cursor. The framework handles the mechanics automatically — layer authors only need to follow one rule: always assign gl_FragColor through gladly_apply_color().

The rule

// ✅ Correct — picking works automatically
void main() {
  vec4 color = /* your color calculation */;
  gl_FragColor = gladly_apply_color(color);
}

// ❌ Wrong — pick pass will not detect this fragment
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

In normal rendering, gladly_apply_color is a pass-through and has no effect on visual output. In a GPU pick pass it encodes the layer and vertex index into the RGBA channels.

Using map_color_s

map_color_s calls gladly_apply_color internally, so layers using it for their final output need no additional call:

void main() {
  // gladly_apply_color is called inside map_color_s — no wrapping needed
  gl_FragColor = map_color_s(colorscale, color_range, value, color_scale_type, 0.0);
}

Only wrap gladly_apply_color explicitly when doing additional processing after map_color_s (e.g. custom alpha):

void main() {
  float t = clamp((value - color_range.x) / (color_range.y - color_range.x), 0.0, 1.0);
  vec4 color = map_color_s(colorscale, color_range, value, color_scale_type, 0.0);
  gl_FragColor = gladly_apply_color(vec4(color.rgb, t));  // explicit wrap needed here
}

Double-calling gladly_apply_color is safe: in pick mode it always returns the correct pick encoding regardless of input.

Instanced layers

For instanced layers (instanceCount !== null), a_pickId is a per-instance attribute (divisor 1). The dataIndex returned by plot.pick() is therefore the instance index into the per-instance attribute arrays. When reading back picked values, filter out per-vertex attributes:

const { layer, dataIndex } = result
const isInstanced = layer.instanceCount !== null
const row = Object.fromEntries(
  Object.entries(layer.attributes)
    .filter(([k]) => !isInstanced || (layer.attributeDivisors[k] ?? 0) === 1)
    .map(([k, v]) => [k, v[dataIndex]])
)

Layers that override createDrawCommand

If a layer type overrides createDrawCommand entirely (e.g. TileLayer), the automatic a_pickId / gladly_apply_color injection does not apply. Such layers are not pickable and plot.pick() will never return a hit for them. This is by design.


API Reference

LayerType Constructor

new LayerType({ name,
                xAxis, xAxisQuantityKind,
                yAxis, yAxisQuantityKind,
                colorAxisQuantityKinds,
                filterAxisQuantityKinds,
                getAxisConfig,
                vert, frag, schema, createLayer })
Parameter Type Description
name string Type identifier (e.g. "points")
xAxis string Static default x-axis position (e.g. "xaxis_bottom"). Optional.
xAxisQuantityKind string Static x-axis quantity kind. Optional.
yAxis string Static default y-axis position (e.g. "yaxis_left"). Optional.
yAxisQuantityKind string Static y-axis quantity kind. Optional.
colorAxisQuantityKinds Record<string,string> Static dict mapping GLSL name suffix → quantity kind for color axes. Optional, defaults to {}.
filterAxisQuantityKinds Record<string,string> Static dict mapping GLSL name suffix → quantity kind for filter axes. Optional, defaults to {}.
getAxisConfig function (parameters, data) => axisConfig — dynamic axis config; overrides static fields wherever it returns a non-undefined value. Optional if statics cover all needed info.
vert string GLSL vertex shader
frag string GLSL fragment shader
schema function (data) => JSONSchema
createLayer function (parameters, data) => Array<{ attributes, uniforms, primitive?, vertexCount?, instanceCount?, attributeDivisors?, blend? }> — GPU data only; each element becomes one Layer

getAxisConfig return shape:

{
  xAxis?: string | null,                       // null suppresses the x axis
  xAxisQuantityKind?: string,
  yAxis?: string | null,                       // null suppresses the y axis
  yAxisQuantityKind?: string,
  colorAxisQuantityKinds?: Record<string, string>,  // suffix → quantity kind
  filterAxisQuantityKinds?: Record<string, string>, // suffix → quantity kind
}

Any field that is undefined (or absent) leaves the corresponding static declaration in effect. null for xAxis/yAxis explicitly suppresses that axis.

Automatically provided shader uniforms:

Uniform GLSL type When Description
xDomain vec2 always [min, max] of the x spatial axis current range
yDomain vec2 always [min, max] of the y spatial axis current range
xScaleType float always 0.0 = linear, 1.0 = log
yScaleType float always 0.0 = linear, 1.0 = log
count int always Number of data points (vertices)
u_pickingMode float always 0.0 = normal render, 1.0 = GPU pick pass
u_pickLayerIndex float always Layer index encoded in the pick pass
colorscale<suffix> int color axes Colorscale index; one per entry in colorAxisQuantityKinds
color_range<suffix> vec2 color axes [min, max] color range; one per color axis
color_scale_type<suffix> float color axes 0.0 = linear, 1.0 = log; one per color axis
filter_range<suffix> vec4 filter axes [min, max, hasMin, hasMax]; one per filter axis
filter_scale_type<suffix> float filter axes 0.0 = linear, 1.0 = log; one per filter axis

Automatically injected GLSL:

// Always injected into vertex shader:
attribute float a_pickId;   // per-vertex id (non-instanced) or per-instance id (instanced)
varying float v_pickId;     // passed to fragment shader; automatically assigned in main()

float normalize_axis(float v, vec2 domain, float scaleType)
// Maps v from data-space to [0, 1], handling both linear and log scales.

// Always injected into fragment shader:
varying float v_pickId;

vec4 gladly_apply_color(vec4 color)
// In normal rendering: returns color unchanged.
// In a GPU pick pass (u_pickingMode > 0.5): ignores color and returns the
// pick-encoded RGBA for this vertex (layer index + data index).
// Call this as the last step before assigning gl_FragColor.

// Injected when color axes are present:
vec4 map_color(int cs, vec2 range, float value)
// Maps value to RGBA using colorscale cs and a linear range.

vec4 map_color_s(int cs, vec2 range, float value, float scaleType, float useAlpha)
// Like map_color but handles log scale (log() applied when scaleType > 0.5).
// When useAlpha > 0.5, replaces the output alpha with the normalized value t
// (making low values fade to transparent).
// Calls gladly_apply_color() internally — no explicit call needed in the shader.

// Injected when filter axes are present (vertex shader only):
bool filter_in_range(vec4 range, float value)
// Returns false when value is outside the filter bounds.
// range: [min, max, hasMin, hasMax]; open bounds (hasMin/hasMax == 0) always pass.

Methods:

Method Description
createDrawCommand(regl, layer) Compiles shaders and returns a regl draw function; maps color/filter axis uniforms via colorAxisQuantityKinds/filterAxisQuantityKinds suffixes
schema(data) Returns JSON Schema for layer parameters
createLayer(parameters, data) Calls user factory + resolveAxisConfig, returns a ready-to-render Layer
resolveAxisConfig(parameters, data) Merges static declarations with getAxisConfig output (dynamic wins on non-undefined)

createLayer Return Value

createLayer must return an array of GPU config objects. Each element becomes one rendered Layer. Returning multiple elements renders multiple draw calls from one layer spec (e.g. one per data series).

Each element in the array:

{
  // GPU attribute values — keyed by GLSL attribute name.
  // Each value is either:
  //   - Float32Array: uploaded directly as a vertex buffer attribute.
  //   - Computed expression { computationName: params }: resolved to a GPU texture or
  //     GLSL expression. See docs/api/ComputedAttributes.md for details.
  attributes: {
    x: Float32Array,
    y: Float32Array,
    color_data: Float32Array,                     // name matches GLSL attribute declaration
    filter_data: Float32Array,                    // name matches GLSL attribute declaration
    count: { histogram: { input: norm, bins } },  // computed attribute expression
    // ...
  },

  // Layer-specific GPU uniforms (in addition to the auto-provided ones).
  // Keys are the GLSL-visible uniform names.
  uniforms: {},

  // Optional: pre-computed [min, max] domains keyed by quantity kind.
  // When present for a quantity kind, auto-range skips scanning layer.attributes
  // for that axis. Works for spatial axes (keyed by xAxisQuantityKind /
  // yAxisQuantityKind), color axes, and filter axes.
  // Useful when the layer's y-range spans multiple arrays (e.g. top + bottom),
  // or for instanced layers where per-instance arrays don't match axis quantity kinds.
  domains: {
    myXQuantityKind: [xMin, xMax],
    myYQuantityKind: [Math.min(topMin, botMin), Math.max(topMax, botMax)],
  },

  // Optional: WebGL primitive type. Defaults to "points".
  // Set per element to use different primitives in different draw calls.
  // Valid values: "points", "lines", "line strip", "line loop",
  //               "triangles", "triangle strip", "triangle fan"
  primitive: "points",

  // Optional: override vertex count (defaults to attributes.x.length).
  // Required for instanced rendering (set to vertices per instance, e.g. 6 for a quad).
  vertexCount: null,

  // Optional: number of instances for instanced rendering (ANGLE_instanced_arrays).
  // When set, the draw call renders vertexCount vertices × instanceCount instances.
  // Omit (or null) for non-instanced rendering.
  instanceCount: null,

  // Optional: per-attribute divisors for instanced rendering.
  // A divisor of 1 means the attribute advances once per instance (per-instance data).
  // A divisor of 0 (or absent) means it advances once per vertex (per-vertex data).
  attributeDivisors: {
    x: 1, xPrev: 1, xNext: 1, top: 1, bot: 1,  // per-instance
    // cx, cy omitted → divisor 0 (per-vertex)
  },

  // Optional: regl blend configuration for this draw call.
  // When null or omitted, blending is disabled (the default for opaque layers).
  // When provided, passed directly to regl as the blend config.
  // Use separate srcAlpha/dstAlpha to avoid writing into the canvas alpha channel:
  blend: {
    enable: true,
    func: {
      srcRGB:   'src alpha',           // RGB: weight by fragment alpha
      dstRGB:   'one minus src alpha', // RGB: preserve background scaled by (1 - alpha)
      srcAlpha: 0,                     // alpha channel: ignore fragment alpha
      dstAlpha: 1,                     // alpha channel: preserve framebuffer alpha unchanged
    },
  },
}

Values in attributes are normally Float32Array. They may also be computed attribute expressions — single-key objects { computationName: params } that the framework resolves into a GPU-sampled texture or injected GLSL expression. See Computed Attributes for the full API and built-in computations.


Built-in layer types

The built-in points, lines, colorbar, and filterbar layer types are documented in Built-in Layer Types.


registerColorscale(name, glslFn)

Registers a custom colorscale.

Parameter Type Description
name string Unique colorscale name
glslFn string GLSL function: vec4 colorscale_<name>(float t) { ... } where t ∈ [0, 1]
import { registerColorscale } from './src/index.js'

registerColorscale("my_scale", `
  vec4 colorscale_my_scale(float t) {
    return vec4(t, 1.0 - t, 0.5, 1.0);
  }
`)

getRegisteredColorscales()

Returns Map<string, string> of all registered colorscale names to GLSL function strings.


buildColorGlsl()

Returns the complete GLSL color dispatch string (all colorscale functions + map_color dispatcher). Injected automatically by createDrawCommand; only needed for custom WebGL integrations.


buildFilterGlsl()

Returns the GLSL filter_in_range helper string. Injected automatically by createDrawCommand; only needed for custom WebGL integrations.


Constants Reference

AXES

["xaxis_bottom", "xaxis_top", "yaxis_left", "yaxis_right"]

Colorscales

All matplotlib colorscales are registered by default on import.

Perceptually uniform sequential: viridis, plasma, inferno, magma, cividis

Sequential (single-hue): Blues, Greens, Reds, Oranges, Purples, Greys

Sequential (multi-hue): YlOrBr, YlOrRd, OrRd, PuRd, RdPu, BuPu, GnBu, PuBu, YlGnBu, PuBuGn, BuGn, YlGn

Diverging: PiYG, PRGn, BrBG, PuOr, RdGy, RdBu, RdYlBu, RdYlGn, Spectral, coolwarm, bwr, seismic

Cyclic: twilight, twilight_shifted, hsv

Sequential (misc): hot, afmhot, gist_heat, copper, bone, pink, spring, summer, autumn, winter, cool, Wistia, gray

Miscellaneous: jet, turbo, rainbow, gnuplot, gnuplot2, CMRmap, cubehelix, nipy_spectral, gist_rainbow, gist_earth, terrain, ocean, brg

Use registerColorscale(name, glslFn) to add custom colorscales.