Skip to content
Open

Dev #97

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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- ECharts categorical legends (and their title graphics) are pinned with
`legend.right` instead of a design-canvas `left` pixel. Hosts that size the
container independently of `_width` and call `chart.resize()` keep the
reserved gutter instead of overlapping the plot or clipping the legend
([#98](https://github.com/microsoft/flint-chart/issues/98)).
- Visible units now require an explicit `unit` in the field's semantic
annotation. Conventional compact units may accompany values, while lexical
units such as `years` are stated once as part of the field title. Bar Tables
also no longer repeat their value column as annotations on the bars.
- A raw sum-stacked chart whose total lands exactly on a clean axis tick now
keeps that edge flush instead of adding an empty interval above it, including
machine-scale residue from calculated shares. Totals meaningfully beyond the
clean endpoint still advance to the next tick; the rule is derived from the
plotted stack and does not special-case percentages or 100.
- Series-end labels now use a bounded screen-space packing pass when endpoints
form one readable column. Small adjustments keep labels attached by proximity;
crowded or horizontally staggered sets fall back together to the next legend
placement instead of leaving a partial or overlapping direct-label system.

## [0.5.1] - 2026-08-13

### Added
Expand Down
15 changes: 14 additions & 1 deletion agent-skills/flint-chart-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,20 @@ understates what you know:
}
```

- `unit` — the unit or currency code: `"USD"`, `"°C"`, `"kg"`.
- `unit` — an optional assertion that authorizes Flint to display a unit. Add
it only when the data or surrounding context establishes the measurement
and seeing it materially changes how a reader interprets the number. A type
such as `Duration`, a field name such as `life_expectancy`, or values that
merely look plausible are not enough evidence by themselves.
- Prefer canonical codes: `"USD"`, `"°C"`, `"kg"`, `"km/h"`, `"min"`.
- Conventional compact units are normalized and may appear beside values
(`USD` → `$`, `hours` → `hr`).
- Lexical units such as `"years"` are stated once beside the field name as
`field (years)`, not repeated after every value.
- Do not put explanatory phrases in `unit`. Put qualifications such as
`"per working-age resident"` or `"constant 2024 prices"` in the subtitle.
- Omit `unit` when its meaning, scale, or denominator is uncertain. Flint
does not infer a visible unit from the semantic type or field name.
- `intrinsicDomain` — the field's own bounds, for bounded scales only: `[1, 5]`
for a five-star rating, `[0, 100]` for a percentage score. Not for
open-ended measures.
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ interface AssembleOptions {
maxStretchX?: number; // per-dimension width cap (derived from canvasSize)
maxStretchY?: number; // per-dimension height cap (derived from canvasSize)
facetElasticity?: number; // facet stretch (default 0.3)
minStep?: number; // min px per discrete item (default 6)
minStep?: number; // min px per discrete item (default 8)
minSubplotSize?: number; // min facet subplot px (default 60)
maxColorValues?: number; // color cardinality before truncation (default 24)
stepPadding?: number; // band inner padding fraction (default 0.1)
Expand Down
63 changes: 63 additions & 0 deletions docs/community-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Community backends

Community backends extend Flint to additional renderers and delivery surfaces.
They use the same `ChartAssemblyInput`, but may have different chart coverage,
release cadence, and gallery, editor, MCP, or ThemeSpec integration from Flint's
core backends.

## Image-Charts

> Originally contributed by
> [François-Guillaume Ribreau](https://github.com/FGRibreau).

The Image-Charts backend compiles a Flint input into an unsigned URL for the
third-party [Image-Charts](https://www.image-charts.com/) service. It is useful
when the output must work as an ordinary image URL, including email, generated
documents, chat messages, and other no-JavaScript environments.

```ts
import {
assembleImageCharts,
isImageChartsSupported,
} from 'flint-chart/image-charts';

if (isImageChartsSupported(input.chart_spec.chartType)) {
const artifact = assembleImageCharts(input);
// { type: 'image-charts', url: 'https://image-charts.com/chart?...' }
}
```

Assembly is pure: it creates the URL without making a network request. Loading
the returned URL sends the encoded chart data to Image-Charts, so do not use it
with confidential data unless sending that data to the service is acceptable
under your privacy and deployment requirements.

### Supported charts

- Bar Chart, Grouped Bar Chart, and Stacked Bar Chart
- Line Chart, Sparkline, and Area Chart
- Scatter Plot
- Pie Chart and Donut Chart
- Radar Chart

Unsupported chart types and faceted inputs throw an error rather than silently
falling back to another representation.

### Current scope

- Output is an unsigned `https://image-charts.com/chart?...` GET URL. Account
identifiers, HMAC signatures, and secrets are outside this pure compiler.
- Width and height are clamped to 999 pixels, and total area is clamped to
998,001 pixels, matching the service's documented chart-size limits.
- Data, labels, legends, colors, and titles are carried in the query string.
Large or label-heavy charts can produce long URLs; Flint does not currently
convert them to Image-Charts POST requests or enforce a maximum URL length.
- Banded bar charts use Flint's overflow filtering before URL serialization.
- The backend uses a fixed categorical palette. ThemeSpec and most
`chartProperties` are not applied.
- Flint does not currently render this artifact in its gallery, editor, or MCP
server. Availability, caching, retention, quotas, and subscription behavior
are controlled by Image-Charts.

See the [Image-Charts API documentation](https://documentation.image-charts.com/)
for the hosted service's current request grammar and limits.
5 changes: 4 additions & 1 deletion docs/design-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,10 @@ Only override native formatting when semantic context adds value: prefix/suffix,
| **Sentiment / Correlation** | `+` + data-driven | — | — | — | Signed decimal |
| **Latitude / Longitude** | — (empty) | — | — | — | VL native |

Unit/currency priority is `annotation.unit` > column-name heuristics > data-value scanning > type defaults.
Visible unit text requires `annotation.unit`; semantic types, column names, and
data values do not authorize display by themselves. Conventional compact units
such as `$`, `%`, `°C`, `kg`, or `min` may accompany values. Lexical units such
as `years` are stated once with the field title (`field (years)`).

**Parsing** is the compiler's job, guided by semantic type rather than stored on context:

Expand Down
8 changes: 4 additions & 4 deletions docs/design-stretch-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,12 @@ The layout balances two directions:
| $L_{\max}$ | Maximum axis length | `base × β` (β from `maxStretch` or `canvasSize`) | 800 px |
| $N$ | Number of banded items | Field cardinality | data-dependent |
| $\ell_0$ | Natural (base) size per band | `defaultBandSize` | ~20 px |
| $\ell_{\min}$ | Minimum size per band | `minStep` option | 6 px |
| $\ell_{\min}$ | Minimum size per band | `minStep` option | 8 px |
| $\ell_{\max}$ | Maximum size per band | `maxBandSize` option | = $\ell_0$ |
| $\alpha$ | Elasticity exponent | `elasticity` option | 0.5 |
| $\beta$ | Maximum stretch multiplier | `maxStretch`, or derived from `canvasSize` | 1.5 |

> **Code defaults:** `elasticity: 0.5`, `minStep: 6`, and `maxStretch: 1.5` when no `canvasSize` ceiling is set. $\ell_0$ and $\ell_{\max}$ are given at a 300 px reference canvas and scaled with size: `round(bandSize × max(1, sizeRatio))`.
> **Code defaults:** `elasticity: 0.5`, `minStep: 8`, and `maxStretch: 1.5` when no `canvasSize` ceiling is set. $\ell_0$ and $\ell_{\max}$ are given at a 300 px reference canvas and scaled with size: `round(bandSize × max(1, sizeRatio))`.

### §2.2.1 Band size bounds — min, base, max

Expand Down Expand Up @@ -422,7 +422,7 @@ Grouped items, such as a grouped bar chart with $m$ sub-bars per group, are trea
| Parameter | Simple discrete | Grouped bar ($m$ sub-bars) |
|---|---|---|
| $\ell_0$ (natural) | `defaultStepSize` | $m \times$ `defaultStepSize` |
| $\ell_{\min}$ (solid) | `minStep` (6 px) | $2m$ px (2 px per sub-bar) |
| $\ell_{\min}$ (solid) | `minStep` (8 px) | $2m$ px (2 px per sub-bar) |
| $N$ (item count) | Field cardinality | Number of **groups** |

The elastic budget formula is unchanged — only the parameter values change.
Expand Down Expand Up @@ -530,7 +530,7 @@ The minimum subplot size ($S_{\min}$) is axis-aware:
|---|---|---|
| $N$ | Number of discrete items | data-dependent |
| $\ell_0$ | Natural step size | ~20 px |
| $\ell_{\min}$ | Minimum step size | 6 px |
| $\ell_{\min}$ | Minimum step size | 8 px |
| $\alpha$ | Elasticity exponent | 0.5 |
| $\beta$ | Maximum stretch | 1.5 |

Expand Down
4 changes: 3 additions & 1 deletion docs/reference-vegalite.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,9 @@ The **Availability** column shows whether a parameter is `always` available or `

**Encoding channels:** `x`, `color`

_No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
| `cornerRadius` | number | 0 – 8 (step 1) | `2` | always | Corner radius for supported marks. |

### ![](chart-icon-bar-table.svg) Bar Table

Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ interface AssembleOptions {
maxStretchX?: number; // per-dimension width cap (derived from canvasSize)
maxStretchY?: number; // per-dimension height cap (derived from canvasSize)
facetElasticity?: number; // facet stretch (default 0.3)
minStep?: number; // min px per discrete item (default 6)
minStep?: number; // min px per discrete item (default 8)
minSubplotSize?: number; // min facet subplot px (default 60)
maxColorValues?: number; // color cardinality before truncation (default 24)
stepPadding?: number; // band inner padding fraction (default 0.1)
Expand Down
8 changes: 4 additions & 4 deletions docs/zh-CN/design-stretch-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ continuousWidth = stepSize × (N + 1)
| $L_{\max}$ | Maximum axis length | `base × β`(β 来自 `maxStretch` 或 `canvasSize`) | 800 px |
| $N$ | Number of banded items | Field cardinality | data-dependent |
| $\ell_0$ | Natural length per item | `defaultStepSize` | ~20 px |
| $\ell_{\min}$ | Minimum length per item | `minStep` option | 6 px |
| $\ell_{\min}$ | Minimum length per item | `minStep` option | 8 px |
| $\alpha$ | Elasticity exponent | `elasticity` option | 0.5 |
| $\beta$ | Maximum stretch multiplier | `maxStretch`,或从 `canvasSize` 推导 | 1.5 |

> **Code defaults:** 未设置 `canvasSize` 上限时,`elasticity: 0.5`、`minStep: 6`、`maxStretch: 1.5`。`defaultStepSize` 根据画布尺寸动态计算:`round(20 × max(1, sizeRatio) × defaultStepMultiplier)`。
> **Code defaults:** 未设置 `canvasSize` 上限时,`elasticity: 0.5`、`minStep: 8`、`maxStretch: 1.5`。`defaultStepSize` 根据画布尺寸动态计算:`round(20 × max(1, sizeRatio) × defaultStepMultiplier)`。

## §2.3 三种状态

Expand Down Expand Up @@ -357,7 +357,7 @@ $$\boxed{\ell = \frac{\kappa \cdot \ell_0 + L_0 / N}{1 + \kappa}}$$
| Parameter | Simple discrete | Grouped bar ($m$ sub-bars) |
|---|---|---|
| $\ell_0$ (natural) | `defaultStepSize` | $m \times$ `defaultStepSize` |
| $\ell_{\min}$ (solid) | `minStep` (6 px) | $2m$ px(每子 bar 2 px) |
| $\ell_{\min}$ (solid) | `minStep` (8 px) | $2m$ px(每子 bar 2 px) |
| $N$ (item count) | Field cardinality | **组**数量 |

elastic budget 公式不变 — 仅参数值变化。
Expand Down Expand Up @@ -465,7 +465,7 @@ gas pressure 模型(§3)在每个子图内运行,容器为 $W_{\text{sub}}
|---|---|---|
| $N$ | Number of discrete items | data-dependent |
| $\ell_0$ | Natural step size | ~20 px |
| $\ell_{\min}$ | Minimum step size | 6 px |
| $\ell_{\min}$ | Minimum step size | 8 px |
| $\alpha$ | Elasticity exponent | 0.5 |
| $\beta$ | Maximum stretch | 1.5 |

Expand Down
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion packages/flint-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@
"import": "./dist/excel/index.js",
"require": "./dist/excel/index.cjs"
},
"./image-charts": {
"types": "./dist/image-charts/index.d.ts",
"import": "./dist/image-charts/index.js",
"require": "./dist/image-charts/index.cjs"
},
"./interactive": {
"types": "./dist/interactive/index.d.ts",
"import": "./dist/interactive/index.js",
"require": "./dist/interactive/index.cjs"
},
"./vegalite/interactive": {
"types": "./dist/vegalite/interactive.d.ts",
"import": "./dist/vegalite/interactive.js",
"require": "./dist/vegalite/interactive.cjs"
},
"./echarts/interactive": {
"types": "./dist/echarts/interactive.d.ts",
"import": "./dist/echarts/interactive.js",
"require": "./dist/echarts/interactive.cjs"
},
"./chartjs/interactive": {
"types": "./dist/chartjs/interactive.d.ts",
"import": "./dist/chartjs/interactive.js",
"require": "./dist/chartjs/interactive.cjs"
},
"./plotly/interactive": {
"types": "./dist/plotly/interactive.d.ts",
"import": "./dist/plotly/interactive.js",
"require": "./dist/plotly/interactive.cjs"
},
"./test-data": {
"types": "./dist/test-data/index.d.ts",
"import": "./dist/test-data/index.js",
Expand Down Expand Up @@ -98,9 +128,11 @@
"peerDependencies": {
"chart.js": "^4.0.0",
"plotly.js": "^2.0.0 || ^3.0.0",
"plotly.js-dist-min": "^2.0.0 || ^3.0.0",
"echarts": "^5.0.0 || ^6.0.0",
"vega": "^5.0.0 || ^6.0.0",
"vega-lite": "^5.0.0 || ^6.0.0"
"vega-lite": "^5.0.0 || ^6.0.0",
"vega-tooltip": "^1.0.0"
},
"peerDependenciesMeta": {
"vega": {
Expand All @@ -109,6 +141,9 @@
"vega-lite": {
"optional": true
},
"vega-tooltip": {
"optional": true
},
"echarts": {
"optional": true
},
Expand All @@ -117,6 +152,9 @@
},
"plotly.js": {
"optional": true
},
"plotly.js-dist-min": {
"optional": true
}
},
"devDependencies": {
Expand Down
23 changes: 23 additions & 0 deletions packages/flint-js/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ Each backend has its own assembly function. All accept the same
| `assembleECharts(input)` | ECharts option object | `import { assembleECharts } from 'flint-chart'` |
| `assembleChartjs(input)` | Chart.js config object | `import { assembleChartjs } from 'flint-chart'` |

### Interactive surface

Interactive renderers are opt-in and shipped separately from the static assembly entry point. The surface owns viewport state, accessible scroll controls, and renderer lifecycle; the caller supplies only a container and chart input.

```ts
import { buildInteractiveChart } from 'flint-chart/interactive';

const surface = buildInteractiveChart(
container,
input,
{
backend: 'vegalite',
renderer: 'canvas',
focusOnClick: true,
},
);

await surface.ready;
// Later: surface.destroy();
```

The facade supports `vegalite`, `echarts`, `chartjs`, and `plotly`, and loads only the selected adapter. Viewport changes retain the backend instance and update it through Vega's dataflow, ECharts `setOption()`, Chart.js `update()`, or Plotly `react()`. Vega-Lite discrete marks also enable local click focus by default: click selects, Shift/Ctrl/Meta-click toggles marks, and clicking empty plot space clears. Set `focusOnClick: false` to disable it. Other backends currently ignore this option. Advanced integrations can use `mountInteractiveChartSurface()` with a custom `InteractiveRendererAdapter`. Existing `assemble*()` calls, static SVG/PNG rendering, and Excel output do not import or execute the interactive surface; they retain the normal first-window overflow fallback.

### Input types

```ts
Expand Down
3 changes: 3 additions & 0 deletions packages/flint-js/src/chartjs/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ export function assembleChartjs(input: ChartAssemblyInput): any {
if (warnings.length > 0) {
cjsConfig._warnings = warnings;
}
if (overflowResult.viewports.length > 0) {
cjsConfig._viewports = overflowResult.viewports;
}

cjsConfig._dataLength = values.length;

Expand Down
Loading
Loading