Skip to content

feat(viz): add the display processor — value mappings and noValue #69

Description

@xe-nvdk

#30 landed units.ts (number → string) and colors.ts (value → colour). Neither handles the step before them: a field config can declare that 1 means "Up", that null renders as "No data", or that anything above 90 is "Critical" — and that substitution has to happen before the formatter ever sees the number.

Grafana calls this the display processor, and it is a single function because the order is not negotiable.

The contract

displayProcessor(config: FieldConfig, ctx: DisplayContext): (value: unknown) => DisplayValue

interface DisplayValue {
  text: string;      // what to render
  suffix?: string;   // kept separate, as units.ts does
  color: string;     // resolved, never a bare token
  numeric: number;   // for sparklines and bar widths — NaN when unmappable
}

The order, which is the whole point

  1. Value mappings first. A mapping that turns 1 into "Up" must win over the unit formatter, or the cell reads 1.0 B.
  2. noValue when the value is null/undefined/NaN and no mapping claimed it. A mapping can legitimately map null to text, so this is not simply a null check at the top.
  3. Units and decimals on whatever numeric value survives.
  4. Colour last, from fieldColorFn — except that a mapping may carry its own colour, which overrides.

The model (src/lib/dashboard/model.ts) already validates MAPPING_TYPES: value, range, regex, special. Mappings are ordered and first match wins — not last, not most specific.

Traps

  • regex mappings are deferred to feat(panel): add the table panel #38 in the model and must stay deferred here. A user-supplied regex run against every cell of a 200k-row frame is a ReDoS vector against the browser tab. Whatever lands needs a linear-time matcher or a hard timeout, and that is its own design conversation.
  • special covers null, nan, null+nan, empty, true, false — and true/false must match the boolean, not the strings "true"/"false".
  • Range mappings are inclusive at both ends and from/to may each be null, meaning unbounded.
  • The processor is called once per cell. A 500×50 table is 25,000 calls per redraw. Build the mapping lookup once in the closure — a Map for exact values, an array only for ranges — not per call. colors.ts has the same shape: thresholdScale is built once, thresholdColorAt is the hot call.
  • A mapping's result.color is a stored token, so it goes through resolveColor, not into the DOM directly.

Scope

src/lib/dashboard/display.ts + tests. Pure and isomorphic, same rules as units.ts, colors.ts and frame.ts — no DOM, no imports beyond the sibling modules.

Blocks the stat (#37), table (#40) and gauge panels, which all render a mapped value rather than a raw one.

Activity

  1. added this to the Dashboards v1 milestone on Sep 14, 2026
  2. xe-nvdk commented on Sep 26, 2026

    @xe-nvdk
    MemberAuthor

    Closing: the dashboards feature has been dropped from Launchpad. The implemented work was removed in #87 and this work will not be picked up. Recoverable from git tag pre-dashboards-removal-backup if the feature is ever revived.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dashboardsDashboarding and visualizationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions