Meridian Office

Meridian Chart — единый движок диаграмм

Документ модуля ядра (§2.2 библии docs/00-foundation.md: «Meridian Chart — единый движок диаграмм во всех приложениях»). Версия 0.1 · 2026-10-03. Крейт kernel/crates/meridian-chart; потребители — Sheets (диаграммы и спарклайны), Write, Slides, Draw, Base (отчёты), Forms (сводки ответов). Рендер — через Meridian Render (список отображения → Skia / SVG / PDF / PNG). Данные — через grid-model Sheets (ссылки на диапазоны) или встроенный кэш. Шаблоны диаграмм — .mcht (пакет MOP с chart.xml без данных). Пространство имён XML — urn:meridian:office:chart:1 (внутри документов любого приложения). Фаза: 2 (вместе с Sheets); современные типы — конец фазы 2 / фаза 3; карты — P2.

Содержание

  1. Назначение и место в архитектуре
  2. Типы диаграмм
  3. Модель диаграммы
  4. Палитры, стили, темы
  5. Алгоритмы
  6. Связь с данными
  7. Сопоставление с OOXML и ODF
  8. MeridianScript API
  9. Критерии готовности
  10. Риски
  11. Вопросы к библии

1. Назначение и место в архитектуре

Одна модель, одна раскладка, один рендер — диаграмма в Sheets, Write, Slides и Draw выглядит и ведёт себя одинаково, а копирование между приложениями переносит объект, а не картинку. Приложение-хост даёт движку источник данных (DataProvider), тему и размер, получает список отображения и hit-test; всё остальное — внутри крейта.

┌───────────── хост (Sheets / Write / Slides / Draw / Base / Forms) ─────────────┐
│ DataProvider (диапазоны, пересчёт) · Theme · размер · события мыши · undo       │
└───────────────┬──────────────────────────────────────────┬─────────────────────┘
                ▼                                          ▲
┌──────────── meridian-chart ──────────────────────────────┴─────────────────────┐
│ ChartModel ──resolve──▶ ResolvedData (кэш значений, категории, даты)            │
│      │                        │                                                 │
│      ▼                        ▼                                                 │
│ Scales (оси, nice ticks) ──▶ Layout (measure → arrange → marks) ──▶ DisplayList │
│                                  │                        │            │        │
│                                  ▼                        ▼            ▼        │
│                              HitIndex              Animator(t)   Render: Skia / │
│                              (tooltips)            (кадр по t)   SVG / PDF / PNG│
└─────────────────────────────────────────────────────────────────────────────────┘
Модуль крейта Содержимое
model ChartModel и вложенные типы (§3), сериализация в XML chart.xml, шаблоны .mcht
data DataSource, DataProvider (трейт хоста), ResolvedData, кэш, подписки (§6)
scale шкалы: линейная, логарифмическая, категорийная, временная; nice ticks; форматирование подписей через meridian-numfmt
layout измерение текста (через text-layout), раскладка заголовков/легенды/осей/области построения, подписи данных (§5.3)
series по типам: bar, line, area, pie, scatter, bubble, radar, stock, surface, treemap, sunburst, histogram, pareto, boxplot, waterfall, funnel, map, sparkline
stats тренды (МНК), гистограммные правила, квартили, сглаживание, LTTB
render DisplayList ядра Meridian Render; экспорт SVG/PNG
hit индекс попаданий, подсказки, выделение элементов (§5.12)
anim параметризованная раскладка для анимации появления и обновления (§5.11)
palette палитры, шкалы, проверка доступности (§4)
style 16 стилей (ChartStyle), макеты (быстрые макеты), темы
a11y текстовое описание диаграммы, таблица данных для скринридера

Поведение в хостах: Sheets — живые ссылки на диапазоны, формула =SERIES(...) в строке формул для выбранного ряда, спарклайны в ячейках (упрощённый конвейер без осей и легенды); Write/Slides — данные во встроенном objects/chart-<ulid>.msx (мини-книга grid-model), редактируются в боковой панели; Draw — диаграмма как узел сцены, данные из полей фигур (P1); Base — диаграммы в отчётах из запросов; Forms — сводки ответов (гистограммы распределения, круговые).


2. Типы диаграмм

Тип Подтипы Параметры P
Гистограмма (column) обычная, с накоплением, нормированная 100 %; «объёмные» варианты рисуются плоскими с имитацией (P2 — 3D) gap_width 0–500 %, overlap −100…100 %, vary_colors, инвертировать для отрицательных P0
Линейчатая (bar) те же те же + ось категорий справа/слева P0
График (line) обычный, с накоплением, нормированный; с маркерами; сглаженный маркеры (9 форм, размер 2–72 pt), smooth (режим §5.6), линии проекции (drop lines), коридор (hi-lo lines), вверх/вниз полосы P0
С областями (area) обычная, с накоплением, нормированная прозрачность, границы P0
Круговая (pie) обычная, вторичная круговая (pie of pie), вторичная линейчатая (bar of pie) first_slice_angle, explosion (общая/по точке), разбиение вторичной: по положению/значению/проценту/вручную, размер вторичной, зазор P0 (P1 — вторичные)
Кольцевая (doughnut) одно- и многокольцевая hole_size 10–90 %, explosion, first_slice_angle P0
Точечная (scatter) только маркеры, с прямыми, с прямыми и маркерами, со сглаженными, со сглаженными и маркерами оси X/Y числовые/лог, линии тренда, планки P0
Пузырьковая (bubble) обычная; «объёмная» — плоско size_represents площадь/диаметр, bubble_scale 0–300 %, отрицательные размеры (показывать/скрывать) P0
Лепестковая (radar) обычная, с маркерами, заполненная ось значений радиальная, категории по кругу, начало сверху P0
Биржевая (stock) HLC, OHLC, V-HLC, V-OHLC коридор, полосы повышения/понижения (цвета), объём на вторичной оси как столбцы P0
Поверхностная (surface) контурная (2D, карта высот) полосы уровней (band formats), шкала P1 (контур) / P2 (3D-каркас)
Комбинированная (combo) любое сочетание столбцов/линий/областей/точечной с вторичной осью тип и ось на каждый ряд P0
Дерево (treemap) по ветвям: без подписей ветвей / подписи-баннеры / подписи поверх иерархия до 8 уровней, цвет по верхнему уровню или по значению P1
Солнечные лучи (sunburst) — уровни-кольца, угол начала, подписи по дуге P1
Гистограмма распределения (histogram) обычная; Парето (отсортированная + кумулятивная линия) корзины: авто (§5.8), по ширине, по числу, границы переполнения/недополнения, разделять ли корзины P1
«Ящик с усами» (box & whisker) — метод квартилей (включающая/исключающая медиана), показывать среднее (маркер/линия), внутренние точки, выбросы, усы 1,5·IQR P1
Каскадная (waterfall) — точки «итог» (от нуля), соединительные линии, цвета рост/падение/итог P1
Воронка (funnel) — зазор, подписи внутри/снаружи, сортировка P1
Карта (map, choropleth) регионы: страны мира, субъекты РФ, штаты США, регионы ЕС шкала цветов (последовательная/расходящаяся), проекция (равновеликая Эккерта IV для мира, ЛКК для РФ), подписи, геоданные Natural Earth 1:50 м (public domain) + административные границы РФ (ОСМ-производные, ODbL — отдельный пакет) P2
Спарклайны (sparkline) линия, столбцы, выигрыш/проигрыш маркеры (макс/мин/первая/последняя/отрицательные), ось, одинаковая шкала в группе, пустые ячейки (пропуск/ноль/соединить) P0 (Sheets)

Общие элементы для всех типов: заголовок и подзаголовок, легенда, подписи данных, оси (там, где применимы), сетка, линии тренда (столбцы, линии, точечная, пузырьковая, области), планки погрешностей (столбцы, линии, точечная, пузырьковая, области), таблица данных под диаграммой (P1), стрелки и аннотации (фигуры поверх — userShapes, P1).


3. Модель диаграммы

