Skip to content

feat: Add comparators/operators for selection params#9831

Open
joelostblom wants to merge 15 commits into
mainfrom
feat/param-comparisons
Open

feat: Add comparators/operators for selection params#9831
joelostblom wants to merge 15 commits into
mainfrom
feat/param-comparisons

Conversation

@joelostblom
Copy link
Copy Markdown
Contributor

This PR extends field predicate comparators (equal, lt, lte, gt, gte) to accept parameter value references and makes them practical for selection params. My original motivation came from #9830 where I had to use a not that intuitive syntax for gt/lt comparisons when using paramaters for the new time/animation functionality. As I was working on this I also thought it was a natural fit in the grammar that already existed for variable parameters and that it had benefits when used with selection parameters not related to time encodings, so I added a couple of example to illustrate that.

Here is how the simple new example in the docs look upon interaction:
recording-2026-04-14_16 19 54-ezgif com-video-to-gif-converter

And here is how the new gallery example looks with a lt comparator and empty initial condition:
recording-2026-04-14_16 16 07-ezgif com-video-to-gif-converter

A few details

  1. Comparator param refs
    • Adds {param: ...} and {param: ..., field: ...} support in field comparators.
    • Applies to both transform filters and condition tests via predicate compilation.
  2. Selection-param value resolution
    • For selection params, comparator refs are resolved from the selection store (projected value index), rather than assuming object-like signal access in all contexts.
  3. Empty-selection semantics
    • Adds optional empty on comparator refs to match variable params:
      • empty: true => empty selection passes predicate
      • empty: false => empty selection fails predicate
    • Defaults comparator refs for selection params to empty: true, avoiding init-time null coercion surprises.
  4. Cross-view composed specs
    • Comparator refs resolve selection components across composed/sibling views (e.g. vconcat where selection is defined in one view and used in another).
  5. Line+point interaction robustness
    • For point selections, ignores clicks on path marks (line/trail/area) for tuple capture.
    • Makes compiled point-overlay symbols (mark.point=true) interactive with pointer cursor so clicks are captured as expected.
    • It was quite unintuitive for lines with point=true how to select the points otherwise
  6. Schema alignment
    • Regenerates schema to include ParameterValueRef (including empty) in comparator definitions.

Checklist

  • This PR is atomic (i.e., it fixes one issue at a time).
  • The title is a concise semantic commit message (e.g. "fix: correctly handle undefined properties").
  • npm test runs successfully
  • For new features:
    • Has unit tests.
    • Has documentation under site/docs/ + examples.

Tips:

Add a parameter value reference form for field predicates so
comparisons can use parameter-backed values directly, including
selection parameter fields via `{param, field}`.

For timer selections, expose the resolved param as a field object
(e.g. `{"x": ANIM_value}`) so filters can use `ANIM.x` semantics
through predicate compilation (`ANIM["x"]`).

Also update animation filter relocation and implicit parse inference
to handle parameter-value predicate references, and add unit tests
for expression generation and timer selection integration.
When field predicates use `{param, field}` with a selection parameter,
compile the comparison against the selection store value instead of
directly indexing the resolved selection signal object. This makes
non-timer point selections usable in `lt/lte/gt/gte` comparisons and
avoids invalid expressions like `PICK["x"]` for resolved selections.

Also thread parameter-value expression resolution through
`fieldFilterExpression` and preserve fallback behavior for variable
params. For timer data routing, include `_store` references as
animation-related filters so these expressions are moved to `*_curr`
frame datasets when needed.
Point selections can be attached to layered line charts where
`mark.point=true` compiles to a line plus a symbol overlay. In that
shape, line items were still receiving clicks while point overlays were
non-interactive, causing invalid tuple values and broken comparator
filters.

Ignore path-mark clicks (`line`/`trail`/`area`) for point selection
tuple updates, and make point-style symbol overlays interactive with a
pointer cursor so clicks are captured by the visible points.

Update point-selection tests to cover the stricter tuple guard and the
interactive point overlay cursor behavior, including store-based timer
frame expression expectations.
Extend `{param, field}` comparator references with an optional
`empty` flag so field predicates can define how empty selection
params should be handled without extra manual guards.

When a comparator references a selection parameter, compile the
expression with explicit empty-selection semantics:
- `empty: true` keeps rows while the selection is empty
- `empty: false` rejects rows while the selection is empty

Add coverage for both modes in selection predicate compilation tests.
Selection-parameter comparator refs previously evaluated against null
when the selection store was empty, which produced operator-dependent
coercion behavior at initialization (for example, `lte` keeping only
the first point or `gt` dropping it).

Default `{param, field}` comparator refs to `empty: true` semantics
for selection params so empty selections behave intuitively by passing
rows until a value is selected. Explicit `empty: false` remains
supported for strict filtering workflows.

