Skip to content

feat: custom chart and component with vega lite ejection - #9850

Open
djbarnwal wants to merge 17 commits into
mainfrom
feat/custom-viz-flint
Open

djbarnwal wants to merge 17 commits into
mainfrom
feat/custom-viz-flint

Conversation

@djbarnwal

Copy link
Copy Markdown
Member

Uses flint for creating a gallery of custom viz with an option to eject to a vega lite spec for more granular control.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

nishantmonu51 and others added 17 commits July 15, 2026 01:57
Extract inline canvas custom charts into reusable component resources:

- Components declare typed params (metrics_view, measure, dimension,
  time_dimension, scalars) validated at parse and reconcile time
- Canvas items reference components via 'component:' + 'params:' bindings;
  param-bound metrics views flow through transitive access security
- ResolveComponent merges defaults, validates bindings, resolves templates,
  and injects scalar params as native Vega-Lite params
- Component workspace with live preview, test bindings, used-by panel, and
  breaking-change warnings
- 'Save as custom viz' extraction from inline charts and 'Detach copy' inverse
- Vega-Lite examples gallery (vendored snapshot with static SVG thumbnails)
  with AI-first import: spec-in-prompt generation, placeholder file, and a
  self-healing validation loop with static structure/consistency checks
- Generated components include customizable order_by and limit params with
  density-based defaults; order_by follows re-bound fields in the editor
- Dev-agent instructions for authoring components (metrics_sql dialect rules,
  encoding/param type matching, row-local calculate preservation for
  diverging charts)

Gated behind the customComponents frontend flag and custom_components
runtime flag.
- Format touched files with prettier
- Fix svelte-check errors: Extract string keys in AllKeys so ComponentRefSpec's
  index signature doesn't widen keys to string | number; narrow the index
  signature values to scalars (matching the parser's scalar-only bindings);
  type initializeRow over AddableItem; pass the required editor binding in
  ComponentWorkspace; remove unused imports and cast union field accesses
- Add customComponents to the expected maps in Test_ResolveFeatureFlags
- Add the component_extract_title message key (en/es)
- Bind the required viewVL prop on the preview's inline VegaLiteRenderer
- Cast metrics_view access in FiltersMapper to string | null
- ResolveCanvas: skip templated metrics_view renderer properties instead of
  looking them up as literal resource names (param-bound metrics views are
  collected from the canvas items)
- Frontend: give canvas items referencing an external component their
  positional instance id as component.id, so two references to the same
  component get distinct DOM ids and consistent selection/cleanup; make the
  drag-move reselect path instance-aware and use getElementById for ids
  containing '::'
- Transitive access: scan metrics_sql lists (multi-query custom charts) for
  metrics view refs, not just single query strings
- Parser: components declaring a metrics_view param with a default now get a
  ref to the default metrics view for DAG ordering and invalidation
# Conflicts:
#	proto/gen/rill/runtime/v1/queries.pb.go
#	proto/gen/rill/runtime/v1/resources.pb.go
#	runtime/parser/parser_test.go
…ct time_dimension picker

- Renderer validators now run structural checks (required keys, types, enums)
  on parameterized components with templated properties; only field-membership
  checks against the metrics view are skipped for unresolvable values.
- The time_dimension param picker now only offers time fields (the primary
  time dimension and time-typed dimensions) and supports search, instead of
  listing every dimension and returning no results for any search term.
@nishantmonu51 nishantmonu51 added Type:Feature New feature request Area:Dashboard Size:XL Very large change: 2,000+ lines labels Sep 4, 2026
@nishantmonu51

Copy link
Copy Markdown
Collaborator

Existing canvases that reference a non-chart component file will break. getComponentInstanceType routes every item with component: <name> and !definedInCanvas to component_ref, and CanvasComponentRef renders only custom_chart. A canvas referencing a standalone markdown/image/kpi/table component now shows "Components with renderer X cannot be referenced on a canvas yet". The render path is not behind custom_components — the flag only gates the add menu and the workspace. web-common/src/features/canvas/components/util.ts:233

The chart recompiles on every resize frame. compiled is reactive on chartWidth/chartHeight from bind:clientWidth/bind:clientHeight, so each frame yields a new spec object and svelte-vega re-embeds. VegaLiteRenderer keeps options identity stable across width/height for exactly this reason (#9859); a fresh spec defeats it. web-common/src/features/components/charts/flint/FlintChartRenderer.svelte:148

The metrics_view naming rule is enforced in one direction only. parseComponentParams requires the name only when type: metrics_view, but both ref-extraction sites key purely off the binding's key name. A component declaring - name: metrics_view, type: string plus a canvas binding metrics_view: anything adds a ref to a nonexistent metrics view and the canvas fails to reconcile. runtime/parser/parse_component.go:337, runtime/parser/parse_canvas.go:350, runtime/reconcilers/canvas.go:220

AddComponentDropdown lists every standalone component regardless of renderer, so a user can add a reference that renders as an unsupported-renderer error. Filter on renderer === "custom_chart". web-common/src/features/canvas/AddComponentDropdown.svelte:66

Duplication. The metrics_view / *_metrics_view key-pattern extraction is copy-pasted (comment included) in parse_canvas.go:349 and reconcilers/canvas.go:217. normalizeMetricsSQL is defined three times — CanvasComponentRef.svelte, ComponentPreview.svelte, eject-component.ts — and the canvas copy differs (no trim(), so a whitespace-only query counts as present).

ResolveComponent passes nil metricsViews to ValidateParamBindings, so field-typed params accept any client-supplied string and are interpolated into metrics_sql. Access control still holds via the resolver's security policy, but the reconciler's field-membership check is worth repeating here.

ResourceKindMap[Component].folderName changes componentsviz_library unconditionally, not behind custom_components. Intentional? web-common/src/features/entity-management/add/new-files.ts:119

Minor: canvas_custom_viz is added to both locales but never used; the Spanish component_examples_description has a typo ("vincúblo" → "vincúlalo"); FlintChartRenderer, WorkspaceDispatcher, AddAssetButton and import-with-ai.ts carry hardcoded English while the rest of the feature is migrated; preview-state.ts never prunes its module-level store map, so entries survive file deletion and rename.

@nishantmonu51 nishantmonu51 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR contains #9685 in full (its head a3c94ec869 is an ancestor of fe70a09aee) and then deletes 9685's "Save as custom viz" extraction (custom-viz/extract/, SaveAsComponentDialog.svelte) and its PNG thumbnails, replacing the Vega gallery with the Flint catalog. The two cannot both merge as they stand: either 9685 lands first and this rebases onto it, or 9685 is closed in favour of this one. Which way do you want to sequence them?

Separately, the branch is 50 commits behind main with content conflicts in 11 files; most are proto regen, but runtime/canvas/component.go needs real work, since main has since gained validateMap (#8931) and an ephemeralNames parameter on validateOptionalMeasureField (#9855) that the rendererValidator receiver refactor has to absorb.

item?: V1CanvasItem,
allowUnvalidatedSpec = true,
): CanvasComponentType {
if (item && !item.definedInCanvas && item.component) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Any item with component: <name> and !definedInCanvas now routes to component_ref, and CanvasComponentRef.svelte:96 renders every renderer other than custom_chart as Components with renderer "..." cannot be referenced on a canvas yet. On main, createComponent keys off the referenced component's renderer, so a canvas referencing a standalone kpi, markdown, table or image component file (documented in docs/docs/reference/project-files/component.md) renders today and breaks after this merges, and the reroute is not behind customComponents. Even for custom_chart, normalizeMetricsSQL at CanvasComponentRef.svelte:86 keeps only the first entry of a metrics_sql list, so a standalone multi-query chart whose vega_spec reads query2 loses that dataset, where main's CustomChartRenderer takes metricsSQL: string[]; AddComponentDropdown.svelte:64 then lists every non-inline component regardless of renderer, so the add menu offers items that render as errors. Either route non-custom_chart renderers and array metrics_sql to their existing classes, or gate the reroute on the flag.

meta: { defaultValue: param.default ?? false },
};
case "string":
case "number":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A number param with options maps to type: "select", ParamMapper.svelte:250 renders that with Select, whose onChange is typed (value: string) => void, and the string is passed straight to component.updateProperty and persisted to the item's params:. With - name: limit, type: number, options: [10, 25, 50], picking 25 writes limit: "25", and validateParamValueType (runtime/canvas/params.go:270) fails with value for param "limit" must be a number, got 25, so the whole canvas goes invalid from a click in its own inspector. The workspace ParamValueInput already coerces with Number(next); the canvas path needs the same.

);
if (binding) return `{{ .fields.${binding.param}.display_name }}`;
}
return substitute(value, replacements);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

