diff --git a/.gitignore b/.gitignore
index 4510397..a5c88ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ build/
coverage/
.nyc_output/
+# Experiment outputs (source, inputs, and manifests stay tracked)
+experiments/*
+
# Logs
npm-debug.log*
yarn-debug.log*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77a3120..5020478 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.4.1] - 2026-07-27
+
+### Changed
+
+- Introduced Flint's faceted flint-shard visual identity across the project
+ README and website, including a public SVG favicon and a compact hero-title
+ lockup.
+
## [0.4.0] - 2026-07-24
### Added
@@ -170,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Treated only lowercase `start` and `end` Waterfall Type values as total
anchors in Vega-Lite; other values now remain floating deltas colored by sign.
-[Unreleased]: https://github.com/microsoft/flint-chart/compare/0.4.0...HEAD
+[Unreleased]: https://github.com/microsoft/flint-chart/compare/0.4.1...HEAD
+[0.4.1]: https://github.com/microsoft/flint-chart/compare/0.4.0...0.4.1
[0.4.0]: https://github.com/microsoft/flint-chart/compare/0.3.0...0.4.0
[0.3.0]: https://github.com/microsoft/flint-chart/compare/88fbeb5ebf07f18a1cf661ebef71cc570b7425d6...0.3.0
[0.2.2]: https://github.com/microsoft/flint-chart/compare/0.2.1...6a9d4e4155e3d9e2bed3fa9adf5316914f791478
diff --git a/README.md b/README.md
index d0903dd..32a93f3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Flint: A Visualization Language for the AI Era
+
Flint: A Visualization Language for the AI Era
[](https://www.npmjs.com/package/flint-chart)
[](https://www.npmjs.com/package/flint-chart-mcp)
diff --git a/assets/flint-logo-pixel.svg b/assets/flint-logo-pixel.svg
new file mode 100644
index 0000000..2003d55
--- /dev/null
+++ b/assets/flint-logo-pixel.svg
@@ -0,0 +1,36 @@
+
\ No newline at end of file
diff --git a/assets/flint-logo.svg b/assets/flint-logo.svg
new file mode 100644
index 0000000..f155dfb
--- /dev/null
+++ b/assets/flint-logo.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/docs/reference-chartjs.md b/docs/reference-chartjs.md
index f06c126..4fece66 100644
--- a/docs/reference-chartjs.md
+++ b/docs/reference-chartjs.md
@@ -125,7 +125,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
###  Bump Chart
@@ -145,7 +145,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
| `opacity` | number | 0.1 – 1 (step 0.05) | `0.4` | conditional | Mark opacity. |
| `stackMode` | choice | Stacked (default) _(default)_, `layered` (Layered (overlap)) | — | always | Stacking strategy for overlapping series. |
diff --git a/docs/reference-echarts.md b/docs/reference-echarts.md
index dc65722..4ee2846 100644
--- a/docs/reference-echarts.md
+++ b/docs/reference-echarts.md
@@ -134,7 +134,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
| `showPoints` | toggle | on / off | `false` | always | Overlay point markers on the line. |
###  Bump Chart
@@ -155,7 +155,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
| `opacity` | number | 0.1 – 1 (step 0.05) | `0.7` | conditional | Mark opacity. |
| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | always | Stacking strategy for overlapping series. |
diff --git a/docs/reference-plotly.md b/docs/reference-plotly.md
index 4bdf2c8..02fc747 100644
--- a/docs/reference-plotly.md
+++ b/docs/reference-plotly.md
@@ -181,7 +181,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
| `showPoints` | toggle | on / off | `false` | always | Overlay point markers on the line. |
###  Area Chart
@@ -190,7 +190,7 @@ _No template-specific parameters._
| Parameter | Control | Domain | Default | Availability | Description |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)) | — | always | Line or area interpolation method. |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. |
| `opacity` | number | 0.1 – 1 (step 0.05) | `0.4` | always | Mark opacity. |
| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `layered` (Layered (overlap)) | — | conditional | Stacking strategy for overlapping series. |
diff --git a/docs/zh-CN/reference-plotly.md b/docs/zh-CN/reference-plotly.md
index c5313b3..ecf7032 100644
--- a/docs/zh-CN/reference-plotly.md
+++ b/docs/zh-CN/reference-plotly.md
@@ -177,7 +177,7 @@ _无模板专用参数。_
| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 说明 |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | 线或区域的插值方式。 |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | 线或区域的插值方式。 |
| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 |
###  Area Chart
@@ -186,7 +186,7 @@ _无模板专用参数。_
| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 说明 |
|---|---|---|---|---|---|
-| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)) | — | always | 线或区域的插值方式。 |
+| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | 线或区域的插值方式。 |
| `opacity` | number | 0.1 – 1 (step 0.05) | `0.4` | always | 标记不透明度。 |
| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `layered` (Layered (overlap)) | — | conditional | 重叠系列的堆叠策略。 |
diff --git a/package-lock.json b/package-lock.json
index 65d5d1a..0553d4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9634,7 +9634,7 @@
},
"packages/flint-js": {
"name": "flint-chart",
- "version": "0.4.0",
+ "version": "0.4.1",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.14.10",
@@ -9679,7 +9679,7 @@
},
"packages/flint-mcp": {
"name": "flint-chart-mcp",
- "version": "0.4.0",
+ "version": "0.4.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.7.4",
@@ -9688,7 +9688,7 @@
"@resvg/resvg-js": "^2.6.2",
"chart.js": "^4.4.0",
"echarts": "^6.0.0",
- "flint-chart": "^0.4.0",
+ "flint-chart": "^0.4.1",
"vega": "^6.0.0",
"vega-interpreter": "^2.2.1",
"vega-lite": "^6.0.0",
diff --git a/packages/flint-js/package.json b/packages/flint-js/package.json
index 893a7d7..9084022 100644
--- a/packages/flint-js/package.json
+++ b/packages/flint-js/package.json
@@ -1,6 +1,6 @@
{
"name": "flint-chart",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "Semantic-level visualization library that compiles data + semantic types for Vega-Lite, ECharts, Chart.js, Plotly, and Excel.",
"keywords": [
"visualization",
diff --git a/packages/flint-js/src/chartjs/assemble.ts b/packages/flint-js/src/chartjs/assemble.ts
index a4eb805..5356314 100644
--- a/packages/flint-js/src/chartjs/assemble.ts
+++ b/packages/flint-js/src/chartjs/assemble.ts
@@ -34,7 +34,7 @@ import {
import type { ChartWarning } from '../core/types';
import { applyEncodingOverrides } from '../core/encoding-overrides';
import { applyAggregation } from '../core/aggregate';
-import { applyPivot, PivotSurface } from '../core/pivot';
+import { applyPivot, applyTransform, type PivotSurface, type TransformSurface } from '../core/pivot';
import { cjsGetTemplateDef } from './templates';
import { resolveChannelSemantics, convertTemporalData } from '../core/resolve-semantics';
import { computeZeroDecision } from '../core/semantic-types';
@@ -109,17 +109,22 @@ export function assembleChartjs(input: ChartAssemblyInput): any {
: { ...enc, type: prelimSemantics[ch]?.type };
}
- const pivoted = applyPivot(chartTemplate, typedRawEncodings, data, chartProperties, cjsGetTemplateDef);
- if (pivoted.chartType && pivoted.chartType !== chartType) {
- const swapped = cjsGetTemplateDef(pivoted.chartType) as ChartTemplateDef | undefined;
+ // Transform (derived Category-B operator): same two-control model as VL —
+ // chartProperties.chartType (θ) + chartProperties.arrange (τ/σ/γ). Legacy
+ // composed `pivot` ids are migrated inside applyTransform. See
+ // design-docs/chart-transform-two-axes.md.
+ const authoredTemplate = chartTemplate;
+ const transformed = applyTransform(chartTemplate, typedRawEncodings, data, chartProperties, cjsGetTemplateDef);
+ if (transformed.chartType && transformed.chartType !== chartType) {
+ const swapped = cjsGetTemplateDef(transformed.chartType) as ChartTemplateDef | undefined;
if (swapped) chartTemplate = swapped;
}
// Compose Category-B encoding-action overrides (stored by the host in
- // chartProperties, keyed by action key) onto the post-pivot encodings before
- // any pipeline phase runs. Flint owns the transform; the host only stores
- // the override value. See applyEncodingOverrides / EncodingActionDef.
- const encodings = applyEncodingOverrides(chartTemplate, pivoted.encodings, chartProperties);
+ // chartProperties, keyed by action key) onto the post-transform encodings
+ // before any pipeline phase runs. Flint owns the transform; the host only
+ // stores the override value. See applyEncodingOverrides / EncodingActionDef.
+ const encodings = applyEncodingOverrides(chartTemplate, transformed.encodings, chartProperties);
// Optional aggregation transform — see vegalite/assemble for rationale.
data = applyAggregation(encodings, data);
@@ -429,19 +434,31 @@ export function assembleChartjs(input: ChartAssemblyInput): any {
cjsConfig._dataLength = values.length;
- if (pivoted.surface) {
- cjsConfig._pivot = pivoted.surface;
+ if (transformed.surface) {
+ cjsConfig._transform = transformed.surface;
+ }
+ // Legacy single-control surface for getChartjsPivot — enumerated from the
+ // authored template so ids/labels match the pre-split contract.
+ const legacyPivot = applyPivot(authoredTemplate, typedRawEncodings, data, chartProperties, cjsGetTemplateDef);
+ if (legacyPivot.surface) {
+ cjsConfig._pivot = legacyPivot.surface;
}
return cjsConfig;
}
-/** Inspect the Chart.js view transformation surface for an input. */
+/** Inspect the Chart.js legacy (composed) view transformation surface for an input. */
export function getChartjsPivot(input: ChartAssemblyInput): PivotSurface | undefined {
const spec = assembleChartjs(input);
return spec && spec._pivot ? (spec._pivot as PivotSurface) : undefined;
}
+/** Inspect the Chart.js two-control transform surface for an input. */
+export function getChartjsTransform(input: ChartAssemblyInput): TransformSurface | undefined {
+ const spec = assembleChartjs(input);
+ return spec && spec._transform ? (spec._transform as TransformSurface) : undefined;
+}
+
/**
* Round a [min, max] interval outward to "nice" round numbers so a shared facet
* axis lands its endpoints on clean tick values (mirrors Vega-Lite's default
diff --git a/packages/flint-js/src/chartjs/index.ts b/packages/flint-js/src/chartjs/index.ts
index 1b3944d..6e1f096 100644
--- a/packages/flint-js/src/chartjs/index.ts
+++ b/packages/flint-js/src/chartjs/index.ts
@@ -18,7 +18,7 @@
*/
// CJS assembly function
-export { assembleChartjs, getChartjsPivot } from './assemble';
+export { assembleChartjs, getChartjsPivot, getChartjsTransform } from './assemble';
// CJS spec instantiation (Phase 2)
export { cjsApplyLayoutToSpec, cjsApplyTooltips } from './instantiate-spec';
diff --git a/packages/flint-js/src/chartjs/templates/area.ts b/packages/flint-js/src/chartjs/templates/area.ts
index 7bc6fcb..e5f126d 100644
--- a/packages/flint-js/src/chartjs/templates/area.ts
+++ b/packages/flint-js/src/chartjs/templates/area.ts
@@ -58,6 +58,10 @@ export const cjsAreaChartDef: ChartTemplateDef = {
const tension = (interpolate === 'monotone' || interpolate === 'basis' ||
interpolate === 'cardinal' || interpolate === 'catmull-rom')
? 0.4 : 0;
+ const stepped = interpolate === 'step' ? 'middle' as const
+ : interpolate === 'step-before' ? 'before' as const
+ : interpolate === 'step-after' ? 'after' as const
+ : false;
const palette = getChartJsPalette(ctx, 'color');
@@ -133,6 +137,7 @@ export const cjsAreaChartDef: ChartTemplateDef = {
borderColor,
backgroundColor: bgColor,
tension,
+ stepped,
fill: stacked ? 'stack' : 'origin',
pointRadius: 2,
});
@@ -154,6 +159,7 @@ export const cjsAreaChartDef: ChartTemplateDef = {
borderColor: getSeriesBorderColor(palette, 0),
backgroundColor: getSeriesBackgroundColor(palette, 0, opacity),
tension,
+ stepped,
fill: 'origin',
pointRadius: 2,
});
@@ -170,6 +176,12 @@ export const cjsAreaChartDef: ChartTemplateDef = {
{ value: undefined, label: 'Default (linear)' },
{ value: 'linear', label: 'Linear' },
{ value: 'monotone', label: 'Monotone (smooth)' },
+ { value: 'step', label: 'Step' },
+ { value: 'step-before', label: 'Step Before' },
+ { value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
{
diff --git a/packages/flint-js/src/chartjs/templates/bar.ts b/packages/flint-js/src/chartjs/templates/bar.ts
index 456fe2c..030772a 100644
--- a/packages/flint-js/src/chartjs/templates/bar.ts
+++ b/packages/flint-js/src/chartjs/templates/bar.ts
@@ -9,21 +9,22 @@
* CJS: explicit datasets[] with stacked option on scales
*/
-import { ChartTemplateDef, ChartPropertyDef } from '../../core/types';
+import { ChartTemplateDef, ChartPropertyDef, EncodingActionDef } from '../../core/types';
import {
- extractCategories,
- groupBy,
- detectAxes,
- buildCategoryAlignedData,
- getChartJsPalette,
- getSeriesBorderColor,
- getSeriesBackgroundColor,
+ resolveCategoryOrder,
+ groupBy,
+ detectAxes,
+ buildCategoryAlignedData,
+ getChartJsPalette,
+ getSeriesBorderColor,
+ getSeriesBackgroundColor,
} from './utils';
import {
detectBandedAxisFromSemantics, detectBandedAxisForceDiscrete,
} from '../../core/axis-detection';
import { planBandDodge, resolveDodge } from '../../core/band-dodge';
import { makeCartesianPivot } from '../../core/pivot';
+import { makeSortAction } from '../../core/encoding-actions';
// ---------------------------------------------------------------------------
// Helpers
@@ -54,7 +55,13 @@ export const cjsBarChartDef: ChartTemplateDef = {
if (!catField || !valField) return;
const catCS = channelSemantics[categoryAxis];
- const categories = extractCategories(table, catField, catCS?.ordinalSortOrder);
+ const catEnc = ctx.encodings?.[categoryAxis];
+ const sortByField = catEnc?.sortBy ? channelSemantics[catEnc.sortBy]?.field : undefined;
+ const categories = resolveCategoryOrder(table, catField, {
+ ordinalSortOrder: catCS?.ordinalSortOrder,
+ sortBy: sortByField,
+ sortOrder: catEnc?.sortOrder,
+ });
const values = buildCategoryAlignedData(table, catField, valField, categories);
const isHorizontal = categoryAxis === 'y';
@@ -111,6 +118,7 @@ export const cjsBarChartDef: ChartTemplateDef = {
properties: [
{ key: 'cornerRadius', label: 'Corners', type: 'continuous', min: 0, max: 15, step: 1, defaultValue: 0 },
] as ChartPropertyDef[],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
@@ -144,7 +152,13 @@ export const cjsStackedBarChartDef: ChartTemplateDef = {
if (!catField || !valField) return;
const catCS = channelSemantics[categoryAxis];
- const categories = extractCategories(table, catField, catCS?.ordinalSortOrder);
+ const catEnc = ctx.encodings?.[categoryAxis];
+ const sortByField = catEnc?.sortBy ? channelSemantics[catEnc.sortBy]?.field : undefined;
+ const categories = resolveCategoryOrder(table, catField, {
+ ordinalSortOrder: catCS?.ordinalSortOrder,
+ sortBy: sortByField,
+ sortOrder: catEnc?.sortOrder,
+ });
const isHorizontal = categoryAxis === 'y';
const palette = getChartJsPalette(ctx, 'color');
@@ -214,6 +228,7 @@ export const cjsStackedBarChartDef: ChartTemplateDef = {
delete spec.mark;
delete spec.encoding;
},
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
@@ -255,7 +270,13 @@ export const cjsGroupedBarChartDef: ChartTemplateDef = {
if (!catField || !valField) return;
const catCS = channelSemantics[categoryAxis];
- const categories = extractCategories(table, catField, catCS?.ordinalSortOrder);
+ const catEnc = ctx.encodings?.[categoryAxis];
+ const sortByField = catEnc?.sortBy ? channelSemantics[catEnc.sortBy]?.field : undefined;
+ const categories = resolveCategoryOrder(table, catField, {
+ ordinalSortOrder: catCS?.ordinalSortOrder,
+ sortBy: sortByField,
+ sortOrder: catEnc?.sortOrder,
+ });
const isHorizontal = categoryAxis === 'y';
const palette = getChartJsPalette(ctx, 'group');
@@ -414,6 +435,7 @@ export const cjsGroupedBarChartDef: ChartTemplateDef = {
},
},
] as ChartPropertyDef[],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
diff --git a/packages/flint-js/src/chartjs/templates/line.ts b/packages/flint-js/src/chartjs/templates/line.ts
index e0a1114..427cb3c 100644
--- a/packages/flint-js/src/chartjs/templates/line.ts
+++ b/packages/flint-js/src/chartjs/templates/line.ts
@@ -197,6 +197,9 @@ export const cjsLineChartDef: ChartTemplateDef = {
{ value: 'step', label: 'Step' },
{ value: 'step-before', label: 'Step Before' },
{ value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
],
diff --git a/packages/flint-js/src/chartjs/templates/utils.ts b/packages/flint-js/src/chartjs/templates/utils.ts
index 1b9abb1..9974866 100644
--- a/packages/flint-js/src/chartjs/templates/utils.ts
+++ b/packages/flint-js/src/chartjs/templates/utils.ts
@@ -59,6 +59,28 @@ export function extractCategories(data: any[], field: string, ordinalSortOrder?:
return result;
}
+/**
+ * Resolve the display order of a categorical axis, honoring either a canonical
+ * `ordinalSortOrder` or a sort-by-measure request (`sortBy` + `sortOrder` from
+ * the shared Sort encoding action).
+ */
+export function resolveCategoryOrder(
+ data: any[],
+ catField: string,
+ opts?: { ordinalSortOrder?: string[]; sortBy?: string; sortOrder?: 'ascending' | 'descending' },
+): string[] {
+ const base = extractCategories(data, catField, opts?.ordinalSortOrder);
+ if (!opts?.sortBy) return base;
+ const agg = new Map();
+ for (const row of data) {
+ const cat = String(row[catField] ?? '');
+ const v = Number(row[opts.sortBy]);
+ if (Number.isFinite(v)) agg.set(cat, (agg.get(cat) ?? 0) + v);
+ }
+ const dir = opts.sortOrder === 'ascending' ? 1 : -1;
+ return [...base].sort((a, b) => dir * ((agg.get(a) ?? 0) - (agg.get(b) ?? 0)));
+}
+
/**
* Group data by a categorical field.
* Returns a map: seriesName → rows[].
diff --git a/packages/flint-js/src/echarts/assemble.ts b/packages/flint-js/src/echarts/assemble.ts
index 47788b8..638c081 100644
--- a/packages/flint-js/src/echarts/assemble.ts
+++ b/packages/flint-js/src/echarts/assemble.ts
@@ -61,7 +61,7 @@ import {
import type { ChartWarning } from '../core/types';
import { applyEncodingOverrides } from '../core/encoding-overrides';
import { applyAggregation } from '../core/aggregate';
-import { applyPivot, PivotSurface } from '../core/pivot';
+import { applyPivot, applyTransform, type PivotSurface, type TransformSurface } from '../core/pivot';
import { ecGetTemplateDef } from './templates';
import { resolveChannelSemantics, convertTemporalData } from '../core/resolve-semantics';
import { toTypeString, type SemanticAnnotation } from '../core/field-semantics';
@@ -141,17 +141,22 @@ export function assembleECharts(input: ChartAssemblyInput): any {
: { ...enc, type: prelimSemantics[ch]?.type };
}
- const pivoted = applyPivot(chartTemplate, typedRawEncodings, data, chartProperties, ecGetTemplateDef);
- if (pivoted.chartType && pivoted.chartType !== chartType) {
- const swapped = ecGetTemplateDef(pivoted.chartType) as ChartTemplateDef | undefined;
+ // Transform (derived Category-B operator): same two-control model as VL —
+ // chartProperties.chartType (θ) + chartProperties.arrange (τ/σ/γ). Legacy
+ // composed `pivot` ids are migrated inside applyTransform. See
+ // design-docs/chart-transform-two-axes.md.
+ const authoredTemplate = chartTemplate;
+ const transformed = applyTransform(chartTemplate, typedRawEncodings, data, chartProperties, ecGetTemplateDef);
+ if (transformed.chartType && transformed.chartType !== chartType) {
+ const swapped = ecGetTemplateDef(transformed.chartType) as ChartTemplateDef | undefined;
if (swapped) chartTemplate = swapped;
}
// Compose Category-B encoding-action overrides (stored by the host in
- // chartProperties, keyed by action key) onto the post-pivot encodings before
- // any pipeline phase runs. Flint owns the transform; the host only stores
- // the override value. See applyEncodingOverrides / EncodingActionDef.
- const encodings = applyEncodingOverrides(chartTemplate, pivoted.encodings, chartProperties);
+ // chartProperties, keyed by action key) onto the post-transform encodings
+ // before any pipeline phase runs. Flint owns the transform; the host only
+ // stores the override value. See applyEncodingOverrides / EncodingActionDef.
+ const encodings = applyEncodingOverrides(chartTemplate, transformed.encodings, chartProperties);
// Optional aggregation transform — see vegalite/assemble for rationale.
data = applyAggregation(encodings, data);
@@ -516,8 +521,14 @@ export function assembleECharts(input: ChartAssemblyInput): any {
// ECharts data is embedded directly in series[].data)
ecOption._dataLength = values.length;
- if (pivoted.surface) {
- ecOption._pivot = pivoted.surface;
+ if (transformed.surface) {
+ ecOption._transform = transformed.surface;
+ }
+ // Legacy single-control surface for getEChartsPivot — enumerated from the
+ // authored template so ids/labels match the pre-split contract.
+ const legacyPivot = applyPivot(authoredTemplate, typedRawEncodings, data, chartProperties, ecGetTemplateDef);
+ if (legacyPivot.surface) {
+ ecOption._pivot = legacyPivot.surface;
}
// Clean internal-only props
@@ -526,12 +537,18 @@ export function assembleECharts(input: ChartAssemblyInput): any {
return ecOption;
}
-/** Inspect the ECharts view transformation surface for an input. */
+/** Inspect the ECharts legacy (composed) view transformation surface for an input. */
export function getEChartsPivot(input: ChartAssemblyInput): PivotSurface | undefined {
const spec = assembleECharts(input);
return spec && spec._pivot ? (spec._pivot as PivotSurface) : undefined;
}
+/** Inspect the ECharts two-control transform surface for an input. */
+export function getEChartsTransform(input: ChartAssemblyInput): TransformSurface | undefined {
+ const spec = assembleECharts(input);
+ return spec && spec._transform ? (spec._transform as TransformSurface) : undefined;
+}
+
// ===========================================================================
// buildECEncodings — Translate abstract semantics → EC-resolved encodings
// ===========================================================================
diff --git a/packages/flint-js/src/echarts/index.ts b/packages/flint-js/src/echarts/index.ts
index 501abae..9787871 100644
--- a/packages/flint-js/src/echarts/index.ts
+++ b/packages/flint-js/src/echarts/index.ts
@@ -17,7 +17,7 @@
*/
// EC assembly function
-export { assembleECharts, getEChartsPivot } from './assemble';
+export { assembleECharts, getEChartsPivot, getEChartsTransform } from './assemble';
// EC spec instantiation (Phase 2)
export { ecApplyLayoutToSpec, ecApplyTooltips } from './instantiate-spec';
diff --git a/packages/flint-js/src/echarts/templates/area.ts b/packages/flint-js/src/echarts/templates/area.ts
index 30bcde0..7412823 100644
--- a/packages/flint-js/src/echarts/templates/area.ts
+++ b/packages/flint-js/src/echarts/templates/area.ts
@@ -300,6 +300,9 @@ export const ecAreaChartDef: ChartTemplateDef = {
{ value: 'step', label: 'Step' },
{ value: 'step-before', label: 'Step Before' },
{ value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
{
diff --git a/packages/flint-js/src/echarts/templates/bar.ts b/packages/flint-js/src/echarts/templates/bar.ts
index e66dfc1..190782a 100644
--- a/packages/flint-js/src/echarts/templates/bar.ts
+++ b/packages/flint-js/src/echarts/templates/bar.ts
@@ -12,7 +12,7 @@
* and barGap/barCategoryGap for grouped layout
*/
-import { ChartTemplateDef, ChartPropertyDef } from '../../core/types';
+import { ChartTemplateDef, ChartPropertyDef, EncodingActionDef } from '../../core/types';
import {
extractCategories, groupBy, detectAxes, getCategoryOrder,
} from './utils';
@@ -23,6 +23,7 @@ import {
detectBandedAxisFromSemantics, detectBandedAxisForceDiscrete,
} from '../../core/axis-detection';
import { makeCartesianPivot } from '../../core/pivot';
+import { makeSortAction } from '../../core/encoding-actions';
// ---------------------------------------------------------------------------
// Helpers
@@ -445,6 +446,7 @@ export const ecBarChartDef: ChartTemplateDef = {
properties: [
{ key: 'cornerRadius', label: 'Corners', type: 'continuous', min: 0, max: 15, step: 1, defaultValue: 0 },
] as ChartPropertyDef[],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
@@ -667,6 +669,7 @@ export const ecStackedBarChartDef: ChartTemplateDef = {
check: (ctx) => ({ applicable: !!ctx.encodings.color?.field }),
},
] as ChartPropertyDef[],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
@@ -975,6 +978,7 @@ export const ecGroupedBarChartDef: ChartTemplateDef = {
},
} as ChartPropertyDef,
],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
pivot: makeCartesianPivot({
transpose: [['x', 'y']],
permute: [['x', 'y', 'color']],
diff --git a/packages/flint-js/src/echarts/templates/line.ts b/packages/flint-js/src/echarts/templates/line.ts
index 27e96b8..25658fe 100644
--- a/packages/flint-js/src/echarts/templates/line.ts
+++ b/packages/flint-js/src/echarts/templates/line.ts
@@ -287,6 +287,9 @@ export const ecLineChartDef: ChartTemplateDef = {
{ value: 'step', label: 'Step' },
{ value: 'step-before', label: 'Step Before' },
{ value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
{ key: 'showPoints', label: 'Points', type: 'binary', defaultValue: false } as ChartPropertyDef,
diff --git a/packages/flint-js/src/echarts/templates/lollipop.ts b/packages/flint-js/src/echarts/templates/lollipop.ts
index bf6d29c..4a86f81 100644
--- a/packages/flint-js/src/echarts/templates/lollipop.ts
+++ b/packages/flint-js/src/echarts/templates/lollipop.ts
@@ -6,10 +6,11 @@
* Vega-Lite: rule strokeWidth 1.5、圆 size 80;茎黑色,圆点用图例色。
*/
-import { ChartTemplateDef, ChartPropertyDef } from '../../core/types';
+import { ChartTemplateDef, ChartPropertyDef, EncodingActionDef } from '../../core/types';
import { extractCategories, groupBy, DEFAULT_COLORS, getCategoryOrder } from './utils';
import { detectAxes } from './utils';
import { detectBandedAxisFromSemantics } from '../../core/axis-detection';
+import { makeSortAction } from '../../core/encoding-actions';
/** Vega-Lite 风格:茎(rule)黑色、细线,圆点与 color 图例一致 */
const STEM_COLOR = '#000000';
@@ -178,4 +179,5 @@ export const ecLollipopChartDef: ChartTemplateDef = {
properties: [
{ key: 'dotSize', label: 'Dot Size', type: 'continuous', min: 20, max: 300, step: 10, defaultValue: 80 },
] as ChartPropertyDef[],
+ encodingActions: [makeSortAction()] as EncodingActionDef[],
};
diff --git a/packages/flint-js/src/echarts/templates/utils.ts b/packages/flint-js/src/echarts/templates/utils.ts
index 3847c7f..4162634 100644
--- a/packages/flint-js/src/echarts/templates/utils.ts
+++ b/packages/flint-js/src/echarts/templates/utils.ts
@@ -11,10 +11,61 @@ import type { ChannelSemantics, InstantiateContext } from '../../core/types';
/**
* Get canonical category order for a channel (from buildECEncodings or channelSemantics).
* Use when calling extractCategories so sort order is consistent across templates.
+ *
+ * Honors the shared Sort encoding action (`encodings[channel].sortBy` =
+ * measure channel + `sortOrder`) by aggregating the measure per category —
+ * same contract as Plotly's `resolveCategoryOrder`.
*/
export function getCategoryOrder(ctx: InstantiateContext, channel: string): string[] | undefined {
- return (ctx.resolvedEncodings as any)?.[channel]?.ordinalSortOrder
+ const resolved = (ctx.resolvedEncodings as any)?.[channel];
+ if (Array.isArray(resolved?.sortValues) && resolved.sortValues.length > 0) {
+ return resolved.sortValues.map(String);
+ }
+
+ const ordinal = resolved?.ordinalSortOrder
?? ctx.channelSemantics?.[channel]?.ordinalSortOrder;
+
+ const enc = ctx.encodings?.[channel];
+ const sortByChannel = enc?.sortBy;
+ if (
+ typeof sortByChannel === 'string'
+ && (sortByChannel === 'x' || sortByChannel === 'y' || sortByChannel === 'color'
+ || sortByChannel === 'size' || sortByChannel === 'group')
+ ) {
+ const catField = ctx.channelSemantics?.[channel]?.field ?? enc?.field;
+ const sortByField = ctx.channelSemantics?.[sortByChannel]?.field
+ ?? ctx.encodings?.[sortByChannel]?.field;
+ if (catField && sortByField && ctx.table) {
+ return resolveCategoryOrder(ctx.table, catField, {
+ ordinalSortOrder: ordinal,
+ sortBy: sortByField,
+ sortOrder: enc?.sortOrder,
+ });
+ }
+ }
+
+ return ordinal;
+}
+
+/**
+ * Resolve the display order of a categorical axis, honoring either a canonical
+ * `ordinalSortOrder` or a sort-by-measure request (`sortBy` + `sortOrder`).
+ */
+export function resolveCategoryOrder(
+ data: any[],
+ catField: string,
+ opts?: { ordinalSortOrder?: string[]; sortBy?: string; sortOrder?: 'ascending' | 'descending' },
+): string[] {
+ const base = extractCategories(data, catField, opts?.ordinalSortOrder);
+ if (!opts?.sortBy) return base;
+ const agg = new Map();
+ for (const row of data) {
+ const cat = String(row[catField] ?? '');
+ const v = Number(row[opts.sortBy]);
+ if (Number.isFinite(v)) agg.set(cat, (agg.get(cat) ?? 0) + v);
+ }
+ const dir = opts.sortOrder === 'ascending' ? 1 : -1;
+ return [...base].sort((a, b) => dir * ((agg.get(a) ?? 0) - (agg.get(b) ?? 0)));
}
// Re-export circumference-pressure functions from core (shared with VL backend)
diff --git a/packages/flint-js/src/plotly/assemble.ts b/packages/flint-js/src/plotly/assemble.ts
index 870037c..0f18d53 100644
--- a/packages/flint-js/src/plotly/assemble.ts
+++ b/packages/flint-js/src/plotly/assemble.ts
@@ -37,6 +37,7 @@ import {
import type { ChartWarning, ChartEncoding } from '../core/types';
import { applyEncodingOverrides } from '../core/encoding-overrides';
import { applyAggregation } from '../core/aggregate';
+import { applyPivot, applyTransform, type PivotSurface, type TransformSurface } from '../core/pivot';
import { plGetTemplateDef } from './templates';
import { resolveChannelSemantics, convertTemporalData } from '../core/resolve-semantics';
import { computeZeroDecision } from '../core/semantic-types';
@@ -71,7 +72,7 @@ export function assemblePlotly(input: ChartAssemblyInput): any {
const baseSize = resolveBaseSize(input.chart_spec.baseSize, sizeCeiling);
const canvasSize = baseSize;
const options = input.options ?? {};
- const chartTemplate = plGetTemplateDef(chartType) as ChartTemplateDef;
+ let chartTemplate = plGetTemplateDef(chartType) as ChartTemplateDef;
if (!chartTemplate) {
throw new Error(`Unknown Plotly chart type: ${chartType}. Use plAllTemplateDefs to see available types.`);
}
@@ -107,7 +108,18 @@ export function assemblePlotly(input: ChartAssemblyInput): any {
for (const [ch, enc] of Object.entries(normalized.encodings)) {
typedRawEncodings[ch] = enc.type ? enc : { ...enc, type: prelimSemantics[ch]?.type };
}
- const encodings = applyEncodingOverrides(chartTemplate, typedRawEncodings, chartProperties);
+
+ // Transform (derived Category-B operator): same two-control model as VL —
+ // chartProperties.chartType (θ) + chartProperties.arrange (τ/σ/γ). Legacy
+ // composed `pivot` ids are migrated inside applyTransform. See
+ // design-docs/chart-transform-two-axes.md.
+ const authoredTemplate = chartTemplate;
+ const transformed = applyTransform(chartTemplate, typedRawEncodings, data, chartProperties, plGetTemplateDef);
+ if (transformed.chartType && transformed.chartType !== chartType) {
+ const swapped = plGetTemplateDef(transformed.chartType) as ChartTemplateDef | undefined;
+ if (swapped) chartTemplate = swapped;
+ }
+ const encodings = applyEncodingOverrides(chartTemplate, transformed.encodings, chartProperties);
// Optional aggregation transform — see vegalite/assemble for rationale.
data = applyAggregation(encodings, data);
@@ -394,5 +406,27 @@ export function assemblePlotly(input: ChartAssemblyInput): any {
figure._dataLength = values.length;
+ if (transformed.surface) {
+ figure._transform = transformed.surface;
+ }
+ // Legacy single-control surface for getPlotlyPivot — enumerated from the
+ // authored template so ids/labels match the pre-split contract.
+ const legacyPivot = applyPivot(authoredTemplate, typedRawEncodings, data, chartProperties, plGetTemplateDef);
+ if (legacyPivot.surface) {
+ figure._pivot = legacyPivot.surface;
+ }
+
return figure;
}
+
+/** Inspect the Plotly legacy (composed) view transformation surface for an input. */
+export function getPlotlyPivot(input: ChartAssemblyInput): PivotSurface | undefined {
+ const spec = assemblePlotly(input);
+ return spec && spec._pivot ? (spec._pivot as PivotSurface) : undefined;
+}
+
+/** Inspect the Plotly two-control transform surface for an input. */
+export function getPlotlyTransform(input: ChartAssemblyInput): TransformSurface | undefined {
+ const spec = assemblePlotly(input);
+ return spec && spec._transform ? (spec._transform as TransformSurface) : undefined;
+}
diff --git a/packages/flint-js/src/plotly/index.ts b/packages/flint-js/src/plotly/index.ts
index 9dd7d02..ae4acbd 100644
--- a/packages/flint-js/src/plotly/index.ts
+++ b/packages/flint-js/src/plotly/index.ts
@@ -20,7 +20,7 @@
*/
// PL assembly function
-export { assemblePlotly } from './assemble';
+export { assemblePlotly, getPlotlyPivot, getPlotlyTransform } from './assemble';
// PL spec instantiation (Phase 2)
export { plApplyLayoutToSpec, plApplyTooltips } from './instantiate-spec';
diff --git a/packages/flint-js/src/plotly/templates/area.ts b/packages/flint-js/src/plotly/templates/area.ts
index 5772528..a80e68e 100644
--- a/packages/flint-js/src/plotly/templates/area.ts
+++ b/packages/flint-js/src/plotly/templates/area.ts
@@ -18,9 +18,29 @@ import {
getPlotlyPalette,
getSeriesColor,
} from './utils';
+import { makeCartesianPivot } from '../../core/pivot';
const isDiscrete = (type: string | undefined) => type === 'nominal' || type === 'ordinal';
+/** Map the shared `interpolate` property onto Plotly's `line.shape`. */
+function lineShape(interpolate: unknown): 'linear' | 'spline' | 'hv' | 'vh' | 'hvh' {
+ switch (interpolate) {
+ case 'monotone':
+ case 'basis':
+ case 'cardinal':
+ case 'catmull-rom':
+ return 'spline';
+ case 'step':
+ return 'hvh';
+ case 'step-before':
+ return 'vh';
+ case 'step-after':
+ return 'hv';
+ default:
+ return 'linear';
+ }
+}
+
/** Hex → rgba with alpha, for translucent area fills. */
function fillColor(hex: string, alpha: number): string {
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
@@ -59,7 +79,7 @@ export const plAreaChartDef: ChartTemplateDef = {
const opacity = Number(chartProperties?.opacity ?? 0.4);
const stackMode = chartProperties?.stackMode;
const stacked = stackMode !== 'layered';
- const smooth = chartProperties?.interpolate === 'monotone';
+ const shape = lineShape(chartProperties?.interpolate);
const palette = getPlotlyPalette(ctx, 'color');
const traces: any[] = [];
@@ -75,7 +95,7 @@ export const plAreaChartDef: ChartTemplateDef = {
name,
x: xVals,
y: yVals,
- line: { color, shape: smooth ? 'spline' : 'linear' },
+ line: { color, shape },
fillcolor: fillColor(color, opacity),
};
if (colorField && stacked) {
@@ -135,6 +155,12 @@ export const plAreaChartDef: ChartTemplateDef = {
{ value: undefined, label: 'Default (linear)' },
{ value: 'linear', label: 'Linear' },
{ value: 'monotone', label: 'Monotone (smooth)' },
+ { value: 'step', label: 'Step' },
+ { value: 'step-before', label: 'Step Before' },
+ { value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
{ key: 'opacity', label: 'Opacity', type: 'continuous', min: 0.1, max: 1, step: 0.05, defaultValue: 0.4 } as ChartPropertyDef,
@@ -148,4 +174,8 @@ export const plAreaChartDef: ChartTemplateDef = {
],
} as ChartPropertyDef,
],
+ pivot: makeCartesianPivot({
+ permute: [['y', 'color']],
+ shift: ['color', 'group', 'column', 'row'],
+ }),
};
diff --git a/packages/flint-js/src/plotly/templates/bar.ts b/packages/flint-js/src/plotly/templates/bar.ts
index 5f02612..433ff34 100644
--- a/packages/flint-js/src/plotly/templates/bar.ts
+++ b/packages/flint-js/src/plotly/templates/bar.ts
@@ -16,6 +16,7 @@ import { extractCategories, resolveCategoryOrder, buildCategoryAlignedData, dete
import { detectBandedAxisFromSemantics, detectBandedAxisForceDiscrete } from '../../core/axis-detection';
import { planBandDodge } from '../../core/band-dodge';
import { makeSortAction } from '../../core/encoding-actions';
+import { makeCartesianPivot } from '../../core/pivot';
/** Corner-radius property shared by the Plotly bar templates (px, matches VL). */
const BAR_CORNER_RADIUS: ChartPropertyDef = {
@@ -100,6 +101,11 @@ export const plBarChartDef: ChartTemplateDef = {
},
properties: [BAR_CORNER_RADIUS],
encodingActions: [makeSortAction()] as EncodingActionDef[],
+ pivot: makeCartesianPivot({
+ transpose: [['x', 'y']],
+ permute: [['x', 'y', 'color']],
+ shift: ['color', 'column', 'row'],
+ }),
};
// ─── Stacked Bar Chart ──────────────────────────────────────────────────────
@@ -193,6 +199,11 @@ export const plStackedBarChartDef: ChartTemplateDef = {
] } as ChartPropertyDef,
],
encodingActions: [makeSortAction()] as EncodingActionDef[],
+ pivot: makeCartesianPivot({
+ transpose: [['x', 'y']],
+ permute: [['x', 'y', 'color']],
+ shift: ['color', 'group', 'column', 'row'],
+ }),
};
// ─── Grouped Bar Chart ──────────────────────────────────────────────────────
@@ -283,10 +294,13 @@ export const plGroupedBarChartDef: ChartTemplateDef = {
delete spec.encoding;
},
encodingActions: [makeSortAction()] as EncodingActionDef[],
+ pivot: makeCartesianPivot({
+ transpose: [['x', 'y']],
+ permute: [['x', 'y', 'color']],
+ shift: ['color', 'group', 'column', 'row'],
+ }),
};
-// ─── Pyramid Chart ──────────────────────────────────────────────────────────
-
/**
* Plotly Pyramid Chart (population pyramid) — two mirrored horizontal bar
* traces sharing one category axis, one side negated so the bars extend left
diff --git a/packages/flint-js/src/plotly/templates/heatmap.ts b/packages/flint-js/src/plotly/templates/heatmap.ts
index 641519a..186637f 100644
--- a/packages/flint-js/src/plotly/templates/heatmap.ts
+++ b/packages/flint-js/src/plotly/templates/heatmap.ts
@@ -11,6 +11,7 @@
import { ChartTemplateDef, EncodingActionDef } from '../../core/types';
import { extractCategories } from './utils';
+import { makeCartesianPivot } from '../../core/pivot';
const SCHEME_COLORSCALES: Record = {
viridis: 'Viridis', inferno: 'Hot', magma: 'Magma', plasma: 'Plasma', turbo: 'Turbo',
@@ -98,4 +99,5 @@ export const plHeatmapDef: ChartTemplateDef = {
set: (enc, value) => ({ ...enc, color: { ...(enc.color as any), scheme: value } }),
},
] as EncodingActionDef[],
+ pivot: makeCartesianPivot({ transpose: [['x', 'y']] }),
};
diff --git a/packages/flint-js/src/plotly/templates/jitter.ts b/packages/flint-js/src/plotly/templates/jitter.ts
index c915f32..48c759f 100644
--- a/packages/flint-js/src/plotly/templates/jitter.ts
+++ b/packages/flint-js/src/plotly/templates/jitter.ts
@@ -12,6 +12,7 @@
import { ChartTemplateDef } from '../../core/types';
import { isDiscreteType, extractCategories, groupBy, getPlotlyPalette, getSeriesColor, seededJitter } from './utils';
+import { makeCartesianPivot } from '../../core/pivot';
export const plStripPlotDef: ChartTemplateDef = {
chart: 'Strip Plot',
@@ -120,4 +121,5 @@ export const plStripPlotDef: ChartTemplateDef = {
delete spec.mark;
delete spec.encoding;
},
+ pivot: makeCartesianPivot({}),
};
diff --git a/packages/flint-js/src/plotly/templates/line.ts b/packages/flint-js/src/plotly/templates/line.ts
index 4109a7b..e7fe6d3 100644
--- a/packages/flint-js/src/plotly/templates/line.ts
+++ b/packages/flint-js/src/plotly/templates/line.ts
@@ -19,6 +19,7 @@ import {
getPlotlyPalette,
getSeriesColor,
} from './utils';
+import { makeCartesianPivot } from '../../core/pivot';
const isDiscrete = (type: string | undefined) => type === 'nominal' || type === 'ordinal';
@@ -137,8 +138,15 @@ export const plLineChartDef: ChartTemplateDef = {
{ value: 'step', label: 'Step' },
{ value: 'step-before', label: 'Step Before' },
{ value: 'step-after', label: 'Step After' },
+ { value: 'basis', label: 'Basis (smooth)' },
+ { value: 'cardinal', label: 'Cardinal' },
+ { value: 'catmull-rom', label: 'Catmull-Rom' },
],
} as ChartPropertyDef,
{ key: 'showPoints', label: 'Show points', type: 'binary', defaultValue: false } as ChartPropertyDef,
],
+ pivot: makeCartesianPivot({
+ permute: [['y', 'color']],
+ shift: ['color', 'group', 'column', 'row'],
+ }),
};
diff --git a/packages/flint-js/src/plotly/templates/scatter.ts b/packages/flint-js/src/plotly/templates/scatter.ts
index d135e7b..1e9fecb 100644
--- a/packages/flint-js/src/plotly/templates/scatter.ts
+++ b/packages/flint-js/src/plotly/templates/scatter.ts
@@ -11,6 +11,7 @@
import { ChartTemplateDef, ChartPropertyDef } from '../../core/types';
import { groupBy, getPlotlyPalette, getSeriesColor } from './utils';
+import { makeCartesianPivot } from '../../core/pivot';
/** Compute a reasonable marker diameter based on canvas area and point count. */
function computeMarkerSize(width: number, height: number, pointCount: number): number {
@@ -120,6 +121,11 @@ export const plScatterPlotDef: ChartTemplateDef = {
properties: [
{ key: 'opacity', label: 'Opacity', type: 'continuous', min: 0.1, max: 1, step: 0.05, defaultValue: 1 } as ChartPropertyDef,
],
+ pivot: makeCartesianPivot({
+ transpose: [['x', 'y']],
+ permute: [['x', 'y', 'color', 'size']],
+ shift: ['color', 'group', 'column', 'row'],
+ }),
postProcess: (figure, ctx) => {
if (!Array.isArray(figure.data)) return;
const w = figure._width || ctx.canvasSize.width;
diff --git a/packages/flint-js/tests/pivot.test.ts b/packages/flint-js/tests/pivot.test.ts
index b6880ff..94ed612 100644
--- a/packages/flint-js/tests/pivot.test.ts
+++ b/packages/flint-js/tests/pivot.test.ts
@@ -6,10 +6,14 @@ import {
assembleVegaLite,
assembleECharts,
assembleChartjs,
+ assemblePlotly,
getChartPivot,
getChartTransform,
getEChartsPivot,
+ getEChartsTransform,
getChartjsPivot,
+ getChartjsTransform,
+ getPlotlyTransform,
} from '../src';
import {
computePivot,
@@ -1056,6 +1060,77 @@ describe('applyTransform — two independent overrides', () => {
});
});
+describe('backend transform parity — chartType/arrange overrides', () => {
+ const scatterProps = (props?: Record) => ({
+ data: {
+ values: [
+ { a: 1, b: 2, c: 'X' }, { a: 3, b: 1, c: 'Y' },
+ { a: 2, b: 4, c: 'X' }, { a: 5, b: 3, c: 'Y' },
+ ],
+ },
+ semantic_types: { a: 'Quantity', b: 'Quantity', c: 'Category' },
+ chart_spec: {
+ chartType: 'Scatter Plot',
+ encodings: { x: 'a', y: 'b', color: 'c' },
+ baseSize: { width: 400, height: 300 },
+ ...(props ? { chartProperties: props } : {}),
+ },
+ });
+
+ const barProps = (props?: Record) => ({
+ data: { values: BAR_DATA },
+ semantic_types: BAR_SEMANTIC,
+ chart_spec: {
+ chartType: 'Bar Chart',
+ encodings: { x: 'region', y: 'sales', color: 'segment' },
+ baseSize: { width: 400, height: 300 },
+ ...(props ? { chartProperties: props } : {}),
+ },
+ });
+
+ it('ECharts honors chartProperties.chartType (gallery two-control model)', () => {
+ const surface = getEChartsTransform(scatterProps())!;
+ expect(surface.chartType!.ids).toContain('type:Strip Plot');
+
+ const option = assembleECharts(scatterProps({ chartType: 'type:Strip Plot' })) as any;
+ expect(option._transform.chartType.index).toBe(
+ option._transform.chartType.ids.indexOf('type:Strip Plot'),
+ );
+ expect(Array.isArray(option.xAxis)).toBe(true);
+ expect(option.xAxis[0].name).toBe('c');
+ });
+
+ it('Chart.js honors chartProperties.chartType (gallery two-control model)', () => {
+ const surface = getChartjsTransform(scatterProps())!;
+ expect(surface.chartType!.ids).toContain('type:Strip Plot');
+
+ const config = assembleChartjs(scatterProps({ chartType: 'type:Strip Plot' })) as any;
+ expect(config._transform.chartType.index).toBe(
+ config._transform.chartType.ids.indexOf('type:Strip Plot'),
+ );
+ expect(config.options.scales.x.title.text).toBe('c');
+ });
+
+ it('Plotly honors chartProperties.chartType and arrange (gallery two-control model)', () => {
+ const typeSurface = getPlotlyTransform(scatterProps())!;
+ expect(typeSurface.chartType!.ids).toContain('type:Strip Plot');
+
+ const strip = assemblePlotly(scatterProps({ chartType: 'type:Strip Plot' })) as any;
+ expect(strip._transform.chartType.index).toBe(
+ strip._transform.chartType.ids.indexOf('type:Strip Plot'),
+ );
+
+ const arrangeSurface = getPlotlyTransform(barProps())!;
+ expect(arrangeSurface.arrange!.ids).toContain('flip:x-y');
+
+ const flipped = assemblePlotly(barProps({ arrange: 'flip:x-y' })) as any;
+ expect(flipped._transform.arrange.index).toBe(
+ flipped._transform.arrange.ids.indexOf('flip:x-y'),
+ );
+ expect(flipped.data?.[0]?.orientation).toBe('h');
+ });
+});
+
describe('getChartTransform — end-to-end through assembleVegaLite', () => {
const scatterInput = (props?: Record) => ({
data: { values: SCATTER_DATA },
diff --git a/packages/flint-js/tests/sort-curve-widgets.test.ts b/packages/flint-js/tests/sort-curve-widgets.test.ts
new file mode 100644
index 0000000..4cc902c
--- /dev/null
+++ b/packages/flint-js/tests/sort-curve-widgets.test.ts
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/**
+ * Regression: ECharts / Chart.js Sort encoding action (gallery Sort control).
+ * Parity with packages/flint-js/tests/plotly-widgets.test.ts.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { assembleECharts, assembleChartjs } from '../src';
+
+const CATS = [{ Cat: 'A', Val: 30 }, { Cat: 'B', Val: 90 }, { Cat: 'C', Val: 50 }];
+const CAT_TYPES = { Cat: 'Category', Val: 'Quantity' } as const;
+
+function ecBandOrder(spec: any): string[] {
+ const cat = [spec.xAxis, spec.yAxis].find((ax) => ax?.type === 'category');
+ return cat?.data ?? [];
+}
+
+function cjsBandOrder(spec: any): string[] {
+ return spec.data?.labels ?? [];
+}
+
+describe('ECharts Sort encoding action', () => {
+ for (const chartType of ['Bar Chart', 'Stacked Bar Chart', 'Grouped Bar Chart', 'Lollipop Chart']) {
+ it(`reorders ${chartType} categories by value`, () => {
+ const mk = (sort?: string) => assembleECharts({
+ data: { values: CATS },
+ semantic_types: CAT_TYPES,
+ chart_spec: {
+ chartType, encodings: { x: 'Cat', y: 'Val' },
+ ...(sort ? { chartProperties: { sort } } : {}),
+ },
+ } as any);
+ expect(ecBandOrder(mk())).toEqual(['A', 'B', 'C']);
+ expect(ecBandOrder(mk('value-desc'))).toEqual(['B', 'C', 'A']);
+ expect(ecBandOrder(mk('value-asc'))).toEqual(['A', 'C', 'B']);
+ });
+ }
+});
+
+describe('Chart.js Sort encoding action', () => {
+ for (const chartType of ['Bar Chart', 'Stacked Bar Chart', 'Grouped Bar Chart']) {
+ it(`reorders ${chartType} categories by value`, () => {
+ const mk = (sort?: string) => assembleChartjs({
+ data: { values: CATS },
+ semantic_types: CAT_TYPES,
+ chart_spec: {
+ chartType, encodings: { x: 'Cat', y: 'Val' },
+ ...(sort ? { chartProperties: { sort } } : {}),
+ },
+ } as any);
+ expect(cjsBandOrder(mk())).toEqual(['A', 'B', 'C']);
+ expect(cjsBandOrder(mk('value-desc'))).toEqual(['B', 'C', 'A']);
+ expect(cjsBandOrder(mk('value-asc'))).toEqual(['A', 'C', 'B']);
+ });
+ }
+});
+
+describe('Line Curve interpolate (non-VL)', () => {
+ it('ECharts applies monotone smooth', () => {
+ const data = [
+ { t: 1, v: 2 }, { t: 2, v: 5 }, { t: 3, v: 3 },
+ ];
+ const option = assembleECharts({
+ data: { values: data },
+ semantic_types: { t: 'Quantity', v: 'Quantity' },
+ chart_spec: {
+ chartType: 'Line Chart',
+ encodings: { x: 't', y: 'v' },
+ chartProperties: { interpolate: 'monotone' },
+ },
+ } as any);
+ expect(option.series?.some((s: any) => s.smooth === true)).toBe(true);
+ });
+
+ it('Chart.js applies monotone tension', () => {
+ const data = [
+ { t: 1, v: 2 }, { t: 2, v: 5 }, { t: 3, v: 3 },
+ ];
+ const config = assembleChartjs({
+ data: { values: data },
+ semantic_types: { t: 'Quantity', v: 'Quantity' },
+ chart_spec: {
+ chartType: 'Line Chart',
+ encodings: { x: 't', y: 'v' },
+ chartProperties: { interpolate: 'monotone' },
+ },
+ } as any);
+ expect(config.data.datasets[0].tension).toBe(0.4);
+ });
+
+ it('ECharts keeps basis (not dropped by normalize)', () => {
+ const data = [
+ { t: 1, v: 2 }, { t: 2, v: 5 }, { t: 3, v: 3 },
+ ];
+ const option = assembleECharts({
+ data: { values: data },
+ semantic_types: { t: 'Quantity', v: 'Quantity' },
+ chart_spec: {
+ chartType: 'Line Chart',
+ encodings: { x: 't', y: 'v' },
+ chartProperties: { interpolate: 'basis' },
+ },
+ } as any);
+ expect(option.series?.some((s: any) => s.smooth === true)).toBe(true);
+ expect(option._warnings?.some((w: any) => w.code === 'invalid-option-value')).toBeFalsy();
+ });
+});
diff --git a/packages/flint-mcp/package.json b/packages/flint-mcp/package.json
index a2f2799..4d7fb2c 100644
--- a/packages/flint-mcp/package.json
+++ b/packages/flint-mcp/package.json
@@ -1,6 +1,6 @@
{
"name": "flint-chart-mcp",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "Model Context Protocol server for Flint — compile, validate, and render semantic chart specs across supported backends.",
"keywords": [
"mcp",
@@ -68,7 +68,7 @@
"@resvg/resvg-js": "^2.6.2",
"chart.js": "^4.4.0",
"echarts": "^6.0.0",
- "flint-chart": "^0.4.0",
+ "flint-chart": "^0.4.1",
"vega": "^6.0.0",
"vega-interpreter": "^2.2.1",
"vega-lite": "^6.0.0",
diff --git a/site/index.html b/site/index.html
index a5b85a1..754e342 100644
--- a/site/index.html
+++ b/site/index.html
@@ -4,7 +4,7 @@
Flint: A Visualization Language for the AI Era
-
+