Skip to content

Commit 77b1d77

Browse files
TheRillJonclaude
andcommitted
feat: add "Go to Explore" for combo charts and custom charts, fix pre-existing bugs
Add "Go to Explore" support for combo_chart and custom_chart canvas components, and fix three bugs (two pre-existing) that prevented correct navigation to the Explore pivot page. Files changed: - Toolbar.svelte: Add "combo_chart" and "custom_chart" to EXPLORE_SUPPORTED_TYPES - BaseCanvasComponent.ts: Fix pre-existing bug — add `selectedTimeRange` to `timeRangeState` for both global and local time intervals, so "Go to Explore" includes the time range in the URL for ALL canvas chart types - ComboChart.ts: Add override `getExploreTransformerProperties()` that explicitly maps x, y1, y2 to pivot chips (skipping synthetic color field) - custom-chart/index.ts: Add `QueryFieldMeta` interface, `queryFieldsMeta` writable store, and `getExploreTransformerProperties()` using server-returned meta.fields - CanvasCustomChart.svelte: Add `handleMetaChange` callback to pipe meta.fields from the renderer into the component store - CustomChartRenderer.svelte: Add `onMetaChange` prop and reactive statement to emit meta.fields from the first query - convertURLToExplorePreset.ts: Fix pre-existing empty-string split bug in `fromPivotUrlParams()` for both PivotRows and PivotColumns Bugs fixed: 1. Missing time range (pre-existing, all chart types): BaseCanvasComponent never populated `selectedTimeRange` in the `timeRangeState`, so the explore URL never included a `tr=` param 2. Synthetic color field (combo-specific): BaseChart.getExploreTransformerProperties() processed the combo chart's synthetic color: {type:"value", field:"measures"} as a real dimension 3. Empty string split (pre-existing): "".split(",") returns [""] not [], causing crashes when PivotRows or PivotColumns are empty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 895b276 commit 77b1d77

7 files changed

Lines changed: 184 additions & 9 deletions

File tree

web-common/src/features/canvas/Toolbar.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"donut_chart",
2929
"pie_chart",
3030
"heatmap",
31+
"combo_chart",
32+
"custom_chart",
3133
] as const;
3234
3335
$: showExplore =

