Primitive Extension API

These methods are the low-level public extension layer, not the recommended chart-authoring API.

editSemantic({ property, value | remove })

Creates, replaces, or removes one supported semantic branch and structurally copies the changed path.

program.editSemantic({
  property: "layer[points].mark.type",
  value: "point"
});

Property paths use singular user-ID selectors such as layer[points] and library-defined keys such as encoding.x.field. Unknown paths are rejected. Dataset values cannot be replaced after creation.

Use remove: true instead of value to remove one supported semantic branch. Empty parent objects are pruned, the earlier program remains unchanged, and the removal is recorded as an editSemantic trace node. Current removable container paths are complete layers such as layer[points], encoding channels such as layer[points].encoding.opacity, and legend branches such as guide.legend.opacity. Source datasets remain immutable; only an unreferenced derived dataset may be removed as a complete dataset resource.

program.editSemantic({
  property: "layer[points].encoding.opacity",
  remove: true
});

Removing a complete layer does not automatically remove its graphics, scales, guides, or materialization configuration. A domain action that owns the whole resource lifecycle must explicitly clean up those consumers as separate wrapped operations.

The primitive semantic grammar also supports the current line-chart contract, including temporal field types, mean aggregation, strokeDash encodings, scale nice/zero policies, combined series legends, and chart title text.

Derived dataset primitives may store an immutable source, a validated filter or regression transform, and materialized values. Regression transforms support linear, polynomial, or LOESS provenance and optional Student-t mean/prediction intervals for linear and polynomial fits. Layer paths also support encoding.y2, field-driven encoding.shape, and scale-free encoding.group for primitive area and grouped-path contracts. These are extension-level building blocks; the corresponding chart-authoring actions are introduced separately when their complete materialization behavior is ready.

createGraphics({ id, type, length?, parent?, before?, after? })

Creates one concrete object, a homogeneous drawable collection, or an empty heterogeneous drawable collection.

program.createGraphics({ id: "points", type: "circle", length: 2 });

Supported types are canvas, collection, circle, rect, line, text, and path. length is a non-negative integer accepted by homogeneous drawable types. A heterogeneous collection is populated through one editGraphics({ property: "items" }) call instead. Equivalent repeated creation is idempotent.

program
  .createGraphics({ id: "symbols", type: "collection" })
  .editGraphics({
    target: "symbols",
    property: "items",
    value: [
      {
        type: "circle",
        properties: { x: 20, y: 30, radius: 4, fill: "red" }
      },
      {
        type: "rect",
        properties: {
          x: 36,
          y: 46,
          width: 8,
          height: 8,
          fill: "blue",
          stroke: "blue",
          strokeWidth: 0
        }
      }
    ]
  });

Collection item IDs are generated as symbols:0, symbols:1, and so on. Each item stores its own concrete primitive type. Shared properties such as opacity can then be broadcast to every compatible item.

parent attaches a named graphic to an existing Canvas or collection. Attached IDs are stored in the parent’s children list; repeated drawable instances stay separate in the owning graphic’s items list. Omitting parent preserves top-level placement in graphicSpec.order.

Ordinary chart actions create and use their Canvas/plot hierarchy automatically. An extension that authors the same structure directly makes every owner explicit:

program
  .createGraphics({ id: "canvas", type: "canvas" })
  .createGraphics({
    id: "plot",
    type: "collection",
    parent: "canvas"
  })
  .createGraphics({
    id: "bars",
    type: "rect",
    length: 3,
    parent: "plot"
  });

before or after places a new graphic relative to a direct sibling. They are mutually exclusive, the referenced graphic must already belong to the same parent, and no top-level graphic can be placed before the Canvas.

program.createGraphics({
  id: "grid",
  type: "line",
  length: 5,
  parent: "plot",
  before: "bars"
});

Graphic IDs are unique across the complete tree. The renderer visits named graphics depth-first in sibling order. Attachment defines ownership and drawing order only; it does not create coordinate transforms, clipping, or layout. Unknown or non-container parents, self-attachment, and cross-parent sibling anchors are rejected during authoring. Rendering rejects orphaned, duplicated, cyclic, or unknown attachments instead of silently skipping them.

Path graphics use backend-neutral path.commands arrays rather than renderer-specific path strings. The closed command vocabulary is M, L, C, and Z. A path starts with one M; L and C draw straight and cubic segments; an optional final Z closes the path. At least one L or C segment is required. Coordinates and cubic control points must be finite numbers. TypeScript users can import the ConcretePathCommand union from ggaction. path.strokeDash and line.strokeDash accept non-negative finite number arrays; an empty array is a solid stroke.

