---
title: "Charts"
url: https://recursiveai.org/onedocument/docs/charts/
description: "How OneDocument reads, lays out and paints charts. Namespace RecursiveAI.OneDocument.Charts in RecursiveAI.OneDocument.Rendering…"
version: "1.0.0"
source: https://github.com/recursive-ai-oss/onedocument-csharp/blob/a85720e075c783fc596641d8586ee126cae088c5/docs/charts.md
---

# Charts

How OneDocument reads, lays out and paints charts. Namespace `RecursiveAI.OneDocument.Charts` in `RecursiveAI.OneDocument.Rendering`
(`src/OneDocument.Rendering/Charts/`). References are to ECMA-376 Part 1 §21.2 (DrawingML charts) and [MS-ODRAWXML] (chartex) unless
noted.

## Liftable by design

The namespace depends on three things only: its own model, the markup layer (`RecursiveAI.OneDocument.Markup`, itself free of
other OneDocument code) and the display list and OneFont. `ChartHost.cs` is the one file that touches the rest of OneDocument
(packages, the font library, the number-format service, workbook cells). Text and number formatting reach the painter through two
small interfaces, `IChartFonts` and `IChartNumberFormatter`. A project that wants the chart code takes the folder plus an adapter.

## Pipeline

`ChartReader.Read(bytes)` → `ChartSpace` (model) → `ChartPainter.Paint(displayList, chart, x, y, w, h, options)` → `ChartLayoutInfo`.
`ChartHost` wraps the common cases: `Load(package, sourcePart, relId)` for the chart a frame points at, `Fonts`, `Numbers`,
`WorkbookSource`, and `Paint`/`WritePdf`/`RenderPng` for a chart on its own. `ChartFrames.Find(part)` lists the chart references of a
slide, spreadsheet drawing or document part (`p:graphicFrame`, `xdr:graphicFrame`, `wp:inline`/`wp:anchor`) with their extent, name
and description, so each host paints the chart at its frame's box and uses the description as the figure's alternative text.

Painting is in CSS pixels (1 pt = 4/3 px) into the caller's display list, wrapped in one `Figure` tag whose alternative text is the
frame description, else the chart title, else the first series name. Strokes are filled outlines because the display list fills
only.

## Model

- **Classic** (`c:chartSpace`): bar/column (clustered, stacked, 100 %), line, area, pie, doughnut, `ofPie` (drawn as one pie, with a
  note), scatter, bubble, radar (standard, markers, filled), stock (high-low-close, open-high-low-close, up/down bars), surface
  (drawn flat as contour bands). The 3-D chart elements read as their 2-D types with the note `chart.3d-flattened`.
- **Chartex** (`cx:chartSpace`): waterfall (subtotals), sunburst, treemap, histogram, Pareto, box and whisker, funnel. A region map
  and any unknown layout read as a placeholder with `chart.region-map` / `chart.layout-unknown`.
- Values and categories come from the cached data (`numCache`, `strCache`, `multiLvlStrCache`, literals; chartex `lvl` points). Nothing
  is recalculated on read. `ChartRefresh.Refresh(chart, source)` replaces the caches from an `IChartDataSource`;
  `ChartHost.WorkbookSource(workbook)` reads cell values as stored.
- Axes (value, category, date, series) with min/max/major/minor units, log base, reverse order, crossing, tick marks, label
  position, skips, titles, gridlines and number format; titles, legends (position, overlay, deleted entries, manual layout), data
  labels, trendlines and error bars (basic), data points, markers, plot- and chart-area formatting, chart style number, theme override.
- `extLst` elements are kept as text in `ChartSpace.Extensions`; `ChartSpace.SourceBytes` is the part's original bytes, which is
  what a package writes back. The model's setters change the model only (there is no writer yet), so an unedited chart part stays
  byte-identical in a saved package.
- Errors: a part that is not well-formed XML raises the markup layer's `OfficeFormatException`; a root other than `chartSpace` raises
  `chart.not-a-chart`; values the reader cannot use raise `chart.malformed`. `TryRead` returns false instead.
- Notes (`ChartSpace.Notes`, `ChartLayoutInfo.Notes`): `chart.region-map`, `chart.layout-unknown`, `chart.group-skipped`,
  `chart.3d-flattened`, `chart.of-pie`, `chart.data-table`, `chart.surface-flattened`, `chart.font-missing`, `chart.placeholder`.

## Colours

