> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Scales

A **scale** turns data values into visual values — a number into a pixel position, a category into a color, a magnitude into a point size. Every [aesthetic](/sdk-next/concepts/mappings) you map needs a scale to interpret it.

```tsx theme={null}
scale.x(); // infer an x-axis scale from the data
scale.y.continuous({ domainMin: 0 }); // an explicit continuous y-axis, pinned to zero
scale.color.discrete({ range: ['#4C6EF5', '#F76707'] });
```

## Position scales must be declared

**Position scales are never created automatically.** If you map `x` or `y`, you must add `scale.x()` / `scale.y()`. Miss one and that axis has no scale, producing `NaN` positions and a blank chart.

```tsx theme={null}
pipe(
  createSpec({ x: 'month', y: 'revenue' }),
  geom.line(),
  scale.x(), // required
  scale.y() // required
);
```

Visual scales (`color`, `size`, …) are more forgiving — but declaring them keeps a chart predictable.

Two scales appear on their own when the spec implies them:

* **A colour scale.** With no `color` scale in the spec, the engine appends `scale.color.palette()`, so mapped series still pick up the brand palette. Under a geom whose value rides on colour (`geom.tile()`), colour is a measure rather than a way of telling series apart, so the appended scale is inferred from the mapped column instead — a numeric one lands on a continuous ramp.
* **A secondary y scale.** As soon as any layer sets `yScaleType: 'secondary'`, an inferred `ySecondary` scale is appended if you didn't declare one.

Declaring two scales for the same aesthetic isn't an error — the last one wins.

## Inference vs explicit types

Position scales are **callable** to infer their type from the mapped column's [value format](/sdk-next/data-structure#value-format-detection). On `x`, `y` and `ySecondary`:

* text → **discrete** (band) scale
* number / percentage / currency → **continuous** scale
* date → **`datetime`** scale

```tsx theme={null}
scale.x(); // inferred
scale.x({ nice: true }); // inferred, with options
```

<Note>
  A geom's demands beat the column's format. Under a bar layer, `scale.x()`
  resolves to a band and `scale.y()` to a continuous scale; under a
  [tile](/sdk-next/graph-types/heatmap) layer, both resolve to bands. A value
  axis that bars or areas stand on holds zero whatever the data spans, named
  explicitly or not.
</Note>

