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-modelSheets (ссылки на диапазоны) или встроенный кэш. Шаблоны диаграмм —.mcht(пакет MOP сchart.xmlбез данных). Пространство имён XML —urn:meridian:office:chart:1(внутри документов любого приложения). Фаза: 2 (вместе с Sheets); современные типы — конец фазы 2 / фаза 3; карты — P2.
Содержание
- Назначение и место в архитектуре
- Типы диаграмм
- Модель диаграммы
- Палитры, стили, темы
- Алгоритмы
- Связь с данными
- Сопоставление с OOXML и ODF
- MeridianScript API
- Критерии готовности
- Риски
- Вопросы к библии
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 Шкалы
- Последовательные: интерполяция в OKLCH от светлого
к тёмному одного оттенка (
--m-ink10 % → 100 %) и многооттеночныеviridis-подобные (встроенная таблица 256 значений; лицензия CC0), 5/7/9 ступеней или непрерывно. - Расходящиеся: синий ↔︎ белый ↔︎ оранжевый
(
#1C7ED6↔︎#F5F6F8↔︎#E67700),RdBu-подобная (ColorBrewer, Apache 2.0), середина — на нуле или на заданном значении. - Текст подписей на заливке — автоматический выбор чёрный/белый по
контрасту WCAG ≥ 4,5:1 (цвета
#1A1D21/#FFFFFF).
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:
simplicity = 1 − (i−1)/(|Q|−1) − j + v,v = 1, если ноль попадает в деления;coverage = 1 − 0,5·((dmax−lmax)² + (dmin−lmin)²) / (0,1·(dmax−dmin))²;density = 2 − max(ρ/ρt, ρt/ρ),ρ = (k−1)/(lmax−lmin),ρt = (m−1)/(max(lmax,dmax) − min(dmin,lmin)),m— целевое число делений;legibility— 1 на этапе шкалы; читаемость (перекрытия, формат) учитывается раскладкой (§5.3), которая при перекрытии запрашивает шкалу с меньшимm.
// 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):
- Столбцы/линейчатые/области/гистограммы: ноль включается всегда
(
dmin = min(0, dmin),dmax = max(0, dmax)). - Линии/точечные/пузырьковые: если
dmin ≥ 0иdmin ≤ 5/6·dmax→dmin = 0; еслиdmax ≤ 0и|dmax| ≤ 5/6·|dmin|→dmax = 0; иначе — отступ:lmin = nice_floor(dmin − 0,05·range). - Отступ сверху:
lmax ≥ dmax + 0,05·range(чтобы маркер не касался рамки), затемextended(lmin, lmax, m)сm = clamp(round(axis_px / 60), 3, 12)для вертикали иaxis_px / 80для горизонтали. - Фиксированные
min/max/majorпользователя переопределяют соответствующую часть (extendedвызывается сonly_looseи зафиксированными границами — перебор только шага). - Лог-ось: границы — степени основания, покрывающие данные; значения ≤ 0 пропускаются с пометкой в a11y-описании.
- Вторичная ось — независимый расчёт; при
align_zero(опция) — подборlmin/lmaxвторой оси так, чтобы нули совпали по пикселям (масштабирование диапазона второй оси). - Единицы отображения (
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 с числом знаков из
шага; «тысячи» — разделители по локали.
Подписи данных (предотвращение наложений):
- Для каждой точки — список кандидатных позиций в порядке предпочтения
(для столбцов:
OutsideEnd, InsideEnd, Center, InsideBase; для точек/линий:Above, Right, Below, Left, AboveRight, …— 8 направлений; для круговой — отдельный алгоритм). - Экран делится на сетку ячеек 4×4 px — битовая карта занятости
(
OccupancyGrid): заранее помечаются оси, легенда, заголовки, столбцы (для «снаружи» подписей — чтобы не накрывать соседние столбцы). - Жадный проход в порядке приоритета (по умолчанию — убывание
|значения|; для линий — последние точки раньше): первая свободная
позиция → подпись ставится, занятость помечается; если свободных нет и
allow_hide— подпись скрывается (отмечается в a11y), иначе — ставится в предпочтительную позицию (наложение допускается, как в Excel приCenter). - Для круговой/кольцевой: подписи
OutsideEndсортируются по углу; на каждой стороне (слева/справа) — «стековая» укладка сверху вниз с минимальным вертикальным интервалом = высота подписи + 2 px; при переполнении стороны — уменьшение радиуса круга (до 70 %) и лидерные линии (две ломаные: от сектора по радиусу, затем горизонтально к подписи);BestFit— внутри сектора, если подпись помещается (проверка bbox внутри сектора по 4 углам), иначе снаружи. - Выноски (
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» по локали
}- Sheets: диаграмма регистрируется в графе
зависимостей
calc-engineкак зависимый узел от всех её диапазонов; после пересчёта затронутых ячеек приходит одно событие на диаграмму (объединение в пределах одного цикла пересчёта), движок перечитывает только изменившиеся диапазоны (сравнение по ревизии диапазона), обновляетcache, перестраивает раскладку (анимация обновления §5.11, если изменилось ≤ 20 % точек; иначе — мгновенно). Ограничение частоты — 16 мс (объединение событий при потоковом вводе/скриптах). Вставка/удаление строк и столбцов корректирует ссылки как у формул (adjust); удаление всего диапазона → ряд переходит вLiteral(cache)с пометкой «связь потеряна» (как#REF!). Именованные диапазоны и структурированные ссылки на таблицы (Таблица1[Выручка]) поддерживаются черезRangeRef. «Переключить строки/столбцы» — переориентация без изменения данных; «Выбрать данные» — диалог Sheets (SH-3x), формула=SERIES(имя; категории; значения; порядок; [размеры])в строке формул при выделении ряда — редактируемая. Скрытые строки/столбцы:plot_visible_only(по умолчанию да). Пустые ячейки — поdisplay_blanks_as; текст в числовом диапазоне — пропуск; ошибки (#Н/Д) — пропуск с пометкой. - Write/Slides/Draw/Forms: встроенная книга
objects/chart-<ulid>.msx(полный grid-model, без UI Sheets) — тот жеDataProvider; документ открывается и рисует диаграмму изcacheбез разбора книги (ленивая загрузка при правке данных); «Связать с файлом» — внешний.msx/.xlsx, обновление по команде/при открытии (external_data{auto_update}); при недоступности файла — кэш и предупреждение. Боковая панель «Данные диаграммы» — мини-редактор grid-model (копирование из Sheets с форматами). - Base: источник — запрос;
RangeRefуказывает на результат запроса (query:<id>— столбцы по именам). - Кэш в
.mcht-шаблонах отсутствует; при применении шаблона данные берутся из текущей диаграммы.
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. Вопросы к библии
- Расположение документа.
docs/apps/sheets.mdссылается наdocs/kernel/chart.md; этот документ —docs/algorithms/chart-engine.md. Предложение: закрепить в §8 библии папку для модулей ядра (docs/kernel/илиdocs/algorithms/) и поправить ссылку в Sheets. - Шаблоны диаграмм
.mcht. Расширение введено вdocs/apps/sheets.md; в таблице §3.2 библии его нет — добавить (application/vnd.meridian.chart-template), а также MIME объекта диаграммы в буфере обменаapplication/vnd.meridian.chart+xml. - Геоданные. Для карт нужна политика по пакетам геоданных (размер ~20 МБ, лицензии, варианты границ) — отдельное решение владельца до фазы с P2.
- 3D. Подтвердить, что объёмные диаграммы (как и 3D-фигуры в Slides/Draw) — вне 1.0 и не входят в критерий «не хуже оригинала» §1.2 библии для корпуса тестов (учитываются как плоские с допуском).