// kernel/crates/meridian-chart/src/model.rs
pub struct ChartModel {
    pub id: Ulid,
    pub title: Option<Title>, pub subtitle: Option<Title>,
    pub plots: Vec<Plot>,                       // одна диаграмма = ≥ 1 «участок» (комбинированная — несколько) / plot groups
    pub axes: Vec<Axis>,                        // 0..4 (+ вторичные), id-ссылки из Plot / referenced by id
    pub legend: Option<Legend>,
    pub plot_area: PlotArea,                    // ручная раскладка (доли) или авто / manual or auto layout
    pub style: ChartStyle,                      // стиль 1–16 + переопределения / style id + overrides
    pub palette: PaletteRef,                    // Theme | Named(id) | Custom(Vec<Color>)
    pub display_blanks_as: BlanksMode,          // Gap | Zero | Span
    pub plot_visible_only: bool,                // скрытые строки/столбцы / hidden rows
    pub rounded_corners: bool,
    pub animation: AnimOptions,                 // появление/обновление / entrance & update
    pub a11y: A11y,                             // описание, таблица / description, data table
    pub data_cache_rev: u64,
    pub ext: Vec<ExtensionBlob>,                // неизвестные части OOXML/ODF для round-trip
}

pub struct Plot {
    pub kind: PlotKind,                         // Bar{dir,grouping}, Line{grouping}, Area{grouping}, Pie, Doughnut, OfPie{..}, Scatter{style}, Bubble, Radar{style}, Stock{kind}, Surface, Treemap, Sunburst, Histogram{..}, Pareto, BoxPlot{..}, Waterfall, Funnel, Map{..}
    pub series: Vec<Series>,
    pub axis_group: AxisGroup,                  // Primary | Secondary
    pub options: PlotOptions,                   // gap_width, overlap, hole_size, first_slice_angle, bubble_scale, …
    pub vary_colors: bool,
    pub drop_lines: Option<Line>, pub hi_lo_lines: Option<Line>, pub up_down_bars: Option<UpDownBars>,
}

pub struct Series {
    pub id: u32, pub order: u32,
    pub name: DataSource<String>,               // ссылка на ячейку или литерал / cell ref or literal
    pub categories: Option<DataSource<Category>>, // текст | число | дата | многоуровневые / multi-level
    pub values: DataSource<f64>,                // Y (или размер сектора) / Y or slice size
    pub x_values: Option<DataSource<f64>>,      // точечная/пузырьковая / scatter, bubble
    pub sizes: Option<DataSource<f64>>,         // пузырьки / bubbles
    pub extra: Vec<DataSource<f64>>,            // open/high/low/close, иерархии treemap и т.п.
    pub fill: Option<Fill>, pub line: Option<Line>, pub marker: Option<Marker>, pub effects: Option<Effects>,
    pub points: BTreeMap<u32, PointOverride>,   // формат отдельных точек (dPt) / per-point overrides
    pub labels: Option<DataLabels>,             // + per-point overrides внутри / per-point inside
    pub trendlines: Vec<Trendline>,
    pub error_bars: Vec<ErrorBars>,
    pub smooth: bool,
    pub invert_if_negative: bool,
    pub kind_override: Option<PlotKind>,        // комбинированная: ряд другого типа / combo override
    pub axis_group: AxisGroup,
    pub explosion: Option<u16>,
    pub show_in_legend: bool,
}

pub enum DataSource<T> {
    Range { reference: RangeRef /* книга/лист/диапазон, абсолютные ссылки */, cache: Vec<T>, format_codes: Vec<Option<String>> },
    Literal(Vec<T>),                            // встроенные значения / inline values
    Formula { expr: String, cache: Vec<T> },    // =SERIES или произвольная формула Sheets (P1)
}

pub struct Axis {
    pub id: AxisId,
    pub kind: AxisKind,                         // Category | Value | Date | Series(3D)
    pub position: AxisPos,                      // Left | Right | Bottom | Top
    pub group: AxisGroup,
    pub cross_axis: AxisId,
    pub crosses: Crosses,                       // AutoZero | Max | Min | At(f64) | Between (категории: между делениями)
    pub scale: Scale,                           // { min: Auto|Fixed(f64), max, major: Auto|Fixed, minor, log_base: Option<f64>, reversed, date_base: Option<TimeUnit>, display_units: Option<DisplayUnit> }
    pub ticks: Ticks,                           // major/minor: None|In|Out|Cross; label position: NextTo|High|Low|None
    pub labels: AxisLabels,                     // number_format{code, source_linked}, font, rotation: Auto|Deg(i16), skip: Auto|N, align, wrap, offset
    pub title: Option<Title>,
    pub gridlines: Gridlines,                   // major: Option<Line>, minor: Option<Line>
    pub line: Option<Line>,
    pub visible: bool,
    pub label_interval: Option<u32>, pub tick_interval: Option<u32>,   // категорийная ось / category axis
    pub multi_level: bool,
}

pub struct Legend { pub position: LegendPos /* Right|Left|Top|Bottom|TopRight|Manual(RectFrac) */, pub overlay: bool, pub entries: BTreeMap<u32, LegendEntry /* hidden, font */>, pub font: Font, pub fill: Option<Fill>, pub line: Option<Line>, pub columns: Option<u8> }
pub struct Title { pub text: RichText /* или DataSource<String> */, pub overlay: bool, pub pos: Option<RectFrac>, pub rotation: i16 }
pub struct DataLabels { pub show: LabelParts /* value, category, series_name, percent, bubble_size, legend_key */, pub position: LabelPos /* Center|InsideEnd|InsideBase|OutsideEnd|Above|Below|Left|Right|BestFit|Callout */, pub number_format: Option<NumFmt>, pub separator: String, pub leader_lines: bool, pub from_cells: Option<DataSource<String>>, pub font: Font, pub fill: Option<Fill>, pub line: Option<Line>, pub points: BTreeMap<u32, DataLabelOverride> }
pub struct Trendline { pub kind: TrendKind /* Linear | Exponential | Logarithmic | Power | Polynomial(u8 /*2..6*/) | MovingAverage(u16) */, pub intercept: Option<f64>, pub forward: f64, pub backward: f64, pub show_equation: bool, pub show_r2: bool, pub name: Option<String>, pub line: Line }
pub struct ErrorBars { pub direction: ErrDir /* X | Y */, pub kind: ErrKind /* Both | Plus | Minus */, pub value: ErrValue /* Fixed(f64) | Percent(f64) | StdDev(f64) | StdErr | Custom{plus: DataSource<f64>, minus: DataSource<f64>} */, pub end_cap: bool, pub line: Line }
pub struct PlotArea { pub layout: Option<ManualLayout /* x,y,w,h в долях, target: Inner|Outer */>, pub fill: Option<Fill>, pub line: Option<Line> }

XML внутри документов (фрагмент charts/chart-<ulid>.xml в .msx/.mwx/.mpx/.mvx):

<chart xmlns="urn:meridian:office:chart:1" id="01J9RB…" style="7" palette="theme" blanks="gap">
  <title><p><r>Выручка по кварталам</r></p></title>
  <plot kind="bar" dir="col" grouping="clustered" gapWidth="150" overlap="-10" axes="ax1 ax2">
    <series id="0" order="0">
      <name ref="Данные!$B$1"><cache><s>План</s></cache></name>
      <cat ref="Данные!$A$2:$A$5"><cache><s>Q1</s><s>Q2</s><s>Q3</s><s>Q4</s></cache></cat>
      <val ref="Данные!$B$2:$B$5" fmt="#,##0"><cache><n>120</n><n>135</n><n>150</n><n>170</n></cache></val>
      <trendline kind="linear" forward="1" showR2="true"/>
    </series>
    <series id="1" order="1">…</series>
  </plot>
  <axis id="ax1" kind="category" pos="bottom" cross="ax2" crosses="autoZero"><labels fmt="General" rotation="auto"/></axis>
  <axis id="ax2" kind="value" pos="left" cross="ax1" crosses="autoZero"><scale min="auto" max="auto" major="auto"/><gridlines major="true"/></axis>
  <legend pos="bottom"/>
</chart>

Шаблон .mcht — тот же chart.xml без <cache>/ref (только структура и форматирование) + thumbnail.png.


4. Палитры, стили, темы

4.1 Категориальные палитры (10 цветов)

