# PPTD Format Specification PPTD (PPT-DSL) is a YAML abstraction layer for PowerPoint presentations, used to describe, generate, and edit slides in an AI-friendly way, with lossless bidirectional conversion to and from PPTX --- ## Conventions in This Document - Uses **TS interfaces** to describe structures, with **field tables** and **minimal YAML examples** to aid understanding - **Default values** are annotated in TS end-of-line comments as `// default: X`. X may be a literal (`1` / `"top"` / `[0, 0]`) or a descriptive phrase (`not applied` / `not shown` / `falls back along the inheritance chain` / `auto-adapts to chart size`, etc.) - **Constraints** are annotated in TS end-of-line comments or below the TS block as `// constraint: ...`, uniformly using interval or inequality notation (`[0, 1]` / `> 0`) or textual descriptions --- ## 1. Global Conventions ### Syntax - Uses **YAML 1.2** syntax - For special characters such as `:`, `#`, `{`, `}`, the value must be wrapped in quotes or written with a block scalar instead - For fields with many special characters such as `content.text`, a block scalar (`|`) should be used as its own block, to prevent content like `style="..."` from being parsed incorrectly ### Coordinate System and Units - All geometry and size units are **px**; the origin `(0, 0)` is the top-left corner of the page - Recommended sizes: 16:9 → `[960, 540]`; 4:3 → `[720, 540]` - This specification defines 1px = 1pt (i.e., `fontSize: 18` is 18pt in PPTX) - Element stacking order is determined by the order of the `Page.elements` array; the later an element, the higher its layer ### Style Priority and Default Values For property values that conflict, the first source with a value is found by searching the following priorities from top to bottom; when none of the levels is set, fall back to the default values at the end of that section. > The following rule applies to all subsections of this section: `lineHeight` (a multiple) and `lineHeightPx` (fixed px) are mutually exclusive; when both are set, `lineHeightPx` takes precedence. #### 1. Text Styles Inside a Text Box **Priority chain:** 1. Rich-text semantic tags such as ``, ``, `` in [Text.content.text](#textcontent) 2. Inline properties set in `` 3. Paragraph properties set in `

` 4. **Style fields set directly on [Text.content](#textcontent)** (distinct from the theme style referenced by `style`; including `color`, `fontSize`, `fontFamily`, `bold`, `italic`, `backgroundColor`, `lineHeight`, `lineHeightPx`, `letterSpacing`, `marginTop`) 5. The [TextStyleConfig](#textstyleconfig) theme style referenced by [Text.content.style](#textcontent) 6. Default values: | Property | Default value | |---|---| | color | `#000000` | | backgroundColor | Not applied | | fontSize | `18` | | fontFamily | `"MiSans"` | | bold | `false` | | italic | `false` | | lineHeight | `1` | | lineHeightPx | Not applied | | letterSpacing | `0` | | marginTop | `0` | #### 2. Table Cell Styles **Priority chain:** 1. Rich-text semantic tags such as ``, ``, `` in [Cell.text](#cell) 2. Inline properties set in `` 3. Paragraph properties set in `

` 4. [Cell](#cell) inline fields 5. The [TextStyleConfig](#textstyleconfig) referenced by [Cell.textStyle](#cell) (**applies only to text fields**; does not include `fill` / `border` / `align`) 6. Position-category styles of [TableStyleConfig](#tablestyleconfig) - On row vs column conflicts, [TableStyleConfig.rowOverColumn](#tablestyleconfig) decides the winner; default `true` = row wins - Row categories: `TableStyleConfig.firstRowStyle` / `TableStyleConfig.lastRowStyle` - Column categories: `TableStyleConfig.firstColumnStyle` / `TableStyleConfig.lastColumnStyle` 7. [TableStyleConfig.bodyStyles](#tablestyleconfig): applies to data rows other than the first and last rows, cycled by data-row index 8. [TableStyleConfig.cellStyle](#tablestyleconfig): the baseline cell style for the whole table 9. Default values | Property | Default value | |---|---| | color | `#000000` | | backgroundColor | Not applied | | fontSize | Auto-adapts based on cell height | | fontFamily | `"MiSans"` | | bold | `false` | | italic | `false` | | lineHeight | `1` | | lineHeightPx | Not applied | | letterSpacing | `0` | | marginTop | `0` | | fill | Not applied (transparent) | | border | `{style: solid, width: 1, color: "#000000"}` | | align | `["center", "middle"]` | #### 3. Chart Styles Charts involve multiple kinds of styles (series body colors, fonts, data labels, axis/legend visibility, etc.), each with its own independent priority chain, explained below. **3.1 Series body color priority chain:** 1. A series' explicit `fill` / `lineColor` / `areaColor` (field names differ per type; see [Color Mechanism](#52-color-mechanism)) 2. The same-named field for the corresponding type in [Chart.seriesDefaults](#seriesdefaults) 3. The [Theme.colors](#theme) theme color cycle (colors are picked in the order the series appear in the array) > [scatter](#scatter) is an exception: marker color resolves as `marker.fill > series.fill > theme color cycle`; marker.border likewise takes precedence over series.border. > > For each type's specific color fields, derivation rules, and role mappings, see [§5.2 Color Mechanism](#52-color-mechanism). **3.2 Font priority chain:** 1. Sub-component `fontFamily` ([TitleConfig](#titleconfig) / [LegendConfig](#legendconfig) / [DataLabelConfig](#datalabelconfig) / [AxisConfig.label](#axisconfig) / [SpokeAxisConfig.label](#spokeaxisconfig)) 2. [Chart.fontFamily](#chart) 3. Theme default ([Theme](#theme) or the PPTX master font) **3.3 dataLabels priority chain:** 1. `series[i].dataLabels` 2. [Chart.dataLabels](#chart) (global default) 3. Not shown (equivalent to `show: false`) > Sub-fields follow a **one-level shallow merge**: `series.dataLabels` only overrides the sub-fields it explicitly provides; unprovided ones fall back from [Chart.dataLabels](#chart); if neither provides them, the per-type default applies (see [dataLabels.content value quick reference](#55-datalabelscontent-value-quick-reference)). **3.4 `seriesDefaults` merge rules:** [Chart.seriesDefaults](#seriesdefaults)`[type]` provides common defaults for all series of that type, merged with each series via a **one-level deep merge**: - **Scalar fields** (string / number / boolean): the series' explicit value overrides defaults - **Object fields** (`marker` / `dataLabels` / `border` / `upBars` / `downBars` / `totalBars` / gradient `fill` objects, etc.): recursive one-level shallow merge — same-named fields of defaults and series are spread respectively, with sub-fields overridden by the nearest source - **Array fields** (`fill: []` / `colorScheme: []`): the series replaces defaults as a whole, with no element-level merging - `type` and `encode` are not allowed inside seriesDefaults - Only multi-series types support seriesDefaults: `bar / line / area / scatter / bubble / candlestick / radar` Counterintuitive example: ```yaml seriesDefaults: bar: {marker: {shape: circle, size: 8}} series: - type: bar marker: {size: 12} # after merge: {shape: circle, size: 12}, not {size: 12} ``` **3.5 `boolean | Config` field convention:** Fields of the form `boolean | XxxConfig` (`marker` / `legend` / `AxisConfig.label / axisLine / gridLine` / `SpokeAxisConfig.label / axisLine / gridLine` / `colorbar`) uniformly follow: - `false` = off - `true` = on with default configuration - Object `{...}` = on + custom configuration > **The only exception**: [scatter.marker](#scatter) cannot be `false` (a scatter plot without a marker has nothing to render). --- ### Multi-File Structure A PPTD project consists of a main entry file and individual page files: ``` project/ slides_name.pptd # main entry (size/theme/title + page reference list) media/ # media resources such as images and videos pages/ # page file directory 1_cover.page # one .page file per page 2_intro.page ``` **Path rules:** 1. **Fully self-contained**: all referenced files must be located inside the folder containing the `.pptd` file; **referencing files outside the directory is not allowed** 2. **Only relative paths are supported** (relative to the directory containing the `.pptd` file): - The `pages` list in `.pptd`: `pages/1_cover.page` - Image paths in `.page`: `media/image1.jpg` 3. **Media supports URLs**: `Image.src`, and the [ImageFill](#fill).src of `background` / `fill`, may be `https://...` (only jpg/jpeg/png/gif supported) **Main entry is required:** everything must be loaded through the `.pptd` main entry file; a `.page` cannot be passed alone to the `convert`/`check` commands --- ## 2. Shared Types The following types are reused in multiple places and are defined together up front. Element sections reference them by type name without repeating the expansion ### Color ```ts type Color = string; ``` > Supports opaque **HEX6** (`#RRGGBB`), alpha **HEX8** (`#RRGGBBAA`), and [Theme.colors](#theme) theme color references (e.g. `$primary`) ### FontFamily ```ts type FontFamily = string | { latin: string; ea: string }; ``` | Form | Example | Description | |------|------|------| | String | `"MiSans"` | Chinese and English use the same font uniformly | | Object | `{latin: "Arial", ea: "MiSans"}` | Explicitly specify Latin (latin) and East Asian (ea) fonts separately | See [fonts.md](./fonts.md) for the list of available fonts ### Alignment ```ts type HorizontalAlign = "left" | "center" | "right" | "justify" | "distributed"; type VerticalAlign = "top" | "middle" | "bottom"; type Alignment = [HorizontalAlign, VerticalAlign]; ``` | Value | Description | |----|------| | `left` / `center` / `right` | Horizontal left / center / right alignment | | `justify` | Justified (last line not stretched) | | `distributed` | Distributed (last line stretched) | | `top` / `middle` / `bottom` | Vertical top / middle / bottom alignment | ### LineStyle ```ts type LineStyle = "solid" | "dash" | "dot"; ``` ### Border ```ts interface Border { style?: LineStyle; // default: "solid" width?: number; // default: 1 color?: Color; // default: "#000000" } ``` #### BorderSpec [Cell](#cell) and [CellStyle](#cellstyle) support an array form of `Border` to set the four side borders separately ```ts type BorderSpec = null | Border | [Border | null, Border | null] | [Border | null, Border | null, Border | null, Border | null]; ``` | Form | Meaning | |------|------| | `null` | Explicit clear: no border on any of the four sides (used to override a border set higher up the inheritance chain)| | `Border` | Same on all four sides | | Two-element array `[Border\|null, Border\|null]` | `[top-bottom, left-right]` | | Four-element array `[Border\|null, Border\|null, Border\|null, Border\|null]` | `[top, right, bottom, left]` (clockwise) | > A `null` inside the array means no border at the corresponding position; a top-level `null` clears everything. ### Shadow ```ts interface Shadow { blur: number; // blur radius color: Color; offset?: [number, number]; // default: [0, 0]; [x, y] offset } ``` ### ColorStop ```ts interface ColorStop { position: number; // constraint: [0, 1] color: Color; } ``` ### ImageFit / ImageCrop ```ts interface ImageFit { mode: "fill" | "contain" | "cover"; } interface ImageCrop { left?: number; top?: number; right?: number; bottom?: number; } ``` > **Constraint:** the four fields of `ImageCrop` are analogous, default 0. A positive value crops inward from the corresponding edge proportionally (inset); a negative value expands outward toward the corresponding edge proportionally and pads with transparent pixels (outset). Must ensure `left + right < 1` and `top + bottom < 1`, otherwise the source rectangle degenerates. | ImageFit.mode | Description | |---|---| | `cover` | Fills the container, keeps aspect ratio, may crop | | `contain` | Shows the image completely, keeps aspect ratio, may leave blank space | | `fill` | Stretches to fill, may distort | ### Fill ```ts type Fill = SolidFill | GradientFill | ImageFill; interface SolidFill { type: "solid"; color: Color; } interface GradientFill { type: "gradient"; gradientType: "linear" | "radial"; stops: ColorStop[]; // constraint: at least 2 angle?: number; // default: 0; only effective for linear } interface ImageFill { type: "image"; src: string; // URL or relative path fit?: ImageFit; // default: {mode: "cover"} crop?: ImageCrop; // always applied; see the rendering order with fit below opacity?: number; // default: 1; constraint: [0, 1] } ``` > `GradientFill.angle` takes values in `[0, 360)`; `0` means left to right, increasing clockwise. Examples: `90` = top→bottom, `180` = right→left. > **ImageFill rendering order:** `crop` (adjust the source rectangle proportionally: positive values crop inward, negative values expand outward and pad with transparent pixels) → `fit` (adapt to the fill container per mode). The specific semantics of each `fit.mode` value are consistent with the "rendering logic" discussion in the [Image](#image-image) section. **Examples:** ```yaml # Solid fill: type: solid color: "$primary" # Gradient fill: type: gradient gradientType: linear angle: 90 stops: - {position: 0, color: "$primary"} - {position: 1, color: "$accent"} # Image fill: type: image src: "media/bg.jpg" fit: {mode: cover} opacity: 0.9 ``` --- ## 3. Main Entry File (.pptd) ### Presentation ```ts interface Presentation { version: "v2"; // required, fixed to "v2" (version identifier) title?: string; // default: no title size: [number, number]; // [width, height]; 16:9 recommended [960, 540], 4:3 recommended [720, 540] theme?: Theme; pages: string[]; // list of relative paths to page files, e.g. "pages/cover.page" } ``` **Example:** ```yaml version: v2 title: Annual Work Summary size: [960, 540] theme: colors: primary: "#2563EB" accent: "#F59E0B" text: "#1F2937" textStyles: title: fontSize: 40 color: "$primary" body: fontSize: 18 color: "$text" lineHeight: 1.6 tableStyles: default: firstRowStyle: fill: {type: solid, color: "$primary"} color: "#ffffff" bold: true bodyStyles: - {fill: {type: solid, color: "#f8fafc"}} - {fill: {type: solid, color: "#ffffff"}} pages: - pages/1_cover.page - pages/2_content.page ``` ### Theme The theme centrally manages colors, text styles, and table styles. Use `$` in relevant fields to reference the theme: | Theme type | Referencing field | Example | |---|---|---| | `colors` | Any [Color](#color) field | `$primary` | | `textStyles` | [TextContent.style](#textcontent) / [Cell.textStyle](#cell) | `$title` | | `tableStyles` | [Table.style](#table-table) | `$default` | ```ts interface Theme { colors?: Record; textStyles?: Record; tableStyles?: Record; } ``` #### TextStyleConfig ```ts interface TextStyleConfig { color?: Color; fontSize?: number; fontFamily?: FontFamily; bold?: boolean; // bold italic?: boolean; // italic backgroundColor?: Color; // text background color (e.g., text highlight) lineHeight?: number; // line-height multiple lineHeightPx?: number; // fixed line height (px); when it conflicts with lineHeight, lineHeightPx prevails letterSpacing?: number; marginTop?: number; } ``` > Unset fields fall back along the inheritance chain (see [Style Priority and Default Values](#style-priority-and-default-values) for details) #### CellStyle ```ts interface CellStyle extends TextStyleConfig { // —— Inherits all properties of TextStyleConfig —— // color / fontSize / fontFamily / bold / italic / backgroundColor / lineHeight / lineHeightPx / letterSpacing / marginTop // —— CellStyle-specific —— fill?: Fill; // background fill border?: BorderSpec; // border align?: Alignment; // text alignment } ``` > Unset fields fall back along the inheritance chain (see [Style Priority and Default Values](#style-priority-and-default-values) for details) #### TableStyleConfig ```ts interface TableStyleConfig { // —— Cell style: applied to every cell —— cellStyle?: CellStyle; // —— Row category overrides —— firstRowStyle?: CellStyle; // first-row style lastRowStyle?: CellStyle; // last-row style // —— Column category overrides —— firstColumnStyle?: CellStyle; lastColumnStyle?: CellStyle; // —— Alternating row styles —— bodyStyles?: CellStyle[]; // data rows other than the first/last row apply these cyclically by data-row index // —— Cross-category rule —— rowOverColumn?: boolean; // default: true; whether the row style wins when a cell is covered by both row and column rules } ``` > **Row/column style rules**: category styles such as `firstRowStyle` / `lastRowStyle` / `firstColumnStyle` / `lastColumnStyle` mean **apply the style independently to every matching cell**, not apply the style to the first row/last column as a whole > - Writing `firstRowStyle.border: {style: solid, width: 2}` → **every cell of the first row** gets a border on all four sides > - To add an outer frame to the first row as a whole, use per-side BorderSpec: `border: [, null, , null]`, then set borders separately on the first-column and last-column cells of the first row > For fallback rules, see [Style Priority and Default Values](#style-priority-and-default-values) ## 4. Page Files (.page) ### Page ```ts interface Page { pageType?: "cover" | "table_of_contents" | "chapter" | "content" | "final" | string; // default: none; category label (does not affect rendering); preset values are recognized as the corresponding page type, arbitrary custom strings are also allowed background?: Fill; // default: {type: solid, color: "#FFFFFF"} (white solid fill) notes?: string; // default: none; speaker notes; plain text elements: Element[]; // the later an element, the higher its layer } ``` **Example:** ```yaml pageType: cover background: type: solid color: "$primary" notes: Speaker notes elements: - elementId: title1 elementType: text bounds: [100, 200, 760, 80] content: style: "$title" align: [center, middle] text: Hello World ``` --- ## 5. Elements ### ElementBase Common properties of all elements. ```ts interface ElementBase { elementId: string; // constraint: unique within the same page; unique element ID elementType: "text" | "shape" | "line" | "image" | "icon" | "table" | "chart"; // element type bounds: [number, number, number, number]; // element size and position, [x, y, width, height] } type Element = Text | Shape | Line | Image | Icon | Table | Chart; ``` --- ### Text (text box) ```ts interface Text extends ElementBase { elementType: "text"; rotation?: number; // default: 0; degrees, clockwise rotation opacity?: number; // default: 1; constraint: [0, 1] flip?: [boolean, boolean]; // default: [false, false]; [horizontal flip, vertical flip] content: TextContent; } ``` #### TextContent ```ts interface TextContent { text: string; // rich text string (block scalar) style?: string; // references theme.textStyles, written as "$key" (e.g. "$title") // —— Style fields (when unset, fall back along the inheritance chain) —— color?: Color; fontSize?: number; fontFamily?: FontFamily; bold?: boolean; // bold: true=on, false/unset=off italic?: boolean; // italic: true=on, false/unset=off backgroundColor?: Color; // text background color (e.g., text highlight) lineHeight?: number; // line-height multiple lineHeightPx?: number; // fixed line height (px) letterSpacing?: number; marginTop?: number; // —— Layout fields —— textDirection?: "horizontal" | "vertical"; // default: "horizontal" wrap?: boolean; // default: true; when false, no wrapping, and the part beyond bounds.width overflows the element boundary; explicitly setting false is recommended for single-line text align?: Alignment; // default: ["left", "top"] // —— Visual decoration (unset = not applied) —— gradient?: GradientFill; // text gradient (applied to the text itself) shadow?: Shadow; // text shadow } ``` **Examples:** ```yaml # Basic: theme style + plain text - elementId: title-1 elementType: text bounds: [100, 50, 760, 80] content: style: "$title" align: [center, middle] text: Annual Work Summary # Rich text + inline property overrides - elementId: body-1 elementType: text bounds: [100, 200, 600, 200] content: fontSize: 20 color: "$text" lineHeight: 1.6 align: [left, top] text: |