Chart authors choose line interpolation through createLineMark({ curve }) or editLineMark({ curve }). Extension authors still write final commands rather than storing curve names in graphicSpec; the renderer never performs interpolation.

Circle graphics support a required fill and an optional concrete stroke/strokeWidth pair. Accepted graphic properties have Canvas rendering semantics; opaque style bags are not stored in graphicSpec.

A path can be open and stroked or Z-closed and filled. Filled paths require a final Z command; their stroke is optional. When both fill and stroke are present, the Canvas renderer fills first and strokes second.

program
  .createGraphics({ id: "band", type: "path" })
  .editGraphics({
    target: "band",
    property: "commands",
    value: [
      { op: "M", x: 10, y: 80 },
      { op: "L", x: 70, y: 40 },
      { op: "L", x: 70, y: 60 },
      { op: "L", x: 10, y: 100 },
      { op: "Z" }
    ]
  })
  .editGraphics({ target: "band", property: "fill", value: "#111111" })
  .editGraphics({ target: "band", property: "opacity", value: 0.18 });

Rect and closed-path fill also accept a backend-neutral LinearGradientPaint. The from and to coordinates are normalized to each item’s own fill bounds, not the whole Canvas. Stops are ordered by offsets from 0 to 1; repeated adjacent offsets create a hard transition. A paint object and its nested stops array are one scalar property value, so broadcasting it to a collection applies the complete paint to every item.

const verticalDensityPaint = {
  type: "linear-gradient",
  from: { x: 0.5, y: 1 },
  to: { x: 0.5, y: 0 },
  stops: [
    { offset: 0, color: "rgba(207, 225, 242, 0)" },
    { offset: 0.5, color: "rgba(79, 142, 195, 0.7)" },
    { offset: 1, color: "rgba(10, 74, 144, 1)" }
  ]
};

program.editGraphics({
  target: "distributionStrips",
  property: "fill",
  value: verticalDensityPaint
});

TypeScript users can import FillPaint, LinearGradientPaint, LinearGradientPoint, and LinearGradientStop from ggaction or ggaction/extension. Structured paint is currently fill-only for rects and closed paths. Circle/text fills, strokes, radial or conic gradients, patterns, and Canvas-wide user-space coordinates are not supported.

Only normalized paint data is stored in graphicSpec. The Canvas renderer resolves the final item-local coordinates and creates the backend gradient while drawing; no Canvas gradient object is retained in the immutable program.

The complete low-level line-chart example is available in primitive.program.js. It explicitly authors semantic line state, paths, axes, a combined legend, and title graphics without chart-level convenience actions.

The regression scatterplot baseline in primitive.program.js uses a heterogeneous point collection, grouped filled confidence-band paths, grouped line paths, and two concrete legends.

editGraphics({ target, property, value | remove })

Sets one validated concrete property or removes one named graphic subtree.

program.editGraphics({
  target: "points",
  property: "x",
  value: [32.5, 81.4]
});

For a homogeneous or heterogeneous collection, an outer array distributes values by index and must match its length. A non-array value is broadcast to every item that supports the property. Nested arrays and objects remain one value per item. Generated item IDs such as points:1 can be targeted.

Use remove: true without property or value to remove a named graphic and its owned named descendants. The action also detaches the root from its parent or top-level order. The Canvas root and generated items cannot be removed; resize or replace an item’s owning collection instead.

program.editGraphics({
  target: "opacityLegendSymbols",
  remove: true
});

Authoring and rendering share the same concrete value contract. Numeric geometry must be finite, dimensions and stroke widths cannot be negative, opacity stays between 0 and 1, text alignment uses the Canvas vocabulary, and appearance strings must be non-empty. Structured rect/path fill follows the FillPaint contract described above. Rendering additionally requires all properties needed to draw the primitive to be present.

Scale materialization

createScale({ id, type?, domain?, range?, nice?, zero?, clamp?, reverse?, base?, exponent?, constant? }) creates an idempotent semantic scale. Domain actions that own scale consumers invoke the internal wrapped rematerializeScale operation to resolve all consumers and apply concrete graphic edits, including connected axis updates. Aggregate line consumers resolve their domains from derived means rather than raw rows. Ordinal ranges may contain color strings, a validated named-palette descriptor, or validated even-length stroke-dash patterns for the matching channel.

program.createScale({ id: "x", type: "linear" });

Direct quantitative position scale types are linear, log, pow, sqrt, and symlog; type-specific parameters use the same contract as encoding scale options. time and ordinal remain available for their current roles.

Extension actions should call the public domain action that owns the affected consumer instead of calling rematerializeScale directly. Rematerialization is still explicit inside that action and remains visible in program.trace; it never runs merely because semanticSpec changed.