№ Meridian (по умолчанию) Назначение/происхождение Доступная (Okabe–Ito + 2)
1 #3B5BDB --app-write #0072B2 синий
2 #E25C3D --app-slides #E69F00 оранжевый
3 #1E8E5A --app-sheets #009E73 зелёный
4 #F2A541 --m-sun #CC79A7 розовый
5 #7048E8 --app-notes #56B4E9 голубой
6 #0E8A8A --app-draw #D55E00 красно-оранжевый
7 #C2309B --app-paint #F0E442 жёлтый
8 #E67700 --app-plan #000000 чёрный (в тёмной теме #E6E8EB)
9 #1C7ED6 --app-mail #999999 серый
10 #5C940D --app-forms #882255 бордовый

Для рядов 11+ — те же цвета с осветлением/затемнением (OKLCH: L ± 0,12, затем ± 0,24), как в Office (lumMod/lumOff). Палитра «Тема» — accent1…accent6 темы документа + их оттенки (поведение Office для совместимости со стилями c:style). Пользовательская палитра — любой список.

4.2 Шкалы

4.3 Доступность для дальтоников

Проверка палитры: симуляция протанопии/дейтеранопии/тританопии матрицами Machado–Oliveira–Fernandes (2009, severity 1,0) в линейном RGB; требование — минимальная попарная разница ΔE2000 ≥ 12 между соседними по порядку цветами в каждой симуляции и ≥ 8 между любыми; «Доступная» палитра удовлетворяет; «Meridian» — удовлетворяет для первых 6 цветов, поэтому при включённом режиме доступности (настройка приложения или системный флаг) движок переключается на «Доступную» и добавляет различение рядов узорами (штриховки для столбцов/областей, формы маркеров и штрихи для линий) — свойство ChartStyle.redundant_encoding.

4.4 Стили и темы

16 стилей (ChartStyle 1…16): сочетания — заливка области построения (нет/светлая/тёмная), сетка (нет/основная/обе), толщина линий (1,5/2,25/3 pt), скругления столбцов, маркеры, положение легенды, шрифты (размер заголовка 14/16/18 pt, подписи 9/10 pt), тени/градиенты (стили 9–16 — «объёмные» плоские с градиентом). Таблица стилей — в style/styles.toml. Шрифты и цвета — из темы документа (+mn-lt, tx1, bg1), размеры — в pt, масштабируются с диаграммой при auto_scale_text (как c:txPr … sz при изменении размера — нет; Office не масштабирует; у нас — опционально). Соответствие стилям Office (c:style 1–48, cs:chartStyle 2013+) — таблица в кодеке: c:style 2 → ChartStyle 1 и т.д.; при импорте неизвестного стиля берутся явные spPr.


5. Алгоритмы

5.1 Красивые деления осей (nice ticks)

Реализация алгоритма Talbot–Lin–Hanrahan («An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes», 2010): перебор шагов из Q = [1, 5, 2, 2.5, 4, 3] × 10^z × j и числа делений k с оценкой score = 0,25·simplicity + 0,2·coverage + 0,5·density + 0,05·legibility:

// kernel/crates/meridian-chart/src/scale/ticks.rs
const Q: [f64; 6] = [1.0, 5.0, 2.0, 2.5, 4.0, 3.0];
const W: [f64; 4] = [0.25, 0.2, 0.5, 0.05];

pub fn extended(dmin: f64, dmax: f64, m: usize, only_loose: bool) -> Ticks {
    let (mut best_score, mut best) = (f64::NEG_INFINITY, Ticks { min: dmin, max: dmax, step: (dmax - dmin) / (m.max(2) - 1) as f64 });
    let mut j = 1usize;
    'outer: loop {
        for (i, &q) in Q.iter().enumerate() {
            let sm = simplicity_max(i, j);
            if score(sm, 1.0, 1.0, 1.0) < best_score { break 'outer; }           // верхняя граница по j / bound on j
            let mut k = 2usize;
            while k < 1000 {
                let dm = density_max(k, m);
                if score(sm, 1.0, dm, 1.0) < best_score { break; }
                let delta = (dmax - dmin) / (k + 1) as f64 / j as f64 / q;
                let mut z = delta.log10().ceil() as i32;
                while z < 60 {
                    let step = j as f64 * q * 10f64.powi(z);
                    let cm = coverage_max(dmin, dmax, step * (k - 1) as f64);
                    if score(sm, cm, dm, 1.0) < best_score { break; }
                    let min_start = ((dmax / step).floor() * j as f64) as i64 - ((k - 1) * j) as i64;
                    let max_start = ((dmin / step).ceil() * j as f64) as i64;
                    for start in min_start..=max_start {
                        let lmin = start as f64 * (step / j as f64);
                        let lmax = lmin + step * (k - 1) as f64;
                        if only_loose && !(lmin <= dmin && lmax >= dmax) { continue; }
                        let s = simplicity(i, j, lmin, lmax, step);
                        let c = coverage(dmin, dmax, lmin, lmax);
                        let d = density(k, m, dmin, dmax, lmin, lmax);
                        let sc = score(s, c, d, 1.0);
                        if sc > best_score { best_score = sc; best = Ticks { min: lmin, max: lmax, step }; }
                    }
                    z += 1;
                }
                k += 1;
            }
        }
        j += 1;
    }
    best
}
fn score(s: f64, c: f64, d: f64, l: f64) -> f64 { W[0]*s + W[1]*c + W[2]*d + W[3]*l }

Шаг округляется к «красивому» представлению через meridian-numfmt (число значащих цифр = по шагу). Логарифмическая ось: основные деления base^k, минорные 2…9·base^k; если диапазон < 1 декады — подшаги 1, 2, 5. Временная ось: единицы sec/min/hour/day/week/month/quarter/year, кандидаты шага {1,2,5,10,15,30} с/мин, {1,2,3,6,12} ч, {1,2,7,14} дн, {1,2,3,6} мес, {1,2,5,10,20,50,100} лет — выбирается первый, дающий ≤ m делений; подписи по формату единицы (локаль ICU4X: «янв 2026», «12.03», «Q1»). Категорийная ось: label_interval авто — минимальный n, при котором подписи не перекрываются (с учётом поворота 0/45/90° и переноса — §5.3).

5.2 Автоматический масштаб

Для оси значений: dmin/dmax по всем рядам группы с учётом стеков (§5.4), планок погрешностей, продолжений трендов (forward/backward) и пузырьков (радиус в единицах оси после масштабирования). Правила (совместимы с Excel):

  1. Столбцы/линейчатые/области/гистограммы: ноль включается всегда (dmin = min(0, dmin), dmax = max(0, dmax)).
  2. Линии/точечные/пузырьковые: если dmin ≥ 0 и dmin ≤ 5/6·dmax → dmin = 0; если dmax ≤ 0 и |dmax| ≤ 5/6·|dmin| → dmax = 0; иначе — отступ: lmin = nice_floor(dmin − 0,05·range).
  3. Отступ сверху: lmax ≥ dmax + 0,05·range (чтобы маркер не касался рамки), затем extended(lmin, lmax, m) с m = clamp(round(axis_px / 60), 3, 12) для вертикали и axis_px / 80 для горизонтали.
  4. Фиксированные min/max/major пользователя переопределяют соответствующую часть (extended вызывается с only_loose и зафиксированными границами — перебор только шага).
  5. Лог-ось: границы — степени основания, покрывающие данные; значения ≤ 0 пропускаются с пометкой в a11y-описании.
  6. Вторичная ось — независимый расчёт; при align_zero (опция) — подбор lmin/lmax второй оси так, чтобы нули совпали по пикселям (масштабирование диапазона второй оси).
  7. Единицы отображения (display_units: тыс., млн, млрд, свои) — деление значений перед форматированием, подпись «млн».

5.3 Раскладка

Конвейер layout(model, size, theme) -> ChartLayout с двумя проходами (размер легенды и подписей осей зависят от шкалы, а шкала — от размера области построения):

measure(title) → measure(legend) → rect_plot₀ = size − title − legend − поля
scales₀ = scales(rect_plot₀) → measure(axis labels, titles) → rect_plot₁ = rect_plot₀ − axes
scales₁ = scales(rect_plot₁) → если подписи категорий перекрываются: rotation 0→45→90, затем skip=n; → rect_plot₂
marks = build(series, scales₁, rect_plot₂) → data labels placement → legend arrange → ChartLayout

Легенда: элементы (маркер 10×10 px + подпись) в потоке: справа/слева — колонка (перенос в несколько колонок при переполнении высоты, ширина ≤ 30 % диаграммы, усечение «…» по середине длинных имён), сверху/снизу — строка с переносом (≤ 4 строки); порядок — порядок рядов (для стеков — обратный, как в Excel); overlay — поверх области построения без вычета места.

Подписи осей: категорийная — измеряем все подписи; если Σ width > axis_px → поворот 45°, затем 90°, затем перенос в 2 строки (если ширина категории ≥ 40 px), затем skip = ceil(Σ width / axis_px); числовая — формат по number_format или General с числом знаков из шага; «тысячи» — разделители по локали.

Подписи данных (предотвращение наложений):

  1. Для каждой точки — список кандидатных позиций в порядке предпочтения (для столбцов: OutsideEnd, InsideEnd, Center, InsideBase; для точек/линий: Above, Right, Below, Left, AboveRight, … — 8 направлений; для круговой — отдельный алгоритм).
  2. Экран делится на сетку ячеек 4×4 px — битовая карта занятости (OccupancyGrid): заранее помечаются оси, легенда, заголовки, столбцы (для «снаружи» подписей — чтобы не накрывать соседние столбцы).
  3. Жадный проход в порядке приоритета (по умолчанию — убывание |значения|; для линий — последние точки раньше): первая свободная позиция → подпись ставится, занятость помечается; если свободных нет и allow_hide — подпись скрывается (отмечается в a11y), иначе — ставится в предпочтительную позицию (наложение допускается, как в Excel при Center).
  4. Для круговой/кольцевой: подписи OutsideEnd сортируются по углу; на каждой стороне (слева/справа) — «стековая» укладка сверху вниз с минимальным вертикальным интервалом = высота подписи + 2 px; при переполнении стороны — уменьшение радиуса круга (до 70 %) и лидерные линии (две ломаные: от сектора по радиусу, затем горизонтально к подписи); BestFit — внутри сектора, если подпись помещается (проверка bbox внутри сектора по 4 углам), иначе снаружи.
  5. Выноски (Callout) — прямоугольник с хвостом к точке; хвост не пересекает другие подписи (проверка по сетке).

5.4 Стеки, проценты, ширина столбцов

/// Накопление: положительные и отрицательные значения складываются раздельно / positive and negative stacks separately.
pub fn stack(values: &[Vec<Option<f64>>] /* [series][category] */, percent: bool) -> Vec<Vec<Option<(f64, f64)>>> {
    let n_cat = values[0].len();
    let mut out = vec![vec![None; n_cat]; values.len()];
    for c in 0..n_cat {
        let (mut pos, mut neg) = (0.0, 0.0);
        let total: f64 = values.iter().filter_map(|s| s[c]).map(f64::abs).sum();   // для 100 % — сумма модулей / sum of abs
        for (s, series) in values.iter().enumerate() {
            let Some(v) = series[c] else { continue };
            let v = if percent { if total == 0.0 { 0.0 } else { v / total } } else { v };
            out[s][c] = Some(if v >= 0.0 { let r = (pos, pos + v); pos += v; r } else { let r = (neg + v, neg); neg += v; r });
        }
    }
    out
}

Ширина категории cat_px = axis_px / n_cat; ширина группы столбцов group_w = cat_px / (1 + gap_width/100); при n_series рядов без накопления ширина столбца bar_w = group_w / (n_series − (n_series − 1)·overlap/100), смещение ряда s: x_s = x_cat_center − group_w/2 + s·bar_w·(1 − overlap/100). Накопление — все ряды в одном столбце (overlap = 100). Области с накоплением — интерполяция верхних границ между категориями; пропуски (Gap) разрывают область. Линии: Gap — разрыв, Zero — нулевая точка, Span — прямая через пропуск.

5.5 Круговая и кольцевая

Доли p_i = |v_i| / Σ|v_j| (отрицательные берутся по модулю с пометкой; Excel делает так же); углы: начало θ₀ = first_slice_angle (по часовой стрелке от 12 часов), сектор i — [θ_i, θ_i + 2π·p_i). Сектор как путь: M center → L (r, θ_i) → Arc(r, θ_i→θ_{i+1}) → Z; для кольца — внешняя дуга + внутренняя дуга в обратную сторону, r_in = r·hole_size. Вынос: центр сектора смещается на explosion/100 · r вдоль биссектрисы; радиус основного круга уменьшается, чтобы вынесенные сектора остались в области. Многокольцевая — ряд k в кольце [r_in + k·w, r_in + (k+1)·w]. Вторичная круговая/линейчатая: выбор точек во вторую диаграмму (по положению: последние n; по значению: < x; по проценту; вручную — PointOverride.secondary), сумма вторых точек — один сектор «Другое» в первой; соединительные линии — от крайних точек сектора к границам второй диаграммы; second_size — доля радиуса/высоты, gap между диаграммами. Hit-test сектора — полярные координаты (§5.12). Подписи — §5.3 п. 4.

5.6 Сглаживание линий

Два режима (Plot.options.smooth_mode): монотонный (по умолчанию для новых диаграмм — не создаёт ложных экстремумов) и Office-совместимый (при импорте из xlsx/pptx — совпадает с Excel визуально).

Монотонный кубический сплайн Фритча–Карлсона (касательные m_k):

pub fn monotone_tangents(x: &[f64], y: &[f64]) -> Vec<f64> {
    let n = x.len(); let mut m = vec![0.0; n];
    let d: Vec<f64> = (0..n-1).map(|k| (y[k+1]-y[k]) / (x[k+1]-x[k])).collect();   // секущие / secant slopes
    m[0] = d[0]; m[n-1] = d[n-2];
    for k in 1..n-1 { m[k] = if d[k-1] * d[k] <= 0.0 { 0.0 } else { (d[k-1] + d[k]) / 2.0 }; }
    for k in 0..n-1 {
        if d[k] == 0.0 { m[k] = 0.0; m[k+1] = 0.0; continue; }
        let (a, b) = (m[k] / d[k], m[k+1] / d[k]);
        let s = a*a + b*b;
        if s > 9.0 { let t = 3.0 / s.sqrt(); m[k] = t * a * d[k]; m[k+1] = t * b * d[k]; }   // ограничение монотонности / monotonicity limiter
    }
    m
}
/// Эрмит → кубическая Безье: P1 = P0 + m0·h/3, P2 = P3 − m1·h/3 / Hermite to cubic Bézier.
pub fn to_bezier(x: &[f64], y: &[f64], m: &[f64]) -> BezPath { /* … */ }

Office-совместимый режим — сплайн Катмулла–Рома (равномерный): контрольные точки P_k + (P_{k+1} − P_{k−1})/6 и P_{k+1} − (P_{k+2} − P_k)/6; на концах — касательная по соседней точке. Точечная «сглаженная» — та же схема по порядку точек (не по x), поэтому петли допустимы (как в Excel).

5.7 Treemap и солнечные лучи

Squarified treemap (Bruls, Huizing, van Wijk, 2000):

fn squarify(items: &mut [Item] /* отсортированы по убыванию площади */, rect: Rect, out: &mut Vec<(Id, Rect)>) {
    let mut row: Vec<&Item> = vec![]; let mut r = rect; let mut i = 0;
    while i < items.len() {
        let w = r.width().min(r.height());                                   // короткая сторона / shorter side
        let it = &items[i];
        if row.is_empty() || worst(&row, Some(it), w) <= worst(&row, None, w) { row.push(it); i += 1; }
        else { r = layout_row(&row, r, out); row.clear(); }                    // зафиксировать ряд / commit row
    }
    if !row.is_empty() { layout_row(&row, r, out); }
}
fn worst(row: &[&Item], extra: Option<&Item>, w: f64) -> f64 {           // худшее отношение сторон в ряду / worst aspect ratio
    let areas = row.iter().map(|i| i.area).chain(extra.map(|e| e.area));
    let (s, max, min) = areas.fold((0.0, f64::MIN, f64::MAX), |(s, mx, mn), a| (s + a, mx.max(a), mn.min(a)));
    (w*w*max / (s*s)).max(s*s / (w*w*min))
}

