@john-guerra/d3-zoomable-axis

d3 component · reactive widget

The axis is the zoom control.

Combine a d3 axis with a dual-handle range slider in one element. Drag the axis on the chart's edge — it zooms into that window while the scale above re-labels live, the selection emitted in data space.

$ npm i @john-guerra/d3-zoomable-axis

Scent + dynamic query

See where the data is — then filter it

The axis draws the distribution along itself (a scented widget). Drag to keep only the values in range — the rest fade out. This is filtering, not zooming: the dots never move.


Live · published npm package

Zoom a penguins scatterplot

Drag a handle, tab to it and use ← →, or drag the shaded band to pan. The chart rescales live to the flipper-length (x) and body-mass (y) window.

Loading the live widget from the CDN…

This runs the real @john-guerra/d3-zoomable-axis@0.0.4 from esm.sh. See all four examples → (zoom, dynamic-query filter, brush-synced, no-scent).


What you get

One element, three good ideas

The axis and the control are the same object, so they can never drift out of alignment.

Zoom by dragging

Share your chart's scale and the axis stays glued to the plot automatically — the selection comes back in data space, no inverting pixels yourself.

See before you zoom

An optional scented distribution — histogram, violin, or area — drawn right on the axis, so you can spot where the data is dense before you pick a window.

Accessible handles

Every handle is a real <input type=range>: keyboard-operable, screen-reader announced, a standard HTML form input. Accessibility isn't bolted on.


Two entry points

Import only what you need

A dependency-light core in the pure d3 idiom, and an accessible reactive widget that wraps it. Each snippet below renders its actual output — drag it.

Core d3 idiom · selection.call

npm i @john-guerra/d3-zoomable-axis

import * as d3 from "d3";
import { zoomableAxisBottom } from "@john-guerra/d3-zoomable-axis";

const x = d3.scaleLinear().domain([24, 92]).range([0, 300]);

const slider = zoomableAxisBottom(x)
  .step(1).ticks(8)
  .value([34, 64])
  .on("input", (v) => render(v));

d3.select("svg g").call(slider);

Rendered — this is the running component

Core is d3-brush-based — its resize handles are invisible hit-areas by default; style .selection / .handle to draw them. The widget ships visible, accessible handles.

Widget DOM element · .value + input

npm i @john-guerra/d3-zoomable-axis reactive-widget-helper

import { zoomableAxisInput } from "@john-guerra/d3-zoomable-axis/input";

const el = zoomableAxisInput([24, 92], {
  orient: "bottom", step: 1, length: 300,
  value: [30, 60], label: "Weeks", units: "wk",
});
el.addEventListener("input", () => render(el.value));
// In Observable, wrap it in view() for a reactive cell.

Rendered — this is the running component


Interop

Plays nice with reactive widgets

The widget follows the reactivewidgets.org / Observable view() convention: it exposes a .value of [lo, hi] and dispatches an input event while you drag. Setting .value programmatically re-renders it silently — no event — the controlled-state pattern that lets several views share one selection without a feedback loop.

Two axes and a 2D box brush, all bound to one [xRange, yRange] — drag any one, the others follow. No loop.