Skip to content

Vis Lowering

Lowering converts an OlliVisSpec into a Hypergraph<VisPayload>. This is the bridge between the declarative spec and the navigable tree structure.

lowerVisSpec

ts
function lowerVisSpec(spec: OlliVisSpec): Hypergraph<VisPayload>

lowerVisSpec is the visualization domain's lowerer. It handles both unit and multi-view specs.

Pipeline

  1. ElaborateelaborateSpec fills in missing fields, infers types, and generates a default structure if none is provided.
  2. Lower — The elaborated spec is walked to produce hyperedges.
  3. BuildbuildHypergraph validates parent/child symmetry, detects cycles, and returns the final Hypergraph.

Unit spec lowering

For a UnitOlliVisSpec, the lowerer:

  1. Creates a root edge with role 'root'.
  2. Walks each OlliNode in structure:
    • OlliGroupNode: Creates a guide edge (e.g., xAxis, yAxis, legend) with one child edge per distinct value or bin of the groupby field.
    • OlliPredicateNode: Creates a filteredData edge with the predicate in its payload.
    • OlliAnnotationNode: Creates an annotations edge containing the annotation children.
  3. If the structure produces a single non-filteredData child, it's collapsed into the root to avoid unnecessary nesting.

Multi-view lowering

For a MultiOlliVisSpec:

  1. Creates a root edge.
  2. Creates one view edge per unit, with specIndex and viewType in the payload.
  3. Each unit is lowered independently under its view edge.

Faceting

When facet is set and the top-level structure is a single groupby on the facet field, the lowerer produces a faceted layout: the root groups by the facet field, and each child becomes a view with viewType: 'facet'.

elaborateSpec

ts
function elaborateSpec(spec: OlliVisSpec): OlliVisSpec

Pre-processes the spec before lowering:

  1. Field inference: If fields is empty, creates one OlliFieldDef per key in data[0].
  2. Type inference: For fields without a type, examines the data column to determine quantitative, temporal, or nominal.
  3. Structure inference: If structure is missing, builds a tree from axes, legends, and guides — each becomes a groupby node.

How groupby expansion works

Given a groupby field, the lowerer calls fieldToPredicates to produce one FieldPredicate per distinct value or bin:

  • Nominal/ordinal fields: One FieldEqualPredicate per distinct value, in data order.
  • Quantitative fields: Binned into ranges using axis ticks or auto-computed edges. Each bin becomes a FieldRangePredicate.
  • Temporal fields: Similar to quantitative, but with date-aware binning.

Each predicate becomes a child edge with role filteredData and the predicate stored in payload.predicate.

Example

Given a simple bar chart spec:

ts
const spec: UnitOlliVisSpec = {
  data: [
    { category: 'A', value: 10 },
    { category: 'B', value: 20 },
    { category: 'C', value: 15 },
  ],
  mark: 'bar',
  axes: [
    { field: 'category', axisType: 'x' },
    { field: 'value', axisType: 'y' },
  ],
};

After elaboration and lowering, the hypergraph looks like:

root ("bar chart")
├── xAxis ("x-axis titled category")
│   ├── filteredData ("A")
│   ├── filteredData ("B")
│   └── filteredData ("C")

The y-axis is quantitative with only three values, so it may also produce a guide node depending on the inferred structure.

Next

Released under the BSD-3-Clause License.