Name the type explicitly to opt out of inference. The scale must still fit the column's type; when it does not, [declare the column's format](/sdk-next/data-structure#declaring-a-format).

```tsx theme={null}
scale.x.continuous();
scale.x.discrete();
scale.x.datetime();
scale.x.log();
scale.x.sqrt();
```

A geom whose band is structural still overrules you: a continuous or datetime scale there is coerced back to a band, with an `UNSUPPORTED_SCALE_TYPE` warning.

A bar's band requirement applies to **the whole plot's shared x scale**, including sibling line layers. With numeric x values, `geom.line()` plus `geom.bar()` and `scale.x.continuous()` resolves to a discrete band scale and emits `UNSUPPORTED_SCALE_TYPE`. The line uses those same bands. A line alone can keep its continuous x scale.

## Scales by aesthetic

| Aesthetic | Methods |
| - | - |
| `x`, `y`, `ySecondary` | callable (inferred), `.continuous()`, `.discrete()`, `.datetime()`, `.log()`, `.sqrt()` |
| `color` | `.continuous()`, `.discrete()`, `.palette()` |
| `size`, `alpha`, `strokeWidth` | `.continuous()`, `.discrete()`, `.identity()` |
| `lineType` | `.discrete()`, `.identity()` |

### Categorical color

```tsx theme={null}
scale.color.palette(); // Graphy's default palette
scale.color.discrete({ domain: ['A', 'B'], range: ['#4C6EF5', '#F76707'] }); // fixed colors
```

`scale.color.palette()` takes two options. `palette` names the palette to draw from:

| `palette` | Notes |
| - | - |
| `{ type: 'default' }` | The default palette, chosen at compile time: the multicolor set, or the brick mono palette when the geoms touch (stacked bars and areas, pies, tiles). This is what you get with no option at all. |
| `{ type: 'graphy' }`, `{ type: 'pastel' }` | Named palettes. Both accept `variant: 'waterfall'` for the positive / negative / total colours. |
| `{ type: 'neon', base }` | A hue-based ramp; `base` picks the hue. Also accepts `variant: 'waterfall'`. |
| `{ type: 'mono', base }` | A single-hue ramp; `variant: 'light' \| 'dark'` tints it for the background. |
| `{ type: 'custom', id }` | A palette registered through `customPalettes` on [`GraphProvider`](/sdk-next/rendering/provider-and-renderer), by `id`. |

`overrides` recolours individual series without replacing the palette. It's keyed by group number, **1-indexed**, and each entry takes a raw `hex` or an `id` to look up in the active custom palette:

```tsx theme={null}
scale.color.palette({
  palette: { type: 'graphy' },
  overrides: { 2: { hex: '#E5484D' } }, // second series only
});
```

### Continuous color

Map a numeric column to `color` and the values run through a ramp instead of a set of hues:

```tsx theme={null}
scale.color.continuous({ scheme: 'viridis' });
scale.color.continuous({ scheme: 'RdBu', domainMid: 0 }); // diverging around zero
scale.color.continuous({ range: ['#FFFFFF', '#0B5FFF'] }); // an explicit ramp
```

<ParamField path="scheme" type="ColorSchemeName" default="null">
  A named colormap, matched case-insensitively. Sequential: `viridis`, `magma`,
  `inferno`, `plasma`, `cividis`, `turbo`, `Blues`, `Greens`, `Greys`,
  `Oranges`, `Purples`, `Reds`. Diverging: `RdBu`, `BrBG`, `PuOr`, `Spectral` —
  each with a friendly alias (`red-blue`, `brown-teal`, `purple-orange`,
  `spectral`).
</ParamField>

<ParamField path="range" type="ReadonlyArray<number | string>" default="null">
  An explicit ramp of two or more stops the value interpolates through. A
  `range` supersedes a `scheme`, and setting both raises a
  `CONFLICTING_COLOR_RAMP` warning.
</ParamField>

<ParamField path="interpolate" type="'rgb' | 'lab' | 'hcl' | 'hsl'" default="lab">
  The space a `range`'s stops are interpolated in. `'lab'` is perceptually
  near-uniform; `'rgb'` matches d3's raw output. Named schemes carry their own
  interpolation, so this doesn't apply to them.
</ParamField>

<ParamField path="domainMid" type="number" default="null">
  Pins the ramp's neutral stop to this data value — usually `0` — rather than
  the data midpoint. A diverging scheme without it raises a
  `DIVERGING_SCHEME_WITHOUT_MIDPOINT` warning.
</ParamField>

<ParamField path="symmetric" type="boolean">
  Symmetrises the domain about `domainMid` so equal magnitudes get equal colour
  intensity. Defaults to `true` once `domainMid` is set, and has no effect
  without it.
</ParamField>

### Identity scales

An identity scale passes data values straight through as visual values — `{ size: 10 }` becomes 10px, no transformation. Useful when your data already holds pixel sizes or CSS colors.

```tsx theme={null}
scale.size.identity();
```

`scale.size.continuous()` is not identity: it defaults to a `sqrt` transform, so a value maps to the marker's **area** rather than its radius. Doubling a value doubles the ink, which is what the eye reads.

## Continuous scale options

The continuous methods (and `.log()` / `.sqrt()`, which are continuous with a transform) accept:

<ParamField path="domainMin" type="number">
  Pin the lower bound of the domain; the maximum still follows the data.
  `domainMin: 0` keeps zero on the axis, from the top when the data is all
  negative.
</ParamField>

<ParamField path="domainMax" type="number">
  Pin the upper bound of the domain; the minimum still follows the data.
</ParamField>

<ParamField path="nice" type="boolean" default="true">
  Extend the domain to round values — `[3, 97]` becomes `[0, 100]`.
</ParamField>

<ParamField path="reverse" type="boolean" default="false">
  Reverse the direction of the scale.
</ParamField>

<ParamField path="transform" type="'linear' | 'log' | 'sqrt'" default="linear">
  A mathematical transform applied to the scale. `scale.y.log()` is shorthand
  for `transform: 'log'`. On `size`, the default is `sqrt`.
</ParamField>

<ParamField path="clamp" type="boolean">
  Pin out-of-domain values to the ends of the range instead of extrapolating
  past them. Defaults to `false` on `x` / `y` / `ySecondary` and `true` on every
  other aesthetic.
</ParamField>

<ParamField path="range" type="[number, number]">
  The output range for the magnitude aesthetics. Ignored on position aesthetics,
  where the range is the panel. Defaults per aesthetic: `size` `[4, 20]`,
  `alpha` `[0.1, 1]`, `strokeWidth` `[1, 4]`. A continuous `color` scale takes a
  colour ramp here instead — see [Continuous color](#continuous-color).
</ParamField>

## Datetime scale options

`scale.x.datetime()` and its siblings take epoch-millisecond bounds:

<ParamField path="domainMin" type="number">
  Lower bound, in milliseconds since the epoch — `Date.parse('2020-01-01')`.
</ParamField>

<ParamField path="domainMax" type="number">
  Upper bound, in milliseconds since the epoch.
</ParamField>

<ParamField path="nice" type="boolean" default="false">
  Extend the domain to round temporal values. Off by default: rounding to whole
  months can stretch or clip a series in ways that read as a bug.
</ParamField>

<ParamField path="reverse" type="boolean" default="false">
  Reverse the direction of the scale.
</ParamField>

<ParamField path="clamp" type="boolean" default="false">
  Pin out-of-domain instants to the ends of the range.
</ParamField>

## Discrete scale options

<ParamField path="domain" type="Array<string | number>">
  The categories, in the order they should appear. Omit to derive them from the
  data.
</ParamField>

<ParamField path="range" type="Array<string | number>">
  Explicit output values — colors, sizes, or dash patterns — aligned to
  `domain`.
</ParamField>

<ParamField path="padding" type="number" default="0.1">
  Gap between bands as a fraction of the band step, `0–1`. It's applied as an
  inner padding of `padding` and an outer padding of `padding / 2`. Under
  [`coord.polar`](/sdk-next/concepts/coordinate-systems) every discrete scale's
  padding is forced to `0`, which is why pie wedges meet with no gap. A geom
  that tiles the plane — `geom.tile()` — defaults its position scales to `0` for
  the same reason; an explicit `padding` still wins.
</ParamField>

<ParamField path="reverse" type="boolean" default="false">
  Reverse the band order — the first domain entry maps to the end of the range.
  Defaults to `true` on the cross axis of a geom banded on both axes, so a
  [heatmap](/sdk-next/graph-types/heatmap) reads first-category-at-top like a
  matrix.
</ParamField>

## Next

* [Coordinate systems](/sdk-next/concepts/coordinate-systems) — the plane scales place values into
* [Chart types](/sdk-next/graph-types/index) — scales at work in each recipe


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.