@visx/scale
@visx/scale is a typed configuration layer over D3 scale functions. It maps data domains to visual ranges such as pixels, radii, colors, or categories and exposes factories for linear, band, point, time, UTC, log, power, square-root, symmetric-log, radial, ordinal, quantile, quantize, and threshold scales. It has no React peer and does not render anything; React charts call the returned functions while drawing axes, shapes, labels, and interactions with other visx packages or their own components.
Use @visx/scale when visx is already your chart foundation or its typed config objects simplify a shared visualization layer. Use `d3-scale` directly when you only need scale math, and choose a higher-level chart library when axes, marks, legends, interaction, and accessibility should arrive together.
Use it if
- You build custom visx charts and want scale factories whose object configs fit the rest of the visx API
- You need TypeScript inference across continuous, discrete, temporal, radial, and threshold scale types
- You want D3 scale behavior without importing from D3 directly throughout a visx-based design system
- You need helpers that create scales from a runtime `type`, copy and update an existing scale, infer its type, or get ticks generically
- You already use `d3-scale` directly: this package wraps vendored D3 scales and adds visx config operators, so a second abstraction may buy little
- You expect chart components: it produces mapping functions only and includes no axes, marks, legends, tooltips, responsive measurement, accessibility layer, or data fetching
- You want a ready-made chart theme and defaults: domains, ranges, zero baselines, clamping, unknown values, color schemes, margins, and tick formatting remain application decisions
- You need animation built in: the visx README explicitly leaves transitions to the React animation library chosen by the application
- You cannot upgrade a mixed visx stack together: the v4 migration guide says all `@visx/*` packages should move to the same major because entry points and internal versions changed together
Setup reality
Install with `npm install @visx/scale`. The package has one runtime dependency, `@visx/vendor` 4.0.0, and no React, React DOM, native, credential, or config requirement. It ships CommonJS in `lib`, ESM in `esm`, TypeScript declarations, and marks itself side-effect-free. Import from the package root, not internal `lib` paths; visx v4 formalized package entry points and its migration guide tells users to remove deep imports. Although the wider visx v4 React packages require React 18 or 19, this root scale package is calculation-only and declares no peer dependency. Its returned values are D3-style callable objects with methods such as `domain()`, `range()`, `ticks()`, `invert()`, `bandwidth()`, or `copy()` depending on scale type. These scales are mutable: calling a method changes that instance. The visx `updateScale()` helper avoids that by copying before applying a new config. Continuous scales extrapolate outside the domain unless `clamp: true`; `nice: true` expands domain endpoints; `round: true` changes output precision; and a reversed SVG y-axis normally needs a range like `[height, 0]`. A band lookup can return `undefined` for an unknown category, so do not pass its result straight to SVG arithmetic without a check. Ordinal scales need an explicit `unknown` policy if accidental new domain values must not be learned. Log domains must be entirely positive or entirely negative and cannot touch or cross zero; use `scaleSymlog` when zero or signs must coexist. `scaleTime` uses local calendar time while `scaleUtc` avoids local-zone tick differences. Quantize divides a continuous interval evenly, quantile divides observed samples by distribution, and threshold uses caller-supplied boundaries; choosing the wrong one silently produces a misleading legend. `nice`, `zero`, `reverse`, padding, and interpolation are applied in a defined operator order, so inspect the final `domain()` and `range()` in tests. Scales do not memoize themselves for React; recreate them with `useMemo` when referential identity matters to children or effects.
Patterns
Map a numeric domain to pixelsmap-linear-values
import { scaleLinear } from '@visx/scale';
const yScale = scaleLinear({
domain: [0, Math.max(...values)],
range: [chartHeight, 0],
nice: true,
clamp: true,
});
const y = yScale(value);SVG y coordinates increase downward, so the range is reversed. `nice` may expand the domain beyond the supplied maximum.
Position categorical barslayout-band-chart
import { scaleBand } from '@visx/scale';
const xScale = scaleBand({
domain: data.map((row) => row.category),
range: [0, innerWidth],
padding: 0.2,
round: true,
});
const x = xScale(row.category);
const width = xScale.bandwidth();A value outside the domain maps to `undefined`. Guard it before assigning an SVG coordinate.
Center categories without bandwidthposition-categorical-points
import { scalePoint } from '@visx/scale';
const xScale = scalePoint({
domain: ['Q1', 'Q2', 'Q3', 'Q4'],
range: [0, innerWidth],
padding: 0.5,
});Point scales have no band width; use them for dots and line vertices, not bars that need a rectangle width.
Create a local-time axis scalescale-local-time
import { scaleTime } from '@visx/scale';
const xScale = scaleTime({
domain: [new Date(2026, 0, 1), new Date(2026, 11, 31)],
range: [0, innerWidth],
nice: true,
});
const ticks = xScale.ticks(6);Local time can produce daylight-saving and locale-dependent boundaries. Use `scaleUtc` for consistent server and client ticks.
Keep temporal ticks in UTCscale-utc-time
import { scaleUtc } from '@visx/scale';
const xScale = scaleUtc({
domain: [new Date('2026-01-01T00:00:00Z'), new Date('2026-02-01T00:00:00Z')],
range: [0, innerWidth],
});UTC scaling avoids local daylight-saving boundaries, but tick labels still need an explicit UTC formatter.
Use a logarithmic scalescale-orders-of-magnitude
import { scaleLog } from '@visx/scale';
const sizeScale = scaleLog({
domain: [1, 1_000_000],
range: [2, 40],
base: 10,
clamp: true,
});A log domain must be strictly positive or strictly negative. It cannot include zero or cross between signs.
Use symmetric log around zeroscale-signed-values
import { scaleSymlog } from '@visx/scale';
const xScale = scaleSymlog({
domain: [-10_000, 10_000],
range: [-200, 200],
constant: 10,
});Symlog accepts zero and both signs. The `constant` controls the size of the near-zero linear region.
Map categories to fixed colorsassign-category-colors
import { scaleOrdinal } from '@visx/scale';
const colorScale = scaleOrdinal({
domain: ['success', 'warning', 'error'],
range: ['#16803c', '#a15c00', '#b42318'],
unknown: '#777777',
});Set `unknown` when unexpected categories must not extend the ordinal domain implicitly or recycle a palette color.
Quantize a numeric intervalbin-even-intervals
import { scaleQuantize } from '@visx/scale';
const severity = scaleQuantize({
domain: [0, 100],
range: ['low', 'medium', 'high'],
});
console.log(severity(72)); // highQuantize makes equal-width numeric bins. It does not balance the number of observations in each bucket.
Create quantile buckets from samplesbin-by-distribution
import { scaleQuantile } from '@visx/scale';
const quartile = scaleQuantile({
domain: observations,
range: ['q1', 'q2', 'q3', 'q4'],
});
const boundaries = quartile.quantiles();The domain is the sample distribution, not just `[min, max]`; outliers and repeated values affect the calculated boundaries.
Map caller-defined breakpointsapply-explicit-thresholds
import { scaleThreshold } from '@visx/scale';
const risk = scaleThreshold({
domain: [20, 50, 80],
range: ['minimal', 'low', 'high', 'critical'],
});A threshold scale needs one more range value than domain boundary. Document which side of each boundary owns equality.
Create a typed scale from configurationcreate-runtime-scale
import { createScale, getTicks, inferScaleType, updateScale } from '@visx/scale';
const scale = createScale({
type: 'linear',
domain: [0, 10],
range: [0, 400],
});
const resized = updateScale(scale, { range: [0, 800] });
console.log(inferScaleType(resized), getTicks(resized, 5));`updateScale` copies the scale before applying config, so `scale` retains its original range. Omitting `type` from `createScale` defaults to linear.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| d3-scale | npm | You want the upstream scale API directly, broader D3 documentation, and no visx-specific config wrapper |
| @nivo/scales | npm | You already use Nivo and want scales aligned with its higher-level chart components and configuration |
| chroma-js | npm | Your main problem is color interpolation, palettes, contrast, and color-space conversion rather than positional scales |