Series colours are the theme's accents in order, and from the seventh series the accents darkened and lightened in fixed steps
(`ChartTheme.AutomaticSeriesColor`); Office varies the steps with the number of series, which is a known approximation. Colour
transforms (`lumMod`, `lumOff`, `satMod`, `tint`, `shade`, `alpha`) are applied in order. Pattern and picture fills draw as flat
colour. Text defaults to the 65 %-grey Office uses, gridlines to 15 % grey.

## Automatic value-axis scale

`AxisScaler.Compute` follows the model below; every number in it is a **hypothesis to settle against Excel**, not a verified rule.

1. Fixed `min`/`max`/`majorUnit` win. A logarithmic axis runs from `base^floor(log min)` to `base^ceil(log max)` with the base as unit.
2. 100 % stacked groups: 0 to 1 in steps of 0.1.
3. Otherwise the raw range comes from the data (running totals for stacked groups, 0 included):
   - all values ≥ 0: if `(max - min) / max` is more than 1/6 the minimum is 0, else `min - (max - min) / 2`; the maximum is
     `max + 5 % × (max - axisMin)`;
   - all values ≤ 0: the mirror image; mixed signs: 5 % of the data range is added at both ends;
   - one value: 0 to 1.05 × the value.
4. The major unit is the smallest of 1, 2 or 5 × 10^k that spans the raw range in at most 10 intervals; the minor unit is a fifth of it.
5. The minimum is floored and the maximum ceiled to a multiple of the major unit.

Worked cases: data 0–100 → 0–120 step 20; 0–10 → 0–12 step 2; 0–1 → 0–1.2 step 0.2; 6–22 → 0–25 step 5; 0–200 → 0–250 step 50.

Unsure points, all in the generated cases below: where exactly the 1/6 rule switches (`just-over-sixth`, `just-under-sixth`), whether
10 or 11 intervals are accepted (`range-101`, `range-99`), tiny and huge magnitudes, negative-only data, single values and all-zero
data, and the head-room for mixed signs.

`tools/ChartProbe gen DIR` writes one workbook per case (a clustered column chart of one series) and `scaling-cases.json` with our
`min`/`max`/`major` and an empty `excel` slot. Excel's answer for each (`Axes(xlValue).MinimumScale`, `MaximumScale`, `MajorUnit`) goes
into that slot at the comparison run; the committed copy is `testdata/charts/`. Until then the cases are unanswered.

## Layout

Order of work: chart area, title, legend, then the plot.

- **Title** at the top, centred, 1.4 × the base size (14 pt from a 10 pt base); a manual layout places it from the chart's top left.
- **Legend** right, left, top, bottom or top-right; entries per series (per category for pie, doughnut and single-series vary-colours
  charts; Increase/Decrease/Total for waterfall); a manual layout or overlay takes no space.
- **Plot** for axis charts: value labels, tick length and axis titles are measured and subtracted; category labels are laid out for
  the slot size (horizontal if they fit, wrapped at spaces in up to three lines, rotated 45° or 90°, else every n-th label). A manual
  inner layout sets the plot directly. Secondary value axes sit on the opposite side; the Pareto line has its own 0–100 % axis.
- **Bars:** cluster width from gap width and overlap (`barWidth = slot / (n - (n-1) × overlap + gap)`); bars grow from the category
  axis crossing. **Lines/areas:** categories at slot centres (`crossBetween between`) or on the ticks (`midCat`); date axes place
  points by date. **Pie:** from `firstSliceAng` clockwise, explosion shrinks the radius to fit; best-fit labels go inside large slices.
- **Charts without axes:** pie, doughnut (one ring per series), radar, sunburst (rings), treemap (squarified, parent headers), funnel.
- Overlap avoidance is basic (label skipping and rotation only). Labels are not nudged apart.

## Known gaps

- Pattern/picture fills, gradient outlines, shadows and 3-D are not drawn. Surface charts are flat.
- Data tables, display units text, trendline equations and R², up/down-bar formatting, and per-label manual positions are not drawn.
- Chartex: region maps are a placeholder; waterfall and box-whisker connector and mean-line options are partial; sunburst and treemap
  label overflow is not handled; histogram bin labels use plain number formatting.
- Auto colours past six series use fixed steps.
- Text uses the substitute fonts; line breaks and widths differ from Office until the same fonts are installed.

## Checks

- Unit tests (`ChartTests`): every generated sample reads, keeps its bytes through a package round trip, and paints a tagged figure;
  scaling rows; frames; refresh; typed errors.
- `tools/ChartProbe sweep INPUT OUT [--render N]` reads every chart part of every document under INPUT and counts types.
- Comparison inputs, unanswered: the 38 generated chart workbooks and the scaling workbooks in `testdata/charts/` for Excel's PDF
  export and axis values, at the 1.0 gate.
