npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noaa-gsl/wizard-charts

v1.2.1

Published

An extension of D3 for visualizing weather data in a variety of charts.

Readme

WIZARD Charts

WIZARD Charts is a React charting library built on top of D3 for weather and forecast-oriented visualizations.

Table of Contents

Installation

d3 is a peer dependency and must be installed alongside WIZARD Charts.

npm install @noaa-gsl/wizard-charts d3

Release Notes

See CHANGELOG.md for user-facing updates by release.

Quick Start

Import the stylesheet once in your app entrypoint or global styles location:

import '@noaa-gsl/wizard-charts/styles.css';

Render a ChartContainer with data and options:

import { ChartContainer } from '@noaa-gsl/wizard-charts';

const data = [
  {
    date: new Date('2026-01-01'),
    temp: { mean: 30, p90: 38 },
  },
  {
    date: new Date('2026-01-02'),
    temp: { mean: 27, p90: 35 },
  },
];

const options = {
  series: [
    {
      type: 'line',
      xKey: 'date',
      yKey: 'temp.mean',
      stroke: '#147AF3',
    },
  ],
  axes: {
    x: { type: 'time' },
    y: { type: 'linear', nice: true },
  },
};

<ChartContainer
  margin={{ left: 40, top: 'auto', right: 'auto', bottom: 'auto' }}
  data={data}
  options={options}
  sx={{ border: '1px solid #737373' }}
/>;

ChartContainer Props

type ChartContainerProps = {
  height?: number | 'auto';
  width?: number | 'auto';
  margin?: {
    top?: number | 'auto';
    right?: number | 'auto';
    bottom?: number | 'auto';
    left?: number | 'auto';
  };
  data: unknown[] | Record<string, unknown[]>;
  options: ChartOptions;
  controller?: ReturnType<typeof useChartController>['controller'];
  ReadoutComponent?: React.ComponentType<{
    readout: ReadoutState;
    options: ChartOptions['readout'];
  }>;
  children?: React.ReactNode;
  className?: string;
  sx?: React.CSSProperties;
};

