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

# Data structure

In the Graphy SDK, your **data** and your **spec** are separate. Data is a plain table you pass to `GraphProvider` as the `data` prop; the spec references that table's columns by key through its [mapping](/sdk-next/quickstart#2-render-your-first-chart). Keeping them apart means the same spec can be re-used across datasets, and the same data can be drawn several ways.

## Basic structure

Data is a table with explicitly defined columns and rows:

```tsx theme={null}
const data = {
  columns: [
    { key: 'category', label: 'Category' },
    { key: 'value', label: 'Value' },
  ],
  rows: [
    { category: 'A', value: 100 },
    { category: 'B', value: 200 },
    { category: 'C', value: 150 },
  ],
};
```

The `columns` array defines the shape; `rows` holds the values.

## Referencing columns from a spec

A spec never contains data — it names the columns it needs through its mapping. The variable strings you pass to `mapping` (or the `createSpec` shorthand) are column `key`s:

```tsx theme={null}
const spec = pipe(
  // 'category' and 'value' are column keys
  createSpec({ x: 'category', y: 'value' }),
  geom.bar(),
  scale.x(),
  scale.y()
);
```

Because the binding is by key, the `label` on a column is purely presentational — it's what shows in axes, legends and tooltips. If you omit it, the key is used.

<Note>
  Row keys must exactly match a column `key`. Values under keys with no matching
  column definition are ignored.
</Note>

## Value format detection

Every column gets one **value format**, which drives how a scale interprets it and how the renderer formats it. Inference runs per column, in two passes:

1. **The year pass.** Across the first 5 non-empty rows, the first undeclared column whose non-empty cells are at least two whole numbers between 1800 and 2199 is read as a temporal `year` column. This is what keeps a `2021, 2022, 2023` column off a numeric axis. If any column declares `valueFormat: { type: 'year' }`, this pass is skipped.
2. **The first non-empty cell.** Every other undeclared column takes its format from its first non-empty cell alone, tried in this order: number → date → weekly date range → percentage → currency → text.

Then every cell in the column is parsed under that one format. **A cell that doesn't fit becomes `null`** — silently, with no warning. Order in the data therefore matters:

```tsx theme={null}
// value → decimal (the first cell is 12); 'n/a' parses to null
rows: [{ value: '12' }, { value: 'n/a' }, { value: '15' }];

// value → text (the first cell is 'n/a'); all three stay strings, and the
// column lands on a discrete scale
rows: [{ value: 'n/a' }, { value: '12' }, { value: '15' }];
```

If a column looks empty on the chart, check its first non-empty cell first.

### Declaring a format

The same cell can mean two things: `2010` is a headcount or a year, `'1'` a category or a number. When you know, set `valueFormat` on the column. A declared column skips inference and is parsed and formatted under that format:

```tsx theme={null}
columns: [
  { key: 'headcount', valueFormat: { type: 'integer' } }, // 2010, 2050, 2100 stay counts, not years
  { key: 'cluster', valueFormat: { type: 'text' } }, // '1', '2', '3' stay categories
  { key: 'price', valueFormat: { type: 'currency', iso: 'usd' } }, // bare numbers format as dollars
],
```

Any format below is accepted; a temporal one can carry the `dateFormat` its cells use, such as `{ type: 'date', dateFormat: 'dd-MM-yyyy' }`. A cell that does not fit becomes `null`, as under inference.

Declare a numeric format for **every count column whose values could look like years**. Declaring a column named `year` as `integer` keeps that column numeric, but the year pass still checks undeclared siblings. For example, an undeclared `population` column containing `1950, 2000` would become dates. Declare both to keep both numeric:

```tsx theme={null}
const data = {
  columns: [
    { key: 'year', valueFormat: { type: 'integer' } },
    { key: 'population', valueFormat: { type: 'integer' } },
  ],
  rows: [
    { year: 2020, population: 1950 },
    { year: 2021, population: 2000 },
  ],
};
```

Only a declaration with `type: 'year'` stops the year pass for the whole table; an `integer` declaration does not.

### Numbers

Numbers and numeric strings are detected as quantitative values. Thousands separators, decimals and magnitude suffixes are supported:

```tsx theme={null}
{ category: 'A', value: 100 }
{ category: 'B', value: '1,250.50' }
{ category: 'C', value: '2.5m' }       // k, m, b, t suffixes supported
```

### Dates

Date-like strings are parsed automatically across a wide range of formats — ISO dates, named months and locale-specific orderings:

```tsx theme={null}
{ date: '2024-01-15', value: 100 }           // YYYY-MM-DD
{ date: 'January 2024', value: 200 }         // month name + year
{ date: '15/01/2024', value: 150 }           // DD/MM/YYYY (en-GB, the default locale)
{ date: '01/15/2024', value: 150 }           // MM/DD/YYYY (en-US)
{ date: '2024-01-15T12:30:00Z', value: 300 } // ISO datetime
{ date: 'Q1 2024', value: 400 }              // quarter
```

Short month names (`Jan`, `Feb`) are recognised alongside full names. Set `data._metadata.parsingLocale` to control whether ambiguous dates like `01/02/2024` are read day-first (`en-GB`, `pt-PT`, `ar`) or month-first (`en-US`).

**Weekly date ranges** are also detected — values like `1 Feb – 7 Feb` or `1 Feb 2024 – 7 Feb 2024` representing 7-day spans.

#### Dates without a year