substitute is applied to every string that is not under a title/text key, and the buildReplacements needles are word-bounded field names, so any Vega-Lite enum or mark value equal to a bound field name is rewritten. The catalog's Histogram binds only x: measure and the compiler supplies the y channel as a count aggregate; bind that param to a measure named count (the name this PR's own fixtures use in runtime/server/canvases_test.go) and eject, and "aggregate": "count" becomes "aggregate": "{{ .params.x_axis }}". That resolves back to count for the original binding but to the field name for any dashboard that binds the param differently, and Vega-Lite rejects the spec, so the ejected component is not reusable, which is the property ejection exists to preserve. The same applies to a dimension named bar/line/point against mark.type, or sum/mean/descending against aggregate/op/sort; restrict substitution to field-valued keys and datum[...] references in expression strings.

$: ejected = parseVegaSpec(vegaSpec);

// Flint reads the data to make layout decisions, so compilation waits for the rows.
$: compiled =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

compiled is reactive on chartWidth/chartHeight, which are bound with bind:clientWidth/bind:clientHeight at line 199, so each frame of a drag-resize produces a new spec object with the full data.values inlined by the compiler. VegaLiteRenderer keeps options identity stable precisely because, as its comment at VegaLiteRenderer.svelte:100 states, svelte-vega re-embeds from options whenever the spec changes, so a fresh spec per frame defeats #9859 for these charts. Debounce the size passed to the compiler, or only recompile once the size has settled.

This branch has not been deployed

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

Labels

Area:Dashboard Size:XL Very large change: 2,000+ lines Type:Feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants