Sometimes you need to plot two things on the same timeline that simply do not belong to the same scale. Revenue and headcount. Inflation and GDP. Price and spread. If you force them onto one axis, one series becomes unreadable, and the chart turns into a polite lie.
A dual-axis chart solves this by keeping a shared X axis (time) while providing two independent Y axes, so each metric is shown in its natural units. The goal is not to pretend the metrics are comparable. The goal is to compare when they move and how their dynamics relate.
In RareCharts, DualAxes is exactly that: a time-based chart with two vertical scales.
To illustrate a real-world use case, this example uses a finance-native story: a convergence (arbitrage) strategy based on the idea that two similar instruments that temporarily diverged in price will later converge again.
One concrete version of this story exists in government bonds, where a newly issued “on-the-run” Treasury can trade slightly richer than the previous “off-the-run” issue for a period of time. Traders may position for convergence by going long the cheaper issue and hedging the richer one, expecting the price gap to compress as the market normalizes.
In the chart, the right axis (Y1) shows the price index of the two instruments over time. The left axis (Y2) shows the spread between them. Prices and spread are different categories, so they get different axes. The chart stays readable, and the relationship stays visible.
Axis behavior and formatting
Both axes are configured independently. You can set titles (y1Title, y2Title), control tick formatting (y1TickFormat, y2TickFormat), and, when needed, override the visible ranges (y1Domain, y2Domain) to keep the chart stable and comparable across screenshots, reports, or multiple panels.
Axis titles are meant to be terse units (PRICE, N · K HLX). A title that would run past its axis margin is trimmed with an ellipsis — measured by rendered width, so a short label that fits is left alone — with the full text on hover and a one-time console hint. Set axisTitleMaxLength to cap it explicitly, or widen margin if you need more room.
In this example, the spread axis uses a signed format and treats very small values as clean zero, so you do not get the infamous +0.00 noise that makes charts look broken even when the data is fine.
Interaction
Dual Axes supports cursor inspection through a crosshair and a tooltip. The tooltip is fully customizable through tooltipFormat ({ date, points }), so you can present values in your product’s language instead of whatever generic tooltip someone thought was “good enough”.
Built-in timeframe controls
Because DualAxes is inherently time-based, it can also render the shared timeframe switcher in its own chart header:
const chart = new RareCharts.DualAxes('#chart', {
timeframes: true,
defaultTimeframe: '1Y',
});
chart.setData(series);
Available pieces:
timeframesrenders the built-in range buttons in the chart header.defaultTimeframeselects the initial visible range and active button.navigator: truerenders the built-in overview strip with a brush under the main chart.setView([start, end])sets any custom visible date window.getView()returns the currently visible[start, end].onViewChange(fn)lets external UI or a linked navigator stay synchronized.
Example:
const chart = new RareCharts.DualAxes('#chart', {
timeframes: true,
defaultTimeframe: '1Y',
navigator: true,
});
Dual-axis charts are often implemented as a hack: two scales, mismatched formatting, confusing labels, and tooltips that quietly mix units. This component exists to make the dual-axis case predictable, explicit, and safe for real reporting: independent scales, consistent structure, and controlled formatting.
Annotations
DualAxes supports the full annotation API — vertical date markers (point and range) and horizontal reference levels (line and band) on either Y axis. Horizontal entries take an explicit axis: 'y1' | 'y2' so the value is mapped to the correct scale. See the dedicated annotations page for the full API.
Dual Axes chart options
Common options shared by all chart types (title, subtitle, legend, legendPosition, source, theme) are documented on the Settings page.
| Option | Type | Default | Description |
|---|---|---|---|
Layout |
|||
height |
number | 280 |
Chart height in px. |
margin |
object | — |
Inner padding {top, right, bottom, left}.Right defaults to 92, left to 64 to give both axes room.
|
xPad |
number | 8 |
Extra horizontal padding on both ends of the X scale. |
timeframes |
boolean | array | — | Built-in timeframe buttons rendered in the chart header. Pass true to use the default set ['1M', '3M', '6M', '1Y', '2Y', 'ALL'], or pass a custom array. |
defaultTimeframe |
string | object | — | Initial active timeframe used on first render when no explicit view has been set. |
defaultView |
array | — | Initial visible date range as [from, to]. An explicit alternative to defaultTimeframe; clamped to the data extent. |
navigator |
boolean | object | — | Built-in overview strip with brush selection under the chart. Pass true for defaults or an object with overview options. |
Right axis — Y1Y1 is drawn on the right side. |
|||
y1Domain |
[min, max] |
auto |
Override the Y1 scale domain. Useful for keeping charts comparable across snapshots. |
y1Ticks |
number | 4 |
Tick count on Y1. |
y1TickFormat |
function | ,.2f |
(value) => string — Y1 tick labels. |
y1LabelsOnly |
boolean | true |
Show only tick labels; suppress the axis line. |
y1Title |
string | — | Axis title, rendered along the right edge. |
Left axis — Y2Y2 is drawn on the left side. Grid lines and the zero baseline are keyed to Y2. |
|||
y2Domain |
[min, max] |
auto | Override the Y2 scale domain. |
y2Ticks |
number | 4 |
Tick count on Y2. |
y2TickFormat |
function | +.2f |
(value) => string — Y2 tick labels. |
y2LabelsOnly |
boolean | true |
Show only tick labels; suppress the axis line. |
y2Title |
string | — | Axis title, rendered along the left edge. |
X axis |
|||
xTickFormat |
function | '%m/%d' |
(date) => string — X tick labels. |
Lines |
|||
curve |
string | 'linear' |
D3 curve type for line series: 'linear', 'monotone', 'step', etc. |
curveTension |
number | 0 |
Tension for the 'cardinal' curve, 0–1. |
strokeDash |
string | — | SVG stroke-dasharray applied to all line series globally. |
area |
boolean | false |
Fill area under line series. |
areaOpacity |
number | 0.12 |
Area fill opacity. |
areaBaseline |
'zero' | 'min' | number |
'zero' |
Area baseline anchor. |
Bars |
|||
barOpacity |
number | 0.35 |
Bar fill opacity. |
barWidthRatio |
number | 0.65 |
Bar width as a fraction of the time step width. |
barGrouping |
'overlap' | 'cluster' |
'overlap' |
How multiple bar series are arranged. 'cluster' places them side by side. |
Visibility |
|||
showGrid |
boolean | true |
Show horizontal grid lines. |
showXAxis |
boolean | true |
Show the X (date) axis at the bottom. Hiding it also collapses the bottom margin, so the plot runs flush. |
showY1Axis |
boolean | true |
Show the Y1 axis on the right. The right margin collapses once endLabels is off and no y1Title is set — they all share that gutter. An explicit margin always wins. |
showY2Axis |
boolean | true |
Show the Y2 axis on the left. The left margin collapses unless a y2Title is set. |
End labels and markers |
|||
endLabels |
boolean | true |
Show last-value labels at the right edge for line series. |
endLabelsAxis |
'y1' | 'y2' |
'y1' |
Which axis to use when formatting end labels. |
markers |
boolean | false |
Point markers at each data sample on line series. |
markerShape |
string | 'circle' |
Marker shape: 'circle', 'square', 'diamond'. |
markerSize |
number | 4 |
Marker radius in px. |
Interaction |
|||
crosshair |
boolean | true |
Vertical tracker with dots at data points and a tooltip on hover. |
tooltipFormat |
function | — |
({ date, points }) => html — where
points is [{name, value, color, fmt}].
|
Annotations |
|||
annotations |
array | — | Event markers and reference levels. Horizontal entries accept axis: 'y1' | 'y2' to choose the scale. See Annotations. |
annotationLabelHeight |
number | 22 |
Pixels reserved above the chart for vertical annotation labels. |
Animation |
|||
animate |
boolean | true |
Animate on first render. |
duration |
number | 650 |
Animation duration in ms. |
ease |
string | 'cubicOut' |
Easing: 'cubicOut', 'cubicInOut', 'linear'. |
Per-series fieldsEach object in the series array passed to |
|||
name |
string | 'Series N' |
Series name — used in legend and tooltip. |
axis |
'y1' | 'y2' |
'y1' |
Which Y axis to plot against. |
type |
'line' | 'bar' |
'line' |
Rendering type for this series. |
color |
CSS color | theme palette | Series color. |
strokeWidth |
number | 2 |
Line thickness in px (lines only). |
strokeDash |
string | — | SVG dash pattern for this series only. |
curve |
string | global curve |
Curve override for this series. |
area |
boolean | global area |
Fill area under this series. |
areaOpacity |
number | global areaOpacity |
Per-series area opacity. |
areaBaseline |
string | number | global areaBaseline |
Per-series area baseline. |
values |
array | — | [{date, value}, ...] — the data points. |