import { MultiAxisLineChart } from "semiotic"
A dual Y-axis line chart for comparing two series with different scales on the same X axis. Data is unitized (normalized to [0,1]) internally so both lines share a common visual scale, while the left axis shows series[0] values and the right axis shows series[1] values in their original units.
Quick Start
import { MultiAxisLineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { time: 0, temperature: 58, humidity: 0.82 }, { time: 6, temperature: 56, humidity: 0.80 }, { time: 12, temperature: 85, humidity: 0.40 }, { time: 18, temperature: 72, humidity: 0.60 }, { time: 23, temperature: 57, humidity: 0.84 }, // ... 24 hourly readings ], /* --- Process --- */ xAccessor: "time", /* --- Customize --- */ title: "Weather Station — 24h", xLabel: "Hour of Day", showLegend: true, legendPosition: "bottom", /* --- Interact --- */ tooltip: true, /* --- Other --- */ series: [ { yAccessor: "temperature", label: "Temperature (°F)" }, { yAccessor: "humidity", label: "Humidity", format: d => (d * 100).toFixed(0) + "%" }, ] } export default () => { return <MultiAxisLineChart {...frameProps} /> }
Examples
Server Metrics — RPS vs Latency
Requests per second and latency have vastly different scales (thousands vs milliseconds). The dual-axis layout lets you see the correlation without one series drowning the other.
import { MultiAxisLineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { minute: 0, rps: 1200, latency: 45 }, { minute: 15, rps: 2800, latency: 85 }, { minute: 25, rps: 3500, latency: 210 }, { minute: 40, rps: 1800, latency: 55 }, // ... 12 readings ], /* --- Process --- */ xAccessor: "minute", /* --- Customize --- */ title: "API Metrics During Load Test", showGrid: true, xLabel: "Minutes", legendPosition: "bottom", /* --- Interact --- */ tooltip: true, /* --- Other --- */ series: [ { yAccessor: "rps", label: "Requests/sec", color: "#4e79a7" }, { yAccessor: "latency", label: "Latency (ms)", color: "#e15759" }, ] } export default () => { return <MultiAxisLineChart {...frameProps} /> }
Custom Extent
When you know the expected range of each series, provideextent to lock the axis scale. This is especially important for the push API, where the unitization range must be stable before data arrives.
JSX
<MultiAxisLineChart data={data} xAccessor="time" series={[ { yAccessor: "temperature", label: "Temp", extent: [0, 120] }, { yAccessor: "humidity", label: "Humidity", extent: [0, 1], format: d => (d * 100).toFixed(0) + "%" }, ]} />
Fallback: More or Fewer Than 2 Series
If series doesn't contain exactly 2 entries, the chart renders as a standard multi-line chart (single Y axis) and logs a warning in the console. This prevents broken layouts while still showing something useful.
JSX
// This renders as a normal line chart with a console warning <MultiAxisLineChart data={data} xAccessor="time" series={[ { yAccessor: "a", label: "A" }, { yAccessor: "b", label: "B" }, { yAccessor: "c", label: "C" }, ]} />
Props
| Prop | Type | Required | Default | Description |
|---|
data | array | — | — | Array of data points shared by both series. Omit when using push API. |
xAccessor | string | function | — | "x" | Field name or function to access x values. |
series | MultiAxisSeriesConfig[] | Yes | — | Array of series configurations. Exactly 2 for dual-axis mode. Each: { yAccessor, label?, color?, format?, extent? }. |
colorScheme | string | array | — | "category10" | Color scheme name or custom colors array. First two colors are used for the two series. |
curve | string | — | "monotoneX" | Curve interpolation: "linear", "monotoneX", "step", etc. |
lineWidth | number | — | 2 | Line stroke width. |
enableHover | boolean | — | true | Enable hover tooltips. |
showGrid | boolean | — | false | Show background grid lines. |
showLegend | boolean | — | true | Show a legend. |
tooltip | boolean | object | function | — | — | Enable/disable default tooltip (boolean), or provide a config object or render function. |
width | number | — | 600 | Chart width in pixels. |
height | number | — | 400 | Chart height in pixels. |
margin | object | — | auto (70px left/right) | Margin around the chart area. Auto-expands left/right to 70px for dual-axis labels. |
xLabel | string | — | — | Label for the x-axis. |
title | string | — | — | Chart title. |
frameProps | object | — | — | Additional StreamXYFrame props for advanced customization. |
MultiAxisSeriesConfig
Each entry in the series array accepts:
| Prop | Type | Required | Default | Description |
|---|
yAccessor | string | function | Yes | — | Field name or function to access y values for this series. |
label | string | — | — | Axis label for this series, displayed on the left (series 0) or right (series 1) axis. |
color | string | — | theme palette | Override color for this series line. |
format | function | — | — | Tick format function for this series' axis, e.g. d => d.toFixed(1). |
extent | [number, number] | — | computed from data | Fixed [min, max] extent. Required for push API streaming so unitization is stable. |
Graduating to the Frame
Under the hood, MultiAxisLineChart unitizes both series into a shared [0,1] y-range and uses lineBy grouping. You can achieve the same result on StreamXYFrame with manual unitization and a right-axis config via the axes prop:
JSX
// HOC (simple) <MultiAxisLineChart data={data} xAccessor="time" series={[ { yAccessor: "temperature", label: "Temp" }, { yAccessor: "humidity", label: "Humidity" }, ]} /> // Frame (full control — you handle unitization) const unitized = data.flatMap(d => [ { time: d.time, __y: (d.temperature - tMin) / (tMax - tMin), __series: "temperature" }, { time: d.time, __y: (d.humidity - hMin) / (hMax - hMin), __series: "humidity" }, ]) <StreamXYFrame chartType="line" data={unitized} xAccessor="time" yAccessor="__y" groupAccessor="__series" yExtent={[0, 1]} axes={[ { orient: "left", tickFormat: v => invertTemp(v).toFixed(0) }, { orient: "right", tickFormat: v => invertHumidity(v).toFixed(1) }, ]} yLabel="Temperature" yLabelRight="Humidity" size={[600, 400]} margin={{ left: 70, right: 70 }} />