d3 component · reactive widget
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
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
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.
—
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
The axis and the control are the same object, so they can never drift out of alignment.
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.
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.
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
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.
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.
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
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.