Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `chart_spec.echarts`, a native escape hatch for the ECharts capabilities the
semantic layer does not model — a second value axis, mixed mark types,
`markLine`, `dataZoom`, formatters, per-series styling. The object is merged
onto the compiled option last, so it wins over the layout pass: plain objects
merge recursively, arrays of plain objects (`series`, `xAxis`, `yAxis`) merge
element-wise by index with entries past the end appended, any other array
replaces, and `_`-prefixed keys are dropped with an `info` warning. Series
entries may bind columns instead of carrying `data`: `field` / `fields` add
series read from the rows (`categoryField` picks the pairing column,
defaulting to the chart's own x field) and `axis: "right"` puts one on a
right-hand value axis, which Flint creates when the chart has none. Bound
entries are added, unbound entries patch by index. Backend-scoped: the other
assemblers ignore the key the way Vega-Lite ignores `theme_spec`.
Guide: `docs/native-echarts-options.md`.
- Viewport changes a gesture commits animate by default. A `navigate` or
`brush-zoom` reset flies home, and a brush zoom tweens into the brushed
region, over 400 ms on every Vega-Lite chart, not only on projected maps.
Expand Down
4 changes: 3 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Conceptual background: [Overview](/documentation/overview) · Pipeline: [Archite
|---------------|-----------|----------|
| Raw table | `data` | `{ values: rows[] }` or `{ url: "..." }` |
| **dataSpec** | `semantic_types` | `field → string` or `field → SemanticAnnotation` |
| **chartSpec** | `chart_spec` | `chartType`, `encodings`, `canvasSize`, `chartProperties` |
| **chartSpec** | `chart_spec` | `chartType`, `encodings`, `canvasSize`, `chartProperties`, `echarts` |

Author `semantic_types` once per dataset and reuse it across charts. During most exploration, `chart_spec` is the only part that changes.

Expand Down Expand Up @@ -122,6 +122,7 @@ interface ChartAssemblyInput {
baseSize?: { width: number; height: number }; // target layout size, default 400×320
canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch
chartProperties?: Record<string, unknown>;
echarts?: Record<string, unknown>;
};
theme_spec?: ThemeSpec | string; // presentation, Vega-Lite only
interaction_spec?: InteractionSpec; // behaviour, Vega-Lite interactive surface only
Expand Down Expand Up @@ -187,6 +188,7 @@ lists the presets a template supports by declaration. See [Using interactions](/
| `baseSize` | **Target** layout size in pixels (default 400×320): the size the chart aims for with typical data. Dense data may stretch past it, up to the ceiling. |
| `canvasSize` | **Hard ceiling:** the maximum size the chart may ever reach, including faceted grids. If omitted, the ceiling is `baseSize × options.maxStretch` (default 1.5×). Per-dimension caps are `βx = canvasSize.width / baseSize.width`, `βy = canvasSize.height / baseSize.height` (each ≥ 1). The base is clamped to the ceiling, so a `canvasSize` on its own acts as a fixed box the chart fills and shrinks to fit without overflowing. |
| `chartProperties` | Template-specific toggles (e.g. `orient`, `opacity`) |
| `echarts` | Native ECharts option patch — the escape hatch for capabilities the semantic layer does not model. ECharts backend only; see [Native ECharts options](./native-echarts-options.md). |

> **base vs. canvas, in one line:** `baseSize` is what the chart *aims for*;
> `canvasSize` is what it *may never exceed*. Use `canvasSize` for a fixed slot,
Expand Down
Binary file added docs/figs/native-echarts-dual-axis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions docs/native-echarts-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Native ECharts options

Flint's semantic layer covers the common cases. ECharts is much larger than any
channel set: a second value axis, a bar and a line in one chart, `markLine` /
`markArea`, `dataZoom`, `visualMap`, axis and tooltip formatters, per-series
`areaStyle`, custom `grid`s — the list does not end. Without a way to reach
those, a caller whose chart needs one of them has to costndon Flint and
hand-write the option, and loses the semantic layer it came for.

`chart_spec.echarts` is that way. It is a **native escape hatch, not a second
semantic language**: nothing in it decides anything about the data. It merges
what you state onto the option Flint compiled, and — optionally — binds extra
series to columns so a native series does not mean re-serialising your rows.

```jsonc
{
"chartType": "Line Chart",
"encodings": { "x": { "field": "month" }, "y": { "field": "revenue" } },
"echarts": {
"legend": { "show": true, "top": 6 },
"yAxis": [{ "name": "Revenue" }, { "name": "Margin", "position": "right" }],
"series": [
{ "name": "Revenue", "lineStyle": { "width": 2 } },
{ "type": "line", "field": "margin", "name": "Margin rate", "axis": "right", "lineStyle": { "type": "dashed" } }
]
}
}
```

That spec compiles to a real dual-axis chart: Flint's own series stays on the
left axis, and the added `margin` series lands on a right-hand axis — no
template support required, and no change to the encodings.

<p align="center">
<img src="figs/native-echarts-dual-axis.png" alt="A line chart with the semantic layer's series on the left axis and a column-bound series on a dashed right axis." width="70%">
</p>

## Where it goes

Inside `chart_spec`, beside `encodings` and `chartProperties`. It is
backend-scoped by name and ignored by the other assemblers, the same way
`theme_spec` is Vega-Lite's. Passing it to a Vega-Lite build is harmless and
has no effect.

The patch is applied **last** — after the layout pass and the template's own
post-processing — so what you state is authoritative over what Flint decided
above it.

## Merge semantics

The whole contract is four rules:

| Patch value | Result |
|---|---|
| Plain object | merged key by key, recursively |
| Array of plain objects (`series`, `xAxis`, `yAxis`, …) | merged **element-wise by index**; entries past the end are appended |
| Any other array (a series' `data`, a `color` list) | **replaces** the compiled value |
| Anything else (scalar, `null`) | replaces |

An array of plain objects merging by index is what makes a single series
addressable: `"series": [{}, { "lineStyle": { "type": "dashed" } }]` restyles
the second series without restating the first. The base series keeps its
`data`; only what you name changes.

An empty array says nothing, and never wipes a value Flint just built.

Keys beginning with `_` are reserved by Flint (`_warnings`, `_pivot`,
`_viewports`, …). They are dropped, with an `info` warning in `_warnings`.

## Binding a series to columns

A series entry may carry `field` / `fields` instead of `data`:

| Key | Meaning |
|---|---|
| `field` | one column → one series; `data` is read from the rows as `[category, value]` pairs |
| `fields` | several columns → one series each, named after the column |
| `categoryField` | which column to pair against (defaults to the chart's own x field) |
| `axis` | `"right"` puts the series on a right-hand value axis |

Entries that bind a column are **added**; entries that do not are **patched by
index**. Both intents can appear in one array, and the position of an added
series among the patches does not matter — only its order among the additions.

`"axis": "right"` is sugar: the series gets `yAxisIndex: 1`, a right-hand value
axis is created if the chart does not have one, and the grid reserves room for
it. If you patch `yAxis` yourself, yours wins.

Binding is skipped — with a warning — when the chart has no category field
(a pie, say: the series then gets plain values) or when the chart is faceted
(one option holds every panel, so "the" category axis is ambiguous; pass
explicit `data` there).

## What it does not do

- It does not add channels. `chart_spec.encodings` still only accepts the
channels a template declares; anything beyond that reaches ECharts through
this hatch, not through a new channel.
- It does not validate ECharts' own option vocabulary. An unknown key is
passed through and ECharts ignores it, exactly as it would in a hand-written
option.
- It does not create data. A native series can bind existing columns; it
cannot compute new ones. Aggregate upstream (in SQL, or with
`chartProperties.aggregate`) and bind the result.
- Only the ECharts backend reads it. Chart.js, Plotly, Excel, and Vega-Lite
builds ignore the key.

## Related

- [ECharts chart reference](./reference-echarts.md) — channels and
`chartProperties` per chart type.
- [API reference](./api-reference.md) — `ChartAssemblyInput` and
`chart_spec`.
97 changes: 97 additions & 0 deletions docs/zh-CN/native-echarts-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 原生 ECharts 能力(逃生口)

Flint 的语义层覆盖的是常见情况,而 ECharts 远大于任何一组通道:第二根数值轴、
柱线混排、`markLine` / `markArea`、`dataZoom`、`visualMap`、轴与 tooltip 的
formatter、逐系列 `areaStyle`、自定义 `grid`……列不完。没有出口的话,需要这些
能力的调用方只能放弃 Flint 手写 option,连带丢掉它本来要用的语义层。

`chart_spec.echarts` 就是这个出口。它是**原生逃生口,不是第二套语义语言**:它
不替你决定任何和数据有关的事,只把你写的东西合并到 Flint 编出来的 option 上;
并且可以选择让新增系列直接绑定数据列,免得为了加一条线重新序列化整个数据集。

```jsonc
{
"chartType": "Line Chart",
"encodings": { "x": { "field": "month" }, "y": { "field": "revenue" } },
"echarts": {
"legend": { "show": true, "top": 6 },
"yAxis": [{ "name": "营收" }, { "name": "毛利率", "position": "right" }],
"series": [
{ "name": "营收", "lineStyle": { "width": 2 } },
{ "type": "line", "field": "margin", "name": "毛利率", "axis": "right", "lineStyle": { "type": "dashed" } }
]
}
}
```

上面这段编出来就是一张真正的双轴图:Flint 自己那条线留在左轴,新增的
`margin` 系列落在右轴——不需要任何模板支持,也不需要改 `encodings`。

<p align="center">
<img src="figs/native-echarts-dual-axis.png" alt="语义层的系列在左轴、绑列新增的系列在右侧虚线轴上的折线图。" width="70%">
</p>

## 放在哪

放在 `chart_spec` 里,与 `encodings`、`chartProperties` 同级。它以名字标明后端,
其他装配器会忽略它——和 `theme_spec` 只属于 Vega-Lite 是同一惯例。把它交给
Vega-Lite 构建是无害的,也不会生效。

补丁**最后**应用(在布局计算与模板自身的后处理之后),所以你写的东西优先于
Flint 在上游做出的任何决定。

## 合并语义

整个契约就四条:

| 补丁里的值 | 行为 |
|---|---|
| 普通对象 | 逐键递归合并 |
| 普通对象组成的数组(`series`、`xAxis`、`yAxis` 等) | **按下标逐项**合并;超出基数组长度的条目追加 |
| 其他数组(某系列的 `data`、`color` 列表) | **整体替换** |
| 其他(标量、`null`) | 替换 |

「对象数组按下标合并」是单条系列可寻址的关键:
`"series": [{}, { "lineStyle": { "type": "dashed" } }]` 就能只改第二条线,不必
把第一条重抄一遍;基系列保留自己的 `data`,只有你点名的键会变。

空数组表示「没有要说的」,绝不会把 Flint 刚构造好的值清空。

以 `_` 开头的键是 Flint 保留的(`_warnings`、`_pivot`、`_viewports` 等),会被
丢弃并在 `_warnings` 里留一条 `info`。

## 把系列绑到数据列

系列条目可以用 `field` / `fields` 代替 `data`:

| 键 | 含义 |
|---|---|
| `field` | 一列 → 一条系列;`data` 从数据行读成 `[类目, 数值]` 对 |
| `fields` | 多列 → 各成一条系列,默认以列名命名 |
| `categoryField` | 与哪一列配对(默认取图表自己的 x 字段) |
| `axis` | `"right"` 表示这条系列走右轴 |

**绑列的条目是「新增」,不绑列的条目是「按下标修补」。** 两种意图可以写在同一个
数组里;新增系列在补丁里的位置无关紧要,只有新增之间的先后有意义。

`"axis": "right"` 是语法糖:该系列拿到 `yAxisIndex: 1`,图表若没有右轴就建一根
(并把 grid 的右边距留出来)。如果你自己补了 `yAxis`,以你的为准。

两种情况下绑列会跳过并给出警告:图表没有类目字段(比如饼图,此时系列拿到纯数值
数组);图表是分面的(一个 option 装所有面板,「那个」类目轴是有歧义的,请显式给
`data`)。

## 它不做什么

- **不增加通道**:`chart_spec.encodings` 仍然只接受模板声明的通道;通道之外的能力
走这个出口,而不是新造一个通道。
- **不校验 ECharts 自己的词表**:没见过的键照传,ECharts 自己忽略它——和手写
option 一样。
- **不造数据**:原生系列只能绑已有列,不能算新列。先在 SQL 或
`chartProperties.aggregate` 里聚合好再绑。
- **只有 ECharts 后端读它**:Chart.js、Plotly、Excel、Vega-Lite 构建会忽略该键。

## 相关

- [ECharts 图表参考](./reference-echarts.md)——每种图型的通道与 `chartProperties`。
- [API 参考](./api-reference.md)——`ChartAssemblyInput` 与 `chart_spec`。
13 changes: 13 additions & 0 deletions packages/flint-js/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,19 @@ export interface ChartAssemblyInput {
canvasSize?: { width: number; height: number };
/** Template-specific configurable properties (e.g., bar corner radius, show labels) */
chartProperties?: Record<string, any>;
/**
* Native ECharts option patch — the escape hatch for everything the
* semantic layer does not model (a second value axis, mixed mark
* types, `markLine`, `dataZoom`, formatters, per-series styling, …).
*
* Merged onto the compiled option by the ECharts assembler; see
* `echarts/native-option.ts` for the exact merge semantics and the
* `field` / `fields` / `axis: "right"` series binding sugar.
*
* Backend-scoped by name, and ignored by the other assemblers — the
* same way `theme_spec` is Vega-Lite's.
*/
echarts?: Record<string, unknown>;
};

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/flint-js/src/echarts/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { toTypeString, type SemanticAnnotation } from '../core/field-semantics';
import { filterOverflow } from '../core/filter-overflow';
import { computeLayout, computeChannelBudgets, deriveStretchCaps, resolveBaseSize, resolveFacetColumnsOption } from '../core/compute-layout';
import { ecApplyLayoutToSpec, ecApplyTooltips } from './instantiate-spec';
import { applyNativeEChartsOption } from './native-option';
import { ecCombineFacetPanels } from './facet';
import { DEFAULT_COLORS } from './templates/utils';
import { inferVisCategory, computeZeroDecision } from '../core/semantic-types';
Expand Down Expand Up @@ -530,6 +531,16 @@ export function assembleECharts(input: ChartAssemblyInput): any {
}
}

// ── Native escape hatch (chart_spec.echarts) ──────────────────────────
// Applied last so the caller's native patch is authoritative over every
// decision above it — including the layout pass and template postProcess.
applyNativeEChartsOption(ecOption, input.chart_spec.echarts, {
rows: values,
categoryField: encodings.x?.field,
warnings,
faceted: hasFacet,
});

// ═══════════════════════════════════════════════════════════════════════
// RESULT
// ═══════════════════════════════════════════════════════════════════════
Expand Down
Loading