Key achievement: completed 3 key projects

—— FY2024

# Text gradient + shadow - elementId: hero-text elementType: text bounds: [100, 100, 760, 120] content: align: [center, middle] gradient: type: gradient gradientType: linear angle: 90 stops: - {position: 0, color: "$primary"} - {position: 1, color: "$accent"} shadow: blur: 6 color: "#00000040" offset: [0, 3] text: |

FUTURE

``` #### Rich Text Rules `TextContent.text` and `Cell.text` follow the rich text rules below for paragraph splitting and for setting paragraph or inline styles. **Supported tags** | Tag | Description | Example | |------|------|------| | `

` | Paragraph; may carry paragraph styles | `

paragraph

` | | `` | Inline style; use this tag to set inline styles | `red` | | `` | Bold | `important` | | `` | Italic | `emphasis` | | `` | Underline | `underline` | | `` | Strikethrough | `deleted` | | `` | Superscript | `E=mc2` | | `` | Subscript | `H2O` | | `` | Hyperlink; supports `https://`, `http://`, `mailto:`; once set, the hyperlink text style (blue with underline) is applied automatically | `link` | | `
    ` | Unordered list | `
    • item
    ` | | `
      ` | Ordered list | `
      1. first item
      ` | | `
    1. ` | List item; must be used together with `
        ` or `
          ` | — | **style attribute mapping** `

          `, `

        1. `, and `` may use `style="..."`. Color-type values may all use theme references (e.g. `$primary`), resolved per the [Color](#color) rules. 1. **Paragraph styles (only `

          ` supports them)** | Property | Description | Values | Example | | --- | --- | --- | --- | | `text-align` | Paragraph horizontal alignment | `left` / `center` / `right` / `justify` / `distributed` | `

          ` | | `line-height` | Line height; **unitless** is treated as a `lineHeight` multiple, **with `px`** as a `lineHeightPx` fixed value | number (e.g. `1.5`) or px string (e.g. `24px`) | `

          ` | | `margin-top` | Spacing before the paragraph | px string (e.g. `8px`) | `

          ` | | `margin-left` | Left margin | px string (e.g. `12px`) | `

          ` | | `margin-right` | Right margin | px string (e.g. `12px`) | `

          ` | > Do not set `letter-spacing` on `

          `; to set letter spacing uniformly, use `content.letterSpacing` or `Cell.letterSpacing`. 2. **List-item styles (only `

        2. ` supports them)** | Property | Description | | --- | --- | | `text-align` | List-item horizontal alignment | | `line-height` | Line height; value rules same as `

          ` | | `letter-spacing` | Letter spacing | | `margin-top` | Spacing before the paragraph | | `margin-left` | Left margin | | `list-style` | List style shorthand | | `list-style-type` | List marker type | | `list-style-position` | List marker position | | `list-style-image` | List marker image | 3. **Inline styles (only `` supports them)** Styles apply only to the text inside that ``. | Property | Description | Values | Example | | --- | --- | --- | --- | | `color` | Text color | [Color](#color) (HEX6 / HEX8 / theme reference) | `` | | `font-size` | Font size | px string (e.g. `24px`) | `` | | `font-family` | Font family | Font name (e.g. `Arial`, `"Arial, 微软雅黑"`) | `` | | `background-color` | Text background color | [Color](#color) (HEX6 / HEX8 / theme reference) | `` | ```yaml content: align: [left, top] lineHeight: 1.2 text: |

          Main TitleSubtitle

          This paragraph is center-aligned with 1.8x line height

          This paragraph is right-aligned; line height inherits the default 1.2

          ``` **Plain-text shorthand** `content.text` may use plain text directly: - Single line: `text: "Hello"` ≡ `text: "

          Hello

          "` - Multi-line (block scalar): ```yaml text: | First line Second line ``` ≡ `

          First line

          Second line

          ` - `
          ` may be used for a line break within a paragraph, but it is not guaranteed to be preserved on re-conversion after editing. When stable line breaks are needed, use multiple `

          `. **LaTeX formulas** Rich text supports embedding LaTeX formulas with the `\(...\)` delimiters: - May form their own paragraph, or be mixed with other text inside a `

          `. - Rich text tags are **not allowed** inside a formula. - A formula **only inherits** the `color` and `font-size` styles from its context; other text styles are not passed through. - A `

          ` tag can wrap a LaTeX formula to control the alignment ```yaml content: text: |

          Pythagorean theorem: \(a^2 + b^2 = c^2\)

          \(\int_0^1 x^2 \mathrm{d}x = \frac{1}{3}\)

          ``` --- ### Shape (shape) ```ts interface Shape extends ElementBase { elementType: "shape"; rotation?: number; // default: 0; degrees, clockwise rotation opacity?: number; // default: 1; constraint: [0, 1] flip?: [boolean, boolean]; // default: [false, false]; [horizontal flip, vertical flip] shapeName: string; // see ./shapes.md adjustments?: number[]; // see ./shapes.md; geometry parameters; default: the default parameter values viewBox?: [number, number]; // view box; used only when shapeName="custom", required in that case path?: string; // SVG shape path; used only when shapeName="custom", required in that case fill?: Fill; // default: not applied border?: Border; // default: not applied shadow?: Shadow; // default: not applied } ``` > Custom shapes: you may specify `shapeName: "custom"` and use `viewBox` and `path` to define a custom shape; these two parameters have no effect when `shapeName` is not `custom` > The `adjustments` parameters: reuse the parameter order and quantity defined by OOXML; see ./shapes.md for value constraints. > **Note**: `shape` does not support embedded text! Add an extra text box to achieve that. **custom path conventions:** - `viewBox`: view box, the path coordinate system `[w, h]` - `path`: SVG path string, supporting the `M / L / H / V / C / S / Q / A / Z` commands. - Multi-segment paths are supported for shapes such as hollow-outs: make the outer contour **clockwise** (`sweep=1`) and the inner contour **counterclockwise** (`sweep=0`) to achieve a hollow cutout - **Scaling and aspect ratio**: changing `bounds` resizes the shape (the path needs no rewriting); but the viewBox is stretched independently to bounds — when the ratios differ, the shape distorts. To keep the ratio, require `viewBoxW : viewBoxH = bounds.w : bounds.h`. **Common shapes** > See [shapes.md](./shapes.md) for the full 177 shapes. | shapeName | Description | adjustments default values | |-----------|------|-------------------| | `rect` | Rectangle | — | | `roundRect` | Rounded rectangle | `[16667]` (corner radius) | | `ellipse` | Ellipse | — | | `triangle` | Triangle | `[50000]` (horizontal position of apex) | | `diamond` | Diamond | — | | `homePlate` | Five-sided arrow | `[50000]` | | `chevron` | V-shaped arrow | `[50000]` | | `donut` | Ring | `[25000]` (ring width ratio) | | `star5` | 5-point star | `[19098, 105146, 110557]` | | `rightArrow` | Right arrow | `[50000, 50000]` (shaft width, arrowhead length) | | `wedgeRectCallout` | Rectangle callout | `[-20833, 62500]` | | `bracePair` | Brace pair | `[8333]` | **Examples:** ```yaml # Built-in shape - elementId: shape-1 elementType: shape bounds: [200, 200, 300, 150] shapeName: roundRect adjustments: [20000] fill: {type: solid, color: "$primary"} border: {style: solid, width: 2, color: "$accent"} # Custom hollow ring (outer contour clockwise + inner contour counterclockwise) - elementId: shape-2 elementType: shape bounds: [400, 200, 150, 150] shapeName: custom viewBox: [1000, 1000] path: "M500,0 A500,500 0 1 1 499,0 Z M500,200 A300,300 0 1 0 499,200 Z" fill: {type: solid, color: "$accent"} ``` --- ### Line (line) ```ts type ArrowType = "arrow" | "stealth" | "diamond" | "oval"; interface Line extends ElementBase { elementType: "line"; rotation?: number; // default: 0; degrees, clockwise rotation opacity?: number; // default: 1; constraint: [0, 1] flip?: [boolean, boolean]; // default: [false, false]; [horizontal flip, vertical flip] viewBox: [number, number]; // path coordinate system [w, h]; points live in this coordinate system, so changing bounds does not require changing points points: string; // bezier path points "x1,y1 x2,y2 ..."; the first/last points are the start/end the curve passes through, the middle points are control points curve?: "sharp" | "round" | "smooth"; // default: "round"; sharp joins / rounded joins / bezier smooth curve arrow?: [ArrowType | null, ArrowType | null]; // start arrow, end arrow; default: [null, null] (no arrows at either end) border?: Border; // default: not applied shadow?: Shadow; // default: not applied } ``` > **Constraint:** `points` needs at least 2 points; the first point and the last point are points the curve passes through, the rest are bezier control points; all coordinates must be within `viewBox`. > **viewBox vs bounds:** at render time, the viewBox is stretched independently to the bounds size; to keep the line from being stretched out of shape, require `viewBoxW : viewBoxH = bounds.w : bounds.h`. **Examples:** ```yaml # Normalized coordinates: from top-left to bottom-right, the two middle points are control points - elementId: l4 elementType: line bounds: [100, 100, 500, 300] viewBox: [1, 1] points: "0,0 0.2,0 0.8,1 1,1" curve: smooth border: {style: solid, width: 2, color: "$primary"} # Bezier arc: passes through the start and end points; the two middle points control the bend direction - elementId: bezier-arc elementType: line bounds: [50, 200, 860, 100] viewBox: [360, 100] points: "0,80 120,0 240,100 360,20" curve: smooth border: {style: solid, width: 2, color: "$primary"} ``` --- ### Image (image) ```ts interface Image extends ElementBase { elementType: "image"; rotation?: number; // default: 0; degrees, clockwise rotation opacity?: number; // default: 1; constraint: [0, 1] flip?: [boolean, boolean]; // default: [false, false]; [horizontal flip, vertical flip] src: string; // URL or local relative path cropShape?: ShapeDef; // default: rectangle (i.e., no shape cropping) fit?: ImageFit; // default: {mode: "cover"} crop?: ImageCrop; // always applied; see the rendering order with fit/cropShape below border?: Border; // default: not applied shadow?: Shadow; // default: not applied } interface ShapeDef { shapeName: string; // see ./shapes.md; use "custom" for a custom path adjustments?: number[]; // default: use the shape's built-in defaults (see ./shapes.md) viewBox?: [number, number]; // used only when shapeName="custom", required in that case path?: string; // used only when shapeName="custom", required in that case } ``` > `ShapeDef` fields correspond one-to-one with the shape fields of the [Shape](#shape-shape) element; for detailed conventions (adjustments values and angle conversion, custom path rules, hollow rules, common shape table), see the [Shape](#shape-shape) section. **Rendering logic:** `crop` (proportionally adjust the source rectangle to get a sub-image: positive values crop inward, negative values expand outward and pad with transparent pixels) → `fit` (adapt the sub-image to the bounds container per mode) → `cropShape` (clip the final display area to the shape outline). All three can be set independently and are applied in the fixed order above. - `fit.mode="cover"`: scale the sub-image proportionally to fill bounds; the overflow is cropped. - `fit.mode="contain"`: scale the sub-image proportionally to display it completely; the shortfall is left blank. - `fit.mode="fill"`: **the sub-image is stretched directly to fill bounds** — although no cropped blank edges are visible in this case, the picture content is still only the sub-region after crop, not the full original image. **Examples:** ```yaml - elementId: img-1 elementType: image bounds: [50, 50, 400, 300] src: "media/cover.jpg" cropShape: {shapeName: roundRect, adjustments: [15000]} fit: {mode: cover} crop: {top: 0.1, bottom: 0.1, left: 0.05, right: 0.05} # crop the surrounding proportions first, then apply cover fitting shadow: blur: 10 color: "#00000033" offset: [0, 4] # Custom clip outline - elementId: img-2 elementType: image bounds: [200, 200, 200, 200] src: "media/avatar.jpg" cropShape: shapeName: custom viewBox: [1000, 1000] path: "M500,0 A500,500 0 1 1 499,0 Z" fit: {mode: cover} ``` --- ### Icon (icon) ```ts interface Icon extends ElementBase { elementType: "icon"; rotation?: number; // default: 0; degrees, clockwise rotation opacity?: number; // default: 1; constraint: [0, 1] flip?: [boolean, boolean]; // default: [false, false]; [horizontal flip, vertical flip] iconName: string; // format "style:name" fill?: Fill; // default: black solid fill border?: Border; // default: not applied shadow?: Shadow; // default: not applied } ``` **iconName format:** `style:name`, using the Font Awesome 7.x free icon library. | Prefix | Style | Example | |------|------|------| | `fas` | Solid (most common) | `fas:house` | | `far` | Regular | `far:heart` | | `fab` | Brands | `fab:github` | Icon search: https://fontawesome.com/search?ic=free-collection **Example:** ```yaml - elementId: icon-1 elementType: icon bounds: [100, 100, 48, 48] iconName: "fas:lightbulb" fill: {type: solid, color: "$primary"} ``` --- ### Table (table) ```ts interface Table extends ElementBase { elementType: "table"; columnWidths: number[]; // array of column-width ratios (not px; relative to the bounds width) rowHeights: number[]; // array of row-height ratios (not px; relative to the bounds height) rows: Cell[][]; // 2-D array; merged regions are declared with rowSpan/colSpan, occupied positions are skipped in the array style?: string | TableStyleConfig; // references theme.tableStyles, written as "$key" (e.g. "$default"), or an inline TableStyleConfig object fill?: Fill; // default: not applied; table-level fill (applied to the whole table, can be overridden by cell fill) shadow?: Shadow; // default: not applied } ``` > **PowerPoint limitation:** native tables cannot be rotated/flipped as a whole; whole-table global opacity including text and borders is also not supported. When whole rotation/flip/opacity is needed, render as an image first and treat it as an [Image](#image-image) element. > **Constraint:** each item of `columnWidths` and `rowHeights` is within `[0, 1]`, and the elements of each sum to 1. #### Cell ```ts interface Cell { // —— Content —— text?: string; // default: empty cell; rich text string (written as a block scalar), rules same as TextContent.text textStyle?: string; // references theme.textStyles, written as "$key" (e.g. "$body") // —— Text styles (when unset, fall back along the inheritance chain) —— color?: Color; fontSize?: number; fontFamily?: FontFamily; bold?: boolean; italic?: boolean; backgroundColor?: Color; // text background color (e.g., text highlight) lineHeight?: number; // line-height multiple lineHeightPx?: number; // fixed line height (px) letterSpacing?: number; marginTop?: number; // —— Cell styles (when unset, fall back along the inheritance chain) —— fill?: Fill; // background fill; supports solid / gradient / image border?: BorderSpec; align?: Alignment; // —— Merging —— rowSpan?: number; // default: 1 colSpan?: number; // default: 1 } ``` **Basic example (using theme styles):** ```yaml - elementId: table-basic elementType: table bounds: [80, 120, 800, 280] columnWidths: [0.3, 0.35, 0.35] rowHeights: [0.33, 0.33, 0.34] style: "$default" rows: - - text: "Metric" - text: "2023" - text: "2024" - - text: "Revenue (100M CNY)" - text: "82.5" - text: "96.3" - - text: "Net profit (100M CNY)" - text: "12.1" - text: "15.8" ``` > **Merged-cell rules:** `rowSpan` / `colSpan` declare the merge range; **cells covered by the merged region are omitted from the `rows` array, with no `null` placeholder needed**. For example, after a top-left 2×2 merge, row 0's colSpan=2 covers (0,1), so that row only has two items ((0,0) merged cell + (0,2)); row 1 has (1,0) and (1,1) occupied by the merge, so it only has one item, (1,2). ```yaml - elementId: table-merged elementType: table bounds: [100, 100, 600, 400] columnWidths: [0.33, 0.33, 0.34] rowHeights: [0.33, 0.33, 0.34] rows: # Row 0: top-left 2×2 merge + C1. The merged (0,1) is omitted - - text: "Merged cell" fill: {type: solid, color: "$accent"} rowSpan: 2 colSpan: 2 - text: "C1" # Row 1: (1,0) and (1,1) are occupied by the merge → only C2 remains - - text: "C2" # Row 2: full three columns - - text: "A3" - text: "B3" - text: "C3" ``` --- ### Chart (charts) PPTD v2's chart element follows the ECharts philosophy: **the chart top level carries no `type` field**; each `series[i].type` determines its own form. 13 series types are supported in total, laid out flat by type name, all equal in status: `bar` / `line` / `area` / `scatter` / `bubble` / `candlestick` / `pie` / `radar` / `waterfall` / `heatmap` / `treemap` / `sunburst` / `sankey` Each type declares its **series constraint** on the first line of its own subsection: the maximum count allowed within the same chart + which other types it may coexist with. #### Chart ```ts interface Chart extends ElementBase { elementType: "chart"; data: ChartData; // required series: SeriesConfig[]; // required; constraint: length ≥ 1 seriesDefaults?: SeriesDefaults; // default: not applied; common defaults grouped by series.type, merged with each series // —— Cartesian coordinate system (conditionally effective by series.type, see §5.3) —— xAxis?: AxisConfig | AxisConfig[]; // default: auto-adapt to data; in array form, referenced via series[i].xAxisIndex yAxis?: AxisConfig | AxisConfig[]; // default: auto-adapt to data; in array form, referenced via series[i].yAxisIndex barWidth?: number; // default: adaptive; constraint: (0, 1]; bar width / category slot width ratio barGap?: number; // default: 0 (flush); constraint: [0, 1); gap between bars when multiple bar series are grouped categoryGap?: number; // default: 0.2; constraint: [0, 1); blank ratio between category slots // —— Radar coordinate system (radar series only) —— spokeAxis?: SpokeAxisConfig; // default: auto-adapt to data; spoke axes + spider grid // —— Global components —— title?: string | TitleConfig; // default: no title legend?: boolean | LegendConfig; // default: varies by type (see the default-value table in [LegendConfig](#legendconfig)) dataLabels?: DataLabelConfig; // default: not applied; global default, can be overridden by series.dataLabels fontFamily?: FontFamily; // default: falls back along the theme/master fonts // —— Chart frame (controls the rectangular container of the whole chart element, independent of series colors) —— fill?: Fill; // default: not applied border?: Border; // default: not applied shadow?: Shadow; // default: not applied } type SeriesConfig = | BarSeries | LineSeries | AreaSeries | ScatterSeries | BubbleSeries | CandlestickSeries | PieSeries | RadarSeries | WaterfallSeries | HeatmapSeries | TreemapSeries | SunburstSeries | SankeySeries; ``` > `fill` / `border` / `shadow` control the **chart element's rectangular frame** (acting on the whole chart container), independent of the series body colors. > > **PowerPoint limitation:** native charts cannot be rotated/flipped as a whole; there is also no single global opacity property covering "the whole chart including title, axes, legend, labels, and series". When whole rotation/flip/opacity is needed, render as an image first and treat it as an [Image](#image-image) element. #### ChartData ```ts interface ChartData { cols: string[]; // column names; constraint: unique, non-empty strings rows: (number | string | null)[][]; // constraint: each row's length = cols.length } ``` > **Data integrity constraints** (validated by the checker): > - Duplicate column names in `cols` → `DuplicateColumnError` > - `cols` contains an empty string → `EmptyColumnError` > - `rows[i].length !== cols.length` → `RowLengthError` > - A column name referenced by encode is not in `cols` → `UnknownColumnError` > - It is legal for the same column to be referenced by multiple series (e.g., the same y column drawn once by bar and once by line) > - When a column value of a numeric channel (`y` / `value` / `open` / `high` / `low` / `close` / `size` / `flow`) is a string, it is parsed as a number; on failure, `NonNumericValueError` is raised > > **How to write missing cells**: fill with `null`, e.g. `[null, null, 2, 3]`. Consecutive commas `[, , 2, 3]` are **not recommended** — strict YAML parsers will error. #### General Rules 1. **The `fill` type of series**: `Color | GradientFill`; a string is treated as a solid [Color](#color) (HEX8 or a `$xxx` theme reference), an object as [GradientFill](#fill) (with `type: "gradient"`); some types support the `(Color | GradientFill)[]` array form (cycled by slice/node). **Series-level fill does not support [ImageFill](#fill)**. 2. **Type mixing**: which types a chart's `series[]` may contain is determined by the "series constraint" on the first line of each type's section; the checker validates accordingly. 3. **Conditionally effective top-level fields**: `xAxis` / `yAxis` / `barWidth` / `barGap` / `categoryGap` / `spokeAxis` are **coordinate-system-level** configurations, conditionally effective based on the `series[].type` set (see [5.3 Applicability of chart top-level fields](#53-applicability-of-chart-top-level-fields) for details). 4. **[Color](#color) theme reference scope**: all fields of type `Color` (including every Color position inside nested arrays and objects) support `$xxx` theme references — e.g. `upBars: {fill: "$success"}`, `colorScheme: ["$bg", "$primary"]`, `fill: ["$primary", "$accent"]`. 5. **Omission semantics of optional object-type fields**: all **object-type** fields marked with `?` (`xAxis` / `yAxis` / `spokeAxis` / `colorScale` / `marker` / `dataLabels`, etc.): when omitted, they are equivalent to an empty configuration `{}` of that object, and all sub-fields take their own default values — i.e., "axes/grids/labels etc. still render by default, just with automatically inferred parameters". This differs from `fill` / `border` / `shadow` of [ElementBase](#elementbase) (where omission means **not applied**). > **bar / waterfall direction**: determined by the axis type — vertical (default) when `xAxis.type === "category"`, horizontal when `yAxis.type === "category"`. `axis.type` is inferred from the data column by default (string → category, number → value); when a numeric column needs to be used as categories (e.g. years), override explicitly with `axis.type: "category"`. In the horizontal case, `encode.x` references the numeric column and `encode.y` the category column, and `numberFormat` is written on the side where the value axis is. For scatter / bubble, both x and y are numeric channels, with no notion of direction. --- #### TextStyle ```ts interface TextStyle { color?: Color; // default: falls back along the inheritance chain (theme text color / PPTX master) fontSize?: number; // default: auto-adapts to chart size fontFamily?: FontFamily; // default: falls back along the inheritance chain to Chart.fontFamily or the theme font } ``` > The common trio of text styles, inherited and reused by [TitleConfig](#titleconfig) / [LegendConfig](#legendconfig) / [DataLabelConfig](#datalabelconfig) / [AxisConfig](#axisconfig).label / [SpokeAxisConfig](#spokeaxisconfig).label; for the font priority chain, see [§3.2](#3-chart-styles). #### LineStyleConfig ```ts interface LineStyleConfig { style?: "solid" | "dash" | "dot"; // default: "solid" color?: Color; // default: falls back to the theme width?: number; // default: 1 } ``` > Generic line style, reused by the `axisLine` / `gridLine` of [AxisConfig](#axisconfig) / [SpokeAxisConfig](#spokeaxisconfig). #### TitleConfig ```ts interface TitleConfig extends TextStyle { text: string; // required // fontSize auto-adapts to chart size by default } ``` #### LegendConfig ```ts interface LegendConfig extends TextStyle { show?: boolean; // default: varies by type (see the table below) position?: "top" | "bottom" | "left" | "right"; // default: "bottom" } ``` `show` defaults by type: | type | Default | |---|---| | bar / line / area / scatter / bubble / candlestick / pie / radar | `true` | | waterfall | `false` | | treemap / sunburst / sankey | `false` (names and values are already shown on the chart) | | heatmap | Does not use `chart.legend` (controlled by [series.colorbar](#heatmap)) | > `legend: false` or `legend: {show: false}` turns it off, **effective for all 13 types**; `legend: true` or the object form only has a visual effect for the types marked applicable in the table above. #### DataLabelConfig ```ts interface DataLabelConfig extends TextStyle { show?: boolean; // default: false content?: "value" | "percentage" | "category"; // default: varies by type (see [5.5 value quick reference](#55-datalabelscontent-value-quick-reference)) numberFormat?: string; // default: no formatting; Excel number-format string (see below) } ``` > **numberFormat standard**: takes a subset of Excel number-format strings — `0` (integer) / `0.0` (one decimal) / `0%` (percentage) / `0.0%` (percentage with decimals) / `#,##0` (thousands separator) / `0.0E+00` (scientific notation). Advanced syntax such as `[Red]` color sections, negative sections, and conditional formatting is **not supported**. #### MarkerConfig ```ts interface MarkerConfig { shape?: "circle" | "rect" | "diamond" | "triangle"; // default: "circle" fill?: Color | GradientFill; // default: follows the series body color border?: Border; // default: not applied size?: number; // default: auto-adapts to chart size; unit px } ``` > The `rect` naming is consistent with the [shapes.md](./shapes.md) shape library. #### AxisConfig ```ts interface AxisConfig { show?: boolean; // default: true type?: "category" | "value"; // default: inferred from the data column (string → category, number → value) min?: number; // default: auto-adapt to data; only effective for value axes max?: number; // default: auto-adapt to data; only effective for value axes reverse?: boolean; // default: false; true = reverse the axis direction (maximum at the origin side) title?: string | TitleConfig; // default: no title; the string form is recommended, use the object only for special styling label?: boolean | (TextStyle & { // default: true; tick labels numberFormat?: string; // default: no formatting; only effective for value axes }); axisLine?: boolean | (LineStyleConfig & { // default: true arrow?: boolean | "start" | "end" | "both"; // default: false; true is equivalent to "end" }); gridLine?: boolean | LineStyleConfig; // default: true } ``` #### SeriesDefaults ```ts interface SeriesDefaults { bar?: Partial>; line?: Partial>; area?: Partial>; scatter?: Partial>; bubble?: Partial>; candlestick?: Partial>; radar?: Partial>; } ``` > Provides common default values for all series of that type, avoiding repetition across multiple series. For the merge algorithm and the range of usable types, see [§3.4](#3-chart-styles). #### SpokeAxisConfig Used only by [radar](#radar) series. ```ts interface SpokeAxisConfig { show?: boolean; // default: true min?: number; // default: 0; minimum of the value axis shared by all dimensions max?: number; // default: auto-adapt to data; maximum of the value axis shared by all dimensions label?: boolean | (TextStyle & { // default: true; tick labels numberFormat?: string; // default: no formatting }); axisLine?: boolean | LineStyleConfig; // default: true; spoke lines from the center to the outer ring gridLine?: boolean | LineStyleConfig; // default: true; spider grid lines (concentric polygons connecting the spoke endpoints) } ``` #### LinearSeriesBase Curve-class common fields shared by [line](#line) / [area](#area) / [radar](#radar). ```ts interface LinearSeriesBase { smooth?: boolean; // default: false lineStyle?: "solid" | "dash" | "dot"; // default: "solid" width?: number; // default: 2 marker?: false | MarkerConfig; // default: not applied nullHandling?: "zero" | "gap" | "connect"; // default: "gap" for line/area, "connect" for radar lineColor?: Color | GradientFill; // default: follows the theme color cycle; line color of line / polygon stroke color of area+radar } ``` > If multiple line/area/radar series within the same chart set different `nullHandling` values, only the **first non-empty value** takes effect and the other series follow; multiple null-handling methods are not supported. --- #### bar > **series constraint**: may be freely mixed with `line / area / scatter / bubble`; may also mix with `candlestick`; no limit on the number of bar series in the same chart. ```ts interface BarSeries { type: "bar"; encode: { x: string; y: string }; // required name?: string; // default: the encode.y column name; for legend display only xAxisIndex?: number; // default: 0; meaningful only when chart.xAxis is an array yAxisIndex?: number; // default: 0; meaningful only when chart.yAxis is an array stack?: "value" | "percent"; // default: no stacking; "value" sums directly, "percent" normalizes to 100% symbol?: ShapeDef; // default: normal rectangular bar; pictographic bar shape definition (see [ShapeDef](#image-image)) fill?: Color | GradientFill; // default: follows the theme color cycle border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` #### line > **series constraint**: may be freely mixed with `bar / area / scatter / bubble`; may also mix with `candlestick`. ```ts interface LineSeries extends LinearSeriesBase { type: "line"; encode: { x: string; y: string }; // required name?: string; // default: the encode.y column name xAxisIndex?: number; // default: 0 yAxisIndex?: number; // default: 0 dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` > line has no area, so `fill` is not provided; for the other curve-class fields, see [LinearSeriesBase](#linearseriesbase). #### area > **series constraint**: may be freely mixed with `bar / line / scatter / bubble`; may also mix with `candlestick`. ```ts interface AreaSeries extends LinearSeriesBase { type: "area"; encode: { x: string; y: string }; // required name?: string; // default: the encode.y column name xAxisIndex?: number; // default: 0 yAxisIndex?: number; // default: 0 stack?: "value" | "percent" | "stream"; // default: no stacking; "stream" = streamgraph (area only) areaColor?: Color | GradientFill; // default: derived from lineColor as semi-transparent dataLabels?: DataLabelConfig; // default: not shown } ``` > **Stacking group rules**: within the same chart, **all series of the same type that set `stack` are automatically grouped into one stack**, with no explicit group identifier needed. At most one stack group of the same type is supported within the same chart — all series that set `stack` must use the same value (all `"value"` / all `"percent"` / all `"stream"`); mixing raises `StackModeMismatchError`; series without `stack` display independently. If multiple independent stack groups are needed, split them into multiple chart elements. > > **`stream` applies only to area**: `value` normalization + central baseline offset; the stacked region is symmetric above and below y=0, taking a "streamgraph" shape. #### scatter > **series constraint**: may be freely mixed with `bar / line / area / bubble`. ```ts interface ScatterSeries { type: "scatter"; encode: { x: string; y: string }; // required; each series references its own x/y column pair name?: string; // default: the encode.y column name yAxisIndex?: number; // default: 0 dataFilter?: { col: string; value: string | number }; // default: no filtering; optional: group with a long table marker?: MarkerConfig; // default: {shape: "circle"}; constraint: cannot be false fill?: Color | GradientFill; // default: follows the theme color cycle; serves as the marker's default fill color (marker.fill takes precedence) border?: Border; // default: not applied; serves as the marker's default border (marker.border takes precedence) dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` #### bubble > **series constraint**: may be freely mixed with `bar / line / area / scatter`. ```ts interface BubbleSeries { type: "bubble"; encode: { x: string; y: string; size: string }; // required name?: string; // default: the encode.y column name yAxisIndex?: number; // default: 0 dataFilter?: { col: string; value: string | number }; // default: no filtering; rows where the col column equals value are used as this series' data sizeScale?: "linear" | "sqrt" | "log"; // default: "sqrt" sizeRange?: [number, number]; // default: auto-adapts to chart size; bubble radius range in px fill?: Color | GradientFill; // default: follows the theme color cycle; bubble fill color border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` > **sizeScale**: `sqrt` (default) makes the area proportional to size; `linear` makes the radius proportional to size; `log` suits scenarios with order-of-magnitude differences. Negative size is treated as 0. For multiple groups, use a wide table + null padding, with each series referencing its own `x/y/size` column triple. #### candlestick > **series constraint**: may only mix with `bar / line / area` (common usage: candlestick body + a line overlaying the MA moving average). ```ts interface CandlestickSeries { type: "candlestick"; /** * encode.open is optional → determines the rendering mode * open provided → OHLC candlestick (rendered with 4 series; a solid body expresses the open-close direction) * open omitted → HLC high-low-close (rendered with 3 series; a vertical line + dot marker at close, no body) */ encode: { x: string; high: string; low: string; close: string; open?: string }; xAxisIndex?: number; // default: 0 yAxisIndex?: number; // default: 0 upBars?: { fill?: Color; border?: Border }; // rising bar (close > open) style; only effective in OHLC mode (HLC has no body) downBars?: { fill?: Color; border?: Border }; // falling bar (close ≤ open) style; only effective in OHLC mode wickStyle?: Border; // wick (high-low vertical line) style; common to HLC / OHLC } ``` > > **Date column handling**: a date column (e.g. `"2024-01-01"`) is treated as string categories, laid out at equal intervals on the x-axis in the order they appear in `rows`, naturally skipping non-trading days. If precise layout by real date intervals is needed, manually padding empty trading days with null rows is recommended. #### pie > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface PieSeries { type: "pie"; encode: { category: string; value: string }; // required innerRadius?: number; // default: 0; constraint: [0, 1]; > 0 = donut startAngle?: number; // default: 0 (12 o'clock direction) fill?: Color | GradientFill | (Color | GradientFill)[]; // default: follows the theme color cycle; an array cycles by slice border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content takes "value" | "percentage" | "category", default "value" } ``` > **Angle direction**: fixed **clockwise** as positive; 0° = 12 o'clock position, 90° = 3 o'clock, 180° = 6 o'clock, 270° = 9 o'clock. #### radar > **series constraint**: multiple radar series are allowed in the same chart (sharing one set of spokes), but the type of all series must be `radar`; it may not coexist with other types. ```ts interface RadarSeries extends LinearSeriesBase { type: "radar"; encode: { category: string; y: string }; // required; the category column holds the spoke labels name?: string; // default: the encode.y column name areaColor?: Color | GradientFill; // default: derived from lineColor as semi-transparent; polygon fill color dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` > The radar chart's spoke axis lines, spider grid, and value range (min/max) are uniformly configured via the chart top-level [spokeAxis](#spokeaxisconfig), shared by multiple series. > > **Dimension-column sharing constraint**: all radar series within the same chart must reference the same `category` column (i.e., all polygons share the same set of spoke labels). To display radar charts with different spoke labels, use multiple chart elements. Checker validation: the `encode.category` of all radar series must be identical. #### waterfall > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface WaterfallSeries { type: "waterfall"; encode: { x: string; // category column y: string; // value column (floating bars hold the increase/decrease amounts; total columns hold the absolute value of the cumulative total) isTotal?: string; // default: omitted = all floating bars; after specifying a bool column, true = total column (opening/subtotal/closing), false/null = floating bar }; totalBars?: { fill?: Color; border?: Border }; // total column (opening/subtotal/closing, isTotal=true) style increaseBars?: { fill?: Color; border?: Border }; // floating increase bar (y > 0) style decreaseBars?: { fill?: Color; border?: Border }; // floating decrease bar (y < 0) style dataLabels?: DataLabelConfig; // default: not shown; content takes "value" | "category", default "value" } ``` > **Colors**: waterfall does not use `fill`; colors are mapped through the three categories `totalBars` / `increaseBars` / `decreaseBars` by isTotal and the sign of y; all total columns (opening/subtotal/closing) share `totalBars`. > > **isTotal semantics**: it may appear at any position (first row / middle subtotal / last row are all legal); every `isTotal=true` is an independent total column whose y value should equal "previous total column's y + the sum of all intermediate floating bars' y" — on mismatch, the checker outputs `WaterfallTotalMismatchWarning` (the first-row total column's y is defined directly). The `isTotal` column only accepts bool or null; the string `"true"` or the number `1` both raise `InvalidValueError`. #### heatmap > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface HeatmapSeries { type: "heatmap"; encode: { x: string; y: string; value: string }; // required; the x and y columns must be categories (string), value is numeric colorScheme?: Color[]; // default: falls back to the theme; gradient color-scale endpoints colorScale?: { type?: "linear" | "diverging"; // default: "linear" domain?: [number, number]; // default: data range; for type=diverging, default [-max(|v|), +max(|v|)], 0 centered }; colorbar?: boolean | LegendConfig; // default: true; color-scale bar legend; position default "right" dataLabels?: DataLabelConfig; // default: not shown; content only takes "value" } ``` > **Colors**: `colorScheme` serves as gradient endpoints, interpolated per `colorScale.type`: > - `linear`: `colorScheme` length ≥ 2, interpolated between the endpoints; > - `diverging`: `colorScheme` length = 3 (low / mid / high); the midpoint is determined by the middle of `domain`, often used in "negative-neutral-positive" scenarios. > > **Data layout**: the x / y columns are fixed as categories (string); category order on the axes follows first-appearance order in `rows`; (x, y) combinations that do not appear in `rows` are treated as missing cells, rendered transparent (the background color in PPTX). For a complete matrix, explicitly listing all (x, y) combinations and using null for missing values is recommended. > > heatmap does not use `chart.legend`; it is replaced by `colorbar`; `colorbar: false` turns off the color-scale bar. #### treemap > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface TreemapSeries { type: "treemap"; encode: { category: string; // node-name column value: string; // value column parent?: string; // parent-node column; null/missing/empty = root node (multiple roots allowed) }; levels?: number; // default: show all levels fill?: Color | GradientFill | (Color | GradientFill)[] | (Color | GradientFill)[][]; // default: follows the theme color cycle; see "color derivation rules" below border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content takes "value" | "category", default "category" } ``` > **Color derivation rules**: > - `fill: Color | GradientFill` (single value): all root nodes share this color; child nodes are derived by decreasing lightness by 10% per level (along the HSL.L dimension). > - `fill: (Color | GradientFill)[]` (1-D array): cycles in the order the root nodes appear; each root node's child nodes are derived by decreasing lightness by 10% per level. > - `fill: (Color | GradientFill)[][]` (2-D array): the outer dimension cycles by root node, the inner dimension specifies levels directly (bypassing automatic derivation); if an inner array is not long enough to cover all levels, the remaining levels are still derived by decreasing lightness by 10%. #### sunburst > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface SunburstSeries { type: "sunburst"; encode: { category: string; value: string; parent?: string }; // required levels?: number; // default: show all levels fill?: Color | GradientFill | (Color | GradientFill)[]; // default: follows the theme color cycle; an array cycles by top-level node border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content takes "value" | "category", default "category" } ``` #### sankey > **series constraint**: the `series` array may only have 1 element, and may not coexist with other types. ```ts interface SankeySeries { type: "sankey"; encode: { source: string; // source-node column target: string; // target-node column flow: string; // flow column }; nodeAlign?: "left" | "right" | "justify"; // default: "justify" fill?: Color | GradientFill // default: follows the theme color cycle (colors picked in node topological order) | (Color | GradientFill)[] // an array cycles by node | Record; // mapped by node name; unspecified nodes fall back to the theme color cycle border?: Border; // default: not applied dataLabels?: DataLabelConfig; // default: not shown; content takes "value" | "category", default "value" } ``` > **Graph constraint**: sankey is restricted to a **directed acyclic graph (DAG)**; when the source/target columns form a cycle, the checker raises `CyclicGraphError`. > > **Node order**: the `source` and `target` columns are deduplicated and arranged in topological order. When `fill` is an array, it cycles in this order; when the array length < node count, it wraps around and reuses; when > node count, it truncates. The object form matches by node name exactly. --- #### Field Quick Reference ##### 5.1 Data encode Channels | type | encode channels | |---|---| | bar / line / area | `x` + `y` | | scatter | `x` + `y` (multi-series use a wide table + null padding) | | bubble | `x` + `y` + `size` (multi-series use a wide table + null padding) | | candlestick | Candlestick: `x` + `open` + `close` + `low` + `high`; overlay: `x` + `y` | | pie | `category` + `value` | | radar | `category` + `y` | | waterfall | `x` + `y` + optional `isTotal` (bool column) | | heatmap | `x` + `y` + `value` | | treemap / sunburst | `category` + `value` + optional `parent` | | sankey | `source` + `target` + `flow` | > **Naming convention**: Cartesian coordinate systems (bar/line/area/scatter/bubble/candlestick/waterfall/heatmap) use `x` / `y` for the horizontal/vertical axes; non-Cartesian category fields are uniformly `category` (pie/radar/treemap/sunburst); sankey graph edge endpoints use `source` / `target`. ##### 5.2 Color Mechanism | type | Color fields | Description | |---|---|---| | bar | `series[].fill` | Bar body fill color | | line | `series[].lineColor` | Line color (no area) | | area | `series[].lineColor` + `series[].areaColor` | Stroke color + area color (when the area color is omitted, it is derived from lineColor as semi-transparent) | | radar | `series[].lineColor` + `series[].areaColor` | Polygon stroke + fill (same as area) | | scatter | `series[].fill` / `marker.fill` | Series level is the marker default color; `marker.fill` takes precedence | | bubble | `series[].fill` | Bubble fill color | | candlestick | `upBars` / `downBars` (body fill+border) + each overlay series' own `lineColor`/`fill` | The candlestick body maps by up/down; overlay line/bar use their own colors | | pie / sunburst / sankey | The single series' `fill` array, cycled by data point/node | Array length cycles and reuses | | treemap | The single series' `fill` (single value / 1-D array / 2-D array) | Same as pie etc.; child nodes decrease from the parent along the HSL.L dimension (`L_new = max(0, L_old - 10)`) | | heatmap | `series[].colorScheme` + `series[].colorScale` | The gradient color scale maps by value; `linear` interpolates between the endpoints, `diverging` aligns three colors at the midpoint | | waterfall | `series[].totalBars` / `increaseBars` / `decreaseBars` | Mapped into three classes — total (total columns) / increase / decrease; does not participate in the theme color cycle | ##### 5.3 Applicability of Chart Top-Level Fields | Top-level field | Applicable series types | |---|---| | `xAxis` | bar / line / area / scatter / bubble / candlestick / waterfall / heatmap | | `yAxis` | bar / line / area / scatter / bubble / candlestick / waterfall / heatmap | | `barWidth` / `barGap` | bar / waterfall (bar layout parameters) | | `categoryGap` | bar / candlestick / waterfall (category spacing parameter) | | `spokeAxis` | radar (includes spoke axis lines + spider grid + min/max) | | `legend` | bar / line / area / scatter / bubble / candlestick / pie / radar / waterfall | | `title` | All | | `dataLabels` | All (candlestick: only effective for overlay series; the candlestick body itself expresses the up/down roles via upBars/downBars) | | `fontFamily` | All | > **Axis single-value vs array rules**: a secondary axis is always placed on the **side where the value axis is** — vertical charts use a `yAxis` array + `yAxisIndex`, horizontal charts use an `xAxis` array + `xAxisIndex`. When any series uses `xAxisIndex > 0` / `yAxisIndex > 0`, the corresponding `xAxis` / `yAxis` must be an array (length ≥ max(index) + 1). ##### 5.4 Type-Mixing Compatibility Quick Reference ``` bar / line / area / scatter / bubble may coexist with each other freely; candlestick may only coexist with bar / line / area ``` > The other 7 types each exclusively own the series array; see the first line of the corresponding section for detailed constraints. ##### 5.5 dataLabels.content Value Quick Reference | type | Allowed values | Default | |---|---|---| | bar / line / area / scatter / bubble / radar / heatmap | `value` | `value` | | candlestick (overlay series only) | `value` | `value` | | pie | `value` / `percentage` / `category` | `value` | | waterfall | `value` / `category` | `value` | | treemap / sunburst | `value` / `category` | `category` | | sankey | `value` / `category` | `value` | > Writing a value outside this table → the checker raises `InvalidValueError`. The candlestick body itself does not support dataLabels. --- #### Examples **Bar chart (stacked)** ```yaml - elementId: c1 elementType: chart bounds: [50, 100, 600, 400] data: cols: [quarter, revenue, cost] rows: - [Q1, 120, 220] - [Q2, 132, 182] - [Q3, 101, 191] - [Q4, 134, 234] seriesDefaults: bar: {stack: value} series: - type: bar encode: {x: quarter, y: revenue} name: Revenue fill: "$primary" - type: bar encode: {x: quarter, y: cost} name: Expenses fill: "$accent" ``` **Line chart (multi-series differentiation)** ```yaml - elementId: c2 elementType: chart bounds: [50, 100, 600, 400] data: cols: [month, actual, target, baseline] rows: - [Jan, 72, 65, 50] - [Feb, 85, 70, 50] - [Mar, null, 78, 50] - [Apr, 90, 82, 50] yAxis: {min: 0, max: 100, gridLine: {color: "#f0f0f0"}} series: - type: line encode: {x: month, y: actual} name: Actual lineColor: "#5470c6" lineStyle: solid width: 3 smooth: true - type: line encode: {x: month, y: target} name: Target lineColor: "#ee6666" lineStyle: dash smooth: true - type: line encode: {x: month, y: baseline} name: Baseline lineColor: "#999999" lineStyle: dot width: 1 marker: false ``` **Area chart (stream stacking)** ```yaml - elementId: c3 elementType: chart bounds: [50, 80, 700, 400] title: Traffic Evolution by Channel data: cols: [week, web, app, partner] rows: - [W1, 200, 120, 80] - [W2, 240, 160, 90] - [W3, 260, 200, 110] - [W4, 280, 240, 130] seriesDefaults: area: {stack: stream} series: - type: area encode: {x: week, y: web} name: Web areaColor: "#5470c6" - type: area encode: {x: week, y: app} name: App areaColor: "#91cc75" - type: area encode: {x: week, y: partner} name: Partner areaColor: "#fac858" ``` **Bubble chart (multi-series grouping)** ```yaml - elementId: c5 elementType: chart bounds: [50, 80, 700, 420] title: User Distribution xAxis: {title: Age} yAxis: {title: "Annual income (10K)"} data: cols: [age_s, inc_s, pop_s, age_w, inc_w, pop_w, age_m, inc_m, pop_m] rows: - [22, 5, 120, 28, 12, 380, 45, 40, 180] - [null, null, null, 35, 25, 260, 52, 60, 90] seriesDefaults: bubble: sizeScale: sqrt sizeRange: [8, 48] series: - type: bubble encode: {x: age_s, y: inc_s, size: pop_s} name: Students fill: "#5470c6" - type: bubble encode: {x: age_w, y: inc_w, size: pop_w} name: White-collar fill: "#91cc75" - type: bubble encode: {x: age_m, y: inc_m, size: pop_m} name: Management fill: "#ee6666" ``` **Candlestick chart (with MA5 overlay line)** ```yaml - elementId: c6 elementType: chart bounds: [50, 80, 700, 420] title: Stock Price Trend data: cols: [date, open, high, low, close, ma5] rows: - ["2024-01-01", 100, 110, 95, 108, null] - ["2024-01-02", 108, 115, 105, 112, null] - ["2024-01-03", 112, 118, 109, 116, null] - ["2024-01-04", 116, 120, 110, 113, null] - ["2024-01-05", 113, 117, 108, 115, 112.8] yAxis: {title: Price} series: - type: candlestick encode: {x: date, open: open, close: close, low: low, high: high} upBars: {fill: "#ee6666"} downBars: {fill: "#5470c6"} - type: line encode: {x: date, y: ma5} name: MA5 smooth: true width: 2 lineColor: "#fac858" ``` **Waterfall chart** ```yaml - elementId: c9 elementType: chart bounds: [50, 80, 700, 380] title: Cash Flow Waterfall data: cols: [phase, amount, total] rows: - [Opening balance, 500, true] - [Sales revenue, 300, null] - [Operating expenses, -180, null] - [Taxes, -60, null] - [Closing balance, 560, true] series: - type: waterfall encode: {x: phase, y: amount, isTotal: total} totalBars: {fill: "#5470c6"} increaseBars: {fill: "#91cc75"} decreaseBars: {fill: "#ee6666"} dataLabels: {show: true} ``` **Heatmap** ```yaml - elementId: c10 elementType: chart bounds: [50, 80, 700, 420] title: User Activity Heatmap data: cols: [hour, day, count] rows: - ["00:00", Mon, 5] - ["00:00", Tue, 8] - ["06:00", Mon, 22] - ["12:00", Mon, 45] series: - type: heatmap encode: {x: hour, y: day, value: count} colorScheme: ["#ffffff", "#5470c6"] colorScale: {domain: [0, 50]} ``` **Treemap (with hierarchy)** ```yaml - elementId: c11 elementType: chart bounds: [50, 80, 700, 420] title: Budget Allocation data: cols: [dept, parentDept, budget] rows: - [Engineering, null, 1000] - [Frontend, Engineering, 400] - [Backend, Engineering, 600] - [Sales, null, 800] series: - type: treemap encode: {category: dept, value: budget, parent: parentDept} fill: ["#5470c6", "#91cc75"] ``` **Sankey diagram** ```yaml - elementId: c13 elementType: chart bounds: [50, 80, 800, 420] title: User Conversion Funnel data: cols: [from, to, users] rows: - [Ad campaign, Landing page, 10000] - [Ad campaign, Direct search, 4000] - [Landing page, Sign-up, 3000] - [Landing page, Drop-off, 7000] - [Sign-up, First order, 1200] - [Direct search, First order, 2000] series: - type: sankey encode: {source: from, target: to, flow: users} nodeAlign: justify fill: ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de"] ``` **seriesDefaults + mixing: bar default width + line default smoothing** ```yaml - elementId: c15 elementType: chart bounds: [50, 80, 700, 420] data: cols: [month, sales, growth] rows: - [Jan, 120, 0.10] - [Feb, 150, 0.25] - [Mar, 180, 0.20] yAxis: - {title: Sales} - {title: Growth rate, label: {numberFormat: "0%"}} seriesDefaults: bar: fill: "#5470c6" line: smooth: true width: 2 lineColor: "#ee6666" barWidth: 0.6 series: - type: bar encode: {x: month, y: sales} name: Sales - type: line encode: {x: month, y: growth} name: Growth rate yAxisIndex: 1 ``` **Horizontal bar chart (category axis on the y side)** ```yaml - elementId: c16 elementType: chart bounds: [50, 80, 600, 360] title: Headcount by Department data: cols: [dept, headcount] rows: - [Engineering, 120] - [Product, 45] - [Design, 30] - [Operations, 60] xAxis: {label: {numberFormat: "#,##0"}} yAxis: {label: {fontSize: 12}} series: - type: bar encode: {x: headcount, y: dept} fill: "$primary" dataLabels: {show: true} ```