web-common/src/features/canvas/components/BaseCanvasComponent.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
TimeAndFilterStore,
2727
TimeRangeState,
2828
} from "../../dashboards/time-controls/time-control-store";
29+
import { TimeRangePreset } from "@rilldata/web-common/lib/time/types";
2930
import type {
3031
CanvasEntity,
3132
ComponentPath,
@@ -212,6 +213,14 @@ export abstract class BaseCanvasComponent<T = ComponentSpec> {
212213
};
213214

214215
let timeRangeState: TimeRangeState | undefined = {
216+
selectedTimeRange: globalInterval
217+
? {
218+
name: TimeRangePreset.CUSTOM,
219+
start: globalInterval.start.toJSDate(),
220+
end: globalInterval.end.toJSDate(),
221+
interval: globalGrainStore,
222+
}
223+
: undefined,
215224
timeStart: globalInterval?.start.toISO(),
216225
timeEnd: globalInterval?.end.toISO(),
217226
};
@@ -273,6 +282,14 @@ export abstract class BaseCanvasComponent<T = ComponentSpec> {
273282
timeGrain = localGrainStore ?? globalGrainStore;
274283

275284
const localTimeRangeState: TimeRangeState = {
285+
selectedTimeRange: localInterval
286+
? {
287+
name: TimeRangePreset.CUSTOM,
288+
start: localInterval.start.toJSDate(),
289+
end: localInterval.end.toJSDate(),
290+
interval: localGrainStore ?? globalGrainStore,
291+
}
292+
: undefined,
276293
timeStart: localInterval?.start.toISO(),
277294
timeEnd: localInterval?.end.toISO(),
278295
};

web-common/src/features/canvas/components/charts/custom-chart/CanvasCustomChart.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { onDestroy } from "svelte";
44
import AgenticChartPrompt from "./AgenticChartPrompt.svelte";
55
import { clearComponentConversation } from "./chart-ai-agent";
6-
import type { CustomChartComponent } from "./index";
6+
import type { CustomChartComponent, QueryFieldMeta } from "./index";
77
88
export let component: CustomChartComponent;
99
export let editable: boolean = false;
@@ -16,6 +16,14 @@
1616
1717
$: hasValidSpec = component.isValid($specStore);
1818
$: hasContent = component.hasContent($specStore);
19+
20+
function handleMetaChange(meta: Record<string, unknown> | undefined) {
21+
if (!meta?.fields || !Array.isArray(meta.fields)) {
22+
component.queryFieldsMeta.set([]);
23+
return;
24+
}
25+
component.queryFieldsMeta.set(meta.fields as QueryFieldMeta[]);
26+
}
1927
</script>
2028

2129
{#if hasValidSpec || hasContent}
@@ -26,6 +34,7 @@
2634
timeRange={$timeAndFilterStore?.timeRange}
2735
metricsSQL={$specStore.metrics_sql}
2836
showDataTable={editable}
37+
onMetaChange={handleMetaChange}
2938
/>
3039
{:else}
3140
<AgenticChartPrompt {component} />

web-common/src/features/canvas/components/charts/custom-chart/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ import type {
1010
CanvasEntity,
1111
ComponentPath,
1212
} from "@rilldata/web-common/features/canvas/stores/canvas-entity";
13+
import {
14+
PivotChipType,
15+
type PivotChipData,
16+
type PivotState,
17+
} from "@rilldata/web-common/features/dashboards/pivot/types";
18+
import type { ExploreState } from "@rilldata/web-common/features/dashboards/stores/explore-state";
19+
import { splitWhereFilter } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils";
20+
import { DashboardState_ActivePage } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb";
1321
import type { V1Resource } from "@rilldata/web-common/runtime-client";
14-
import { get } from "svelte/store";
22+
import { get, writable, type Writable } from "svelte/store";
1523
import CanvasCustomChart from "./CanvasCustomChart.svelte";
1624

1725
export interface CustomChart
@@ -23,12 +31,19 @@ export interface CustomChart
2331
vega_spec: string;
2432
}
2533

34+
export interface QueryFieldMeta {
35+
type: "dimension" | "measure";
36+
name: string;
37+
display_name?: string;
38+
}
39+
2640
export class CustomChartComponent extends BaseCanvasComponent<CustomChart> {
2741
minSize = { width: 4, height: 4 };
2842
defaultSize = { width: 6, height: 4 };
2943
resetParams = [];
3044
type: CanvasComponentType = "custom_chart";
3145
component = CanvasCustomChart;
46+
queryFieldsMeta: Writable<QueryFieldMeta[]> = writable([]);
3247

3348
constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) {
3449
const defaultSpec: CustomChart = {
@@ -93,6 +108,54 @@ export class CustomChartComponent extends BaseCanvasComponent<CustomChart> {
93108
);
94109
}
95110

111+
getExploreTransformerProperties(): Partial<ExploreState> {
112+
const fields = get(this.queryFieldsMeta);
113+
const timeAndFilter = get(this.timeAndFilterStore);
114+
115+
const { dimensionFilters, dimensionThresholdFilters } = splitWhereFilter(
116+
timeAndFilter?.where,
117+
);
118+
119+
const columns: PivotChipData[] = [];
120+
const rows: PivotChipData[] = [];
121+
122+
for (const field of fields) {
123+
if (field.type === "measure") {
124+
columns.push({
125+
id: field.name,
126+
title: field.display_name ?? field.name,
127+
type: PivotChipType.Measure,
128+
});
129+
} else {
130+
rows.push({
131+
id: field.name,
132+
title: field.display_name ?? field.name,
133+
type: PivotChipType.Dimension,
134+
});
135+
}
136+
}
137+
138+
const pivot: PivotState = {
139+
columns,
140+
rows,
141+
expanded: {},
142+
sorting: [],
143+
columnPage: 0,
144+
rowPage: 0,
145+
enableComparison: false,
146+
tableMode: "nest",
147+
activeCell: null,
148+
};
149+
150+
return {
151+
whereFilter: dimensionFilters,
152+
dimensionThresholdFilters,
153+
showTimeComparison: false,
154+
activePage: DashboardState_ActivePage.PIVOT,
155+
pivot,
156+
};
157+
}
158+
96159
inputParams(): InputParams<CustomChart> {
97160
return {
98161
options: {

web-common/src/features/canvas/components/charts/variants/ComboChart.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ import {
99
type ChartFieldsMap,
1010
type FieldConfig,
1111
} from "@rilldata/web-common/features/components/charts/types";
12+
import { splitWhereFilter } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils";
13+
import {
14+
PivotChipType,
15+
type PivotChipData,
16+
type PivotState,
17+
} from "@rilldata/web-common/features/dashboards/pivot/types";
18+
import type { ExploreState } from "@rilldata/web-common/features/dashboards/stores/explore-state";
1219
import type { TimeAndFilterStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store";
20+
import { DashboardState_ActivePage } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb";
1321
import {
1422
MetricsViewSpecDimensionType,
1523
type V1MetricsViewSpec,
1624
type V1Resource,
1725
} from "@rilldata/web-common/runtime-client";
26+
import { V1TimeGrain } from "@rilldata/web-common/runtime-client";
1827
import { get, type Readable } from "svelte/store";
1928
import type { ChartDataQuery } from "../../../../components/charts/types";
2029
import type {
@@ -263,4 +272,67 @@ export class ComboChartComponent extends BaseChart<ComboCanvasChartSpec> {
263272
const measures = get(measuresStore);
264273
return this.provider.getChartDomainValues(measures);
265274
}
275+
276+
override getExploreTransformerProperties(): Partial<ExploreState> {
277+
const spec = get(this.specStore);
278+
const { dimensionFilters, dimensionThresholdFilters } = splitWhereFilter(
279+
this.componentFilters,
280+
);
281+
const timeGrain = get(this.timeAndFilterStore)?.timeGrain;
282+
283+
const columns: PivotChipData[] = [];
284+
const rows: PivotChipData[] = [];
285+
286+
if (spec.x?.field) {
287+
if (spec.x.type === "temporal") {
288+
columns.push({
289+
id: timeGrain || V1TimeGrain.TIME_GRAIN_DAY,
290+
title: spec.x.field,
291+
type: PivotChipType.Time,
292+
});
293+
} else {
294+
columns.push({
295+
id: spec.x.field,
296+
title: spec.x.field,
297+
type: PivotChipType.Dimension,
298+
});
299+
}
300+
}
301+
302+
if (spec.y1?.field && spec.y1.type === "quantitative") {
303+
columns.push({
304+
id: spec.y1.field,
305+
title: spec.y1.field,
306+
type: PivotChipType.Measure,
307+
});
308+
}
309+
310+
if (spec.y2?.field && spec.y2.type === "quantitative") {
311+
columns.push({
312+
id: spec.y2.field,
313+
title: spec.y2.field,
314+
type: PivotChipType.Measure,
315+
});
316+
}
317+
318+
const pivot: PivotState = {
319+
columns,
320+
rows,
321+
expanded: {},
322+
sorting: [],
323+
columnPage: 0,
324+
rowPage: 0,
325+
enableComparison: false,
326+
tableMode: "nest",
327+
activeCell: null,
328+
};
329+
330+
return {
331+
whereFilter: dimensionFilters,
332+
dimensionThresholdFilters,
333+
showTimeComparison: false,
334+
activePage: DashboardState_ActivePage.PIVOT,
335+
pivot,
336+
};
337+
}
266338
}

web-common/src/features/components/charts/custom/CustomChartRenderer.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
} from "@rilldata/web-common/runtime-client";
1515
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
1616
import type { View, VisualizationSpec } from "svelte-vega";
17-
import { derived } from "svelte/store";
17+
import { derived, get } from "svelte/store";
1818
import { convertV1ExpressionToMapstructure } from "./expression-utils";
1919
2020
export let spec: string | undefined = undefined;
@@ -24,6 +24,9 @@
2424
export let timeRange: V1TimeRange | undefined = undefined;
2525
export let showDataTable = false;
2626
export let name: string = "Custom Chart";
27+
export let onMetaChange:
28+
| ((meta: Record<string, unknown> | undefined) => void)
29+
| undefined = undefined;
2730
2831
const viewOptions = ["Chart", "Data"];
2932
@@ -81,6 +84,13 @@
8184
})),
8285
);
8386
87+
$: if (onMetaChange && $combinedResults[0]?.isSuccess) {
88+
const firstMeta = get(dataQueries[0])?.data?.meta;
89+
if (firstMeta) {
90+
onMetaChange(firstMeta as Record<string, unknown>);
91+
}
92+
}
93+
8494
$: vegaData = $combinedResults.reduce<Record<string, unknown>>(
8595
(acc, result, idx) => {
8696
acc[`query${idx + 1}`] = result.data;

web-common/src/features/dashboards/url-state/convertURLToExplorePreset.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -588,9 +588,10 @@ function fromPivotUrlParams(
588588
const errors: Error[] = [];
589589

590590
if (searchParams.has(ExploreStateURLParams.PivotRows)) {
591-
const rows = (
592-
searchParams.get(ExploreStateURLParams.PivotRows) as string
593-
).split(",");
591+
const rowsParam = searchParams.get(
592+
ExploreStateURLParams.PivotRows,
593+
) as string;
594+
const rows = rowsParam === "" ? [] : rowsParam.split(",");
594595
const validRows = rows.filter(
595596
(r) => dimensions.has(r) || r in FromURLParamTimeDimensionMap,
596597
);
@@ -602,9 +603,10 @@ function fromPivotUrlParams(
602603
}
603604

604605
if (searchParams.has(ExploreStateURLParams.PivotColumns)) {
605-
const cols = (
606-
searchParams.get(ExploreStateURLParams.PivotColumns) as string
607-
).split(",");
606+
const colsParam = searchParams.get(
607+
ExploreStateURLParams.PivotColumns,
608+
) as string;
609+
const cols = colsParam === "" ? [] : colsParam.split(",");
608610
const validCols = cols.filter(
609611
(c) =>
610612
dimensions.has(c) ||

0 commit comments

Comments
 (0)