Иерархия: площадь узла = сумма листьев (или значение узла, если задано); раскладка рекурсивна внутри прямоугольника узла с отступом padding и полосой заголовка ветви (при branch_labels = Banner); цвет — по ветви верхнего уровня (палитра) или по значению (шкала). Минимальный размер прямоугольника для подписи — 24×14 px, иначе подпись скрывается. Солнечные лучи: уровень d → кольцо [r₀ + d·w, r₀ + (d+1)·w], угловой сектор узла пропорционален его значению внутри родителя; подписи вдоль дуги (текст по пути из Draw) при длине дуги ≥ ширины текста, иначе — радиально или скрыты.

5.8 Гистограмма распределения, Парето, «ящик с усами», каскадная, воронка

Корзины: n значений, IQR = Q3 − Q1, σ — выборочное СКО.

Правило Формула Когда
Стёрджес k = ⌈log₂ n⌉ + 1 авто, если IQR = 0 и σ = 0 (вырожденно)
Фридман–Диаконис h = 2·IQR·n^(−1/3) авто по умолчанию, если IQR > 0
Скотт h = 3,49·σ·n^(−1/3) авто, если IQR = 0, σ > 0

Ширина h округляется к «красивой» (nice_num(h, round=true)), границы — от nice_floor(min) с шагом h, корзины [a, a+h), последняя — закрытая; overflow/underflow — отдельные корзины «> x» / «< y»; «по числу корзин» — h = range / k; подписи корзин — [10; 20) по локали. Парето: корзины (или категории) по убыванию частоты + кумулятивная доля на вторичной оси 0–100 % (линия с маркерами). Ящик с усами: квартили — включающая медиана (QUARTILE.INC, интерполяция по (n−1)·p) или исключающая (QUARTILE.EXC, (n+1)·p); ящик [Q1, Q3], медиана — линия, среднее — маркер ×; усы — до крайних точек внутри [Q1 − 1,5·IQR, Q3 + 1,5·IQR]; выбросы — маркеры за усами; «внутренние точки» — джиттер по горизонтали (детерминированный хэш индекса). Каскадная: нарастающий итог t_i = t_{i−1} + v_i, столбец [t_{i−1}, t_i], точки set_as_total — столбец [0, v_i] и сброс t = v_i; соединители — горизонтальные линии между концом i и началом i+1; три цвета (рост/падение/итог) из палитры (1/2/9). Воронка: столбцы, центрированные по вертикальной оси, ширина ∝ v_i / max, зазор gap, подписи — внутри при ширине ≥ текста, иначе справа.

5.9 Линии тренда (метод наименьших квадратов)

Вид Модель Решение
Линейная y = a + b·x замкнутые формулы (b = Σ(x−x̄)(y−ȳ)/Σ(x−x̄)²); при заданном intercept c: b = Σx(y−c)/Σx²
Экспоненциальная y = a·e^{b·x} линейная МНК по ln y (только y > 0; иначе — тренд недоступен, предупреждение)
Логарифмическая y = a + b·ln x линейная МНК по ln x (x > 0)
Степенная y = a·x^b линейная МНК по ln x, ln y
Полиномиальная y = Σ_{j=0}^{d} c_j x^j, d = 2…6 нормальные уравнения с центрированием и масштабированием x в [−1, 1] + решение Гаусса с выбором ведущего элемента; для d ≥ 4 — QR (Хаусхолдер) на матрице Вандермонда
Скользящее среднее y_i = mean(y_{i−p+1..i}) окно p ≥ 2; первые p−1 точек отсутствуют
/// Полиномиальная аппроксимация степени d по МНК через QR / Polynomial least squares via Householder QR.
pub fn poly_fit(x: &[f64], y: &[f64], d: usize, intercept: Option<f64>) -> Vec<f64> {
    let (xm, xs) = (mean(x), x.iter().fold(0.0f64, |m, v| m.max((v - mean(x)).abs())).max(1e-300));
    let xn: Vec<f64> = x.iter().map(|v| (v - xm) / xs).collect();               // масштабирование / scaling
    let rows = x.len(); let cols = d + 1;
    let mut a = vec![0.0; rows * cols]; let mut b = y.to_vec();
    for i in 0..rows { for j in 0..cols { a[i*cols + j] = xn[i].powi(j as i32); } }
    if let Some(c) = intercept { for i in 0..rows { b[i] -= c; } /* столбец j=0 исключается / drop column 0 */ }
    let coef_n = householder_qr_solve(&a, rows, cols, &b);                       // коэффициенты в масштабе xn / coefficients in xn
    unscale(&coef_n, xm, xs)                                                      // пересчёт в исходный x (биномиальное разложение) / back to x
}
/// R² = 1 − SS_res / SS_tot (для преобразованных моделей — по исходным y, как Excel).
pub fn r_squared(y: &[f64], y_hat: &[f64]) -> f64 { let ym = mean(y); 1.0 - zip_sq(y, y_hat) / y.iter().map(|v| (v-ym).powi(2)).sum::<f64>() }

Уравнение выводится с форматом y = 1,23x² − 4,5x + 6 (коэффициенты — 4 значащие цифры, локаль), продолжение forward/backward — в единицах оси X (категорийная ось: индексы 1…n). Для категорийной оси x_i = i (1-based), как в Excel.

5.10 Планки погрешностей

Fixed(v) — ±v; Percent(p) — ±p %·y; StdDev(k) — ±k·σ ряда от среднего ряда (одна планка на весь ряд, центр — среднее); StdErr — ±σ/√n; Custom — из диапазонов plus/minus поточечно. Направление X — для точечных/пузырьковых; рисуются линия + конечные засечки (end_cap, 6 px); участвуют в автомасштабе (§5.2).

5.11 Анимация появления и обновления

Раскладка параметризована временем t ∈ [0, 1]: marks(t); появление (entrance, 500 мс, ease-out-cubic, уважение «уменьшить движение» → t = 1 сразу):

Тип Эффект
Столбцы/линейчатые/гистограммы/каскад/воронка рост от базовой линии: высота h·t, по рядам с задержкой 60 мс
Линии/области «рисование» слева направо: отсечение по x ≤ x_min + t·width (область — вместе с заливкой)
Круговая/кольцевая/солнечные лучи развёртка: угол sweep·t от first_slice_angle
Точечная/пузырьковая масштаб маркеров 0 → 1 с задержкой по индексу (≤ 300 мс суммарно)
Treemap прямоугольники из центра родителя (lerp)
Лепестковая радиус r·t

Обновление данных: старые и новые marks сопоставляются по ключу (series_id, category_key | index); для совпавших — интерполяция геометрии (положение/размер/угол/путь — по точкам при равном числе, иначе ресэмплинг), для новых — появление, для исчезнувших — выцветание 200 мс; оси — интерполяция min/max/step (деления перерисовываются с кросс-фейдом). В Slides анимация «по сериям/категориям» (bldChart) реализуется планировщиком Slides поверх marks с маской видимости по (series, category) — движок даёт разбиение списка отображения на группы.

5.12 Hit-testing и подсказки

HitIndex строится вместе с раскладкой:

Элементы Структура Проверка
Столбцы, ящики, прямоугольники treemap, воронка, легенда, заголовки, подписи список bbox (≤ 5 000) или R-дерево (rstar) точка в прямоугольнике, последний по z
Сектора массив секторов по углу r_in ≤ ρ ≤ r_out и θ ∈ [θ_i, θ_{i+1}) в полярных координатах относительно центра (с учётом выноса)
Маркеры точечной/пузырьковой/линий KD-дерево (kiddo) по пикселям ближайшая точка в радиусе max(8, r_marker + 4) px
Линии/области ломаные расстояние до сегмента ≤ 6 px; для подсказки «по категории» — ближайшая по x вершина (режим «вертикальная линия» показывает все ряды в категории)
Оси полоса ±6 px вдоль оси —

Результат — Hit { element: Series(s) | Point(s, i) | Legend(s) | Axis(id) | Title | PlotArea | Trendline(s, k) | ErrorBar(s, i) }; подсказка — шаблон {series}\n{category}: {value} с форматом ячейки; хост рисует её своим виджетом. Клик по точке — Point → выделение элемента (в Sheets подсвечивается диапазон на листе). Производительность: 100 000 точек — построение KD-дерева ≤ 20 мс, запрос ≤ 50 мкс.

