From 834264ec55577e55f77439dde18634d54c6ef8ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:06:51 +0000 Subject: [PATCH 1/3] Initial plan From 04bffe5da58366563bac2d86da4eb19b25b6c816 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:14:08 +0000 Subject: [PATCH 2/3] Validate discrete chartProperties values; docs show accepted values --- docs/reference-chartjs.md | 16 ++-- docs/reference-echarts.md | 40 ++++----- docs/reference-vegalite.md | 58 ++++++------ packages/flint-js/src/chartjs/assemble.ts | 10 ++- .../flint-js/src/core/normalize-properties.ts | 88 +++++++++++++++++++ packages/flint-js/src/echarts/assemble.ts | 10 ++- packages/flint-js/src/vegalite/assemble.ts | 12 ++- .../tests/chart-properties-validation.test.ts | 77 ++++++++++++++++ scripts/gen-chart-reference.ts | 16 +++- 9 files changed, 265 insertions(+), 62 deletions(-) create mode 100644 packages/flint-js/src/core/normalize-properties.ts create mode 100644 packages/flint-js/tests/chart-properties-validation.test.ts diff --git a/docs/reference-chartjs.md b/docs/reference-chartjs.md index 2b06ac5..49344a4 100644 --- a/docs/reference-chartjs.md +++ b/docs/reference-chartjs.md @@ -119,7 +119,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `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) | — | always | Line or area interpolation method. | ### ![](chart-icon-slope.svg) Slope Chart @@ -133,9 +133,9 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)` | — | always | Line or area interpolation method. | +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)) | — | always | Line or area interpolation method. | | `opacity` | number | 0.1 – 1 (step 0.05) | `0.4` | always | Mark opacity. | -| `stackMode` | choice | `Stacked (default)`, `Layered (overlap)` | — | always | Stacking strategy for overlapping series. | +| `stackMode` | choice | Stacked (default) _(default)_, `layered` (Layered (overlap)) | — | always | Stacking strategy for overlapping series. | ### ![](chart-icon-range-area.svg) Range Area Chart @@ -160,7 +160,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `innerRadius` | number | 0 – 60 (step 5) | `0` | always | Inner radius as a percentage of the outer radius. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | ### ![](chart-icon-doughnut.svg) Doughnut Chart @@ -169,7 +169,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `innerRadius` | number | 20 – 80 (step 5) | `55` | always | Inner radius as a percentage of the outer radius. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | ## Polar @@ -179,7 +179,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `filled` | choice | `Filled (default)`, `Outline only` | — | always | Fill the enclosed radar area. | +| `filled` | choice | `true` (Filled (default)), `false` (Outline only) | — | always | Fill the enclosed radar area. | | `fillOpacity` | number | 0.05 – 0.8 (step 0.05) | `0.3` | always | Fill opacity for the area or region. | ### ![](chart-icon-rose.svg) Rose Chart @@ -188,5 +188,5 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `alignment` | choice | `Left (default)`, `Center` | — | always | Segment alignment for radial charts. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | Segment alignment for radial charts. | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | diff --git a/docs/reference-echarts.md b/docs/reference-echarts.md index 9e14e87..2498867 100644 --- a/docs/reference-echarts.md +++ b/docs/reference-echarts.md @@ -43,7 +43,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `regressionMethod` | choice | `Linear`, `Logarithmic`, `Exponential`, `Power`, `Quadratic`, `Polynomial` | `Linear` | always | Regression fit method. | +| `regressionMethod` | choice | `linear` (Linear), `log` (Logarithmic), `exp` (Exponential), `pow` (Power), `quad` (Quadratic), `poly` (Polynomial) | `linear` | always | Regression fit method. | | `polyOrder` | number | 2 – 10 (step 1) | `3` | always | Polynomial order for the regression fit. | ### ![](chart-icon-connected-scatter.svg) Connected Scatter Plot @@ -64,7 +64,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `whiskerMethod` | choice | `Tukey (1.5 × IQR)`, `Min–Max` | `Tukey (1.5 × IQR)` | always | Whiskers | +| `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | Whiskers | | `showOutliers` | toggle | on / off | `true` | conditional | Outliers | ### ![](chart-icon-strip-plot.svg) Strip Plot @@ -95,7 +95,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `stackMode` | choice | `Stacked (default)`, `Normalize (100%)`, `Layered (overlap)` | — | conditional | Stacking strategy for overlapping series. | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `layered` (Layered (overlap)) | — | conditional | Stacking strategy for overlapping series. | ### ![](chart-icon-lollipop.svg) Lollipop Chart @@ -131,7 +131,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `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) | — | always | Line or area interpolation method. | | `showPoints` | toggle | on / off | `false` | always | Overlay point markers on the line. | ### ![](chart-icon-bump.svg) Bump Chart @@ -152,9 +152,9 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `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) | — | always | Line or area interpolation method. | | `opacity` | number | 0.1 – 1 (step 0.05) | `0.7` | always | Mark opacity. | -| `stackMode` | choice | `Stacked (default)`, `Normalize (100%)`, `Center`, `Layered (overlap)` | — | always | Stacking strategy for overlapping series. | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | always | Stacking strategy for overlapping series. | ### ![](chart-icon-streamgraph.svg) Streamgraph @@ -178,8 +178,8 @@ _No template-specific parameters._ |---|---|---|---|---|---| | `innerRadius` | number | 0 – 60 (step 5) | `0` | always | Inner radius as a percentage of the outer radius. | | `cornerRadius` | number | 0 – 10 (step 1) | `0` | always | Corner radius for supported marks. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | -| `labelType` | choice | `Name + %`, `Name`, `Value`, `Percent`, `None` | `Name + %` | always | Labels | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | +| `labelType` | choice | `categoryPercent` (Name + %), `category` (Name), `value` (Value), `percent` (Percent), `none` (None) | `categoryPercent` | always | Labels | ### ![](chart-icon-funnel.svg) Funnel Chart @@ -187,8 +187,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `sort` | choice | `Descending (default)`, `Ascending`, `Original order` | — | always | Sort order for ordered stages or categories. | -| `orient` | choice | `Vertical (default)`, `Horizontal` | — | always | Chart orientation. | +| `sort` | choice | `descending` (Descending (default)), `ascending` (Ascending), `none` (Original order) | — | always | Sort order for ordered stages or categories. | +| `orient` | choice | `vertical` (Vertical (default)), `horizontal` (Horizontal) | — | always | Chart orientation. | | `gap` | number | 0 – 20 (step 1) | `2` | always | Gap between segments. | ### ![](chart-icon-treemap.svg) Treemap @@ -197,7 +197,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `breadcrumb` | choice | `Show (default)`, `Hide` | — | always | Show or hide treemap breadcrumb navigation. | +| `breadcrumb` | choice | `true` (Show (default)), `false` (Hide) | — | always | Show or hide treemap breadcrumb navigation. | ### ![](chart-icon-sunburst.svg) Sunburst Chart @@ -206,7 +206,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `innerRadius` | number | 0 – 80 (step 5) | `0` | always | Inner radius as a percentage of the outer radius. | -| `labelRotate` | choice | `Radial (default)`, `Tangential`, `Horizontal` | — | always | Label orientation for sunburst sectors. | +| `labelRotate` | choice | `radial` (Radial (default)), `tangential` (Tangential), `0` (Horizontal) | — | always | Label orientation for sunburst sectors. | ### ![](chart-icon-tree.svg) Tree @@ -214,7 +214,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `orient` | choice | `Left → Right (default)`, `Top → Bottom` | — | always | Chart orientation. | +| `orient` | choice | `LR` (Left → Right (default)), `TB` (Top → Bottom) | — | always | Chart orientation. | ## Statistical @@ -287,8 +287,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `shape` | choice | `Polygon (default)`, `Circle` | — | always | Grid | -| `filled` | choice | `Filled (default)`, `Outline only` | — | always | Fill the enclosed radar area. | +| `shape` | choice | Polygon (default) _(default)_, `circle` (Circle) | — | always | Grid | +| `filled` | choice | `true` (Filled (default)), `false` (Outline only) | — | always | Fill the enclosed radar area. | | `fillOpacity` | number | 0.05 – 0.8 (step 0.05) | `0.3` | always | Fill opacity for the area or region. | ### ![](chart-icon-rose.svg) Rose Chart @@ -297,8 +297,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `alignment` | choice | `Left (default)`, `Center` | — | always | Segment alignment for radial charts. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | Segment alignment for radial charts. | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | ## Indicator @@ -310,7 +310,7 @@ _No template-specific parameters._ |---|---|---|---|---|---| | `min` | number | 0 – 1000 (step 10) | `0` | always | Min | | `max` | number | 0 – 10000 (step 100) | `100` | always | Max | -| `showProgress` | choice | `Show (default)`, `Hide` | — | always | Progress | +| `showProgress` | choice | `true` (Show (default)), `false` (Hide) | — | always | Progress | ## Flow @@ -320,7 +320,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `orient` | choice | `Horizontal (default)`, `Vertical` | — | always | Chart orientation. | +| `orient` | choice | `horizontal` (Horizontal (default)), `vertical` (Vertical) | — | always | Chart orientation. | | `nodeWidth` | number | 5 – 40 (step 5) | `20` | always | Node Width | | `nodeGap` | number | 2 – 30 (step 2) | `10` | always | Node Gap | @@ -330,4 +330,4 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `layout` | choice | `Circular (default)`, `Force-directed` | — | always | Layout | +| `layout` | choice | `circular` (Circular (default)), `force` (Force-directed) | — | always | Layout | diff --git a/docs/reference-vegalite.md b/docs/reference-vegalite.md index 3775551..2950015 100644 --- a/docs/reference-vegalite.md +++ b/docs/reference-vegalite.md @@ -48,7 +48,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `regressionMethod` | choice | `Linear`, `Logarithmic`, `Exponential`, `Power`, `Quadratic`, `Polynomial` | `Linear` | always | Regression fit method. | +| `regressionMethod` | choice | `linear` (Linear), `log` (Logarithmic), `exp` (Exponential), `pow` (Power), `quad` (Quadratic), `poly` (Polynomial) | `linear` | always | Regression fit method. | | `polyOrder` | number | 2 – 10 (step 1) | `3` | always | Polynomial order for the regression fit. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | @@ -104,8 +104,8 @@ The **Availability** column shows whether a parameter is `always` available or ` |---|---|---|---|---|---| | `cornerRadius` | number | 0 – 15 (step 1) | `0` | always | Corner radius for supported marks. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | -| `xAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | -| `yAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | ### ![](chart-icon-column-grouped.svg) Grouped Bar Chart @@ -121,7 +121,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `stackMode` | choice | `Stacked (default)`, `Normalize (100%)`, `Center`, `Layered (overlap)` | — | conditional | Stacking strategy for overlapping series. | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | conditional | Stacking strategy for overlapping series. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | ### ![](chart-icon-lollipop.svg) Lollipop Chart @@ -132,8 +132,8 @@ The **Availability** column shows whether a parameter is `always` available or ` |---|---|---|---|---|---| | `dotSize` | number | 20 – 300 (step 10) | `80` | always | Size of the dot mark. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | -| `xAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | -| `yAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | ### ![](chart-icon-waterfall.svg) Waterfall Chart @@ -142,7 +142,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `cornerRadius` | number | 0 – 8 (step 1) | `0` | always | Corner radius for supported marks. | -| `totals` | choice | `Auto`, `None`, `First`, `Last`, `Both` | `Auto` | conditional | Totals | +| `totals` | choice | `auto` (Auto), `none` (None), `first` (First), `last` (Last), `both` (Both) | `auto` | conditional | Totals | | `showTextLabels` | toggle | on / off | `false` | always | Render value labels on the marks. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | @@ -214,7 +214,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `whiskerMethod` | choice | `Tukey (1.5 × IQR)`, `Min–Max` | `Tukey (1.5 × IQR)` | always | Whiskers | +| `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | Whiskers | | `showOutliers` | toggle | on / off | `true` | conditional | Outliers | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | @@ -248,15 +248,15 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `Step After`, `Basis (smooth)`, `Cardinal`, `Catmull-Rom` | — | 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. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | | `logScale_y` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the y-axis. | | `includeZero_x` | toggle | on / off | `false` | conditional | Anchor the x-axis at zero. | | `includeZero_y` | toggle | on / off | `false` | conditional | Anchor the y-axis at zero. | -| `xAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | -| `yAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | ### Sparkline @@ -264,8 +264,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `Step After`, `Basis (smooth)`, `Cardinal`, `Catmull-Rom` | — | always | Line or area interpolation method. | -| `baseline` | choice | `Average`, `Zero`, `Median`, `None` | `Average` | always | Reference line | +| `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. | +| `baseline` | choice | `mean` (Average), `zero` (Zero), `median` (Median), `none` (None) | `mean` | always | Reference line | | `trendWidth` | number | 80 – 600 (step 10) | `240` | always | Sparkline width | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | @@ -303,12 +303,12 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `Step After`, `Basis (smooth)`, `Cardinal`, `Catmull-Rom` | — | 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.1) | `0.7` | always | Mark opacity. | -| `stackMode` | choice | `Stacked (default)`, `Normalize (100%)`, `Center`, `Layered (overlap)` | — | conditional | Stacking strategy for overlapping series. | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | conditional | Stacking strategy for overlapping series. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | -| `xAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | -| `yAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | ### ![](chart-icon-streamgraph.svg) Streamgraph @@ -316,7 +316,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `Step After`, `Basis (smooth)`, `Cardinal`, `Catmull-Rom` | — | 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. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | ### ![](chart-icon-range-area.svg) Range Area Chart @@ -325,7 +325,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `interpolate` | choice | `Default (linear)`, `Linear`, `Monotone (smooth)`, `Step`, `Step Before`, `Step After`, `Basis (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)) | — | always | Line or area interpolation method. | | `opacity` | number | 0.1 – 1 (step 0.1) | `0.5` | always | Mark opacity. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | @@ -338,7 +338,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `innerRadius` | number | 0 – 100 (step 5) | `0` | always | Inner radius as a percentage of the outer radius. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | ### ![](chart-icon-rose.svg) Rose Chart @@ -349,8 +349,8 @@ _No template-specific parameters._ |---|---|---|---|---|---| | `innerRadius` | number | 0 – 100 (step 5) | `0` | always | Inner radius as a percentage of the outer radius. | | `padAngle` | number | 0 – 0.1 (step 0.005) | `0` | always | Angular gap between radial segments. | -| `alignment` | choice | `Left (default)`, `Center` | — | always | Segment alignment for radial charts. | -| `sortSlices` | choice | `Data order`, `Largest first`, `Smallest first` | `Data order` | always | Sort slices | +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | Segment alignment for radial charts. | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | Sort slices | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | ### ![](chart-icon-radar.svg) Radar Chart @@ -378,8 +378,8 @@ _No template-specific parameters._ |---|---|---|---|---|---| | `showTextLabels` | toggle | on / off | `false` | always | Render value labels on the marks. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | -| `xAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | -| `yAxisType` | choice | `Temporal`, `Discrete` | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. | ### ![](chart-icon-bar-table.svg) Bar Table @@ -397,7 +397,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `layout` | choice | `Horizontal`, `Vertical`, `Grid` | `Grid` | always | Layout | +| `layout` | choice | `horizontal` (Horizontal), `vertical` (Vertical), `grid` (Grid) | `grid` | always | Layout | | `style` | toggle | on / off | `true` | always | Card style | | `behindThreshold` | number | 0 – 1 (step 0.05) | `0.5` | conditional | Behind threshold | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | @@ -411,9 +411,9 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `region` | choice | `Auto-detect`, `United States`, `World` | `Auto-detect` | always | Region | -| `projection` | choice | `Default`, `Mercator`, `Equal Earth`, `Orthographic (Globe)`, `Stereographic`, `Conic Equal Area`, `Conic Equidistant`, `Azimuthal Equidistant`, `Mollweide` | `Default` | conditional | Projection | -| `projectionCenter` | choice | `Default`, `World (Atlantic) [0, 0]`, `World (Pacific) [150, 0]`, `China [105, 35]`, `USA [-98, 39]`, `Europe [10, 50]`, `Japan [138, 36]`, `India [78, 22]`, `Brazil [-52, -14]`, `Australia [134, -25]`, `Russia [100, 60]`, `Africa [20, 0]`, `Middle East [45, 28]`, `Southeast Asia [115, 5]`, `South America [-60, -15]`, `North America [-100, 45]`, `UK [-2, 54]`, `Germany [10, 51]`, `France [2, 47]`, `Korea [128, 36]` | — | conditional | Center | +| `region` | choice | `auto` (Auto-detect), `us` (United States), `world` (World) | `auto` | always | Region | +| `projection` | choice | `default` (Default), `mercator` (Mercator), `equalEarth` (Equal Earth), `orthographic` (Orthographic (Globe)), `stereographic` (Stereographic), `conicEqualArea` (Conic Equal Area), `conicEquidistant` (Conic Equidistant), `azimuthalEquidistant` (Azimuthal Equidistant), `mollweide` (Mollweide) | `default` | conditional | Projection | +| `projectionCenter` | choice | Default _(default)_, `0,0` (World (Atlantic) [0, 0]), `150,0` (World (Pacific) [150, 0]), `105,35` (China [105, 35]), `-98,39` (USA [-98, 39]), `10,50` (Europe [10, 50]), `138,36` (Japan [138, 36]), `78,22` (India [78, 22]), `-52,-14` (Brazil [-52, -14]), `134,-25` (Australia [134, -25]), `100,60` (Russia [100, 60]), `20,0` (Africa [20, 0]), `45,28` (Middle East [45, 28]), `115,5` (Southeast Asia [115, 5]), `-60,-15` (South America [-60, -15]), `-100,45` (North America [-100, 45]), `-2,54` (UK [-2, 54]), `10,51` (Germany [10, 51]), `2,47` (France [2, 47]), `128,36` (Korea [128, 36]) | — | conditional | Center | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | | `logScale_y` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the y-axis. | | `includeZero_x` | toggle | on / off | `false` | conditional | Anchor the x-axis at zero. | @@ -425,4 +425,4 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `region` | choice | `Auto-detect`, `United States`, `World` | `Auto-detect` | always | Region | +| `region` | choice | `auto` (Auto-detect), `us` (United States), `world` (World) | `auto` | always | Region | diff --git a/packages/flint-js/src/chartjs/assemble.ts b/packages/flint-js/src/chartjs/assemble.ts index f97bbce..92aad55 100644 --- a/packages/flint-js/src/chartjs/assemble.ts +++ b/packages/flint-js/src/chartjs/assemble.ts @@ -43,6 +43,7 @@ import { computeLayout, computeChannelBudgets, deriveStretchCaps, resolveBaseSiz import { decideColorMaps } from '../core/color-decisions'; import { cjsApplyLayoutToSpec, cjsApplyTooltips } from './instantiate-spec'; import { normalizeStaticSeries } from '../core/static-series'; +import { normalizeChartProperties } from '../core/normalize-properties'; // --------------------------------------------------------------------------- // Public API @@ -71,7 +72,6 @@ export function assembleChartjs(input: ChartAssemblyInput): any { const sizeCeiling = input.chart_spec.canvasSize; const baseSize = resolveBaseSize(input.chart_spec.baseSize, sizeCeiling); const canvasSize = baseSize; - const chartProperties = input.chart_spec.chartProperties; const options = input.options ?? {}; let chartTemplate = cjsGetTemplateDef(chartType) as ChartTemplateDef; if (!chartTemplate) { @@ -80,6 +80,14 @@ export function assembleChartjs(input: ChartAssemblyInput): any { const warnings: ChartWarning[] = []; + // Validate discrete property values against the template's options before + // they reach `instantiate` (map known labels → values, drop unknowns). + const normalizedProps = normalizeChartProperties( + chartTemplate.properties, input.chart_spec.chartProperties, + ); + const chartProperties = normalizedProps.chartProperties; + warnings.push(...normalizedProps.warnings); + // ═══════════════════════════════════════════════════════════════════════ // PRE-PHASE: Static Series Normalization // ═══════════════════════════════════════════════════════════════════════ diff --git a/packages/flint-js/src/core/normalize-properties.ts b/packages/flint-js/src/core/normalize-properties.ts new file mode 100644 index 0000000..00f20a7 --- /dev/null +++ b/packages/flint-js/src/core/normalize-properties.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Normalize user-supplied `chart_spec.chartProperties` against a template's + * declared `properties`, so an invalid *value* never reaches the backend spec. + * + * The reference docs (see `scripts/gen-chart-reference.ts`) list the accepted + * `value` for each discrete property, but a caller may still pass a human label + * (e.g. `"Logarithmic"` instead of `"log"`) or an outright unknown value. Left + * unchecked, the template's `instantiate` copies that string verbatim into the + * backend transform (e.g. Vega-Lite's `regression` `method`), which the renderer + * silently rejects — producing a blank chart with no error. + * + * For every discrete property present in `chartProperties` this helper: + * - keeps the value untouched when it already matches an option `value`; + * - maps a known option `label` (case-insensitive) to its `value`, emitting an + * `info` warning so the caller learns the canonical value; + * - drops an unrecognized value (falling back to the property default) and + * emits a `warning`, keeping the "safe to pass" contract that a bad input + * degrades gracefully rather than rendering nothing. + * + * Non-discrete properties and keys not declared by the template (pivot state, + * axis overrides, encoding overrides, …) are passed through unchanged. + */ + +import type { ChartPropertyDef, ChartWarning } from './types'; + +export interface NormalizePropertiesResult { + chartProperties: Record | undefined; + warnings: ChartWarning[]; +} + +export function normalizeChartProperties( + properties: ChartPropertyDef[] | undefined, + chartProperties: Record | undefined, +): NormalizePropertiesResult { + const warnings: ChartWarning[] = []; + if (!properties || !chartProperties) { + return { chartProperties, warnings }; + } + + let result: Record | undefined; + const ensureCopy = (): Record => { + if (!result) result = { ...chartProperties }; + return result; + }; + + for (const def of properties) { + if (def.type !== 'discrete') continue; + if (!(def.key in chartProperties)) continue; + const value = chartProperties[def.key]; + if (value == null) continue; + + // Already a valid accepted value — nothing to do. + if (def.options.some((o) => o.value === value)) continue; + + // Known label (case-insensitive) → coerce to the canonical value. + const byLabel = + typeof value === 'string' + ? def.options.find( + (o) => o.label.toLowerCase() === value.trim().toLowerCase(), + ) + : undefined; + if (byLabel) { + ensureCopy()[def.key] = byLabel.value; + warnings.push({ + severity: 'info', + code: 'coerced-option-label', + message: `chartProperties.${def.key}: '${value}' is a display label; using the accepted value '${byLabel.value}' instead.`, + }); + continue; + } + + // Unrecognized value → drop it so the property falls back to its default, + // instead of emitting an invalid backend spec that renders blank. + const accepted = def.options.map((o) => `'${o.value}'`).join(', '); + const copy = ensureCopy(); + delete copy[def.key]; + warnings.push({ + severity: 'warning', + code: 'invalid-option-value', + message: `chartProperties.${def.key}: '${value}' is not a valid option (accepted: ${accepted}). Falling back to the default.`, + }); + } + + return { chartProperties: result ?? chartProperties, warnings }; +} diff --git a/packages/flint-js/src/echarts/assemble.ts b/packages/flint-js/src/echarts/assemble.ts index b96d8e8..8ab5bc7 100644 --- a/packages/flint-js/src/echarts/assemble.ts +++ b/packages/flint-js/src/echarts/assemble.ts @@ -74,6 +74,7 @@ import { inferVisCategory, computeZeroDecision } from '../core/semantic-types'; import { decideColorMaps } from '../core/color-decisions'; import { getPaletteForScheme } from './colormap'; import { normalizeStaticSeries } from '../core/static-series'; +import { normalizeChartProperties } from '../core/normalize-properties'; // --------------------------------------------------------------------------- // Public API @@ -103,7 +104,6 @@ export function assembleECharts(input: ChartAssemblyInput): any { const sizeCeiling = input.chart_spec.canvasSize; const baseSize = resolveBaseSize(input.chart_spec.baseSize, sizeCeiling); const canvasSize = baseSize; - const chartProperties = input.chart_spec.chartProperties; const options = input.options ?? {}; let chartTemplate = ecGetTemplateDef(chartType) as ChartTemplateDef; if (!chartTemplate) { @@ -112,6 +112,14 @@ export function assembleECharts(input: ChartAssemblyInput): any { const warnings: ChartWarning[] = []; + // Validate discrete property values against the template's options before + // they reach `instantiate` (map known labels → values, drop unknowns). + const normalizedProps = normalizeChartProperties( + chartTemplate.properties, input.chart_spec.chartProperties, + ); + const chartProperties = normalizedProps.chartProperties; + warnings.push(...normalizedProps.warnings); + // ═══════════════════════════════════════════════════════════════════════ // PRE-PHASE: Static Series Normalization // ═══════════════════════════════════════════════════════════════════════ diff --git a/packages/flint-js/src/vegalite/assemble.ts b/packages/flint-js/src/vegalite/assemble.ts index cecc007..309bc31 100644 --- a/packages/flint-js/src/vegalite/assemble.ts +++ b/packages/flint-js/src/vegalite/assemble.ts @@ -64,6 +64,7 @@ import { filterOverflow } from '../core/filter-overflow'; import { computeLayout, computeChannelBudgets, computeMinSubplotDimensions, deriveStretchCaps, resolveBaseSize } from '../core/compute-layout'; import { vlApplyLayoutToSpec, vlApplyTooltips } from './instantiate-spec'; import { normalizeStaticSeries } from '../core/static-series'; +import { normalizeChartProperties } from '../core/normalize-properties'; // --------------------------------------------------------------------------- // Helpers @@ -114,7 +115,6 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { const sizeCeiling = input.chart_spec.canvasSize; const baseSize = resolveBaseSize(input.chart_spec.baseSize, sizeCeiling); const canvasSize = baseSize; - const chartProperties = input.chart_spec.chartProperties; const options = input.options ?? {}; let chartTemplate = vlGetTemplateDef(chartType) as ChartTemplateDef; if (!chartTemplate) { @@ -123,6 +123,16 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { const warnings: ChartWarning[] = []; + // Validate discrete property *values* against the template's declared + // options before they reach `instantiate`. This maps a known display label + // to its accepted value and drops unrecognized values, so a bad input never + // produces an invalid backend spec (which would render blank). + const normalizedProps = normalizeChartProperties( + chartTemplate.properties, input.chart_spec.chartProperties, + ); + const chartProperties = normalizedProps.chartProperties; + warnings.push(...normalizedProps.warnings); + // ═══════════════════════════════════════════════════════════════════════ // PRE-PHASE: Static Series Normalization // ═══════════════════════════════════════════════════════════════════════ diff --git a/packages/flint-js/tests/chart-properties-validation.test.ts b/packages/flint-js/tests/chart-properties-validation.test.ts new file mode 100644 index 0000000..d0443dd --- /dev/null +++ b/packages/flint-js/tests/chart-properties-validation.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; + +const BASE = { + data: { + values: [ + { flops: 1.2e20, loss: 2.81 }, + { flops: 7.0e20, loss: 2.42 }, + { flops: 2.8e21, loss: 2.34 }, + { flops: 5.4e21, loss: 2.11 }, + ], + }, + semantic_types: { flops: 'Quantity', loss: 'Quantity' }, +}; + +function assemble(chartProperties: Record): any { + return assembleVegaLite({ + ...BASE, + chart_spec: { + chartType: 'Regression', + encodings: { x: { field: 'flops' }, y: { field: 'loss' } }, + chartProperties, + }, + }); +} + +/** Pull the regression transform out of the assembled layered spec. */ +function regressionTransform(spec: any): any { + const layer = spec.layer.find((l: any) => + (l.transform ?? []).some((t: any) => 'regression' in t), + ); + return layer.transform.find((t: any) => 'regression' in t); +} + +describe('Regression — discrete property value validation', () => { + it('passes a valid accepted value through unchanged', () => { + const spec = assemble({ regressionMethod: 'log' }); + expect(regressionTransform(spec).method).toBe('log'); + expect(spec._warnings).toBeUndefined(); + }); + + it('coerces a known display label to its accepted value', () => { + const spec = assemble({ regressionMethod: 'Logarithmic' }); + // The invalid label must not reach the backend transform verbatim. + expect(regressionTransform(spec).method).toBe('log'); + const warn = (spec._warnings ?? []).find( + (w: any) => w.code === 'coerced-option-label', + ); + expect(warn).toBeDefined(); + expect(warn.severity).toBe('info'); + }); + + it('is case-insensitive when matching a label', () => { + const spec = assemble({ regressionMethod: 'EXPONENTIAL' }); + expect(regressionTransform(spec).method).toBe('exp'); + }); + + it('drops an unrecognized value and warns instead of emitting it', () => { + const spec = assemble({ regressionMethod: 'Nonsense' }); + // Falls back to the default (linear) → no `method` on the transform. + expect(regressionTransform(spec).method).toBeUndefined(); + const warn = (spec._warnings ?? []).find( + (w: any) => w.code === 'invalid-option-value', + ); + expect(warn).toBeDefined(); + expect(warn.severity).toBe('warning'); + }); + + it('leaves a valid default value without warnings', () => { + const spec = assemble({ regressionMethod: 'linear' }); + expect(regressionTransform(spec).method).toBeUndefined(); + expect(spec._warnings).toBeUndefined(); + }); +}); diff --git a/scripts/gen-chart-reference.ts b/scripts/gen-chart-reference.ts index 59be907..863eea2 100644 --- a/scripts/gen-chart-reference.ts +++ b/scripts/gen-chart-reference.ts @@ -183,7 +183,17 @@ function domain(p: ChartPropertyDef): string { return `${p.min} – ${p.max}${step}`; } case 'discrete': - return p.options.map((o) => `\`${o.label}\``).join(', '); + // Show the accepted `value` (what callers pass in + // `chartProperties`), with the human label as secondary text — the + // labels are not valid inputs. An `undefined` value means "omit the + // property to get this default", so render just its label. + return p.options + .map((o) => + o.value === undefined || o.value === null + ? `${o.label} _(default)_` + : `\`${o.value}\`${o.label ? ` (${o.label})` : ''}`, + ) + .join(', '); case 'binary': return 'on / off'; } @@ -197,8 +207,10 @@ function defaultValue(p: ChartPropertyDef): string { if (raw == null) return '—'; if (p.key === 'binCount' && raw === 0) return '`Auto`'; if (p.type === 'discrete') { + // Render the accepted `value`, not the display label, so the default + // shown matches what a caller passes in `chartProperties`. const match = p.options.find((o) => o.value === raw); - return match ? `\`${match.label}\`` : `\`${String(raw)}\``; + return match ? `\`${String(match.value)}\`` : `\`${String(raw)}\``; } return `\`${String(raw)}\``; } From df74c998a184da9860de1cdf187d464be385f556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:16:14 +0000 Subject: [PATCH 3/3] Guard against null option label/value in normalize-properties --- packages/flint-js/src/core/normalize-properties.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/flint-js/src/core/normalize-properties.ts b/packages/flint-js/src/core/normalize-properties.ts index 00f20a7..dc5f7fa 100644 --- a/packages/flint-js/src/core/normalize-properties.ts +++ b/packages/flint-js/src/core/normalize-properties.ts @@ -59,7 +59,9 @@ export function normalizeChartProperties( const byLabel = typeof value === 'string' ? def.options.find( - (o) => o.label.toLowerCase() === value.trim().toLowerCase(), + (o) => + o.label != null && + o.label.toLowerCase() === value.trim().toLowerCase(), ) : undefined; if (byLabel) { @@ -74,7 +76,9 @@ export function normalizeChartProperties( // Unrecognized value → drop it so the property falls back to its default, // instead of emitting an invalid backend spec that renders blank. - const accepted = def.options.map((o) => `'${o.value}'`).join(', '); + const accepted = def.options + .map((o) => (o.value == null ? '(default)' : `'${o.value}'`)) + .join(', '); const copy = ensureCopy(); delete copy[def.key]; warnings.push({