margin defaults to { top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' }.

width and height default to 'auto' (100% of the parent container on that dimension). If you provide one or both as numbers, only those dimensions are fixed.

width and height define the chart's outer SVG box. Internal chart layout (scales, axes, and plot area) is measured from the SVG content box, so box-sizing: border-box with border and/or padding in sx is accounted for automatically.

Series rendering inside ChartContainer is clipped to the computed inner plot area (inside margins). This prevents plot layers from visually spilling into axis/legend space.

Dynamic Margins

Each margin side can be either:

  • a number: fixed pixel margin for that side
  • 'auto': measured from axis rendering requirements

Auto margins are computed independently per side (top, right, bottom, left) using the axis mapped to that side:

  • axis line width (when hasAxisLine is enabled)
  • outward tick length (negative tick length is treated as inward and does not add outside space)
  • tick label padding
  • measured tick label text bounds
  • measured axis label text bounds from axes.*.label

When options.legend.enabled is true and at least one visible series is eligible for the legend, bottom auto-margin also reserves space for the legend block below the primary x-axis.

Axis labels are sourced from axes.x.label, axes.x2.label, axes.y.label, and axes.y2.label.

Default behavior (all auto):

<ChartContainer data={data} options={options} />

Mixed overrides (fixed left margin, other sides auto):

<ChartContainer
  width={800}
  margin={{ left: 48, top: 'auto', right: 'auto', bottom: 'auto' }}
  data={data}
  options={options}
/>

Data Model

WIZARD Charts supports two input data shapes:

  1. Row-based arrays (default)
  2. Columnar object-of-arrays (auto-normalized)

Row-Based Data (Default)

Each array element maps to one position on the x-axis.

[
  {
    date: 1775075088409,
    series1: {
      mean: 31.45,
      p10: 27.66,
      p25: 30.6,
      p50: 31.4,
      p75: 31.82,
      p90: 32.63,
    },
    series2: {
      mean: 81.28,
      p10: 81.18,
      p25: 81.17,
      p50: 81.5,
      p75: 82.0,
      p90: 81.69,
    },
  },
];

Accessors and Dot Notation

Series values are read with keys like xKey, yKey, minYKey, etc. Dot notation is supported:

  • xKey: 'date'
  • yKey: 'series2.p50'

Columnar Data (Per-Series Override)

You can provide data directly on a series as an object-of-arrays. This overrides root-level data for that series.

{
  type: 'line',
  xKey: 'date',
  yKey: 'mean',
  data: {
    date: [
      new Date('2024-01-01'),
      new Date('2024-01-02'),
      new Date('2024-01-03'),
    ],
    mean: [10, 20, 15],
  },
}

All column arrays must be the same length.

Options Overview

options drives how series and axes are rendered:

{
  series: [],
  axes: {
    x: {
      // optional reference lines for this axis
      lineMarkers: [],
    },
    y: {
      // optional reference lines for this axis
      lineMarkers: [],
    },
    // optional secondary axes
    // x2: {},
    // y2: {},
  },
  legend: {
    enabled: true,
    gap: 8, // space above the legend
    rowGap: 8, // space between rows when the legend wraps
    itemGap: 16, // space between items
    markerSize: 12, // size of shaded line, square or circle identifier
    fontFamily: 'inherit',
    fontSize: 12,
    fontWeight: 500,
    fontColor: 'currentColor',
    colorbar: {
      width: 120,
      height: 10,
      tickGap: 4,
      tickFontSize: 10,
      tickFontWeight: 400,
    },
    className: '',
    sx: {},
  },
  zoom: {
    enabled: true,
    wheelEnabled: true,
    dragEnabled: true,
    panEnabled: true,
    rightClickResetEnabled: true,
    modifierKey: 'ctrl', // 'ctrl' | 'shift' | 'alt' | 'meta'
    panCursor: 'move',
    wheelZoomSpeed: 0.1,
    minWindow: 0, // minimum x-domain span; 0 disables clamping
    minDragPixels: 4,
    dragBox: {
      fill: '#147AF333',
      stroke: '#147AF3',
      strokeWidth: 1,
    },
  },
  readout: {
    hoverMode: 'local',
    showVerticalLine: true,
    showTooltip: true,
    className: '',
    sx: {},
    tooltip: {
      fill: '#111827',
      fillOpacity: 0.95,
      stroke: '#374151',
      strokeWidth: 1,
      cornerRadius: 6,
      className: '',
      sx: {},
    },
    displayUnits: true,
    rowOrder: 'seriesIndex', // 'seriesIndex' | 'distance'
    xEligibility: 'withinBounds', // 'withinBounds' | 'withinTolerance' | 'anyDistance'
    xTolerance: undefined, // data-domain units; Date/time axes use milliseconds
    missingSeries: 'placeholder', // 'placeholder' | 'omit'
    missingText: '---',
    boxPlotFields: 'auto', // 'auto' | key | key[]
    areaFields: 'auto', // 'auto' | key | key[]
    titleFormatter: null, // (value, context?) => string
    valueFormatter: null, // (numericValue, context?) => string
    padding: { x: 8, y: 8 }, // number | { x, y }
    rowGap: 4,
    title: {
      fontSize: 12,
      fontWeight: 700,
      fontFamily: 'inherit',
      fontColor: 'currentColor',
    },
    row: {
      fontSize: 12,
      fontWeight: 400,
      fontFamily: 'inherit',
      fontColor: 'currentColor',
    },
    showPointMarkers: true,
    tooltipOffset: 12,
    markerRadius: 4,
    markerStroke: '#ffffff',
    markerStrokeWidth: 1.25,
    markerFill: 'none',
    debug: false,
  },
  animationDuration: 1000, // ms (set 0 to disable animation)
}

Zoom

Zoom currently supports wheel zoom, drag-select zoom-in, and modifier-drag panning, and updates x-domains only.

  • Zoom is enabled by default through options.zoom.enabled: true.
  • Wheel zoom is controlled by options.zoom.wheelEnabled (true by default).
  • Drag-select zoom is controlled by options.zoom.dragEnabled (true by default).
  • Modifier-drag panning is controlled by options.zoom.panEnabled (true by default).
  • Y-axis domains remain fixed while zooming; only x/x2 domains change.
  • Zoom focus is anchored to the pointer x-position in the plot area.
  • Wheel zoom requires a modifier key by default (modifierKey: 'ctrl').
  • Panning uses the same modifierKey as wheel zoom and left-drag in the plot area.
  • While modifier is held over the inner plot area, cursor switches to panCursor ('move' by default).
  • Drag-select zoom uses plain left-click drag in the plot area by default.
  • Right-click inside the inner plot area resets zoom back to the starting x-domain extent.
  • Supported zoom axis scale types are linear and time only.

Zoom options:

  • enabled (boolean, default true): master toggle for zoom features.
  • wheelEnabled (boolean, default true): enables/disables wheel zoom.
  • dragEnabled (boolean, default true): enables/disables drag-select zoom.
  • panEnabled (boolean, default true): enables/disables modifier-drag panning.
  • rightClickResetEnabled (boolean, default true): enables/disables right-click zoom reset in the inner plot area.
  • modifierKey ('ctrl' | 'shift' | 'alt' | 'meta', default 'ctrl'): required key while scrolling and modifier-drag panning.
  • panCursor (string, default 'move'): cursor shown while modifier is held over the inner plot area.
  • wheelZoomSpeed (number, default 0.1): zoom sensitivity. Larger values zoom faster per wheel step.
  • minWindow (number, default 0): minimum x-domain span. Use 0 to disable minimum-span clamping.
  • minDragPixels (number, default 4): minimum horizontal drag distance in pixels before drag zoom is applied.
  • dragBox.fill (string, default '#147AF333'): fill color for the drag selection rectangle.
  • dragBox.stroke (string, default '#147AF3'): stroke color for the drag selection rectangle.
  • dragBox.strokeWidth (number, default 1): stroke width in pixels for the drag selection rectangle.

Examples:

<ChartContainer
  data={data}
  options={{
    ...options,
    zoom: {
      enabled: true,
      wheelEnabled: true,
      dragEnabled: true,
      panEnabled: true,
      rightClickResetEnabled: true,
      modifierKey: 'ctrl',
      panCursor: 'move',
      wheelZoomSpeed: 0.08,
      minWindow: 0,
      minDragPixels: 4,
      dragBox: {
        fill: '#147AF333',
        stroke: '#147AF3',
        strokeWidth: 1,
      },
    },
  }}
/>

Disable zoom:

<ChartContainer
  data={data}
  options={{
    ...options,
    zoom: {
      enabled: false,
    },
  }}
/>

Programmatic Zoom Control

Use useChartController when controls outside the SVG need to read or change zoom state.

import { ChartContainer, useChartController } from '@noaa-gsl/wizard-charts';

function ForecastChart({ data, options }) {
  const { controller, zoomState } = useChartController({
    onZoomStateChange: (nextZoomState, context) => {
      console.log('zoom changed', context.source, nextZoomState);
    },
  });

  return (
    <>
      <button type="button" onClick={() => controller.resetZoom()}>
        Reset Zoom
      </button>
      <button
        type="button"
        onClick={() =>
          controller.setZoomWindow({
            center: new Date('2026-01-01T12:00:00Z'),
            windowSize: 6 * 60 * 60 * 1000,
          })
        }
      >
        Center Six-Hour Window
      </button>
      <ChartContainer controller={controller} data={data} options={options} />
      <output>{zoomState.center?.toString() || 'Full extent'}</output>
    </>
  );
}

Controller commands:

  • controller.resetZoom(): reset x/x2 zoom domains to the starting extent.
  • controller.setZoomWindow({ center, windowSize }): center the zoom window on a data value. For time axes, center can be a Date or timestamp and windowSize is milliseconds. For linear axes, both values are numeric domain units.
  • controller.setZoomCenter(center): move the current zoom window while keeping the current windowSize.
  • controller.setZoomStart(start): move the current zoom window so it starts at start while keeping the current windowSize.
  • controller.setZoomEnd(end): move the current zoom window so it ends at end while keeping the current windowSize.
  • controller.getZoomState(): read the latest zoom state outside render.

Reactive zoom state is available from the hook return value and from controller.zoomState. Use the hook return value when rendering UI so React updates when zoom changes.

const { controller, zoomState } = useChartController();

<input
  type="range"
  min={zoomState.bounds.x?.[0]?.valueOf() ?? 0}
  max={zoomState.bounds.x?.[1]?.valueOf() ?? 0}
  value={zoomState.centerValue ?? 0}
  onChange={(event) => controller.setZoomCenter(Number(event.target.value))}
/>;

For zoom state fields, values without the Value suffix use the chart domain type: Date objects for time axes and numbers for linear axes. Fields ending in Value are always numeric, so they are usually the best fit for sliders, math, comparisons, and telemetry. For time axes, numeric values are JavaScript timestamps in milliseconds.

| Property | Meaning | Linear-axis example | Time-axis example | Recommended use | | ------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | domain | Active zoom domains by axis. x and/or x2 contain [start, end]; axes without an active override are null. | { x: [20, 80], x2: null } | { x: [Date('2026-01-01T00:00Z'), Date('2026-01-01T06:00Z')], x2: null } | Low-level access to exact active domains. | | start | Typed start of the primary active zoom domain (x, then x2). | 20 | Date('2026-01-01T00:00Z') | Display, formatting, and passing a domain value back to controller methods. | | startValue | Numeric start of the primary active zoom domain. | 20 | 1767225600000 | Sliders, range math, comparisons, and telemetry. | | center | Typed center of the primary active zoom domain. | 50 | Date('2026-01-01T03:00Z') | Display, formatting, and controller.setZoomCenter(center). | | centerValue | Numeric center of the primary active zoom domain. | 50 | 1767236400000 | Slider values and numeric positioning controls. | | end | Typed end of the primary active zoom domain. | 80 | Date('2026-01-01T06:00Z') | Display, formatting, and passing a domain value back to controller methods. | | endValue | Numeric end of the primary active zoom domain. | 80 | 1767247200000 | Sliders, range math, comparisons, and telemetry. | | windowSize | Numeric span of the primary active zoom domain. | 60 | 21600000 | Preserving, displaying, or calculating zoom window size. For time axes this is milliseconds. | | bounds | Starting zoomable domain bounds by axis. These are the clamp limits for programmatic zoom, wheel zoom, drag zoom, and pan. | { x: [0, 100], x2: null } | { x: [Date('2026-01-01T00:00Z'), Date('2026-01-02T00:00Z')], x2: null } | Setting control min/max values and understanding clamp limits. | | isZoomed | Whether any x-axis zoom domain override is active. | true | true | Enabling reset controls or showing zoomed state. | | source | Last zoom update source: 'wheel', 'drag', 'pan', 'reset', 'programmatic', or null. | 'programmatic' | 'wheel' | Telemetry, debugging, and UI labels. |

When no zoom window is active, start, startValue, center, centerValue, end, endValue, and windowSize are null.

Programmatic zoom control applies to mapped zoomable x-axes automatically (x and/or x2). Supported scale types are linear and time.

Hover Readout

Tooltips render as HTML/CSS through a React portal into document.body. The default tooltip and a custom ReadoutComponent share the same measured-box positioning and containment. Vertical guide lines and point markers remain SVG. Import the package stylesheet as shown in Quick Start; it includes the default HTML layout and styling hooks.

options.readout.hoverMode supports two modes:

  • 'local' (default): hover tracking is local to each chart and does not require any wrapper.
  • 'global': charts synchronize hover readout events through HoverPointProvider.

options.readout.showVerticalLine controls whether a dashed vertical guide line renders at the hovered x position.

  • true (default): show vertical guide line.
  • false: hide vertical guide line.

options.readout.showTooltip controls whether a tooltip box renders with readout values.

  • true (default): show tooltip.
  • false: hide the default or custom hosted tooltip. Sampling, external readout subscriptions, vertical guides, and point markers remain independent.

options.readout.className and options.readout.sx apply to the readout overlay root <g> element.

  • Use className for CSS-based overrides.
  • Use sx for inline style overrides.

options.readout.tooltip controls the HTML content surface for both default and custom tooltips. The library owns a separate positioning/clipping container.

  • fill: background color.
  • fillOpacity: background-only opacity (0..1), without fading text.
  • stroke: border color.
  • strokeWidth: border width in pixels.
  • cornerRadius: border radius in pixels.
  • className: class applied to the HTML tooltip surface.
  • sx: inline CSS style object applied to the HTML tooltip surface.

Default appearance uses low-specificity CSS so consumer classes can override it; sx takes precedence over ordinary stylesheet rules. Use backgroundColor to override the background directly, or --readout-background and --readout-background-opacity to reuse the default background-only alpha blending (CSS color-mix(), supported by current browsers). Use CSS color for text, not SVG fill. Default title/row font options still apply; custom components own their internal typography and can use the supplied options to reuse those settings.

The portal copies the SVG's computed font family and color, but it does not inherit ancestor-specific CSS selectors or other custom properties from the chart's DOM ancestors. Put theme rules on the tooltip class or pass sx. Position, clipping, and pointer transparency remain library-controlled.

options.readout.displayUnits controls whether series units are appended to readout values.

  • true (default): append units when available.
  • false: suppress unit suffixes in readout rows.
  • Readout units use series units first.
  • If a series units is empty, axis-linked series (line, bar, boxPlot, area, areaStacked, circle) fall back to their mapped y-axis axes.y.units or axes.y2.units.
  • matrix, heatmap, and contourGrid values do not fall back to axis units; provide series.units when you want value units in those readouts.
  • Units only render when both readout.displayUnits and series.displayUnits are true.
  • If no series units are provided, nothing is appended.

options.readout.rowOrder controls tooltip row ordering.

  • 'seriesIndex' (default): order rows by series index in options.series.
  • 'distance': order rows by distance from the hover pointer.

options.readout.rowGap controls vertical spacing in pixels between tooltip rows and between the title and first row.

  • 4 (default).

options.readout.xEligibility controls whether a nearest-style series is eligible to show a real value at the hovered x-value.

  • 'withinBounds' (default): show a real value only when the hovered x-value is within that series' own x extent.
  • 'withinTolerance': show a real value when the hovered x-value is within the series x extent or the nearest sample is within xTolerance.
  • 'anyDistance': always show the nearest point, matching the previous behavior.
  • series.readoutXEligibility overrides the readout-level setting for one series.

options.readout.xTolerance sets the tolerance for 'withinTolerance'.

  • The value uses data-domain units. For linear forecast-hour axes, 2 means two hours when your x values are hours.
  • For Date/time axes, use milliseconds, for example 2 * 60 * 60 * 1000 for two hours.
  • series.readoutXTolerance overrides the readout-level setting for one series.

options.readout.missingSeries controls unavailable rows when a series is outside its x eligibility.

  • 'placeholder' (default): keep the row and render missingText, preserving tooltip height as series become unavailable.
  • 'omit': remove unavailable rows from the readout.

options.readout.missingText controls placeholder text for unavailable rows.

  • '---' (default).

Unavailable placeholder rows do not render SVG point markers/circles, because no value is being shown for that series at the hovered x-value.

options.readout.boxPlotFields controls which box-plot values render in the readout row.

  • 'auto' (default): uses median when available, then falls back to box midpoint.
  • string or string[]: choose from 'median', 'q1', 'q3', 'min', 'max'.
  • aliases: 'lower' -> 'q1', 'upper' -> 'q3'.

options.readout.areaFields controls which area values render in the readout row.

  • 'auto' (default): uses y when available, then falls back to band midpoint.
  • string or string[]: accepts field ids.
  • built-in aliases: 'median' -> 'y', 'q1' -> 'lower', 'q3' -> 'upper'.
  • for plain area, you can keep using legacy ids ('y', 'lower', 'upper', 'min', 'max') or define custom ids/labels on the series via lowerField, lowerLabel, medianField, medianLabel, upperField, upperLabel, minField, minLabel, maxField, maxLabel.
  • for custom area ids derived from data keys, full accessor keys are accepted as aliases (for example series1.p25).

For areaStacked, areaFields accepts field ids derived from each band key plus optional medianField.

  • Band field ids are inferred from key suffixes: series1.p05 -> p05, series1.p95 -> p95.

  • Full keys are also accepted as aliases in areaFields (for example series1.p05).

  • 'auto' (default): orders fields from high to low: upper bounds in configured band order, then median, then lower bounds in reverse order.

  • string or string[]: explicit field id order (for example ['p05', 'p10', 'p25', 'p50', 'p75', 'p90', 'p95']).

When multiple fields are configured for boxPlot/area/areaStacked, the tooltip renders labeled values on indented sub-lines with an aligned value column. The first valid configured field also drives marker y-position and distance ranking.

options.readout.titleFormatter optionally formats the x-value shown in the tooltip title.

  • null (default): uses built-in formatting.
  • (value, context) => string: custom formatter.
  • context: { axisKey: 'x' | 'x2', isDate: boolean }.

Series readout controls:

  • series.readoutPrecision: optional fixed decimal precision used by default readout formatting.
  • series.readoutXEligibility: optional per-series override for readout.xEligibility.
  • series.readoutXTolerance: optional per-series override for readout.xTolerance.
  • series.units: optional unit suffix for readout values.
  • series.displayUnits: per-series unit toggle in readout (true by default).

options.readout.valueFormatter optionally formats numeric values shown in tooltip rows.

  • null (default): uses built-in numeric formatting.
  • (value, context) => string: custom formatter.
  • value: numeric value after parsing.
  • context: { seriesType, fieldKey, fieldLabel, variant, units, displayUnits, readoutDisplayUnits, seriesDisplayUnits, readoutPrecision, defaultText, summary } where variant is 'row' or 'detail'.
  • If a formatter throws or returns null/undefined, readout falls back to default formatting.

Default readout formatting uses series.readoutPrecision when provided; otherwise it uses the built-in adaptive formatter.

Example formatter usage:

{
  readout: {
    titleFormatter: timeFormatter('%m-%d %Hz'),
    valueFormatter: (value, { fieldKey, defaultText }) => {
      if (fieldKey === 'value') {
        return `${value.toFixed(0)}%`;
      }
      return defaultText;
    },
  },
}

options.readout.padding controls outer padding around the tooltip title and rows.

  • number: applies same padding to x and y.
  • { x, y }: sets horizontal and vertical padding independently.
  • default: { x: 8, y: 8 }.

options.readout.title and options.readout.row controls tooltip title and row text styling.

  • fontSize
  • fontWeight
  • fontFamily
  • fontColor

Row labels and values share the same row font settings.

options.readout.showPointMarkers controls point marker circles at readout points.

  • true (default): show marker circles.
  • false: hide marker circles.
  • For boxPlot, area, and areaStacked, markers follow configured readout fields: when multiple fields are selected (for example ['q1', 'q3']), one marker is rendered per field.
  • For area and line series on continuous x-scales, marker x-position follows the raw x-scale value.
  • For bar/boxPlot series, marker x-position follows the rendered rectangle center (including alignment and width).
  • Unavailable placeholder rows do not render markers.

options.readout.tooltipOffset sets the horizontal pixel distance from pointer to tooltip anchor.

Marker style options:

  • markerRadius
  • markerStroke
  • markerStrokeWidth
  • markerFill

Readout overlays are constrained to chart/SVG bounds:

  • Readout rendering only occurs while the pointer is inside the plot area.
  • Tooltip containment uses the total SVG rectangle, including chart margins, intersected with the visible viewport when the chart is partially offscreen.
  • Tooltip prefers the right of the pointer, flips left when appropriate, and clamps to all four edges. Near an edge, the box stops following the pointer while its sampled values continue updating.
  • HTML/CSS lays out the content; only the final box is measured. Bounds and placement update on hover, chart/content resize, zoom, and scrolling.
  • Long text wraps. Content larger than the available chart area is clipped; hosted tooltips are non-interactive and do not have scrollbars. Use an external readout for full-size or interactive content.
  • Custom content is subject to the same limits. Separate portals created by a custom component are outside this containment contract.
  • The body portal uses fixed positioning. Arbitrarily rotated charts, transformed html/body containing blocks, and top-layer dialogs require application-level handling (for example an external readout); there is no portal-target prop.

Custom HTML Tooltip

Pass a component, not its rendered element, to ChartContainer.ReadoutComponent. It receives { readout, options } and may use React hooks. Define the component outside the chart's render function to preserve its identity between updates. Returning null renders no tooltip content. The component is mounted only while there is an active sample and showTooltip is true; no portal is rendered during SSR.

function ForecastReadout({ readout }) {
  return (
    <section>
      <strong>{readout.title}</strong>
      {readout.rows.map((row) => (
        <div key={row.seriesIndex}>
          <span style={{ color: row.color }}>{row.label}</span>: {row.text}
        </div>
      ))}
    </section>
  );
}

<ChartContainer
  data={data}
  options={{
    ...options,
    readout: {
      ...options.readout,
      tooltip: {
        className: 'forecast-readout',
        sx: { color: '#fff', borderRadius: 4, padding: 12 },
      },
    },
  }}
  ReadoutComponent={ForecastReadout}
/>;

External Readout

useChartReadoutState(controller) subscribes to the same model without requiring an SVG descendant or a portal. Keep the subscription in the component that renders the readout, rather than the component that owns the chart. Hover updates do not notify the controller's zoom subscribers.

import {
  ChartContainer,
  useChartController,
  useChartReadoutState,
} from '@noaa-gsl/wizard-charts';

function ForecastPanel({ controller }) {
  const readout = useChartReadoutState(controller);
  if (!readout) return null;

  return (
    <aside>
      <h3>{readout.title}</h3>
      {readout.rows.map((row) => (
        <p key={row.seriesIndex}>
          {row.label}: {row.text}
        </p>
      ))}
    </aside>
  );
}

function ForecastChart({ data, options }) {
  const { controller } = useChartController();
  return (
    <>
      <ChartContainer
        controller={controller}
        data={data}
        options={{
          ...options,
          readout: { ...options.readout, showTooltip: false },
        }}
      />
      <ForecastPanel controller={controller} />
    </>
  );
}

Use one controller per chart. controller.getReadoutState() reads the latest snapshot outside render. controller.subscribeReadout(listener) subscribes to changes and returns an unsubscribe function. Readout and zoom subscriptions are separate; useChartController() does not implicitly subscribe to readout updates.

Readout Model

ReadoutComponent receives this non-null model as readout. The hook returns the model or null when inactive, outside the receiving chart's plot, without samples, or after the chart unmounts. In global mode each chart samples its own series and uses its own mapped coordinates. Treat snapshots and referenced data as read-only.

| Property | Meaning | | -------------------------- | -------------------------------------------------------------------------------------------- | | chartId, sourceChartId | Receiving chart and originating hover chart identifiers. | | mode | Effective 'local' or 'global' mode. | | xValue, axisKey | Hovered domain value and primary readout axis ('x' or 'x2'). Dates retain their type. | | title | Title formatted using readout.titleFormatter. | | local | { x, y } in the receiving SVG coordinate system, not tooltip position. | | sourceClient | { x, y } browser coordinates of the originating pointer; not a target-chart portal anchor. | | rows | Per-series samples ordered by readout.rowOrder. |

Each row contains:

| Property | Meaning | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id, seriesIndex, seriesType, axisKeys | Configured series id (index fallback), index, plot type, and mapped axes. | | label, color, units | Resolved series name, readout color, and units. | | values | Plot-specific sampled values, such as y, area bounds, value, or wind speed/direction. These are not necessarily original raw data; existing wind precision rounding is preserved. | | status | 'available' for rows with a real sampled value, or 'outOfRange' when the series is outside its x eligibility and rendered as a placeholder. | | sampling | 'nearest' or 'interpolate'. Matrix uses nearest cell selection. | | datum, dataIndex | Selected normalized source row/index where available; null for interpolated values and grid samples without a source index. A heatmap's nearest supporting sample is not claimed as its interpolated datum. | | entries | Selected fields as { key, label, value }, before display formatting. | | text, detailLines | Formatted summary and { key, label, text } detail lines, honoring existing formatters, units, precision and field selection. | | sampleX, xDistanceValue, xExtent | Domain-space sampling metadata for x eligibility and custom out-of-range displays. | | distancePx, xPixel, yPixel, markerPoints | Sample distance and SVG marker geometry; not HTML positioning coordinates. Placeholder rows have no marker geometry. |

Consumers may ignore all formatted fields and use values, entries, or datum to build their own presentation. The public model is separate from debug payloads.

SVG Tooltip Migration

The default tooltip is now HTML; there is no legacy SVG tooltip mode. readout.className/sx still target the SVG annotation group, while readout.tooltip.className/sx target the HTML surface. Update selectors that previously targeted tooltip g, rect, or text elements and replace SVG-only style properties with their CSS equivalents. Practical appearance options and formatters remain supported, but browser text layout is not pixel-identical to the previous SVG layout. Serializing the chart SVG no longer includes its tooltip.

options.readout.debug controls console debug payload logging.

  • false (default): no readout debug logging.
  • true: emit throttled console.debug payloads (roughly every 100ms while hovering).

Local Mode

<ChartContainer
  data={data}
  options={{ ...options, readout: { hoverMode: 'local' } }}
/>

Global Mode

Wrap charts that should share hover state in one HoverPointProvider group:

import { ChartContainer, HoverPointProvider } from '@noaa-gsl/wizard-charts';

<HoverPointProvider>
  <ChartContainer
    data={dataA}
    options={{ ...optionsA, readout: { hoverMode: 'global' } }}
  />
  <ChartContainer
    data={dataB}
    options={{ ...optionsB, readout: { hoverMode: 'global' } }}
  />
</HoverPointProvider>;

If hoverMode is 'global' but no provider is present, charts fall back to local mode.

When readout.debug is enabled, debug payloads include hover coordinates plus nearest values per chart. Supported series for nearest-value debug output are line, bar, circle, area, areaStacked, boxPlot, matrix, heatmap, contourGrid, and windBarbs.

Series Configuration

Each entry in options.series renders one plot layer.

Common Series Options

{
  type: 'line', // 'line' | 'bar' | 'boxPlot' | 'circle' | 'area' | 'areaStacked' | 'matrix' | 'heatmap' | 'contourGrid' | 'windBarbs'
  name: undefined, // legend label; falls back to yKey
  xKey: 'x',
  yKey: 'y',
  units: '', // optional readout unit suffix for this series
  displayUnits: true, // toggle unit suffix in readout for this series
  readoutPrecision: undefined, // optional fixed decimal precision for readout values
  data: undefined, // optional per-series dataset
  // set true to map this series to x2 / y2 instead of x / y
  isSecondaryYAxis: false,
  isSecondaryXAxis: false,
  isVisible: true,
  showInLegend: true,
  stroke: null,
  fill: null,
  className: '',
  sx: {},
}

Legend

Legend behavior is enabled by default.

  • The legend renders beneath the primary x-axis when at least one series is visible and showInLegend is not false.
  • Set options.legend.enabled: false to hide legend rendering and skip legend auto-margin reservation.
  • Set series.showInLegend: false to hide a single series from legend output.
  • Series labels use name first, then fall back to yKey.
  • Matrix, heatmap, and contourGrid series render a per-series colorbar legend entry instead of a marker.

Example:

{
  legend: {
    enabled: true,
  },
  series: [
    {
      type: 'line',
      name: 'Mean Temperature',
      xKey: 'date',
      yKey: 'temp.mean',
    },
  ],
}

Matrix Data Shape (Long Form)

Use one row per cell for matrix charts.

[
  { date: new Date('2026-01-01'), category: 'model1', value: 62.3 },
  { date: new Date('2026-01-01'), category: 'model2', value: 58.1 },
  { date: new Date('2026-01-02'), category: 'model1', value: 64.8 },
];

Suggested matrix series config:

{
  type: 'matrix',
  xKey: 'date',
  yKey: 'category',
  valueKey: 'value',
  thresholds: [50, 58, 66, 74],
  colors: ['#1f3b66', '#245f8f', '#2c8f9f', '#5ac18e', '#d4e77a'],
}

Threshold bins are applied in ascending order using value <= threshold. The color array should usually contain thresholds.length + 1 colors.

Stacked Bar Behavior

For bar series:

  • stacked: true enables stacking with compatible bar series.
  • isCumulative: true sums stacked values on the y-domain.
  • Stacking is computed by matching x positions and compatible bar-series settings.
{
  type: 'bar',
  xKey: 'date',
  yKey: 'precip',
  stacked: true,
  isCumulative: true,
}

Combining Multiple Plot Types

You can render multiple plot types in one chart by adding multiple entries to options.series.

  • Each series entry renders one layer.
  • You can mix line, bar, boxPlot, area, areaStacked, circle, matrix, and heatmap in the same chart.
  • contourGrid is currently validated for mixing with line, area, areaStacked, circle, and windBarbs in v1.
  • Render order follows array order: later series draw on top of earlier series.

Example:

const options = {
  series: [
    {
      type: 'bar',
      xKey: 'date',
      yKey: 'hourlyPrecip.mean',
      fill: '#72E06A88',
      alignment: 'center',
    },
    {
      type: 'line',
      xKey: 'date',
      yKey: 'accumulatedPrecip.mean',
      fill: '#71da6e',
      strokeWidth: 4,
    },
    {
      type: 'circle',
      xKey: 'date',
      yKey: 'windDir.mean',
      stroke: '#147AF3',
      isSecondaryYAxis: true,
    },
  ],
  axes: {
    x: { type: 'time' },
    y: { type: 'linear' },
    y2: { type: 'linear' },
  },
};

Tip: if mixed series use very different units, map one group to y2 using isSecondaryYAxis: true.

Per-Plot Defaults

Use these as references when building options.

Area

{
  xKey: 'date',
  minYKey: 'series1.p10',
  q1YKey: 'series1.p25',
  medianYKey: 'series1.p50',
  q3YKey: 'series1.p75',
  maxYKey: 'series1.p90',
  lowerField: 'p25',
  lowerLabel: '25th',
  medianField: 'p50',
  medianLabel: '50th',
  upperField: 'p75',
  upperLabel: '75th',
  minField: 'p10',
  minLabel: '10th',
  maxField: 'p90',
  maxLabel: '90th',
  className: '',
  fill: `${dataVizColors.tropicalIndigo}88`,
  isVisible: true,
  stroke: 'none',
  strokeWhisker: dataVizColors.tropicalIndigo,
  strokeWidth: 2,
  sx: {},
}

AreaStacked

areaStacked is designed for layered probabilistic bands in a single series.

{
  xKey: 'date',
  bands: [
    {
      lowerKey: 'series1.p05',
      upperKey: 'series1.p95',
      lowerLabel: '5th',
      upperLabel: '95th',
      fill: '#F6851122',
    },
    {
      lowerKey: 'series1.p10',
      upperKey: 'series1.p90',
      lowerLabel: '10th',
      upperLabel: '90th',
      fill: '#F6851133',
    },
    {
      lowerKey: 'series1.p25',
      upperKey: 'series1.p75',
      lowerLabel: '25th',
      upperLabel: '75th',
      fill: '#F6851155',
    },
  ],
  medianKey: 'series1.p50',
  medianField: 'p50',
  medianLabel: '50th',
  medianStroke: dataVizColors.palatinateBlue,
  medianStrokeWidth: 2,
  medianIsVisible: true,
  className: '',
  isVisible: true,
  fill: `${dataVizColors.tropicalIndigo}33`, // can also be per-band array
  stroke: 'none', // can also be per-band array
  strokeWidth: 1,
  sx: {},
}

Recommended band order is outer-to-inner (for example 5-95, 10-90, 25-75) so the smallest interval renders on top.

Bar

{
  alignment: 'center',
  cornerRadius: 2,
  className: '',
  fill: dataVizColors.tropicalIndigo,
  isVisible: true,
  paddingFactor: 0.8, // 0-1
  stacked: false,
  isCumulative: false,
  stroke: 'none',
  strokeWidth: 2,
  sx: {},
}

BoxPlot

{
  xKey: 'date',
  minYKey: 'series1.p10',
  q1YKey: 'series1.p25',
  medianYKey: 'series1.p50',
  q3YKey: 'series1.p75',
  maxYKey: 'series1.p90',
  alignment: 'center',
  cornerRadius: 2,
  className: '',
  fill: dataVizColors.tropicalIndigo,
  isVisible: true,
  paddingFactor: 0.8, // 0-1
  stroke: 'none',
  strokeMedian: '#ffffff88',
  strokeWhisker: dataVizColors.tropicalIndigo,
  strokeWidth: 2,
  sx: {},
}

Circle

{
  className: '',
  fill: dataVizColors.tropicalIndigo,
  isVisible: true,
  stroke: 'none',
  radius: 4,
  sx: {},
}

Matrix

{
  xKey: 'x',
  yKey: 'y',
  valueKey: 'value',
  labelKey: undefined,
  // optional; auto-generated from data range and colors.length when omitted
  thresholds: undefined,
  colors: ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'],
  timeAnchor: 'center', // 'start' | 'center' | 'end'
  cellPadding: 1,
  cellWidthFactor: 1,
  fill: dataVizColors.tropicalIndigo,
  stroke: '#20202055',
  strokeWidth: 0,
  showLabels: false,
  labelFormatter: null,
  labelColor: '#f2f2f2',
  labelFontSize: 10,
  labelFontWeight: 600,
  missingCellMode: 'sparse',
  isVisible: true,
  sx: {},
}

Matrix placement is inferred from the resolved x-scale:

  • axes.x.type: 'band' => band cells
  • non-band x scales (time, linear) => continuous placement

Matrix option details:

| Property | Type | Default | Description | | ----------------- | ------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | xKey | string \| (row) => any | 'x' | Accessor for x position. Supports dot notation (for example forecast.validTime). | | yKey | string \| (row) => any | 'y' | Accessor for matrix row/category. Use categorical values when axes.y.type is band. | | valueKey | string \| (row) => number | 'value' | Numeric value used for threshold binning and color selection. | | labelKey | string \| (row) => any | undefined | Optional accessor for label text. Falls back to valueKey when omitted. | | thresholds | number[] \| undefined | undefined | Optional threshold breakpoints. If omitted, thresholds are auto-generated from the matrix value extent using colors.length - 1 evenly spaced breaks. | | colors | string[] | ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'] | Fill colors for threshold bins. Recommended length is thresholds.length + 1. | | fill | string | dataVizColors.tropicalIndigo | Fallback fill color when value is non-numeric or colors/thresholds are not usable. | | timeAnchor | 'start' \| 'center' \| 'end' | 'center' | Anchor for non-band x scales. start: tick at left edge of cell. center: tick at center. end: tick at right edge. Aliases left/right are also accepted. | | cellPadding | number | 1 | Inner pixel padding on each side of each cell. Larger values create visible gaps between cells. | | cellWidthFactor | number | 1 | Width multiplier for non-band x cells. Effective range is clamped to 0.05..1. | | stroke | string | '#20202055' | Cell border color. | | strokeWidth | number | 0 | Cell border width in pixels. | | showLabels | boolean | false | Whether to render text labels centered in each cell. | | labelFormatter | (labelValue, row) => string | null | Optional formatter for label text. Ignored when showLabels is false. | | labelColor | string | '#f2f2f2' | Label text color. | | labelFontSize | number | 10 | Label text size in pixels. | | labelFontWeight | number \| string | 600 | Label text weight. | | className | string | '' | Class applied to the matrix <g> container. | | sx | object | {} | Inline style object applied to the matrix <g> container. | | isVisible | boolean | true | Toggles matrix visibility while preserving layout/scales. | | missingCellMode | string | 'sparse' | Reserved for future behavior. Current implementation renders only cells present in data. |

Additional notes:

  • Matrix currently requires a band y-axis (axes.y.type: 'band') for uniform row heights.
  • For non-band x scales, cell widths are based on local spacing between neighboring x-values and then adjusted by timeAnchor and cellWidthFactor.
  • Matrix x-axis ticks for time and linear scales use matrix data x-values by default unless axes.x.ticks.values is explicitly provided.
  • When thresholds is omitted, matrix computes evenly spaced thresholds across the data value range based on colors.length - 1.

Heatmap

Heatmap renders continuous contour bands plus optional contour lines from scattered x/y/value points.

{
  xKey: 'x',
  yKey: 'y',
  valueKey: 'value',
  // optional; auto-generated from data range and colors.length when omitted
  thresholds: undefined,
  colors: ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'],
  fill: '#d6e6f2',
  resolution: 64,
  interpolationMethod: 'idw',
  idwPower: 2,
  idwNeighbors: 16,
  showContourFill: true,
  fillOpacity: 0.85,
  showContourLines: true,
  contourLineColor: null,
  contourLineWidth: 1,
  contourLineOpacity: 0.85,
  isVisible: true,
  sx: {},
}

Heatmap option details:

| Property | Type | Default | Description | | --------------------- | --------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | xKey | string \| (row) => any | 'x' | Accessor for x coordinates. Supports dot notation. | | yKey | string \| (row) => any | 'y' | Accessor for y coordinates. Supports dot notation. | | valueKey | string \| (row) => number | 'value' | Numeric value used for interpolation and contour thresholds. | | thresholds | number[] \| undefined | undefined | Optional contour levels. If omitted, thresholds are auto-generated from the value extent using colors.length - 1 evenly spaced breaks. | | colors | string[] | ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'] | Color bins for threshold bands. Recommended length is thresholds.length + 1. | | fill | string | '#d6e6f2' | Fallback base fill color when bins/colors are insufficient. | | resolution | number | 16 | Interpolation grid resolution. Higher values produce smoother contours with higher cost. | | interpolationMethod | 'idw' | 'idw' | Scattered-point interpolation method used before contour extraction. | | idwPower | number | 2 | IDW distance exponent. Larger values emphasize nearby points. | | idwNeighbors | number | 8 | Number of nearest points sampled for each interpolated grid node. | | showContourFill | boolean | true | Render filled contour bands. | | fillOpacity | number | 0.85 | Opacity applied to filled contour bands. | | showContourLines | boolean | true | Render contour line overlays on top of fills. | | contourLineColor | string \| null | null | Line color override. When null, each line uses its threshold-bin color. | | contourLineWidth | number | 1 | Contour line width in pixels. | | contourLineOpacity | number | 0.85 | Contour line opacity. | | className | string | '' | Class applied to the heatmap container <g>. | | sx | object | {} | Inline style object applied to the heatmap container <g>. | | isVisible | boolean | true | Toggles heatmap visibility while preserving layout/scales. |

Heatmap notes:

  • Heatmap requires continuous x/y scales. Use axes.x.type of linear or time, and axes.y.type of linear (log may work if your data domain is strictly positive).
  • Threshold color semantics match matrix: bins are interpreted in ascending order with value <= threshold for boundary inclusion.
  • When thresholds is omitted, heatmap computes evenly spaced thresholds across the data value range based on colors.length - 1.
  • For large datasets, start with moderate resolution values (for example 16) and increase only when you need smoother contours.

ContourGrid

ContourGrid renders contour fills/lines from structured gridded x/y/value data. Unlike heatmap, it does not run scattered-point IDW interpolation and is intended for fast, spatially consistent contouring on regular grids.

{
  xKey: 'x',
  yKey: 'y',
  valueKey: 'value',
  thresholds: undefined,
  colors: ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'],
  fill: '#d6e6f2',
  showContourFill: true,
  fillOpacity: 0.85,
  showContourLines: true,
  contourLineColor: null,
  contourLineWidth: 1,
  contourLineOpacity: 0.85,
  readoutSamplingMode: 'interpolate', // 'interpolate' | 'nearest'
  isVisible: true,
  sx: {},
}

ContourGrid option details:

| Property | Type | Default | Description | | --------------------- | ---------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | xKey | string \| (row) => any | 'x' | Accessor for x coordinates. Supports dot notation. | | yKey | string \| (row) => any | 'y' | Accessor for y coordinates. Supports dot notation. | | valueKey | string \| (row) => number | 'value' | Numeric field used for contour thresholds and readout values. | | thresholds | number[] \| undefined | undefined | Optional contour levels. If omitted, thresholds are auto-generated from the value extent using colors.length - 1 evenly spaced breaks. | | colors | string[] | ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c'] | Color bins for threshold bands. Recommended length is thresholds.length + 1. | | fill | string | '#d6e6f2' | Fallback base fill color when bins/colors are insufficient. | | showContourFill | boolean | true | Render filled contour bands. | | fillOpacity | number | 0.85 | Opacity applied to filled contour bands. | | showContourLines | boolean | true | Render contour line overlays on top of fills. | | contourLineColor | string \| null | null | Line color override. When null, each line uses its threshold-bin color. | | contourLineWidth | number | 1 | Contour line width in pixels. | | contourLineOpacity | number | 0.85 | Contour line opacity. | | readoutSamplingMode | 'interpolate' \| 'nearest' | 'interpolate' | Hover sampling mode for readout value lookup at pointer x/y. | | className | string | '' | Class applied to the contourGrid container <g>. | | sx | object | {} | Inline style object applied to the contourGrid container <g>. | | isVisible | boolean | true | Toggles contourGrid visibility while preserving layout/scales. |

ContourGrid notes:

  • ContourGrid assumes structured gridded data and currently supports continuous axes only (linear, log, time).
  • Band scales are not supported for contourGrid.
  • In mixed charts, contourGrid is currently validated for line, area, circle, and windBarbs overlays in v1.

WindBarbs

WindBarbs renders meteorological wind barbs at each data point. It works both as a standalone scatter-style overlay (like circle) and as a gridded overlay paired with contourGrid.

Each datum requires an x position, a y position, a speed value, and a direction value. Speed maps to the barb shape in 5-unit buckets (unit-agnostic — the chart does not interpret or convert values; provide series.units for readout display).

Barb shapes are drawn as inline SVG paths. The color option controls the stroke and fill of all barb elements, so any valid CSS color value produces correctly colored barbs.

{
  xKey: 'x',
  yKey: 'y',
  speedKey: 'speed',
  directionKey: 'direction',
  color: '#404040',
  size: 20,         // staff length in pixels
  strokeWidth: 1.5,
  isVisible: true,
  className: '',
  sx: {},
}

WindBarbs option details:

| Property | Type | Default | Description