5.13 Рендер, экспорт, большие данные

ChartLayout → DisplayList (команды ядра: Rect, RRect, Path, Text(TextLayout), Image, Clip, Group{opacity, transform, tag}, Marker(preset)): группы помечаются тегами (series, point) для анимации и a11y. Skia рисует с антиалиасингом, толщины линий в pt → px по DPI; текст — через text-layout ядра (шейпинг, RTL, подстановка шрифтов). Экспорт: SVG — общий писатель ядра (<g id="series-0">, <text> с font-family и резервами, градиенты в <defs>, clipPath для области построения; размер в pt, viewBox в px), PNG — растр Skia с заданным масштабом/dpi и фоном (прозрачный/заливка), PDF — Meridian Print из того же списка (вектор, текст извлекаемый), буфер обмена — объект Chart (application/vnd.meridian.chart+xml + данные) + SVG + PNG.

Большие данные: линии/точечные > 10 000 точек на ряд — даунсэмплинг LTTB (Largest-Triangle-Three-Buckets) до 2·plot_px точек для рендера (данные и hit-test — исходные); столбцы при cat_px < 1 — агрегирование min/max по пикселю; обновление по пересчёту — не чаще 60 раз/с (объединение событий).

5.14 Спарклайны

Отдельный быстрый конвейер: SparklineGroup { kind, cells, axis: {min: Auto|Group|Fixed, max}, markers, colors, blanks }; рендер прямо в ячейку (список отображения листа) без осей/легенды/подписей, толщина линии 1 pt, маркеры 2×2 px; общая шкала группы — min/max по всем ячейкам; «выигрыш/проигрыш» — столбцы одинаковой высоты по знаку. Совместимость с xlsx x14:sparklineGroups 1:1.

5.15 Карта (P2)

Регионы — полигоны в WGS84 из встроенного пакета (TopoJSON, упрощённые до 1:50 м), сопоставление названий регионов с категориями по словарю (русские/английские названия, коды ISO 3166-1/2, ОКАТО/ОКТМО для РФ) с нечётким поиском; проекция — равновеликая (Эккерт IV для мира, Ламберта коническая для РФ/стран), значение → цвет по шкале; hit-test — точка в полигоне (R-дерево + even-odd); регионы без данных — серые; подписи — в центроиде (полюс недоступности) при размере ≥ 40 px.


6. Связь с данными

pub trait DataProvider {
    fn resolve(&self, r: &RangeRef) -> Result<ResolvedRange, DataError>;              // значения + форматы + типы / values, formats, types
    fn subscribe(&self, r: &RangeRef, cb: SubscriberId) -> SubscriptionId;             // уведомления о пересчёте / recalc notifications
    fn unsubscribe(&self, id: SubscriptionId);
    fn adjust(&self, r: &RangeRef, change: &StructuralChange) -> Option<RangeRef>;     // вставка/удаление строк и столбцов / structural edits
    fn display_name(&self, r: &RangeRef) -> String;                                    // «Данные!$B$2:$B$5» по локали
}

7. Сопоставление с OOXML и ODF

7.1 OOXML DrawingML Charts (c: — chart1.xml, cx: — chartEx1.xml)

