> ## 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.

# Mappings & aesthetics

A **mapping** binds your data columns to **aesthetics** — the visual properties a chart draws with, like horizontal position, vertical position, and color. It's the bridge between a row of data and what's drawn on screen.

```tsx theme={null}
createSpec({ x: 'month', y: 'revenue', color: 'product' });
```

This reads: put `month` on the x-axis, `revenue` on the y-axis, and give each `product` its own color. The strings are column [keys](/sdk-next/data-structure).

## Built-in aesthetics

These are the built-in aesthetics you can map:

| Aesthetic | Encodes | Typical column |
| - | - | - |
| `x` | Horizontal position | Category, date, or number |
| `y` | Vertical position | Number |
| `label` | The text [data labels](/sdk-next/config/data-labels) show | Any column |
| `color` | Fill / stroke color | Category (or number, for a gradient) |
| `size` | Size of each observation | Number |
| `alpha` | Opacity (0–1) | Number |
| `strokeWidth` | Line/border thickness | Number |
| `lineType` | Dash pattern (solid, dashed, dotted) | Category |
| `group` | Splits observations into groups **without** a visual encoding | Category |

Not every geom reads every aesthetic — a `size` mapping means something to `geom.point()` but nothing to `geom.line()`. Each [geom](/sdk-next/concepts/geoms) documents the aesthetics it uses. A [custom geom](/sdk-next/extending/custom-geoms) can declare positional channels of its own — a candlestick's `open` / `high` / `low` / `close`, say — and the engine trains and scales them exactly like the built-in ones.

### What splits data into series

Grouping follows one rule:

* If `group` is mapped, it is the **only** thing that groups. Nothing else splits the data.
* Otherwise, a series is formed per unique combination of every **categorical visual** aesthetic that is mapped — `color`, `size`, `alpha`, `strokeWidth` and `lineType`. A lone `lineType: 'scenario'` mapping splits one line into several, with no colour involved.

Positional aesthetics never split series, and a visual aesthetic pointing at the same column as `x` or `y` is left out of the grouping — position has already partitioned the data by it.

`color` and `group` differ in what else they do: `color` assigns a visual (a hue per category, with a legend) as well as splitting; `group` only separates the observations. Use `group` when you want, say, one line per category but a single color for all of them.

## Variables and constants

An aesthetic can be bound to a **variable** (a column, read per row) or a **constant** (one literal value applied to every observation):

```tsx theme={null}
createSpec({
  y: 'revenue', // variable — shorthand for { variable: 'revenue' }
  color: { value: 'red' }, // constant — every observation is red
});
```

* `'revenue'` — string shorthand for a variable mapping.
* `{ variable: 'revenue' }` — the explicit variable form.
* `{ value: 'red' }` — a constant. The value still flows through the aesthetic's [scale](/sdk-next/concepts/scales), so `{ value: 'Pro' }` on `color` resolves to whatever color the scale assigns the `Pro` category.

## Where a mapping lives

A mapping can sit at two levels:

**Spec-level** — shared by every layer. Set it with `createSpec({ ... })`:

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

**Layer-level** — merged over the spec-level mapping for one geom only, via a geom's `aes` option. This is how combos give each layer a different `y`:

```tsx theme={null}
pipe(
  createSpec({ x: 'month' }), // shared x
  geom.bar({ aes: { y: 'revenue' } }), // this layer's y, on the primary axis
  geom.line({ aes: { y: 'profit' }, yScaleType: 'secondary' }), // its own axis
  scale.x(),
  scale.y(),
  scale.ySecondary()
);
```

Layer mappings win where they overlap; anything they don't set falls through to the spec-level mapping.

Two metrics on one chart need more than a second mapping: set `yScaleType: 'secondary'` on the layer that belongs to the right-hand axis. Without it the layer is positioned against the primary domain, which draws a plausible-looking but wrong chart. The `scale.ySecondary()` above is optional — the engine adds an inferred secondary scale as soon as a layer asks for one — but declaring it lets you give that axis its own options.

<Note>
  A layer's `aes` option is named `mapping` in the serialized spec: `geom.line({ aes: { y: 'profit' } })`
  becomes `{ "type": "layer", "geom": "line", "mapping": { "y": "profit" } }`. Worth knowing if you
  [hand-write the JSON](/sdk-next/concepts/serializable-spec).
</Note>

## Every mapped position needs a scale

Mapping a column to `x` or `y` is only half the story — you must also declare the [scale](/sdk-next/concepts/scales) that turns those values into positions. `scale.x()` and `scale.y()` are never created automatically.

<Warning>
  A mapped position aesthetic with no matching `scale.*()` produces `NaN`
  positions. If a chart renders blank, a missing scale is the first thing to
  check.
</Warning>

## Next

* [Scales](/sdk-next/concepts/scales) — turn mapped values into positions and colors
* [Geoms & layers](/sdk-next/concepts/geoms) — which aesthetics each geom reads


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