diff --git a/web-common/src/features/canvas/Toolbar.svelte b/web-common/src/features/canvas/Toolbar.svelte index 8b349672aba..1143b2aae72 100644 --- a/web-common/src/features/canvas/Toolbar.svelte +++ b/web-common/src/features/canvas/Toolbar.svelte @@ -28,6 +28,8 @@ "donut_chart", "pie_chart", "heatmap", + "combo_chart", + "custom_chart", ] as const; $: showExplore = diff --git a/web-common/src/features/canvas/components/BaseCanvasComponent.ts b/web-common/src/features/canvas/components/BaseCanvasComponent.ts index 47831b787e4..7169eefdc76 100644 --- a/web-common/src/features/canvas/components/BaseCanvasComponent.ts +++ b/web-common/src/features/canvas/components/BaseCanvasComponent.ts @@ -26,6 +26,7 @@ import type { TimeAndFilterStore, TimeRangeState, } from "../../dashboards/time-controls/time-control-store"; +import { TimeRangePreset } from "@rilldata/web-common/lib/time/types"; import type { CanvasEntity, ComponentPath, @@ -168,11 +169,13 @@ export abstract class BaseCanvasComponent { this.parent.timeManager.state.comparisonRangeStore, this.parent.timeManager.state.comparisonIntervalStore, this.parent.timeManager.state.timeZoneStore, + this.parent.timeManager.state.rangeStore, this.localTimeControls.interval, this.localTimeControls.comparisonIntervalStore, this.localTimeControls.showTimeComparisonStore, this.localTimeControls.grainStore, this.localTimeControls.comparisonRangeStore, + this.localTimeControls.rangeStore, this.parent.filterManager.metricsViewFilters, this.parent.specStore, this.parent.timeManager.hasTimeSeriesMap, @@ -186,11 +189,13 @@ export abstract class BaseCanvasComponent { globalComparisonRange, globalComparisonInterval, timeZone, + globalRange, localInterval, localComparisonInterval, localShowTimeComparison, localGrainStore, localComparisonRange, + localRange, metricsViewFilters, canvasData, hasTimeSeriesMap, @@ -212,6 +217,14 @@ export abstract class BaseCanvasComponent { }; let timeRangeState: TimeRangeState | undefined = { + selectedTimeRange: globalInterval + ? { + name: globalRange ?? TimeRangePreset.CUSTOM, + start: globalInterval.start.toJSDate(), + end: globalInterval.end.toJSDate(), + interval: globalGrainStore, + } + : undefined, timeStart: globalInterval?.start.toISO(), timeEnd: globalInterval?.end.toISO(), }; @@ -273,6 +286,14 @@ export abstract class BaseCanvasComponent { timeGrain = localGrainStore ?? globalGrainStore; const localTimeRangeState: TimeRangeState = { + selectedTimeRange: localInterval + ? { + name: localRange ?? TimeRangePreset.CUSTOM, + start: localInterval.start.toJSDate(), + end: localInterval.end.toJSDate(), + interval: localGrainStore ?? globalGrainStore, + } + : undefined, timeStart: localInterval?.start.toISO(), timeEnd: localInterval?.end.toISO(), }; diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 2445a36392c..f0d78def30c 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -129,10 +129,17 @@ export abstract class BaseChart< const timeGrain = get(this.timeAndFilterStore)?.timeGrain; const tddLink = getLinkStateForTimeDimensionDetail(spec, this.type); + const comparisonChartTypes = [ + "bar_chart", + "stacked_bar", + "stacked_bar_normalized", + ]; + const passComparison = comparisonChartTypes.includes(this.type); + return { whereFilter: dimensionFilters, dimensionThresholdFilters, - showTimeComparison: false, + ...(passComparison ? {} : { showTimeComparison: false }), activePage: tddLink.canLink ? DashboardState_ActivePage.TIME_DIMENSIONAL_DETAIL : DashboardState_ActivePage.PIVOT, diff --git a/web-common/src/features/canvas/components/charts/custom-chart/CanvasCustomChart.svelte b/web-common/src/features/canvas/components/charts/custom-chart/CanvasCustomChart.svelte index a967fc4b826..8b92ff6177d 100644 --- a/web-common/src/features/canvas/components/charts/custom-chart/CanvasCustomChart.svelte +++ b/web-common/src/features/canvas/components/charts/custom-chart/CanvasCustomChart.svelte @@ -3,7 +3,7 @@ import { onDestroy } from "svelte"; import AgenticChartPrompt from "./AgenticChartPrompt.svelte"; import { clearComponentConversation } from "./chart-ai-agent"; - import type { CustomChartComponent } from "./index"; + import type { CustomChartComponent, QueryFieldMeta } from "./index"; export let component: CustomChartComponent; export let editable: boolean = false; @@ -16,6 +16,14 @@ $: hasValidSpec = component.isValid($specStore); $: hasContent = component.hasContent($specStore); + + function handleMetaChange(meta: Record | undefined) { + if (!meta?.fields || !Array.isArray(meta.fields)) { + component.queryFieldsMeta.set([]); + return; + } + component.queryFieldsMeta.set(meta.fields as QueryFieldMeta[]); + } {#if hasValidSpec || hasContent} @@ -26,6 +34,7 @@ timeRange={$timeAndFilterStore?.timeRange} metricsSQL={$specStore.metrics_sql} showDataTable={editable} + onMetaChange={handleMetaChange} /> {:else} diff --git a/web-common/src/features/canvas/components/charts/custom-chart/index.ts b/web-common/src/features/canvas/components/charts/custom-chart/index.ts index 06cc4063496..543dadde83c 100644 --- a/web-common/src/features/canvas/components/charts/custom-chart/index.ts +++ b/web-common/src/features/canvas/components/charts/custom-chart/index.ts @@ -10,8 +10,16 @@ import type { CanvasEntity, ComponentPath, } from "@rilldata/web-common/features/canvas/stores/canvas-entity"; +import { + PivotChipType, + type PivotChipData, + type PivotState, +} from "@rilldata/web-common/features/dashboards/pivot/types"; +import type { ExploreState } from "@rilldata/web-common/features/dashboards/stores/explore-state"; +import { splitWhereFilter } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils"; +import { DashboardState_ActivePage } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb"; import type { V1Resource } from "@rilldata/web-common/runtime-client"; -import { get } from "svelte/store"; +import { get, writable, type Writable } from "svelte/store"; import CanvasCustomChart from "./CanvasCustomChart.svelte"; export interface CustomChart @@ -23,12 +31,19 @@ export interface CustomChart vega_spec: string; } +export interface QueryFieldMeta { + type: "dimension" | "measure"; + name: string; + display_name?: string; +} + export class CustomChartComponent extends BaseCanvasComponent { minSize = { width: 4, height: 4 }; defaultSize = { width: 6, height: 4 }; resetParams = []; type: CanvasComponentType = "custom_chart"; component = CanvasCustomChart; + queryFieldsMeta: Writable = writable([]); constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { const defaultSpec: CustomChart = { @@ -93,6 +108,54 @@ export class CustomChartComponent extends BaseCanvasComponent { ); } + getExploreTransformerProperties(): Partial { + const fields = get(this.queryFieldsMeta); + const timeAndFilter = get(this.timeAndFilterStore); + + const { dimensionFilters, dimensionThresholdFilters } = splitWhereFilter( + timeAndFilter?.where, + ); + + const columns: PivotChipData[] = []; + const rows: PivotChipData[] = []; + + for (const field of fields) { + if (field.type === "measure") { + columns.push({ + id: field.name, + title: field.display_name ?? field.name, + type: PivotChipType.Measure, + }); + } else { + rows.push({ + id: field.name, + title: field.display_name ?? field.name, + type: PivotChipType.Dimension, + }); + } + } + + const pivot: PivotState = { + columns, + rows, + expanded: {}, + sorting: [], + columnPage: 0, + rowPage: 0, + enableComparison: false, + tableMode: "nest", + activeCell: null, + }; + + return { + whereFilter: dimensionFilters, + dimensionThresholdFilters, + showTimeComparison: false, + activePage: DashboardState_ActivePage.PIVOT, + pivot, + }; + } + inputParams(): InputParams { return { options: { diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 207ac1a8afd..931b858796f 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -28,36 +28,41 @@ export function getLinkStateForTimeDimensionDetail( dimensionName?: string; } { if (!allowedTimeDimensionDetailTypes.includes(type)) - return { - canLink: false, - }; + return { canLink: false }; const hasXAxis = "x" in spec; const hasYAxis = "y" in spec; if (!hasXAxis || !hasYAxis) - return { - canLink: false, - }; + return { canLink: false }; const xAxis = spec.x; const yAxis = spec.y; - if (isFieldConfig(xAxis) && isFieldConfig(yAxis)) { - const colorDimension = spec.color; - if (isFieldConfig(colorDimension)) { - return { - canLink: xAxis.type === "temporal", - measureName: yAxis.field, - dimensionName: colorDimension.field, - }; - } + if (!isFieldConfig(xAxis) || !isFieldConfig(yAxis)) + return { canLink: false }; + if (yAxis.fields && yAxis.fields.length > 1) + return { canLink: false }; + + const colorDimension = spec.color; + const hasDimensionBreakout = + isFieldConfig(colorDimension) && + colorDimension.type !== "quantitative" && + colorDimension.type !== "value"; + + if (hasDimensionBreakout && xAxis.type === "nominal") + return { canLink: false }; + + if (hasDimensionBreakout) { return { canLink: xAxis.type === "temporal", measureName: yAxis.field, + dimensionName: colorDimension.field, }; } + return { - canLink: false, + canLink: xAxis.type === "temporal", + measureName: yAxis.field, }; } diff --git a/web-common/src/features/canvas/components/charts/variants/ComboChart.ts b/web-common/src/features/canvas/components/charts/variants/ComboChart.ts index a1a49219baa..2153f954fcf 100644 --- a/web-common/src/features/canvas/components/charts/variants/ComboChart.ts +++ b/web-common/src/features/canvas/components/charts/variants/ComboChart.ts @@ -9,12 +9,21 @@ import { type ChartFieldsMap, type FieldConfig, } from "@rilldata/web-common/features/components/charts/types"; +import { splitWhereFilter } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils"; +import { + PivotChipType, + type PivotChipData, + type PivotState, +} from "@rilldata/web-common/features/dashboards/pivot/types"; +import type { ExploreState } from "@rilldata/web-common/features/dashboards/stores/explore-state"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store"; +import { DashboardState_ActivePage } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb"; import { MetricsViewSpecDimensionType, type V1MetricsViewSpec, type V1Resource, } from "@rilldata/web-common/runtime-client"; +import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; import { get, type Readable } from "svelte/store"; import type { ChartDataQuery } from "../../../../components/charts/types"; import type { @@ -263,4 +272,78 @@ export class ComboChartComponent extends BaseChart { const measures = get(measuresStore); return this.provider.getChartDomainValues(measures); } + + override getExploreTransformerProperties(): Partial { + const spec = get(this.specStore); + const { dimensionFilters, dimensionThresholdFilters } = splitWhereFilter( + this.componentFilters, + ); + const timeGrain = get(this.timeAndFilterStore)?.timeGrain; + + const columns: PivotChipData[] = []; + const rows: PivotChipData[] = []; + + if (spec.x?.field) { + if (spec.x.type === "temporal") { + rows.push({ + id: timeGrain || V1TimeGrain.TIME_GRAIN_DAY, + title: spec.x.field, + type: PivotChipType.Time, + }); + } else { + rows.push({ + id: spec.x.field, + title: spec.x.field, + type: PivotChipType.Dimension, + }); + } + } + + if (spec.y1?.field && spec.y1.type === "quantitative") { + columns.push({ + id: spec.y1.field, + title: spec.y1.field, + type: PivotChipType.Measure, + }); + } + + if (spec.y2?.field && spec.y2.type === "quantitative") { + columns.push({ + id: spec.y2.field, + title: spec.y2.field, + type: PivotChipType.Measure, + }); + } + + const hasDimensionRows = rows.some( + (r) => r.type === PivotChipType.Dimension, + ); + if (hasDimensionRows) { + const timeChips = rows.filter((r) => r.type === PivotChipType.Time); + const nonTimeRows = rows.filter((r) => r.type !== PivotChipType.Time); + columns.push(...timeChips); + rows.length = 0; + rows.push(...nonTimeRows); + } + + const pivot: PivotState = { + columns, + rows, + expanded: {}, + sorting: [], + columnPage: 0, + rowPage: 0, + enableComparison: false, + tableMode: "nest", + activeCell: null, + }; + + return { + whereFilter: dimensionFilters, + dimensionThresholdFilters, + showTimeComparison: false, + activePage: DashboardState_ActivePage.PIVOT, + pivot, + }; + } } diff --git a/web-common/src/features/components/charts/custom/CustomChartRenderer.svelte b/web-common/src/features/components/charts/custom/CustomChartRenderer.svelte index ebb1ba8033b..bd8bc7606b9 100644 --- a/web-common/src/features/components/charts/custom/CustomChartRenderer.svelte +++ b/web-common/src/features/components/charts/custom/CustomChartRenderer.svelte @@ -14,7 +14,7 @@ } from "@rilldata/web-common/runtime-client"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import type { View, VisualizationSpec } from "svelte-vega"; - import { derived } from "svelte/store"; + import { derived, get } from "svelte/store"; import { convertV1ExpressionToMapstructure } from "./expression-utils"; export let spec: string | undefined = undefined; @@ -24,6 +24,9 @@ export let timeRange: V1TimeRange | undefined = undefined; export let showDataTable = false; export let name: string = "Custom Chart"; + export let onMetaChange: + | ((meta: Record | undefined) => void) + | undefined = undefined; const viewOptions = ["Chart", "Data"]; @@ -81,6 +84,13 @@ })), ); + $: if (onMetaChange && $combinedResults[0]?.isSuccess) { + const firstMeta = get(dataQueries[0])?.data?.meta; + if (firstMeta) { + onMetaChange(firstMeta as Record); + } + } + $: vegaData = $combinedResults.reduce>( (acc, result, idx) => { acc[`query${idx + 1}`] = result.data; diff --git a/web-common/src/features/components/charts/explore-transformer.ts b/web-common/src/features/components/charts/explore-transformer.ts index 9e58360fcd0..531e128af24 100644 --- a/web-common/src/features/components/charts/explore-transformer.ts +++ b/web-common/src/features/components/charts/explore-transformer.ts @@ -33,6 +33,10 @@ export function transformChartSpecToPivotState( const fieldConfig = value; + if (fieldConfig.type === "value") { + continue; + } + // Handle multiple fields case if (fieldConfig.fields?.length) { columns.push( @@ -55,13 +59,24 @@ export function transformChartSpecToPivotState( type: chipType, }; - if (key === "x" || chipType === PivotChipType.Measure) { + if (chipType === PivotChipType.Measure) { columns.push(chipData); } else { rows.push(chipData); } } + const hasDimensionRows = rows.some( + (r) => r.type === PivotChipType.Dimension, + ); + if (hasDimensionRows) { + const timeChips = rows.filter((r) => r.type === PivotChipType.Time); + const nonTimeRows = rows.filter((r) => r.type !== PivotChipType.Time); + columns.push(...timeChips); + rows.length = 0; + rows.push(...nonTimeRows); + } + return { columns, rows, diff --git a/web-common/src/features/dashboards/url-state/convertURLToExplorePreset.ts b/web-common/src/features/dashboards/url-state/convertURLToExplorePreset.ts index 0855b589886..c755707a069 100644 --- a/web-common/src/features/dashboards/url-state/convertURLToExplorePreset.ts +++ b/web-common/src/features/dashboards/url-state/convertURLToExplorePreset.ts @@ -588,9 +588,10 @@ function fromPivotUrlParams( const errors: Error[] = []; if (searchParams.has(ExploreStateURLParams.PivotRows)) { - const rows = ( - searchParams.get(ExploreStateURLParams.PivotRows) as string - ).split(","); + const rowsParam = searchParams.get( + ExploreStateURLParams.PivotRows, + ) as string; + const rows = rowsParam === "" ? [] : rowsParam.split(","); const validRows = rows.filter( (r) => dimensions.has(r) || r in FromURLParamTimeDimensionMap, ); @@ -602,9 +603,10 @@ function fromPivotUrlParams( } if (searchParams.has(ExploreStateURLParams.PivotColumns)) { - const cols = ( - searchParams.get(ExploreStateURLParams.PivotColumns) as string - ).split(","); + const colsParam = searchParams.get( + ExploreStateURLParams.PivotColumns, + ) as string; + const cols = colsParam === "" ? [] : colsParam.split(","); const validCols = cols.filter( (c) => dimensions.has(c) ||