Chart builder: drop unparseable expressions mid-edit, polite glyphed inline feedback

This commit is contained in:
2026-06-11 16:08:03 +03:00
parent 05df0cd7d3
commit d666599a58
9 changed files with 157 additions and 24 deletions
+13 -6
View File
@@ -26,6 +26,7 @@ import type { ColumnStats } from './profile';
import { DISTINCT_CAP } from './profile';
import type { ColumnType } from './type-inference';
import { VEGA_LITE_SCHEMA_URL } from './snippet';
import { validateExpression } from './expr-validate';
/** The five mark types the builder offers, in selector order (spec §06). */
export const MARK_TYPES = ['bar', 'line', 'point', 'area', 'circle'] as const;
@@ -835,25 +836,31 @@ function predicateObject(filter: BuilderFilter): Record<string, unknown> | null
function filterTransformObject(filter: BuilderFilter): Record<string, unknown> | null {
if (filter.mode === 'expression') {
const expr = (filter.expr ?? '').trim();
return expr === '' ? null : { filter: expr };
// A syntactically-invalid expression is dropped like an empty one: a half-typed
// `datum.x *` must not reach the renderer and blank the preview mid-edit — the
// inline feedback already flags it. (Same mid-edit resilience as a partial predicate.)
return expr === '' || !validateExpression(expr).valid ? null : { filter: expr };
}
const predicate = predicateObject(filter);
return predicate ? { filter: predicate } : null;
}
/** One calculate's `{ calculate, as }` transform entry, or `null` when incomplete. */
/** One calculate's `{ calculate, as }` transform entry, or `null` when incomplete or unparseable. */
function calculateTransformObject(calc: BuilderCalculate): Record<string, unknown> | null {
const expr = calc.expr.trim();
const as = calc.as.trim();
return expr === '' || as === '' ? null : { calculate: expr, as };
return expr === '' || as === '' || !validateExpression(expr).valid
? null
: { calculate: expr, as };
}
/**
* The complete top-level `transform` array for a configuration: every calculated
* field first (so filters and encodings can reference the derived columns), then
* every filter, each in the user's list order. Incomplete entries (a half-typed
* filter, an unnamed calculate) are dropped so a configuration mid-edit still
* produces a renderable spec. An empty result means no `transform` key is emitted.
* every filter, each in the user's list order. Incomplete or unparseable entries (a
* half-typed predicate, an unnamed calculate, an expression that doesn't parse) are
* dropped so a configuration mid-edit still produces a renderable spec. An empty
* result means no `transform` key is emitted.
*/
export function buildTransforms(config: BuilderConfig): Array<Record<string, unknown>> {
const out: Array<Record<string, unknown>> = [];