A format that carries no year — `month` (`Jan`), `day_month` (`14 Jan`) or a weekly range without one — gets a **synthetic year** assigned by walking the column in row order, one sequence per color group. Each time the sequence wraps past December, the year advances. That's why `Jan … Dec, Jan, Feb` reads as 14 consecutive months rather than folding back onto itself — and why reordering the rows moves the axis.

### Percentages

Values with a `%` suffix are detected as percentages, and **stored as fractions**: `'64.5%'` becomes `0.645`. The renderer multiplies by 100 again when it formats, so the axis reads back as you wrote it.

```tsx theme={null}
{ category: 'Desktop', share: '64.5%' }
{ category: 'Mobile', share: '28.3%' }
```

A bare numeric cell inside a percentage column passes through unscaled, so a `12` sitting among `'64.5%'` values renders as 1200%. Write `'12%'` to keep the column consistent.

### Currencies

Currency-formatted strings are parsed with automatic symbol recognition. Supported symbols: `$`, `€`, `£`, `¥`, `₹`, `₱`, `₩`, `₪`, `₫`, `₽`, `฿`, `₦`, `₺`, `zł`, `kr`, `Fr`, `R`, `P`, `R$`, `Rp`, `RM`, `د.إ`, `﷼`, `Ch$`, `NT$`, `HK$`, `S$`, `A$`, `C$`, `NZ$`, `MX$`.

```tsx theme={null}
{ product: 'A', price: '$1,250' }
{ product: 'B', price: '£2,450.50' }
{ product: 'C', price: '€3,000' }
```

Symbols can appear as prefix or suffix, and negative values are supported (`-$100`, `$-100`).

### Text

Any value that doesn't match the above is treated as text (categorical data).

## How formats reach scales

The inferred format feeds the scale you declare for each aesthetic. Calling a position scale bare (`scale.x()`) lets the engine choose a scale type from the column's format. For the position aesthetics — `x`, `y`, `ySecondary`:

* A **text** column → a discrete (band) scale.
* A **numeric**, **percentage** or **currency** column → a continuous scale.
* A **date** column → a `datetime` scale.

The visual aesthetics read the same formats differently. On `color`, `size`, `alpha` and `strokeWidth`, a date column infers **continuous** — timestamps become plain numbers, since a colour ramp needs no date-aware ticks. On `lineType`, every format infers **discrete**. And an aesthetic bound to a constant (`{ value: 5 }`), left unmapped, or pointing at a column that isn't in the data falls back to continuous.

You can always override the inference with an explicit method — `scale.x.continuous()`, `scale.x.discrete()`, `scale.x.datetime()` — when you want to force a particular treatment. See the [line chart guide](/sdk-next/graph-types/line) for worked examples.

## Empty and missing values

A cell counts as **empty** when it is `null`, `undefined`, an empty or whitespace-only string, or `'-'`. Empties become `null` in the parsed column, and two rules follow from that:

* A row whose every cell is empty is dropped.
* A column with no non-empty cell at all is dropped, so nothing can map to it.

How a geom treats the resulting gaps is geom-specific — for lines, the `missingValues` param chooses whether to break the path, bridge it, or treat the gap as zero.

```tsx theme={null}
rows: [
  { month: 'Jan', revenue: 12000 },
  { month: 'Feb', revenue: null }, // missing
  { month: 'Mar', revenue: 18000 },
];
```

## Schema

<ParamField path="data.columns" type="Column[]" required>
  Array of column definitions. Each column defines the structure and metadata
  for a data field.
</ParamField>

<ParamField path="columns[].key" type="string" required>
  Unique, stable identifier for the column. Must match the keys used in `rows`
  objects, and is what a spec's mapping references.
</ParamField>

<ParamField path="columns[].label" type="string" optional>
  Human-readable label shown in the UI (axis labels, legends, tooltips).
  Defaults to the `key` if not provided.
</ParamField>

<ParamField path="columns[].valueFormat" type="ExplicitValueFormat" optional>
  How to read the column's cells when you know better than inference, for
  example `{ type: 'integer' }`, `{ type: 'text' }` or
  `{ type: 'currency', iso: 'usd' }`. A declared column skips inference; see
  [Declaring a format](#declaring-a-format).
</ParamField>

<ParamField path="data.rows" type="Row[]" required>
  Array of data rows. Each row is an object with keys matching the column keys.
  Values can be a `string`, `number`, `Date` or `null`.
</ParamField>

<ParamField path="data._metadata" type="object" optional>
  Host state carried alongside the table. The engine reads `parsingLocale`; the
  remaining keys are set by the Graphy editor and are not part of the SDK's
  authoring surface.

  <Expandable title="properties">
    <ParamField path="parsingLocale" type="'en-GB' | 'en-US' | 'ar' | 'pt-PT'" optional>
      Locale used to infer column formats and parse cells (dates, numbers). Defaults to `'en-GB'`.

      * `'en-GB'`, `'pt-PT'`, `'ar'`: DD/MM/YYYY date format
      * `'en-US'`: MM/DD/YYYY date format
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  There are two `parsingLocale` settings, and they do different jobs.
  `data._metadata.parsingLocale` (default `'en-GB'`) decides how **input cells are
  parsed**. `config({ parsingLocale })` (default `'en-US'`) decides how values are
  **formatted for display**, and is what `formattingLocale` on `GraphProvider`
  overrides. Setting the config one does not change how a date string is read.
</Note>


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