OOXML ChartModel Примечание
c:chartSpace (c:roundedCorners, c:style, mc:AlternateContent/c14:style, c:clrMapOvr, c:spPr, c:txPr) ChartModel{rounded_corners, style, ...} c:style 1–48 → таблица ChartStyle; cs:chartStyle (part chartStyle1.xml, 2013+) и cs:colorStyle — приоритетнее
c:chart/c:title, c:autoTitleDeleted, c:chart/c:plotVisOnly, c:dispBlanksAs, c:showDLblsOverMax title, plot_visible_only, display_blanks_as заголовок как c:tx/c:rich или c:tx/c:strRef
c:plotArea/c:layout/c:manualLayout (xMode/yMode = edge, x,y,w,h, layoutTarget inner/outer) PlotArea.layout доли от области диаграммы
c:barChart (c:barDir, c:grouping clustered/stacked/percentStacked, c:varyColors, c:gapWidth, c:overlap, c:serLines) Plot{Bar} c:bar3DChart → плоский с пометкой ext:3d (round-trip c:view3D)
c:lineChart (c:grouping, c:marker, c:dropLines, c:hiLowLines, c:upDownBars, c:smooth) Plot{Line} c:line3DChart → плоский
c:areaChart, c:area3DChart Plot{Area} —
c:pieChart (c:firstSliceAng, c:ser/c:explosion), c:doughnutChart (c:holeSize), c:ofPieChart (c:ofPieType pie/bar, c:splitType pos/val/percent/cust, c:splitPos, c:custSplit, c:secondPieSize, c:gapWidth, c:serLines) Plot{Pie | Doughnut | OfPie} c:pie3DChart → плоский
c:scatterChart (c:scatterStyle none/line/lineMarker/marker/smooth/smoothMarker) Plot{Scatter} x_values из c:xVal, values из c:yVal
c:bubbleChart (c:bubbleScale, c:showNegBubbles, c:sizeRepresents area/w, c:bubble3D) Plot{Bubble} —
c:radarChart (c:radarStyle standard/marker/filled) Plot{Radar} —
c:stockChart (c:hiLowLines, c:upDownBars), 3–4 ряда в порядке O/H/L/C Plot{Stock} объём — отдельный c:barChart на вторичной оси
c:surfaceChart/c:surface3DChart (c:wireframe, c:bandFmts) Plot{Surface} контур P1, 3D P2
c:ser: c:idx, c:order, c:tx, c:spPr, c:marker, c:dPt, c:dLbls, c:trendline, c:errBars, c:cat/c:xVal, c:val/c:yVal, c:bubbleSize, c:smooth, c:invertIfNegative, c:explosion, c:pictureOptions Series c:extLst/c15:* (invertSolidFillFmt, datalabelsRange, filteredSeries…)
c:cat/c:numRef | c:strRef | c:multiLvlStrRef, c:f, c:numCache/c:strCache (c:ptCount, c:pt idx, c:formatCode), c:numLit/c:strLit DataSource::Range{reference, cache} / Literal c:f разбирается парсером ссылок Sheets (внешние книги [1]Лист!A1 → связь по externalData)
c:catAx, c:valAx, c:dateAx, c:serAx: c:axId, c:scaling (c:orientation, c:min, c:max, c:logBase), c:delete, c:axPos, c:majorGridlines, c:minorGridlines, c:title, c:numFmt (sourceLinked), c:majorTickMark, c:minorTickMark, c:tickLblPos, c:spPr, c:txPr, c:crossAx, c:crosses/c:crossesAt, c:auto, c:lblAlgn, c:lblOffset, c:tickLblSkip, c:tickMarkSkip, c:noMultiLvlLbl, c:crossBetween, c:majorUnit, c:minorUnit, c:dispUnits, c:baseTimeUnit, c:majorTimeUnit Axis c:txPr/a:bodyPr/@rot → поворот подписей (−5400000 = −90°)
c:legend (c:legendPos r/l/t/b/tr, c:legendEntry/c:idx/c:delete, c:layout, c:overlay) Legend —
c:dLbls/c:dLbl (c:numFmt, c:spPr, c:txPr, c:dLblPos, c:showLegendKey/Val/CatName/SerName/Percent/BubbleSize, c:separator, c:showLeaderLines, c15:showDataLabelsRange, c15:layout) DataLabels выноски — c15:spPr + c:dLbl/c15:layout
c:trendline (c:name, c:spPr, c:trendlineType exp/linear/log/movingAvg/poly/power, c:order, c:period, c:forward, c:backward, c:intercept, c:dispRSqr, c:dispEq, c:trendlineLbl) Trendline —
c:errBars (c:errDir x/y, c:errBarType both/minus/plus, c:errValType cust/fixedVal/percentage/stdDev/stdErr, c:noEndCap, c:plus/c:minus, c:val) ErrorBars —
c:dTable (c:showHorzBorder/Vert/Outline/Keys) таблица данных (P1) —
c:externalData r:id + c:autoUpdate, part embeddings/*.xlsx встроенная книга → objects/chart-*.msx (конвертация xlsx ↔︎ msx при импорте/экспорте) —
c:userShapes (part drawing) фигуры поверх диаграммы (сцена) P1
c:pivotSource, c:pivotFmts сводная диаграмма (Sheets) —
c:printSettings, c:protection настройки печати/защиты round-trip
cx:chartSpace/cx:chart (cx:plotArea/cx:plotAreaRegion/cx:series layoutId="treemap | sunburst | clusteredColumn | paretoLine | boxWhisker | waterfall | funnel | regionMap", cx:binning (cx:binSize, cx:binCount, cx:underflow/overflow), cx:subtotals (waterfall totals), cx:statistics quartileMethod, cx:parentLabelLayout, cx:regionLabelLayout, cx:geography, cx:axis, cx:dataLabels, cx:legend, cx:title) + cx:chartData/cx:data/cx:numDim | cx:strDim (cx:f, cx:lvl) современные типы Plot{Treemap | Sunburst | Histogram | Pareto | BoxPlot | Waterfall | Funnel | Map} в документе — mc:AlternateContent с Requires="cx1..cx8" и Fallback (растр или пустая рамка); при экспорте пишем cx: + PNG-резерв
x14:sparklineGroups (xlsx worksheet extLst) SparklineGroup 1:1

7.2 ODF (office:chart в объекте Object N/content.xml)

ODF ChartModel Примечание
chart:chart (chart:class: chart:bar, chart:line, chart:area, chart:circle, chart:ring, chart:scatter, chart:bubble, chart:radar, chart:filled-radar, chart:stock, chart:surface, chart:gantt), svg:width/height, chart:style-name ChartModel + первый Plot chart:gantt не поддерживается (нет в LibreOffice)
chart:title, chart:subtitle, chart:footer (с text:p, table:cell-range для связанного) title, subtitle footer → подзаголовок снизу (расширение meridian:footer)
chart:legend (chart:legend-position start/end/top/bottom/top-start…, style:legend-expansion, svg:x/y) Legend —
chart:plot-area (table:cell-range-address, chart:data-source-has-labels none/row/column/both, svg:x/y/width/height, chart:series-source columns/rows) PlotArea, ориентация данных svg:x/y → layout
chart:axis (chart:dimension x/y/z, chart:name primary-x/secondary-y…, chart:categories, chart:title, chart:grid chart:class major/minor) + стиль style:chart-properties (chart:logarithmic, chart:minimum/maximum/origin, chart:interval-major/interval-minor-divisor, chart:display-label, chart:tick-marks-major-inner/outer, chart:visible, chart:reverse-direction, chart:axis-position, chart:axis-label-position, chart:tick-mark-position, style:rotation-angle, chart:link-data-style-to-source, style:data-style-name) Axis формат чисел через number:number-style
chart:series (chart:values-cell-range-address, chart:label-cell-address, chart:class для комбинированных, chart:attached-axis, chart:style-name, chart:domain для X/размеров) Series chart:domain — X точечной (первый) и размеры пузырьков (второй)
chart:data-point (chart:repeated, chart:style-name) points —
chart:regression-curve + chart:equation + стиль chart:regression-type linear/logarithmic/exponential/power/polynomial/moving-average, chart:regression-max-degree, chart:regression-moving-type, chart:regression-period, chart:regression-force-intercept/intercept-value, chart:regression-extrapolate-forward/backward, chart:regression-name Trendline —
chart:error-indicator + стиль chart:error-category none/variance/standard-deviation/percentage/error-margin/constant/standard-error/cell-range, chart:error-percentage, chart:error-margin, chart:error-lower-limit/upper-limit, chart:error-upper-indicator/lower-indicator, chart:error-lower-range/upper-range, chart:dimension ErrorBars —
chart:mean-value линия среднего (P1) —
chart:stock-gain-marker, chart:stock-loss-marker, chart:stock-range-line UpDownBars, hi_lo_lines —
chart:wall, chart:floor заливка области построения 3D — игнор
стиль style:chart-properties: chart:vertical, chart:stacked, chart:percentage, chart:overlap, chart:gap-width, chart:interpolation none/cubic-spline/b-spline, chart:spline-resolution, chart:symbol-type none/automatic/named-symbol, chart:symbol-name, chart:symbol-width/height, chart:lines, chart:solid-type, chart:three-dimensional, chart:deep, chart:angle-offset, chart:pie-offset, chart:hole-size (loext:), chart:treat-empty-cells leave-gap/use-zero/ignore, chart:include-hidden-cells, chart:data-label-number value/percentage/value-and-percentage/none, chart:data-label-text, chart:data-label-symbol, chart:data-label-series, chart:label-position, chart:label-position-negative, chart:japanese-candle-stick, chart:connect-bars, chart:group-bars-per-axis, chart:sort-by-x-values, chart:right-angled-axes, chart:auto-position/auto-size, chart:regression-* соответствующие поля cubic-spline → монотонный (ближайший) с пометкой; b-spline → Catmull-Rom
table:table внутри объекта (локальная копия данных) cache в документе Sheets — ссылки table:cell-range-address первичны
loext: расширения LibreOffice (loext:custom-leader-lines, loext:label-stroke, loext:data-table, loext:hide-legend, loext:regression-*) по возможности неизвестные — round-trip

7.3 Известные трудности

Трудность Решение
Автомасштаб Excel не описан в стандарте; правила «5/6», отступы и число делений подобраны эмпирически корпус 5 000 диаграмм xlsx → снимки; правила §5.2 проверяются сравнением границ осей (допуск: совпадение min/max/major в 97 % случаев)
Автоцвета по c:style (48 стилей × тема) и cs:colorStyle (meth="cycle"/"withinLinear"/"acrossLinear", variation lumMod/lumOff) таблица стилей + реализация методов colorStyle; снимочные тесты против Excel
c:numFmt sourceLinked="1" — формат из ячеек format_codes в кэше; при отсутствии — General
Авторазмер области построения: Excel уменьшает область по измерениям текста своим шрифтовым движком (Calibri) метрически совместимые шрифты (Carlito); допуск ±2 px; при manualLayout — точно
Положения подписей по умолчанию различаются по типам (bestFit для круговых, outEnd для столбцов, r для точек) таблица умолчаний на тип в кодеке
Сглаживание Excel (кардинальный сплайн) ≠ монотонному режим Office-совместимый при импорте (§5.6)
3D-диаграммы (c:view3D rotX/rotY/perspective, c:floor/sideWall/backWall) плоский рендер с имитацией (градиенты), round-trip 3D-параметров; настоящее 3D — P2
Даты в категорийной оси (c:dateAx с baseTimeUnit) и «пробелы» между датами временная шкала с единицами, как Excel: ось дат — равномерная по времени, не по индексу
Многоуровневые категории (c:multiLvlStrRef) группы подписей с объединяющими линиями (до 3 уровней)
cx: chartex: отдельная модель данных (cx:data по измерениям), нет осей в классическом смысле, mc:AlternateContent в drawing отдельный парсер; при записи — cx: + Fallback с PNG; старые приложения увидят картинку
ODF хранит диаграмму как объект с локальной таблицей, ссылки на ячейки — через table:cell-range-address; ориентация данных (chart:series-source) и флаги заголовков нормализация к Series при чтении; при записи — columns и явные chart:series
Нет treemap/sunburst/waterfall/funnel/histogram/boxplot в ODF 1.3 запись как ближайший класс (chart:bar) + meridian: расширение с полной моделью + растровый кэш draw:image в draw:frame (LibreOffice покажет картинку)
Спарклайны в ODS loext:sparkline-groups (LibreOffice 7.4+) 1:1
Внешние книги в c:f ([1]Лист1!$A$1) externalReferences xlsx → RangeRef::External{book}; обновление по запросу
Диаграммы в docx/pptx с c:externalData без встроенной книги (ссылка на файл) кэш + предупреждение; «Связать с файлом»

8. MeridianScript API

Пространство имён meridian.charts; объект Chart доступен из sheets.Sheet.charts, write.Document.charts, slides.Shape.chart, draw.Node.chart — один и тот же тип.

Объект Основные члены
Chart id, type (составной: "column.clustered", "line.smooth", "pie", "scatter.markers", "treemap", …), title, subtitle, series: SeriesCollection, axes: { category, value, secondaryCategory?, secondaryValue? }, legend, plotArea, style (1–16), palette, dataLabels (по умолчанию для всех), blanksAs, plotVisibleOnly, setData(range | values, opts), switchRowCol(), refresh(), applyTemplate(path), saveTemplate(path), toSvg(opts), toPng(opts), toImageData(opts), describe() (a11y-текст), on('dataChanged' | 'select' | 'hover', cb)
SeriesCollection count, item(i), byName, add({ name, values, categories?, xValues?, sizes?, type?, axisGroup? }), remove(i), move(i, j)
Series name, values: DataRef, categories, xValues, sizes, type, axisGroup, fill, line, marker, smooth, invertIfNegative, explosion, points: Point[], point(i), dataLabels, trendlines: TrendlineCollection, errorBars, showInLegend
DataRef formula (строка ссылки, например "Данные!$B$2:$B$5"), values: (number | string | Date)[], set(range | array), isLinked
Axis visible, min, max, majorUnit, minorUnit, logBase, reversed, crosses, crossesAt, title, numberFormat, sourceLinked, labels: { position, rotation, skip, font }, gridlines: { major, minor }, ticks, displayUnits, dateUnits
Legend visible, position, overlay, font, entries[i].hidden
DataLabels visible, show: { value, category, seriesName, percent, bubbleSize, legendKey }, position, numberFormat, separator, leaderLines, fromRange, font
Trendline type, order, period, forward, backward, intercept, showEquation, showR2, name, line, coefficients(): number[], r2(): number, predict(x)
ErrorBars direction, type, valueType, value, plus, minus, endCap, line
charts.palettes list(), get(name), register(name, colors), checkAccessibility(colors): Report
charts.templates list(), load(path), save(chart, path)
// Sheets: создать диаграмму из диапазона, добавить тренд и подписи / Build a chart from a range with a trendline
import { sheets } from "meridian";
const sh = sheets.activeWorkbook.sheet("Данные");
const ch = sh.charts.add("column.clustered", { anchor: "E2", width: 480, height: 300 });
ch.setData(sh.range("A1:C13"), { seriesIn: "columns", firstRowLabels: true, firstColCategories: true });
ch.title.text = "Выручка и прибыль, 2026";
ch.axes.value.numberFormat = "#,##0 ₽";
ch.series.item(1).type = "line.markers"; ch.series.item(1).axisGroup = "secondary";
const t = ch.series.item(0).trendlines.add({ type: "polynomial", order: 2, forward: 1, showR2: true });
console.log("R² =", t.r2().toFixed(3), "коэффициенты:", t.coefficients());
ch.dataLabels = { visible: true, show: { value: true }, position: "outsideEnd" };
// Slides: обновить данные всех диаграмм презентации из книги Sheets и экспортировать PNG / Refresh from a workbook
import { slides, sheets } from "meridian";
const wb = await sheets.open("Квартал.msx");
for (const s of slides.activePresentation.slides)
  for (const sh of s.shapes) if (sh.chart && sh.name.startsWith("chart:")) {
    const range = sh.name.slice(6);                       // имя фигуры хранит адрес, например "chart:Итоги!A1:D5"
    sh.chart.setData(wb.range(range), { seriesIn: "columns", firstRowLabels: true, firstColCategories: true });
    await sh.chart.toPng({ path: `out/${s.index}-${sh.id}.png`, scale: 2 });
  }
// Доступность и подсказки: описание диаграммы и реакция на наведение / a11y description and hover
const ch = sheets.activeSheet.charts.item(0);
console.log(ch.describe());   // «Гистограмма с 2 рядами и 12 категориями. Выручка: от 120 до 170 … максимум в Q4 …»
ch.on("hover", (e) => { if (e.point) sheets.ui.statusBar.text = `${e.series.name} · ${e.point.category}: ${e.point.formattedValue}`; });

9. Критерии готовности

№ Критерий Метрика
1 Типы все P0-типы и элементы §2 рендерятся; P1-типы — к концу фазы 3
2 Точность импорта корпус ≥ 5 000 диаграмм из xlsx/pptx/docx/ods/odp/odt: 100 % открытие без сбоев (фаззинг 48 ч); SSIM рендера ≥ 0,95 против Excel/PowerPoint для 90 % диаграмм, ≥ 0,9 для 98 %; границы осей совпадают в 97 %
3 Round-trip xlsx → msx → xlsx без потерь для всех свойств P0; Excel открывает без «восстановления»; chartex сохраняет cx: и резерв
4 ODF LibreOffice открывает наш ods/odp с диаграммами без предупреждений; корпус 1 000 файлов LibreOffice: SSIM ≥ 0,95
5 Производительность раскладка + рендер 1 000 точек ≤ 4 мс; 100 000 точек точечной ≤ 16 мс кадр (LTTB/KD-дерево); обновление по пересчёту ≤ 16 мс; 500 диаграмм на листе — прокрутка 60 к/с (ленивый рендер видимых)
6 Деления осей extended на наборе из 10 000 случайных диапазонов: шаги только из Q, покрытие ≥ 95 %, число делений в [m−2, m+2]
7 Статистика тренды совпадают с Excel LINEST/LOGEST/TREND до 1e−9 относительной погрешности; квартили — с QUARTILE.INC/EXC; гистограммные корзины — с Excel для авто-режима
8 Подписи на корпусе нет наложений подписей данных при allow_hide (проверка по пересечению bbox)
9 Экспорт SVG валиден и открывается в браузерах/Inkscape с совпадением ≥ 0,98 SSIM; PNG 1×/2×/3×; PDF с извлекаемым текстом
10 Доступность describe() для всех типов; таблица данных для скринридера; «Доступная» палитра проходит проверку CVD; redundant_encoding
11 API §8 покрыт тестами; примеры в галерее Sheets/Slides
12 Единство одна и та же диаграмма в Sheets/Write/Slides/Draw рендерится пиксельно одинаково (снимочный тест через все хосты)

10. Риски

Риск Вероятность Влияние Снижение
Эмпирические правила Excel (автомасштаб, авторазмер, цвета стилей) не воспроизводятся полностью высокая средняя большой корпус, допуски, приоритет явных значений при импорте
chartex (cx:) — отдельная и слабо документированная модель (документация MS-ODRAWXML) средняя средняя тесты на файлах Excel 2016–2024; резерв PNG
3D-диаграммы ожидаются пользователями Excel средняя низкая плоская имитация в 1.0; 3D (простая проекция без освещения) — P2
Карты: лицензии геоданных и спорные границы средняя средняя Natural Earth (PD) + отдельные пакеты по регионам; границы РФ — по законодательству РФ в российской сборке, настройка «вариант границ»
Производительность текста (тысячи подписей) средняя средняя кэш TextLayout по строке+стилю, пропуск подписей, LTTB
Несовпадение шрифтовых метрик (Calibri ↔︎ Carlito) влияет на авторазмер высокая низкая допуски, manualLayout при экспорте для точного совпадения
Расхождение ожиданий «умных подписей» (Excel не прячет, мы прячем) низкая низкая настройка allow_hide, по умолчанию как Excel при импорте

11. Вопросы к библии

  1. Расположение документа. docs/apps/sheets.md ссылается на docs/kernel/chart.md; этот документ — docs/algorithms/chart-engine.md. Предложение: закрепить в §8 библии папку для модулей ядра (docs/kernel/ или docs/algorithms/) и поправить ссылку в Sheets.
  2. Шаблоны диаграмм .mcht. Расширение введено в docs/apps/sheets.md; в таблице §3.2 библии его нет — добавить (application/vnd.meridian.chart-template), а также MIME объекта диаграммы в буфере обмена application/vnd.meridian.chart+xml.
  3. Геоданные. Для карт нужна политика по пакетам геоданных (размер ~20 МБ, лицензии, варианты границ) — отдельное решение владельца до фазы с P2.
  4. 3D. Подтвердить, что объёмные диаграммы (как и 3D-фигуры в Slides/Draw) — вне 1.0 и не входят в критерий «не хуже оригинала» §1.2 библии для корпуса тестов (учитываются как плоские с допуском).