/** * Chart - High-level chart object for worksheet embedding. * * Similar to Image, a Chart belongs to a Worksheet and carries the * structural data needed for both the DrawingML anchor and the * standalone chart XML part. */ import { Anchor, type AnchorModel } from "../anchor.js"; import type { ChartExModel } from "./chart-ex-types.js"; import type { AddChartRange, ChartModel, ChartStyleModel, ChartColorsModel, ChartTypeGroup, ChartAxis, ChartLegend, PlotArea, ShapeProperties, ChartRichText, SeriesBase, AddChartSeriesOptions } from "./types.js"; import type { Worksheet } from "../worksheet.js"; import { buildChartModel } from "./chart-builder.js"; import { type ChartRenderOptions } from "./chart-renderer.js"; /** * Relationship entry written to `chart{N}.xml.rels` / `chartEx{N}.xml.rels`. * Kept as a lightweight structural record (not a full rels type) so the * round-trip path works uniformly with what the xform reader/writer emits. */ export interface ChartRelEntry { /** `rId…` identifier referenced from the chart part XML. */ Id: string; /** Relationship type URI (e.g. the user-shapes / style / colours type). */ Type: string; /** Target path, relative to the chart part. */ Target: string; /** Optional mode for external targets (`"External"`). */ TargetMode?: string; } /** * Internal model stored on the Workbook for each chart. * Contains the fully-parsed (or programmatically created) chart data * plus ancillary style/colors files. */ export interface ChartEntry { /** 1-based chart number (matches chart{N}.xml) */ chartNumber: number; /** Full chart model */ model: ChartModel; /** Original chart XML bytes from a loaded workbook, used for clean round-trip passthrough */ rawData?: Uint8Array; /** JSON snapshot of `model` taken when `rawData` was parsed */ modelSnapshot?: string; /** True once a high-level API mutates the parsed chart model */ dirty?: boolean; /** When true, simple high-level mutations may patch raw XML instead of full re-render. */ preferRawPatch?: boolean; /** When true, writing fails instead of re-rendering if raw XML cannot be safely patched. */ requireRawPatch?: boolean; /** Chart style (styleN.xml) — raw XML for round-trip */ style?: ChartStyleModel; /** Chart colors (colorsN.xml) — raw XML for round-trip */ colors?: ChartColorsModel; /** Chart rels (chart{N}.xml.rels) — entries for round-trip */ rels?: ChartRelEntry[]; /** * Raw bytes of the user-shapes drawing part targeted by this chart's * `c:userShapes r:id="…"` reference. OOXML stores annotation shapes * (arrows, callouts, text boxes the user drew on top of the chart) * in a separate `xl/drawings/drawingN.xml` part that uses relative * anchors instead of the regular `xdr:twoCellAnchor` schema. The * full DrawingML subsystem is out of scope for this library, so the * bytes are kept verbatim for round-trip and exposed programmatically * via {@link Chart.userShapesXml} so callers can inject / replace the * drawing part if they need to. */ userShapesXml?: Uint8Array; } /** * Stored entry for a structured ChartEx (Office 2016+ extended chart). * When a ChartEx is created programmatically via `addChartEx()`, a structured * model is stored here and serialised through the builder/renderer on write. * When a ChartEx is round-tripped, raw bytes are used instead (stored under * `workbook._chartExEntries`). */ export interface ChartExEntry { /** 1-based chartEx number (matches chartEx{N}.xml) */ chartExNumber: number; /** Structured model (built from addChartEx options) */ model: ChartExModel; /** Original chartEx XML bytes from a loaded workbook, used for clean round-trip passthrough */ rawData?: Uint8Array; /** JSON snapshot of `model` taken when `rawData` was parsed */ modelSnapshot?: string; /** True once a high-level API mutates the parsed chartEx model */ dirty?: boolean; /** When true, simple high-level mutations may patch raw ChartEx XML instead of full re-render. */ preferRawPatch?: boolean; /** When true, writing fails instead of re-rendering if raw ChartEx XML cannot be safely patched. */ requireRawPatch?: boolean; /** ChartEx rels — preserved for round-trip */ rels?: ChartRelEntry[]; } /** * The range a chart occupies on a worksheet. */ interface ChartAnchorRange { /** Top-left anchor (always present) */ tl: Anchor; /** Bottom-right anchor (only for twoCellAnchor) */ br?: Anchor; /** Absolute position in EMU (only for absoluteAnchor) */ pos?: { x: number; y: number; }; /** Extent in EMU (for oneCellAnchor and absoluteAnchor) */ ext?: { cx: number; cy: number; }; /** Anchor behaviour: oneCell, twoCell, or absolute */ editAs?: string; } /** * A chart embedded in a worksheet. * * Charts come in two flavours: * * - **Classic** (`chartNumber` set): a fully-parsed `c:chart` with a * {@link ChartModel}. All public accessors (`chartModel`, * `chartTypes`, `axes`, series mutators, `style`, `mutate`, …) are * backed by the structured model. * * - **ChartEx** (`chartExNumber` set): Office 2016+ `cx:chart`. The * structured model (`chartExModel`) is populated; a parallel * `rawXml` buffer is kept so byte-for-byte round-trip is the default * when no mutation happens. The high-level accessors that make * sense on both flavours (`title`, `legend`, `spPr`, `toSVG`, * `toPNG`, `unknownElements`) work uniformly. ChartTypeGroup-level * APIs (`chartTypes`, `axes`, `plotArea`, `getAxis`, `categoryAxis`, * `valueAxis`, `addSeries`, `removeSeries`, `getSeries`, * `updateSeries`, `addSeriesFromOptions`, `getSeriesCount`, `mutate`, * `setStyle`) are classic-only — ChartEx has its own topology that * doesn't map cleanly onto the classic group/series abstraction; * use {@link Chart.mutateChartEx} for ChartEx mutations. */ declare class Chart { readonly worksheet: Worksheet; /** 1-based chart number for classic c:chart (0 when this is a chartEx) */ chartNumber: number; /** 1-based chartEx number for cx:chart (0 when this is a classic chart) */ chartExNumber: number; range: ChartAnchorRange; constructor(worksheet: Worksheet, ids: { chartNumber?: number; chartExNumber?: number; }, range: AddChartRange | ChartAnchorModel["range"]); /** Whether this is an Office 2016+ extended chart (cx:chart) */ get isChartEx(): boolean; private static parseRange; get model(): ChartAnchorModel; /** Get the full ChartModel from the workbook's chart entries (classic charts only) */ get chartModel(): ChartModel | undefined; /** Get the structured ChartEx model, when this chart is an Office 2016+ chartEx. */ get chartExModel(): ChartExModel | undefined; /** * Vendor-extension elements the parser observed but could not map to a * structured field. Populated when the chart was loaded from an existing * `.xlsx` whose author emitted `c15:`/`cx14:` style extension tags (often * MSO-internal; the OOXML spec treats them as implementation-specific). * * The array is **purely informational** in the default `preserve` writer * mode — structural rebuilds keep the raw XML pass-through that contains * those tags. In **`strictTemplateMode`** the writer surfaces this list * in its failure message when a mutation cannot be expressed as a raw * patch, so authors can decide between: * * 1. Relaxing `strictTemplateMode` and accepting that a rebuild will * drop these vendor tags, or * 2. Reshaping the mutation to land on a patch-friendly path (for * example editing `title` text rather than replacing the whole * `c:chart` subtree). * * Returns `undefined` if the chart was freshly created (no raw XML was * ever parsed) or if every child was recognised. See * {@link XlsxWriteOptions.strictTemplateMode} for the writer surface. */ get unknownElements(): Array<{ name: string; path: string; }> | undefined; /** * Raw XML bytes of the `c:userShapes` drawing part attached to this * chart, or `undefined` when the chart has no user shapes. User * shapes are annotation overlays (callouts, arrows, free text) that * Excel stores in a separate `xl/drawings/drawingN.xml` part using * relative anchors — the OOXML spec keeps their DrawingML schema * distinct from worksheet drawings, so the library treats the part * as opaque bytes for round-trip and programmatic replacement * instead of exposing a full structural API. * * Classic charts only. Returns `undefined` on chartEx charts (they * have their own extension mechanism and do not use `c:userShapes`). */ get userShapesXml(): Uint8Array | undefined; /** * Replace the `c:userShapes` drawing part for this chart with the * supplied raw XML bytes. Passing `undefined` or an empty byte * array removes the user-shapes reference entirely (equivalent to * {@link removeUserShapes}). * * The XML must be a complete DrawingML document whose root is a * `c:userShapes` element containing `c:relSizeAnchor` / * `c:absSizeAnchor` children. The library performs a shallow sanity * check only — no schema validation. * * When a chart did not previously have user shapes, this allocates * a new `r:id` and adds the drawing-part rel so the reference is * discoverable in `chartN.xml.rels`. When a chart already had user * shapes, the existing `r:id` is kept and only the bytes are * updated. * * Classic charts only. Throws on chartEx charts. */ setUserShapesXml(xml: Uint8Array | string | undefined): void; /** * Drop the `c:userShapes` reference and its backing drawing part. * Classic charts only. No-op when the chart has no user shapes. */ removeUserShapes(): void; /** * Render this chart as a **zero-dependency deterministic preview** SVG. * * The output is suitable for thumbnails, email attachments, * server-side report generation, CI smoke tests, and README images. * It is **not** an Excel-pixel-perfect compositor — text layout, * font metrics, and 3D projection are approximated for a stable * preview rather than reproduced from Excel's internal renderer. * * For production-grade rendering (Excel-identical layout, real 3D * for non-bar types, exact font hinting), round-trip the `.xlsx` * through headless LibreOffice (`soffice --convert-to pdf`) — the * byte-preserving round-trip + `templateMode: "strict"` guarantees * in this library make that a safe handoff. * * See `src/modules/excel/README.md` → "Rendering scope" for the * complete boundary list. */ toSVG(options?: ChartRenderOptions): string; /** * Render this chart as a **zero-dependency deterministic preview** PNG. * * Browsers use a `` pipeline; Node.js uses the built-in * `BasicRasterCanvas` rasteriser (a pure-JS SVG-subset rasteriser — * no native canvas dependency). DrawingML effect filters * (shadow/glow/soft-edge/blur/reflection) round-trip through XML and * emit as SVG ``, but the Node PNG rasteriser silently drops * them; the browser canvas path renders them natively. * * See {@link toSVG} for the full scope-boundary note. For pixel-perfect * output, convert through LibreOffice. */ toPNG(options?: ChartRenderOptions): Promise; /** Get the chart title text. Returns undefined if no title. */ get title(): string | undefined; /** * Set the chart title. * * Accepts: * - `undefined` → removes title (sets autoTitleDeleted). * - `string` → sets title text. If the existing title had rich-text formatting, * the formatting of the first run is preserved and only the text is replaced. * - `ChartRichText` → replaces the title with fully-structured rich text. * - `{ formula: string }` → sets the title to a worksheet formula reference. */ set title(value: string | ChartRichText | { formula: string; } | undefined); /** * Set the chart title using structured rich text. * Convenience method equivalent to `chart.title = richText`. */ setTitleRichText(richText: ChartRichText): void; /** * Get the structured rich text of the chart title. * Returns undefined if the title is unset, formula-only, or captured as rawTx. */ get titleRichText(): ChartRichText | undefined; /** Get the chart type groups in the plot area */ get chartTypes(): ChartTypeGroup[]; /** Get the chart axes */ get axes(): ChartAxis[]; /** Get the chart legend */ get legend(): ChartLegend | undefined; /** Set the chart legend */ set legend(value: ChartLegend | undefined); /** * Get the chart-level shape properties. Works for both classic charts * (`c:chartSpace/c:spPr`) and ChartEx (`cx:chartSpace/cx:spPr` — the * Chart2014 schema puts `spPr` on `chartSpace`, not on `chart`). */ get spPr(): ShapeProperties | undefined; /** * Set the chart-level shape properties. Routes to either * `ChartModel.spPr` (classic) or `ChartExModel.chartSpace.spPr` * (ChartEx); previously the ChartEx branch was a silent no-op, and * the fix before that mis-targeted `chart.spPr` — which is not a * valid child of `CT_Chart` in the Chart2014 schema (chart-frame * styling belongs to `CT_ChartSpace/spPr`). */ set spPr(value: ShapeProperties | undefined); /** * Get an axis by its ID. */ getAxis(axId: number): ChartAxis | undefined; /** * Get the category (X) axis, if any. */ get categoryAxis(): ChartAxis | undefined; /** * Get the value (Y) axis, if any. */ get valueAxis(): ChartAxis | undefined; /** Get the plot area for direct manipulation */ get plotArea(): PlotArea | undefined; mutate(mutator: (model: ChartModel) => void, options?: { preferRawPatch?: boolean; requireRawPatch?: boolean; }): this; mutateChartEx(mutator: (model: ChartExModel) => void, options?: { preferRawPatch?: boolean; requireRawPatch?: boolean; }): this; /** * Set the built-in chart style by index. * * Writes `` on the classic chart (`chartN.xml`). * Valid values are 1–48, matching the legacy Excel 2007/2010 style * catalogue and `xlsxwriter`'s `set_style(N)`. This is the lightweight * option — it does **not** emit a `styleN.xml` / `colorsN.xml` sidecar; * for modern Office-2013-era styling use {@link setChartStyle} instead. * * ChartEx charts do not honour this field; calling `setStyle` on one * throws, which matches OOXML: `c:style` only exists in the classic * `c:` namespace. * * @param style - Integer in the range 1–48. * @throws {ChartOptionsError} when `style` is outside 1–48 or the * chart is a ChartEx. */ setStyle(style: number): this; /** * Alias for {@link setStyle} that matches the `xlsxwriter` terminology * used by Python/Rust users migrating their chart code. Equivalent in * every way — both write the same `` attribute. */ setBuiltInStyle(style: number): this; /** * Add a series to a chart type group. * * The series' `index` and `order` fields are rewritten to the next * available slot in the target group so callers can safely push * series that were built with a placeholder index (e.g. a reused * result of `buildChartSeriesForType(..., 0)`). OOXML requires * `c:ser/@idx` to be unique within the chart; leaving caller-provided * values alone silently produces a chart with duplicate `` * entries that Excel either rejects or collapses to a single series. * * @param series - The series object matching the expected series type for the chart. * @param groupIndex - 0-based index of the chart type group (for combo charts). Defaults to 0. */ addSeries(series: SeriesBase, groupIndex?: number): void; /** * Remove a series from a chart type group by index. * * @param index - 0-based index of the series within the group. * @param groupIndex - 0-based index of the chart type group (for combo charts). Defaults to 0. * @returns The removed series, or undefined if out of range. */ removeSeries(index: number, groupIndex?: number): SeriesBase | undefined; /** * Get a series from a chart type group. * * @param index - 0-based index of the series within the group. * @param groupIndex - 0-based index of the chart type group (for combo charts). Defaults to 0. */ getSeries(index: number, groupIndex?: number): SeriesBase | undefined; /** Update common fields on an existing classic chart series. */ updateSeries(index: number, options: Partial, groupIndex?: number): boolean; /** Add a series from high-level options, matching the target chart type group. */ addSeriesFromOptions(options: AddChartSeriesOptions, groupIndex?: number): boolean; /** Update the value range for a series. */ setSeriesValues(index: number, values: string, groupIndex?: number): boolean; /** Update the category range for a series. */ setSeriesCategories(index: number, categories: string, groupIndex?: number): boolean; /** Update the series display name or formula reference. */ setSeriesName(index: number, name: NonNullable, groupIndex?: number): boolean; /** Get the total number of series across all chart type groups. */ get totalSeriesCount(): number; /** * Get the number of series in a specific chart type group. * * @param groupIndex - 0-based index of the chart type group (defaults to 0). */ getSeriesCount(groupIndex?: number): number; /** * Create a deep copy of this chart and add it to a target worksheet. * The new chart receives a fresh chartNumber; the original is untouched. * * @param targetWs - Worksheet to receive the clone. Defaults to this chart's worksheet. * @param range - Anchor range for the clone. Defaults to the original range. * @returns The new chartNumber in the target workbook. */ copyTo(targetWs: Worksheet, range?: AddChartRange): number; /** * Create a deep copy of this chart in the same worksheet. * @param range - Anchor for the clone (defaults to shifting right of original). */ clone(range?: AddChartRange): number; private _cloneRange; /** * Compute the next available series index across all chart type groups. * OOXML `c:ser/@idx` must be unique within the entire chart. Scans for * the maximum authored index (not the series count) so post-removeSeries * state and non-contiguous authored indices still produce a unique slot. */ private _nextSeriesIndex; private _markDirty; /** * Resolve any `_pendingImage` payloads this chart's series carry into * workbook media entries + chart relationships. Used by * `updateSeries` / `addSeriesFromOptions` when a caller adds a * picture fill *after* registration — the initial `addChart` / * `addChartsheet` path already calls `resolvePendingChartImages`, * but post-registration mutations previously left `_pendingImage` * un-registered and the writer emitted `` pointing at a * missing rel. * * Uses the same resolver helper as the initial path, so rel id * allocation, collision checks against `_chartRels`, and media * naming stay centralised. */ private _resolveSeriesPictureFills; /** * Rebuild numeric / string caches on this chart's model from the * current worksheet data. Callers mutating formula references after * registration (`updateSeries`, `addSeriesFromOptions`, title setter * with `{ formula }`) invoke this so preview renders and the * snapshot-based change detector see populated points instead of * the empty `{ points: [] }` shell the builders install. * * `fillChartCaches` short-circuits on already-populated caches, so * repeated calls across a burst of mutations are effectively O(N) * in the number of *new* references. */ private _refreshCaches; /** * Populate ChartEx data caches (`strDim.levels` / `numDim.levels`) from * the workbook's worksheet data so that preview renders see actual * values instead of empty arrays. * * The builder marks hierarchical dimensions with `_skipCache` to * prevent the XML writer from emitting flat cache levels (which * confuses Excel's hierarchy renderer). However, the in-memory * renderer still needs the data. We temporarily clear the flag, * fill, then restore it so the writer behaviour is unaffected. */ private _refreshChartExCaches; private _extractTextFromRichText; private _extractTextFromRawTx; private _decodeXmlEntities; /** * Extract the run properties of the first text run in an existing title, * if any. Used to preserve formatting when replacing plain-string title * text. * * We prefer the structured path (`title.text.paragraphs[0].runs[0]`) and * fall back to parsing the first `...` block out of * `rawTx`. The fallback used to only read *attributes* off the opening * tag (size / bold / italic / underline / strike / lang), which meant * the `` colour child and `` / `` typefaces * were silently stripped when a plain-string title replaced a * rich-text title. Now we delegate to `parseTxPr` (the same helper the * chart-space xform uses for the full txPr tree) so every supported * rPr field round-trips faithfully. */ private _extractFirstRunProperties; } export interface ChartAnchorModel { chartNumber: number; /** 1-based chartEx number (cx:chart). 0 or absent for classic charts. */ chartExNumber?: number; range: { tl: AnchorModel; /** Bottom-right (only for twoCellAnchor) */ br?: AnchorModel; /** Absolute position in EMU (only for absoluteAnchor) */ pos?: { x: number; y: number; }; /** Extent in EMU (for oneCellAnchor and absoluteAnchor) */ ext?: { cx: number; cy: number; }; editAs?: string; }; } export { Chart, buildChartModel };