Add regression coverage for default empty behavior and updated timer
frame filter expressions.
Regenerate the Vega-Lite JSON schema so field comparator predicates
(`lt/lte/gt/gte/equal`) accept `ParameterValueRef` objects in
validation, including `{param, field}` and the new optional
`empty` semantics.

This aligns editor/schema validation with the implemented compiler
behavior for parameter-based comparator predicates.
Comparator predicates that reference selection params by `{param, field}`
can appear in sibling views (for example, a `vconcat` line view filtered by
a bar selection defined in another view).

Lookup previously only walked local ancestors, which failed for sibling
selection definitions and produced fallback expressions against the raw
param signal. Traverse from the root model when needed so comparator
refs resolve against the correct selection component and store.

Add regression coverage for composed-view selection comparator filters.
Document `{param, field}` comparator predicates for field filters and
condition tests, including selection-empty semantics and default
behavior.

Add a new interactive gallery example demonstrating cross-view
selection comparators in a vconcat dashboard and register it in the
examples catalog.
@joelostblom joelostblom requested a review from a team as a code owner April 14, 2026 14:21
Update selection clear and legend unit test expectations after
point-selection tuple guards began excluding path mark types
(`line`, `trail`, `area`).

Also fix lint violations in predicate compilation introduced during
comparator ref work by switching to template literals for store
signal naming.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 14, 2026

Deploying vega-lite with  Cloudflare Pages  Cloudflare Pages

Latest commit: e3aea81
Status: ✅  Deploy successful!
Preview URL: https://8436c88e.vega-lite.pages.dev
Branch Preview URL: https://feat-param-comparisons.vega-lite.pages.dev

View logs

GitHub Actions Bot and others added 6 commits April 14, 2026 14:27
Restore top-level selection param signal assembly to use
`vlSelectionResolve(...)` for timer selections.

A prior change emitted `{field: <param>_value}` at top level, but
`<param>_value` is a unit-scope signal. During example compilation this
produced parser errors (e.g. `Unrecognized signal name: "animation_frame_value"`)
for animated examples.

Update affected point selection expectation accordingly.
Add selection-comparator predicate tests to cover operator-specific
`{param, field}` handling (`equal`, `lt`, `gt`, `gte`) and the
non-selection fallback path when a referenced selection is missing.

These tests increase branch coverage for predicate compilation and
protect against regressions in empty/default semantics and store-based
value resolution.
Comment thread src/compile/predicate.ts
import {Model} from './model.js';
import {parseSelectionPredicate} from './selection/parse.js';

function findSelectionComponent(model: Model, param: string) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparator refs can be evaluated in a different unit model than where the selection is declared (for example, vconcat sibling views). Ancestor-only lookup misses that case, so this helper first tries normal lookup and then scans from the root model to find the declared selection component.

Comment thread src/compile/predicate.ts
return undefined;
}

function resolveSelectionParameterValueExpr(model: Model, v: any): string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolves {param, field} using the selection store’s projected value index (values[idx]) rather than assuming param[field] exists everywhere.

Comment thread src/compile/predicate.ts
return `(length(data(${store})) ? data(${store})[0].values[${idx}] : null)`;
}

function getParameterValueRef(predicate: FieldPredicate): ParameterValueRef {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extracts the comparator’s value-ref payload uniformly across equal/lt/lte/gt/gte so empty-handling logic is operator-agnostic and stays in one place.

}
],
"update": "datum && item().mark.marktype !== 'group' && indexof(item().mark.role, 'legend') < 0 ? {unit: \"layer_2\", fields: org_tuple_fields, values: [(item().isVoronoi ? datum.datum : datum)[\"origin\"]]} : null",
"update": "datum && item().mark.marktype !== 'group' && indexof(item().mark.role, 'legend') < 0 && indexof(['line', 'trail', 'area'], item().mark.marktype) < 0 ? {unit: \"layer_2\", fields: org_tuple_fields, values: [(item().isVoronoi ? datum.datum : datum)[\"origin\"]]} : null",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that pretty that this change shows up in so many examples, but I didn't find another way to ensure that points received clicks/interactions instead of lines when plotted together.

// find animation-related filters to be applied on the per-frame dataset
const timerValueSignal = `${selCmpt.name}_value`;
const timerObjectSignal = `${selCmpt.name}[`;
const timerStoreSignal = `${selCmpt.name}_store`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparator refs for selection params compile to expressions using data('<param>_store'). For timer selections, those filters must be moved to *_curr alongside other animation filters; otherwise frame/dataflow behavior diverges.

Comment thread src/compile/predicate.ts
const store = stringValue(`${valueRef.param}_store`);
const isEmptyExpr = `!length(data(${store}))`;
const empty = valueRef.empty ?? true;
return empty ? `(${isEmptyExpr} || (${expr}))` : `(!${isEmptyExpr} && (${expr}))`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to true here matches the selection-predicate default (empty passes) and prevents surprising partial data at initialization before any click.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant