From 34554e00a8c6954057a6b0127b0fa62d73f0eda9 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 10 Apr 2025 21:00:39 +0530 Subject: [PATCH 01/62] Add abstract base chart class --- .../canvas/components/charts/BaseChart.ts | 99 ++++++++++++++ .../components/charts/CartesianChart.ts | 64 +++++++++ .../canvas/components/charts/Chart.svelte | 4 +- .../canvas/components/charts/ChartFactory.ts | 19 +++ .../canvas/components/charts/_to_delete.ts | 128 ++++++++++++++++++ .../canvas/components/charts/index.ts | 127 ----------------- .../canvas/components/charts/types.ts | 8 +- .../src/features/canvas/components/util.ts | 20 +-- .../canvas/inspector/ParamMapper.svelte | 8 +- .../inspector/chart/ChartTypeSelector.svelte | 4 +- web-common/src/features/canvas/layout-util.ts | 8 +- 11 files changed, 339 insertions(+), 150 deletions(-) create mode 100644 web-common/src/features/canvas/components/charts/BaseChart.ts create mode 100644 web-common/src/features/canvas/components/charts/CartesianChart.ts create mode 100644 web-common/src/features/canvas/components/charts/ChartFactory.ts create mode 100644 web-common/src/features/canvas/components/charts/_to_delete.ts diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts new file mode 100644 index 00000000000..33b53a52c97 --- /dev/null +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -0,0 +1,99 @@ +import { BaseCanvasComponent } from "@rilldata/web-common/features/canvas/components/BaseCanvasComponent"; +import type { + ChartType, + CommonChartProperties, + FieldConfig, +} from "@rilldata/web-common/features/canvas/components/charts/types"; +import { + commonOptions, + getFilterOptions, +} from "@rilldata/web-common/features/canvas/components/util"; +import type { + AllKeys, + ComponentInputParam, + InputParams, +} from "@rilldata/web-common/features/canvas/inspector/types"; +import type { V1Resource } from "@rilldata/web-common/runtime-client"; +import { get, writable, type Writable } from "svelte/store"; +import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity"; +import type { + ComponentCommonProperties, + ComponentFilterProperties, +} from "../types"; +import Chart from "./Chart.svelte"; + +// Base interface for all chart configurations +export type BaseChartConfig = ComponentFilterProperties & + ComponentCommonProperties & + CommonChartProperties; + +export abstract class BaseChart< + TConfig extends BaseChartConfig, +> extends BaseCanvasComponent { + minSize = { width: 4, height: 4 }; + defaultSize = { width: 6, height: 4 }; + resetParams = []; + type: ChartType; + chartType: Writable; + component = Chart; + + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + const baseSpec: BaseChartConfig = { + metrics_view: "", + title: "", + description: "", + }; + super(resource, parent, path, baseSpec as TConfig); + + this.type = resource.component?.state?.validSpec?.renderer as ChartType; + this.chartType = writable(this.type); + } + + isValid(spec: TConfig): boolean { + return typeof spec.metrics_view === "string"; + } + + inputParams(): InputParams { + return { + options: { + metrics_view: { type: "metrics", label: "Metrics view" }, + ...this.getChartSpecificOptions(), + ...commonOptions, + }, + filter: getFilterOptions(false), + }; + } + + protected abstract getChartSpecificOptions(): Record< + AllKeys, + ComponentInputParam + >; + + protected getDefaultFieldConfig(): Partial { + return { + showAxisTitle: true, + zeroBasedOrigin: true, + showNull: false, + }; + } + + updateChartType(key: ChartType) { + if (!this.parent.fileArtifact) return; + const currentSpec = get(this.specStore); + + const parentPath = this.pathInYAML.slice(0, -1); + + this.chartType.set(key); + + const parseDocumentStore = this.parent.parsedContent; + const parsedDocument = get(parseDocumentStore); + + const { updateEditorContent } = this.parent.fileArtifact; + + const width = parsedDocument.getIn([...parentPath, "width"]); + + parsedDocument.setIn(parentPath, { [key]: currentSpec, width }); + + updateEditorContent(parsedDocument.toString(), false, true); + } +} diff --git a/web-common/src/features/canvas/components/charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/CartesianChart.ts new file mode 100644 index 00000000000..2c2ef7daf45 --- /dev/null +++ b/web-common/src/features/canvas/components/charts/CartesianChart.ts @@ -0,0 +1,64 @@ +import type { FieldConfig } from "@rilldata/web-common/features/canvas/components/charts/types"; +import type { + V1MetricsViewSpec, + V1Resource, +} from "@rilldata/web-common/runtime-client"; +import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity"; +import { BaseChart, type BaseChartConfig } from "./BaseChart"; + +export type CartesianChartConfig = BaseChartConfig & { + x?: FieldConfig; + y?: FieldConfig; + color?: FieldConfig | string; +}; + +export class CartesianChartComponent extends BaseChart { + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + super(resource, parent, path); + } + + protected getChartSpecificOptions(): Record { + return { + x: { type: "positional", label: "X-axis" }, + y: { type: "positional", label: "Y-axis" }, + color: { type: "mark", label: "Color", meta: { type: "color" } }, + tooltip: { type: "tooltip", label: "Tooltip", showInUI: false }, + vl_config: { type: "config", showInUI: false }, + }; + } + + static newComponentSpec( + metricsViewName: string, + metricsViewSpec: V1MetricsViewSpec | undefined, + ): CartesianChartConfig { + // Randomly select a measure and dimension if available + const measures = metricsViewSpec?.measures || []; + const timeDimension = metricsViewSpec?.timeDimension; + const dimensions = metricsViewSpec?.dimensions || []; + + const randomMeasure = measures[Math.floor(Math.random() * measures.length)] + ?.name as string; + + let randomDimension = ""; + if (!timeDimension) { + randomDimension = dimensions[ + Math.floor(Math.random() * dimensions.length) + ]?.name as string; + } + + return { + metrics_view: metricsViewName, + x: { + type: timeDimension ? "temporal" : "nominal", + field: timeDimension || randomDimension, + sort: "-y", + limit: 20, + }, + y: { + type: "quantitative", + field: randomMeasure, + zeroBasedOrigin: true, + }, + }; + } +} diff --git a/web-common/src/features/canvas/components/charts/Chart.svelte b/web-common/src/features/canvas/components/charts/Chart.svelte index 8eb34e81204..79e1f43eeea 100644 --- a/web-common/src/features/canvas/components/charts/Chart.svelte +++ b/web-common/src/features/canvas/components/charts/Chart.svelte @@ -1,7 +1,7 @@ -{#if component instanceof ChartComponent} +{#if component instanceof BaseChart} {/if} diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index 826d462e547..aef42505a96 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -3,11 +3,11 @@ import InputLabel from "@rilldata/web-common/components/forms/InputLabel.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; + import type { CartesianChartComponent } from "@rilldata/web-common/features/canvas/components/charts/CartesianChart"; import type { ChartMetadata } from "@rilldata/web-common/features/canvas/components/charts/types"; import { chartMetadata } from "@rilldata/web-common/features/canvas/components/charts/util"; - import type { ChartComponent } from "../../components/charts"; - export let component: ChartComponent; + export let component: CartesianChartComponent; $: ({ chartType } = component); diff --git a/web-common/src/features/canvas/layout-util.ts b/web-common/src/features/canvas/layout-util.ts index 1150931650a..0bcf816c007 100644 --- a/web-common/src/features/canvas/layout-util.ts +++ b/web-common/src/features/canvas/layout-util.ts @@ -1,16 +1,16 @@ import type { - V1CanvasRow, V1CanvasItem, + V1CanvasRow, + V1ComponentSpecRendererProperties, V1MetricsViewSpec, V1ResolveCanvasResponseResolvedComponents, V1Resource, - V1ComponentSpecRendererProperties, } from "@rilldata/web-common/runtime-client"; +import { writable } from "svelte/store"; import { YAMLMap, YAMLSeq } from "yaml"; +import { ResourceKind } from "../entity-management/resource-selectors"; import type { CanvasComponentType } from "./components/types"; -import { writable } from "svelte/store"; import { COMPONENT_CLASS_MAP } from "./components/util"; -import { ResourceKind } from "../entity-management/resource-selectors"; export const initialHeights: Record = { line_chart: 320, From 4d3875530f553fb89a84581b8fad3a1c95d59882 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Fri, 11 Apr 2025 16:12:07 +0530 Subject: [PATCH 02/62] Move to index file --- .../canvas/components/charts/ChartFactory.ts | 19 ------------------- .../canvas/components/charts/index.ts | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 web-common/src/features/canvas/components/charts/ChartFactory.ts diff --git a/web-common/src/features/canvas/components/charts/ChartFactory.ts b/web-common/src/features/canvas/components/charts/ChartFactory.ts deleted file mode 100644 index 81398841cfe..00000000000 --- a/web-common/src/features/canvas/components/charts/ChartFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CartesianChartComponent } from "./CartesianChart"; -import type { ChartType } from "./types"; - -type ChartComponent = typeof CartesianChartComponent; - -export function createChart(type: ChartType): ChartComponent { - switch (type) { - case "bar_chart": - case "line_chart": - case "area_chart": - case "stacked_bar": - case "stacked_bar_normalized": - return CartesianChartComponent; - - // Add other chart types here as they are implemented - default: - throw new Error(`Unsupported chart type: ${type}`); - } -} diff --git a/web-common/src/features/canvas/components/charts/index.ts b/web-common/src/features/canvas/components/charts/index.ts index 0659e3e8d49..f8eab12452f 100644 --- a/web-common/src/features/canvas/components/charts/index.ts +++ b/web-common/src/features/canvas/components/charts/index.ts @@ -1 +1,20 @@ +import { CartesianChartComponent } from "./CartesianChart"; +import type { ChartType } from "./types"; + export { default as Chart } from "./Chart.svelte"; + +type ChartComponent = typeof CartesianChartComponent; + +export function createChart(type: ChartType): ChartComponent { + switch (type) { + case "bar_chart": + case "line_chart": + case "area_chart": + case "stacked_bar": + case "stacked_bar_normalized": + return CartesianChartComponent; + + default: + throw new Error(`Unsupported chart type: ${type}`); + } +} From e26dfef8fa3ef7a53d7e34477d947cff2be19016 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Sat, 12 Apr 2025 22:31:13 +0530 Subject: [PATCH 03/62] Use base chart reference --- .../features/canvas/components/charts/BaseChart.ts | 2 ++ .../canvas/components/charts/CartesianChart.ts | 2 -- .../features/canvas/components/charts/Chart.svelte | 5 +++-- .../src/features/canvas/components/charts/index.ts | 4 ++-- web-common/src/features/canvas/components/util.ts | 11 ++++++----- .../canvas/inspector/chart/ChartTypeSelector.svelte | 5 +++-- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 33b53a52c97..67d08ef5d0f 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -57,6 +57,8 @@ export abstract class BaseChart< return { options: { metrics_view: { type: "metrics", label: "Metrics view" }, + tooltip: { type: "tooltip", label: "Tooltip", showInUI: false }, + vl_config: { type: "config", showInUI: false }, ...this.getChartSpecificOptions(), ...commonOptions, }, diff --git a/web-common/src/features/canvas/components/charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/CartesianChart.ts index 2c2ef7daf45..b73c8f8f4bb 100644 --- a/web-common/src/features/canvas/components/charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/CartesianChart.ts @@ -22,8 +22,6 @@ export class CartesianChartComponent extends BaseChart { x: { type: "positional", label: "X-axis" }, y: { type: "positional", label: "Y-axis" }, color: { type: "mark", label: "Color", meta: { type: "color" } }, - tooltip: { type: "tooltip", label: "Tooltip", showInUI: false }, - vl_config: { type: "config", showInUI: false }, }; } diff --git a/web-common/src/features/canvas/components/charts/Chart.svelte b/web-common/src/features/canvas/components/charts/Chart.svelte index 79e1f43eeea..6ffadb4669a 100644 --- a/web-common/src/features/canvas/components/charts/Chart.svelte +++ b/web-common/src/features/canvas/components/charts/Chart.svelte @@ -1,7 +1,8 @@ @@ -105,9 +117,7 @@ data={{ "metrics-view": data }} {spec} renderer={isChartLineLike(chartType) ? "svg" : "canvas"} - expressionFunctions={{ - [measureName]: { fn: (val) => measureFormatter(val) }, - }} + {expressionFunctions} {config} /> {/if} diff --git a/web-common/src/features/canvas/components/charts/_to_delete.ts b/web-common/src/features/canvas/components/charts/_to_delete.ts deleted file mode 100644 index 1d40b2e7cf2..00000000000 --- a/web-common/src/features/canvas/components/charts/_to_delete.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { BaseCanvasComponent } from "@rilldata/web-common/features/canvas/components/BaseCanvasComponent"; -import type { - ChartConfig, - ChartType, -} from "@rilldata/web-common/features/canvas/components/charts/types"; -import { - commonOptions, - getFilterOptions, -} from "@rilldata/web-common/features/canvas/components/util"; -import type { InputParams } from "@rilldata/web-common/features/canvas/inspector/types"; -import { defaultPrimaryColors } from "@rilldata/web-common/features/themes/color-config"; -import type { - V1MetricsViewSpec, - V1Resource, -} from "@rilldata/web-common/runtime-client"; -import { get, writable, type Writable } from "svelte/store"; -import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity"; -import type { - ComponentCommonProperties, - ComponentFilterProperties, -} from "../types"; -import Chart from "./Chart.svelte"; - -export { default as Chart } from "./Chart.svelte"; - -export type ChartSpec = ComponentFilterProperties & - ComponentCommonProperties & - ChartConfig; - -export class ChartComponent extends BaseCanvasComponent { - minSize = { width: 4, height: 4 }; - defaultSize = { width: 6, height: 4 }; - resetParams = []; - type: ChartType; - chartType: Writable; - component = Chart; - - constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { - const defaultSpec: ChartSpec = { - metrics_view: "", - title: "", - description: "", - }; - - super(resource, parent, path, defaultSpec); - - this.type = resource.component?.state?.validSpec?.renderer as ChartType; - this.chartType = writable(this.type); - } - - isValid(spec: ChartSpec): boolean { - return typeof spec.metrics_view === "string" && Boolean(spec.x || spec.y); - } - - inputParams(): InputParams { - return { - options: { - metrics_view: { type: "metrics", label: "Metrics view" }, - x: { type: "positional", label: "X-axis" }, - y: { type: "positional", label: "Y-axis" }, - color: { type: "mark", label: "Color", meta: { type: "color" } }, - tooltip: { type: "tooltip", label: "Tooltip", showInUI: false }, - vl_config: { type: "config", showInUI: false }, - ...commonOptions, - }, - filter: getFilterOptions(false), - }; - } - - static newComponentSpec( - metricsViewName: string, - metricsViewSpec: V1MetricsViewSpec | undefined, - ): ChartSpec { - // Randomly select a measure and dimension if available - const measures = metricsViewSpec?.measures || []; - - const timeDimension = metricsViewSpec?.timeDimension; - const dimensions = metricsViewSpec?.dimensions || []; - - const randomMeasure = measures[Math.floor(Math.random() * measures.length)] - ?.name as string; - - let randomDimension = ""; - if (!timeDimension) { - randomDimension = dimensions[ - Math.floor(Math.random() * dimensions.length) - ]?.name as string; - } - - const spec: ChartSpec = { - metrics_view: metricsViewName, - x: { - type: timeDimension ? "temporal" : "nominal", - field: timeDimension || randomDimension, - sort: "-y", - limit: 20, - }, - y: { - type: "quantitative", - field: randomMeasure, - zeroBasedOrigin: true, - }, - color: `hsl(${defaultPrimaryColors[500].split(" ").join(",")})`, - }; - - return spec; - } - - updateChartType(key: ChartType) { - if (!this.parent.fileArtifact) return; - const currentSpec = get(this.specStore); - - const parentPath = this.pathInYAML.slice(0, -1); - - this.chartType.set(key); - - const parseDocumentStore = this.parent.parsedContent; - const parsedDocument = get(parseDocumentStore); - - const { updateEditorContent } = this.parent.fileArtifact; - - const width = parsedDocument.getIn([...parentPath, "width"]); - - parsedDocument.setIn(parentPath, { [key]: currentSpec, width }); - - updateEditorContent(parsedDocument.toString(), false, true); - } -} diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index 5d6444a787f..701450e1f20 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -1,14 +1,30 @@ -import type { FieldConfig } from "@rilldata/web-common/features/canvas/components/charts/types"; import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; -import type { - V1MetricsViewSpec, - V1Resource, +import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; +import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; +import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; +import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; +import { + createQueryServiceMetricsViewAggregation, + type V1Expression, + type V1MetricsViewAggregationDimension, + type V1MetricsViewAggregationMeasure, + type V1MetricsViewAggregationResponse, + type V1MetricsViewAggregationSort, + type V1MetricsViewSpec, + type V1Resource, } from "@rilldata/web-common/runtime-client"; +import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; +import { + keepPreviousData, + type CreateQueryResult, +} from "@tanstack/svelte-query"; +import { derived, get, readable, type Readable } from "svelte/store"; import type { CanvasEntity, ComponentPath, } from "../../../stores/canvas-entity"; import { BaseChart, type BaseChartConfig } from "../BaseChart"; +import type { ChartDataQuery, ChartSortDirection, FieldConfig } from "../types"; export type CartesianChartSpec = BaseChartConfig & { x?: FieldConfig; @@ -29,6 +45,138 @@ export class CartesianChartComponent extends BaseChart { }; } + createChartDataQuery( + ctx: CanvasStore, + timeAndFilterStore: Readable, + ): ChartDataQuery { + const config = get(this.specStore); + + let measures: V1MetricsViewAggregationMeasure[] = []; + let dimensions: V1MetricsViewAggregationDimension[] = []; + + if (config.y?.type === "quantitative" && config.y?.field) { + measures = [{ name: config.y?.field }]; + } + + let sort: V1MetricsViewAggregationSort | undefined; + let limit: number | undefined; + let hasColorDimension = false; + + return derived( + [ctx.runtime, timeAndFilterStore], + ([runtime, $timeAndFilterStore], set) => { + const { timeRange, where, timeGrain } = $timeAndFilterStore; + + let outerWhere = where; + + if (config.x?.type === "nominal" && config.x?.field) { + limit = config.x.limit; + sort = this.vegaSortToAggregationSort(config.x?.sort, config); + dimensions = [{ name: config.x?.field }]; + + const showNull = !!config.x.showNull; + if (!showNull) { + const excludeNullFilter = createInExpression( + config.x?.field, + [null], + true, + ); + outerWhere = mergeFilters(where, excludeNullFilter); + } + } else if (config.x?.type === "temporal" && timeGrain) { + dimensions = [{ name: config.x?.field, timeGrain }]; + } + + if (typeof config.color === "object" && config.color?.field) { + dimensions = [...dimensions, { name: config.color.field }]; + hasColorDimension = true; + } + + let topNQuery: + | Readable + | CreateQueryResult = + readable(null); + + const enabled = !!timeRange?.start && !!timeRange?.end; + + if (limit && hasColorDimension) { + topNQuery = createQueryServiceMetricsViewAggregation( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions: [{ name: config.x?.field }], + sort: sort ? [sort] : undefined, + where: outerWhere, + timeRange, + limit: limit.toString(), + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ctx.queryClient, + ); + } + + return derived(topNQuery, ($topNQuery, topNSet) => { + if ($topNQuery !== null && !$topNQuery?.data) { + return topNSet({ + isFetching: $topNQuery.isFetching, + error: $topNQuery.error, + data: undefined, + }); + } + + const dimensionName = config.x?.field; + + let combinedWhere: V1Expression | undefined = outerWhere; + if ($topNQuery?.data?.data?.length && dimensionName) { + const topValues = $topNQuery?.data?.data.map( + (d) => d[dimensionName] as string, + ); + const filterForTopValues = createInExpression( + dimensionName, + topValues, + ); + + combinedWhere = mergeFilters(where, filterForTopValues); + } + + const dataQuery = createQueryServiceMetricsViewAggregation( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions, + sort: sort ? [sort] : undefined, + where: combinedWhere, + timeRange, + limit: hasColorDimension || !limit ? "5000" : limit.toString(), + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ctx.queryClient, + ); + + return derived(dataQuery, ($dataQuery) => { + return { + isFetching: $dataQuery.isFetching, + error: $dataQuery.error, + data: $dataQuery?.data?.data, + }; + }).subscribe(topNSet); + }).subscribe(set); + }, + ); + } + static newComponentSpec( metricsViewName: string, metricsViewSpec: V1MetricsViewSpec | undefined, @@ -63,4 +211,19 @@ export class CartesianChartComponent extends BaseChart { }, }; } + + protected vegaSortToAggregationSort( + sort: ChartSortDirection | undefined, + config: CartesianChartSpec, + ): V1MetricsViewAggregationSort | undefined { + if (!sort) return undefined; + const field = + sort === "x" || sort === "-x" ? config.x?.field : config.y?.field; + if (!field) return undefined; + + return { + name: field, + desc: sort === "-x" || sort === "-y", + }; + } } diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index 4bad17f817b..51a135c175c 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -16,7 +16,7 @@ type CircularChartEncoding = { }; export type CircularChartSpec = BaseChartConfig & CircularChartEncoding; -export class CartesianChartComponent extends BaseChart { +export class CircularChartComponent extends BaseChart { constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { super(resource, parent, path); } diff --git a/web-common/src/features/canvas/components/charts/index.ts b/web-common/src/features/canvas/components/charts/index.ts index b649d92d3e7..127d5d65c6f 100644 --- a/web-common/src/features/canvas/components/charts/index.ts +++ b/web-common/src/features/canvas/components/charts/index.ts @@ -9,11 +9,11 @@ export type ChartComponent = typeof CartesianChartComponent; export type ChartSpec = CartesianChartSpec | CircularChartSpec; export type ChartType = - | "line_chart" | "bar_chart" + | "line_chart" + | "area_chart" | "stacked_bar" - | "stacked_bar_normalized" - | "area_chart"; + | "stacked_bar_normalized"; export function getChartComponent(type: ChartType): ChartComponent { switch (type) { diff --git a/web-common/src/features/canvas/components/charts/query.ts b/web-common/src/features/canvas/components/charts/query.ts deleted file mode 100644 index d262e4cdc4c..00000000000 --- a/web-common/src/features/canvas/components/charts/query.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; -import type { ChartSortDirection } from "@rilldata/web-common/features/canvas/components/charts/types"; -import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; -import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; -import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; -import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; -import { - createQueryServiceMetricsViewAggregation, - type V1Expression, - type V1MetricsViewAggregationDimension, - type V1MetricsViewAggregationMeasure, - type V1MetricsViewAggregationResponse, - type V1MetricsViewAggregationResponseDataItem, - type V1MetricsViewAggregationSort, -} from "@rilldata/web-common/runtime-client"; -import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; -import { - keepPreviousData, - type CreateQueryResult, -} from "@tanstack/svelte-query"; -import { derived, readable, type Readable } from "svelte/store"; - -export function createChartDataQuery( - ctx: CanvasStore, - config: CartesianChartSpec, - timeAndFilterStore: Readable, -): Readable<{ - isFetching: boolean; - error: HTTPError | null; - data: V1MetricsViewAggregationResponseDataItem[] | undefined; -}> { - let measures: V1MetricsViewAggregationMeasure[] = []; - let dimensions: V1MetricsViewAggregationDimension[] = []; - - if (config.y?.type === "quantitative" && config.y?.field) { - measures = [{ name: config.y?.field }]; - } - - let sort: V1MetricsViewAggregationSort | undefined; - let limit: number | undefined; - let hasColorDimension = false; - - return derived( - [ctx.runtime, timeAndFilterStore], - ([runtime, $timeAndFilterStore], set) => { - const { timeRange, where, timeGrain } = $timeAndFilterStore; - - let outerWhere = where; - - if (config.x?.type === "nominal" && config.x?.field) { - limit = config.x.limit; - sort = vegaSortToAggregationSort(config.x?.sort, config); - dimensions = [{ name: config.x?.field }]; - - const showNull = !!config.x.showNull; - if (!showNull) { - const excludeNullFilter = createInExpression( - config.x?.field, - [null], - true, - ); - outerWhere = mergeFilters(where, excludeNullFilter); - } - } else if (config.x?.type === "temporal" && timeGrain) { - dimensions = [{ name: config.x?.field, timeGrain }]; - } - - if (typeof config.color === "object" && config.color?.field) { - dimensions = [...dimensions, { name: config.color.field }]; - hasColorDimension = true; - } - - let topNQuery: - | Readable - | CreateQueryResult = - readable(null); - - const enabled = !!timeRange?.start && !!timeRange?.end; - - if (limit && hasColorDimension) { - topNQuery = createQueryServiceMetricsViewAggregation( - runtime.instanceId, - config.metrics_view, - { - measures, - dimensions: [{ name: config.x?.field }], - sort: sort ? [sort] : undefined, - where: outerWhere, - timeRange, - limit: limit.toString(), - }, - { - query: { - enabled, - placeholderData: keepPreviousData, - }, - }, - ctx.queryClient, - ); - } - - return derived(topNQuery, ($topNQuery, topNSet) => { - if ($topNQuery !== null && !$topNQuery?.data) { - return topNSet({ - isFetching: $topNQuery.isFetching, - error: $topNQuery.error, - data: undefined, - }); - } - - const dimensionName = config.x?.field; - - let combinedWhere: V1Expression | undefined = outerWhere; - if ($topNQuery?.data?.data?.length && dimensionName) { - const topValues = $topNQuery?.data?.data.map( - (d) => d[dimensionName] as string, - ); - const filterForTopValues = createInExpression( - dimensionName, - topValues, - ); - - combinedWhere = mergeFilters(where, filterForTopValues); - } - - const dataQuery = createQueryServiceMetricsViewAggregation( - runtime.instanceId, - config.metrics_view, - { - measures, - dimensions, - sort: sort ? [sort] : undefined, - where: combinedWhere, - timeRange, - limit: hasColorDimension || !limit ? "5000" : limit.toString(), - }, - { - query: { - enabled, - placeholderData: keepPreviousData, - }, - }, - ctx.queryClient, - ); - - return derived(dataQuery, ($dataQuery) => { - return { - isFetching: $dataQuery.isFetching, - error: $dataQuery.error, - data: $dataQuery?.data?.data, - }; - }).subscribe(topNSet); - }).subscribe(set); - }, - ); -} - -function vegaSortToAggregationSort( - sort: ChartSortDirection | undefined, - config: CartesianChartSpec, -): V1MetricsViewAggregationSort | undefined { - if (!sort) return undefined; - const field = - sort === "x" || sort === "-x" ? config.x?.field : config.y?.field; - if (!field) return undefined; - - return { - name: field, - desc: sort === "-x" || sort === "-y", - }; -} diff --git a/web-common/src/features/canvas/components/charts/selector.ts b/web-common/src/features/canvas/components/charts/selector.ts index 277273dd9f5..95ccfa69682 100644 --- a/web-common/src/features/canvas/components/charts/selector.ts +++ b/web-common/src/features/canvas/components/charts/selector.ts @@ -1,8 +1,5 @@ -import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; -import { - validateDimensions, - validateMeasures, -} from "@rilldata/web-common/features/canvas/components/validators"; +import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; +import type { BaseChart } from "@rilldata/web-common/features/canvas/components/charts/BaseChart"; import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; import { TIME_GRAIN } from "@rilldata/web-common/lib/time/config"; @@ -13,8 +10,7 @@ import { } from "@rilldata/web-common/runtime-client"; import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; import { derived, type Readable } from "svelte/store"; -import { createChartDataQuery } from "./query"; -import { timeGrainToVegaTimeUnitMap } from "./util"; +import { getFieldsByType, timeGrainToVegaTimeUnitMap } from "./util"; export type ChartDataResult = { data: V1MetricsViewAggregationResponseDataItem[]; @@ -38,40 +34,42 @@ export interface TimeDimensionDefinition { export function getChartData( ctx: CanvasStore, - config: CartesianChartSpec, + component: BaseChart, + config: ChartSpec, timeAndFilterStore: Readable, ): Readable { - const chartDataQuery = createChartDataQuery(ctx, config, timeAndFilterStore); + const chartDataQuery = component.createChartDataQuery( + ctx, + timeAndFilterStore, + ); const { spec } = ctx.canvasEntity; - const fields: { name: string; type: "measure" | "dimension" | "time" }[] = []; - if (config.y?.field) fields.push({ name: config.y.field, type: "measure" }); - if (config.x?.field) - fields.push({ - name: config.x.field, - type: config.x.type === "temporal" ? "time" : "dimension", - }); - if (typeof config.color === "object" && config.color?.field) { - fields.push({ name: config.color.field, type: "dimension" }); - } + const { measures, dimensions, timeDimensions } = getFieldsByType(config); + + // Combine all fields with their types + const allFields = [ + ...measures.map((field) => ({ field, type: "measure" })), + ...dimensions.map((field) => ({ field, type: "dimension" })), + ...timeDimensions.map((field) => ({ field, type: "time" })), + ]; // Match each field to its corresponding measure or dimension spec. - const fieldReadableMap = fields.map((field) => { + const fieldReadableMap = allFields.map((field) => { if (field.type === "measure") { - return spec.getMeasureForMetricView(field.name, config.metrics_view); + return spec.getMeasureForMetricView(field.field, config.metrics_view); } else if (field.type === "dimension") { - return spec.getDimensionForMetricView(field.name, config.metrics_view); + return spec.getDimensionForMetricView(field.field, config.metrics_view); } else { - return getTimeDimensionDefinition(field.name, timeAndFilterStore); + return getTimeDimensionDefinition(field.field, timeAndFilterStore); } }); return derived( [chartDataQuery, ...fieldReadableMap], ([chartData, ...fieldMap]) => { - const fieldSpecMap = fields.reduce( + const fieldSpecMap = allFields.reduce( (acc, field, index) => { - acc[field.name] = fieldMap?.[index]; + acc[field.field] = fieldMap?.[index]; return acc; }, {} as Record< @@ -116,67 +114,3 @@ export function getTimeDimensionDefinition( }; }); } - -export function validateChartSchema( - ctx: CanvasStore, - chartSpec: CartesianChartSpec, -): Readable<{ - isValid: boolean; - error?: string; - isLoading?: boolean; -}> { - const { metrics_view, x, y, color } = chartSpec; - let measures: string[] = []; - let dimensions: string[] = []; - - if (y?.field) measures = [y.field]; - if (typeof color === "object" && color?.field) - dimensions = [...dimensions, color.field]; - - return derived( - ctx.canvasEntity.spec.getMetricsViewFromName(metrics_view), - (metricsViewQuery) => { - if (metricsViewQuery.isLoading) { - return { - isValid: true, - isLoading: true, - }; - } - const metricsView = metricsViewQuery.metricsView; - if (!metricsView) { - return { - isValid: false, - error: `Metrics view ${metrics_view} not found`, - }; - } - - const timeDimension = metricsView.timeDimension; - if (x?.field && x.field !== timeDimension) dimensions = [x.field]; - - const validateMeasuresRes = validateMeasures(metricsView, measures); - if (!validateMeasuresRes.isValid) { - const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", "); - return { - isValid: false, - error: `Invalid measure ${invalidMeasures} selected`, - }; - } - - const validateDimensionsRes = validateDimensions(metricsView, dimensions); - - if (!validateDimensionsRes.isValid) { - const invalidDimensions = - validateDimensionsRes.invalidDimensions.join(", "); - - return { - isValid: false, - error: `Invalid dimension(s) ${invalidDimensions} selected`, - }; - } - return { - isValid: true, - error: undefined, - }; - }, - ); -} diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index 05e561aead4..bc5d9fe2900 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -1,6 +1,21 @@ +import type { + V1Expression, + V1MetricsViewAggregationDimension, + V1MetricsViewAggregationMeasure, + V1MetricsViewAggregationSort, +} from "@rilldata/web-common/runtime-client"; +import { type V1MetricsViewAggregationResponseDataItem } from "@rilldata/web-common/runtime-client"; +import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; import type { ComponentType, SvelteComponent } from "svelte"; +import { type Readable } from "svelte/store"; import type { ChartType } from "./"; +export type ChartDataQuery = Readable<{ + isFetching: boolean; + error: HTTPError | null; + data: V1MetricsViewAggregationResponseDataItem[] | undefined; +}>; + export type ChartSortDirection = "x" | "y" | "-x" | "-y"; export interface FieldConfig { @@ -43,3 +58,11 @@ export interface TooltipValue { formatType?: string; type: "quantitative" | "ordinal" | "temporal" | "nominal"; } + +export interface ChartQueryConfig { + measures: V1MetricsViewAggregationMeasure[]; + dimensions: V1MetricsViewAggregationDimension[]; + sort?: V1MetricsViewAggregationSort[]; + where?: V1Expression; + limit?: string; +} diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 8ba5375c6f2..f5736fe99c7 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -8,7 +8,7 @@ import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/ch import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; import type { Config } from "vega-lite"; -import type { ChartType } from "./"; +import type { ChartSpec, ChartType } from "./"; import { generateVLAreaChartSpec } from "./cartesian-charts/area/spec"; import { generateVLBarChartSpec } from "./cartesian-charts/bar-chart/spec"; import type { CartesianChartSpec } from "./cartesian-charts/CartesianChart"; @@ -124,9 +124,53 @@ export function sanitizeFieldName(fieldName: string) { return `rill_${sanitizedFieldName}`; } -// export function getMeasureForMetricView( -// spec: Record, -// metricsView: MetricsView, -// ) { -// return metricsView.measures.find((measure) => measure.name === yField); -// } +export interface FieldsByType { + measures: string[]; + dimensions: string[]; + timeDimensions: string[]; +} + +export function getFieldsByType(spec: ChartSpec): FieldsByType { + const measures: string[] = []; + const dimensions: string[] = []; + const timeDimensions: string[] = []; + + // Recursively check all properties for FieldConfig objects + const checkFields = (obj: unknown): void => { + if (!obj || typeof obj !== "object") { + return; + } + + // Check if current object is a FieldConfig with type and field + if ("type" in obj && "field" in obj && typeof obj.field === "string") { + const type = obj.type as string; + const field = obj.field; + + switch (type) { + case "quantitative": + measures.push(field); + break; + case "nominal": + dimensions.push(field); + break; + case "temporal": + timeDimensions.push(field); + break; + } + return; + } + + Object.values(obj).forEach((value) => { + if (typeof value === "object" && value !== null) { + checkFields(value); + } + }); + }; + + checkFields(spec); + return { + measures, + dimensions, + timeDimensions, + }; +} diff --git a/web-common/src/features/canvas/components/charts/validate.ts b/web-common/src/features/canvas/components/charts/validate.ts new file mode 100644 index 00000000000..9afa53ce6d1 --- /dev/null +++ b/web-common/src/features/canvas/components/charts/validate.ts @@ -0,0 +1,74 @@ +import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; +import { getFieldsByType } from "@rilldata/web-common/features/canvas/components/charts/util"; +import { + validateDimensions, + validateMeasures, +} from "@rilldata/web-common/features/canvas/components/validators"; +import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; +import { derived, type Readable } from "svelte/store"; + +export function validateChartSchema( + ctx: CanvasStore, + chartSpec: ChartSpec, +): Readable<{ + isValid: boolean; + error?: string; + isLoading?: boolean; +}> { + const { metrics_view } = chartSpec; + + const { measures, dimensions, timeDimensions } = getFieldsByType(chartSpec); + + return derived( + ctx.canvasEntity.spec.getMetricsViewFromName(metrics_view), + (metricsViewQuery) => { + if (metricsViewQuery.isLoading) { + return { + isValid: true, + isLoading: true, + }; + } + const metricsView = metricsViewQuery.metricsView; + if (!metricsView) { + return { + isValid: false, + error: `Metrics view ${metrics_view} not found`, + }; + } + + const timeDimension = metricsView.timeDimension; + + if (timeDimensions.length > 0 && timeDimension !== timeDimensions[0]) { + return { + isValid: false, + error: `Invalid time dimension ${timeDimension} selected`, + }; + } + + const validateMeasuresRes = validateMeasures(metricsView, measures); + if (!validateMeasuresRes.isValid) { + const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", "); + return { + isValid: false, + error: `Invalid measure ${invalidMeasures} selected`, + }; + } + + const validateDimensionsRes = validateDimensions(metricsView, dimensions); + + if (!validateDimensionsRes.isValid) { + const invalidDimensions = + validateDimensionsRes.invalidDimensions.join(", "); + + return { + isValid: false, + error: `Invalid dimension(s) ${invalidDimensions} selected`, + }; + } + return { + isValid: true, + error: undefined, + }; + }, + ); +} diff --git a/web-common/src/features/canvas/components/types.ts b/web-common/src/features/canvas/components/types.ts index ebab2168bf2..16120e95e78 100644 --- a/web-common/src/features/canvas/components/types.ts +++ b/web-common/src/features/canvas/components/types.ts @@ -1,7 +1,7 @@ import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; import type { CircularChartSpec } from "@rilldata/web-common/features/canvas/components/charts/circular-charts/CircularChart"; import type { KPIGridSpec } from "@rilldata/web-common/features/canvas/components/kpi-grid"; -import type { ChartType } from "./charts/types"; +import type { ChartType } from "./charts"; import type { ImageSpec } from "./image"; import type { KPISpec } from "./kpi"; import type { LeaderboardSpec } from "./leaderboard"; diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index 1ce8f2c2e51..7b986b1f9e1 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -3,12 +3,12 @@ import InputLabel from "@rilldata/web-common/components/forms/InputLabel.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; + import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; import type { BaseChart } from "@rilldata/web-common/features/canvas/components/charts/BaseChart"; - import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; import type { ChartMetadata } from "@rilldata/web-common/features/canvas/components/charts/types"; import { chartMetadata } from "@rilldata/web-common/features/canvas/components/charts/util"; - export let component: BaseChart; + export let component: BaseChart; $: ({ chartType } = component); From b0cce256716e30c4a50b3514c2db485ae8450eb7 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Mon, 14 Apr 2025 21:15:18 +0530 Subject: [PATCH 08/62] Add pie chart component --- .../charts/cartesian-charts/CartesianChart.ts | 2 +- .../charts/circular-charts/CircularChart.ts | 65 +++++++++++++++++++ .../canvas/components/charts/index.ts | 15 +++-- .../features/canvas/components/charts/util.ts | 7 +- .../src/features/canvas/components/util.ts | 3 + web-common/src/features/canvas/layout-util.ts | 1 + 6 files changed, 85 insertions(+), 8 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index 701450e1f20..d9316599f27 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -212,7 +212,7 @@ export class CartesianChartComponent extends BaseChart { }; } - protected vegaSortToAggregationSort( + private vegaSortToAggregationSort( sort: ChartSortDirection | undefined, config: CartesianChartSpec, ): V1MetricsViewAggregationSort | undefined { diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index 51a135c175c..8655a06b4dd 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -1,14 +1,24 @@ import type { FieldConfig } from "@rilldata/web-common/features/canvas/components/charts/types"; import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; +import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; +import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; import type { V1MetricsViewSpec, V1Resource, } from "@rilldata/web-common/runtime-client"; +import { + createQueryServiceMetricsViewAggregation, + type V1MetricsViewAggregationDimension, + type V1MetricsViewAggregationMeasure, +} from "@rilldata/web-common/runtime-client"; +import { keepPreviousData } from "@tanstack/svelte-query"; +import { derived, get, type Readable } from "svelte/store"; import type { CanvasEntity, ComponentPath, } from "../../../stores/canvas-entity"; import { BaseChart, type BaseChartConfig } from "../BaseChart"; +import type { ChartDataQuery } from "../types"; type CircularChartEncoding = { measure?: FieldConfig; @@ -28,6 +38,61 @@ export class CircularChartComponent extends BaseChart { }; } + createChartDataQuery( + ctx: CanvasStore, + timeAndFilterStore: Readable, + ): ChartDataQuery { + const config = get(this.specStore); + + let measures: V1MetricsViewAggregationMeasure[] = []; + let dimensions: V1MetricsViewAggregationDimension[] = []; + + if (config.measure?.field) { + measures = [{ name: config.measure.field }]; + } + + let limit: number; + if (config.color?.field) { + limit = config.color.limit ?? 20; + dimensions = [{ name: config.color.field }]; + } + + return derived( + [ctx.runtime, timeAndFilterStore], + ([runtime, $timeAndFilterStore], set) => { + const { timeRange, where } = $timeAndFilterStore; + const enabled = !!timeRange?.start && !!timeRange?.end; + + const dataQuery = createQueryServiceMetricsViewAggregation( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions, + where, + timeRange, + limit: limit.toString(), + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ctx.queryClient, + ); + + return derived(dataQuery, ($dataQuery) => { + return { + isFetching: $dataQuery.isFetching, + error: $dataQuery.error, + data: $dataQuery?.data?.data, + }; + }).subscribe(set); + }, + ); + } + static newComponentSpec( metricsViewName: string, metricsViewSpec: V1MetricsViewSpec | undefined, diff --git a/web-common/src/features/canvas/components/charts/index.ts b/web-common/src/features/canvas/components/charts/index.ts index 127d5d65c6f..b0598039b83 100644 --- a/web-common/src/features/canvas/components/charts/index.ts +++ b/web-common/src/features/canvas/components/charts/index.ts @@ -1,10 +1,15 @@ import type { CartesianChartSpec } from "./cartesian-charts/CartesianChart"; import { CartesianChartComponent } from "./cartesian-charts/CartesianChart"; -import type { CircularChartSpec } from "./circular-charts/CircularChart"; +import { + CircularChartComponent, + type CircularChartSpec, +} from "./circular-charts/CircularChart"; export { default as Chart } from "./Chart.svelte"; -export type ChartComponent = typeof CartesianChartComponent; +export type ChartComponent = + | typeof CartesianChartComponent + | typeof CircularChartComponent; export type ChartSpec = CartesianChartSpec | CircularChartSpec; @@ -13,7 +18,8 @@ export type ChartType = | "line_chart" | "area_chart" | "stacked_bar" - | "stacked_bar_normalized"; + | "stacked_bar_normalized" + | "pie_chart"; export function getChartComponent(type: ChartType): ChartComponent { switch (type) { @@ -23,7 +29,8 @@ export function getChartComponent(type: ChartType): ChartComponent { case "stacked_bar": case "stacked_bar_normalized": return CartesianChartComponent; - + case "pie_chart": + return CircularChartComponent; default: throw new Error(`Unsupported chart type: ${type}`); } diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index f5736fe99c7..fbdf3559e22 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -15,12 +15,13 @@ import type { CartesianChartSpec } from "./cartesian-charts/CartesianChart"; import { generateVLLineChartSpec } from "./cartesian-charts/line-chart/spec"; import { generateVLStackedBarChartSpec } from "./cartesian-charts/stacked-bar/default"; import { generateVLStackedBarNormalizedSpec } from "./cartesian-charts/stacked-bar/normalized"; +import { generateVLPieChartSpec } from "./circular-charts/pie"; import type { ChartDataResult } from "./selector"; import type { ChartMetadata } from "./types"; export function generateSpec( chartType: ChartType, - rillChartSpec: CartesianChartSpec, + rillChartSpec: ChartSpec, data: ChartDataResult, ) { if (data.isFetching || data.error) return {}; @@ -35,8 +36,8 @@ export function generateSpec( return generateVLLineChartSpec(rillChartSpec, data); case "area_chart": return generateVLAreaChartSpec(rillChartSpec, data); - // case "pie_chart": - // return generateVLPieChartSpec(rillChartSpec, data); + case "pie_chart": + return generateVLPieChartSpec(rillChartSpec, data); } } diff --git a/web-common/src/features/canvas/components/util.ts b/web-common/src/features/canvas/components/util.ts index d2b0be3da03..c2b1edfe888 100644 --- a/web-common/src/features/canvas/components/util.ts +++ b/web-common/src/features/canvas/components/util.ts @@ -66,6 +66,7 @@ const CHART_TYPES = [ "stacked_bar", "stacked_bar_normalized", "area_chart", + "pie_chart", ] as const; const NON_CHART_TYPES = [ "markdown", @@ -111,6 +112,7 @@ export const COMPONENT_CLASS_MAP: Record< line_chart: getChartComponent("line_chart"), stacked_bar: getChartComponent("stacked_bar"), stacked_bar_normalized: getChartComponent("stacked_bar_normalized"), + pie_chart: getChartComponent("pie_chart"), area_chart: getChartComponent("area_chart"), } as const; @@ -127,6 +129,7 @@ const DISPLAY_MAP: Record = { stacked_bar: "Chart", stacked_bar_normalized: "Chart", area_chart: "Chart", + pie_chart: "Chart", } as const; export function createComponent( diff --git a/web-common/src/features/canvas/layout-util.ts b/web-common/src/features/canvas/layout-util.ts index 0a338dc44c1..e433ac177a9 100644 --- a/web-common/src/features/canvas/layout-util.ts +++ b/web-common/src/features/canvas/layout-util.ts @@ -18,6 +18,7 @@ export const initialHeights: Record = { area_chart: 320, stacked_bar: 320, stacked_bar_normalized: 320, + pie_chart: 320, markdown: 40, kpi_grid: 128, image: 80, From 562fb97e5d2a04234caf9685d8fb86161bdffc3c Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Tue, 15 Apr 2025 16:16:12 +0530 Subject: [PATCH 09/62] Reorganize types and title --- .../canvas/AddComponentDropdown.svelte | 4 +- .../canvas/components/charts/BaseChart.ts | 3 ++ .../canvas/components/charts/Chart.svelte | 3 +- .../canvas/components/charts/builder.ts | 2 +- .../charts/cartesian-charts/CartesianChart.ts | 25 +++++++++++- .../charts/cartesian-charts/area/spec.ts | 2 +- .../charts/cartesian-charts/bar-chart/spec.ts | 2 +- .../cartesian-charts/line-chart/spec.ts | 2 +- .../cartesian-charts/stacked-bar/default.ts | 2 +- .../stacked-bar/normalized.ts | 2 +- .../charts/circular-charts/CircularChart.ts | 18 ++++++++- .../components/charts/circular-charts/pie.ts | 2 +- .../canvas/components/charts/index.ts | 5 ++- .../canvas/components/charts/selector.ts | 23 +---------- .../canvas/components/charts/types.ts | 27 ++++++++++++- .../features/canvas/components/charts/util.ts | 28 +------------ .../src/features/canvas/components/util.ts | 39 ++++++++++--------- 17 files changed, 107 insertions(+), 82 deletions(-) diff --git a/web-common/src/features/canvas/AddComponentDropdown.svelte b/web-common/src/features/canvas/AddComponentDropdown.svelte index 2d2af3e26f1..5bcf6b82230 100644 --- a/web-common/src/features/canvas/AddComponentDropdown.svelte +++ b/web-common/src/features/canvas/AddComponentDropdown.svelte @@ -1,9 +1,9 @@ @@ -36,61 +44,67 @@
- {isDimension ? "X-axis" : "Y-axis"} Configuration + {label} Configuration
-
- Show axis title - { - onChange("showAxisTitle", !fieldConfig?.showAxisTitle); - }} - /> -
- {#if isDimension && !isTemporal} + {#if showAxisTitle}
- Show null values + Show axis title { - onChange("showNull", !fieldConfig?.showNull); - }} - /> -
-
- Sort - { - onChange("limit", limit); - }} - onEnter={() => { - onChange("limit", limit); + onChange("showAxisTitle", !fieldConfig?.showAxisTitle); }} />
{/if} - {#if !isDimension} + {#if isDimension} + {#if showNull} +
+ Show null values + { + onChange("showNull", !fieldConfig?.showNull); + }} + /> +
+ {/if} + {#if showSort} +
+ Sort + { + onChange("limit", limit); + }} + onEnter={() => { + onChange("limit", limit); + }} + /> +
+ {/if} + {/if} + {#if isMeasure && showOrigin}
Zero based origin
- + {#if Object.keys(config.meta?.chartFieldInput ?? {}).length > 1} + + {/if}
Date: Tue, 15 Apr 2025 17:37:31 +0530 Subject: [PATCH 11/62] By default hide nulls --- .../charts/circular-charts/CircularChart.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index e9e041def0c..0319f13dc73 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -5,6 +5,8 @@ import type { import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; +import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; +import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import type { V1MetricsViewSpec, V1Resource, @@ -73,9 +75,11 @@ export class CircularChartComponent extends BaseChart { } let limit: number; + let showNull = false; if (config.color?.field) { limit = config.color.limit ?? 20; dimensions = [{ name: config.color.field }]; + showNull = !!config.color.showNull; } return derived( @@ -84,13 +88,23 @@ export class CircularChartComponent extends BaseChart { const { timeRange, where } = $timeAndFilterStore; const enabled = !!timeRange?.start && !!timeRange?.end; + let mergedWhere = where; + if (!showNull && config.color?.field) { + const excludeNullFilter = createInExpression( + config.color?.field, + [null], + true, + ); + mergedWhere = mergeFilters(where, excludeNullFilter); + } + const dataQuery = createQueryServiceMetricsViewAggregation( runtime.instanceId, config.metrics_view, { measures, dimensions, - where, + where: mergedWhere, timeRange, limit: limit.toString(), }, From 5fafd67900521e5e3b6166316408318991db12b9 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Tue, 15 Apr 2025 21:01:49 +0530 Subject: [PATCH 12/62] Add icon for donut --- web-common/src/components/icons/Donut.svelte | 26 +++++++++++++++++++ .../components/charts/circular-charts/pie.ts | 4 +++ .../features/canvas/components/charts/util.ts | 4 +-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 web-common/src/components/icons/Donut.svelte diff --git a/web-common/src/components/icons/Donut.svelte b/web-common/src/components/icons/Donut.svelte new file mode 100644 index 00000000000..c6fe11cb8d2 --- /dev/null +++ b/web-common/src/components/icons/Donut.svelte @@ -0,0 +1,26 @@ + + + + + + diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index 3d91ff6fe5d..403599640d5 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -14,6 +14,10 @@ export function generateVLPieChartSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("arc"); + spec.mark = { + type: "arc", + innerRadius: 30, + }; const theta = createXEncoding(config.measure, data); const color = createColorEncoding(config.color, data); const tooltip = createDefaultTooltipEncoding( diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 7c2ead2af04..fbd59091bdb 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,4 +1,5 @@ import BarChart from "@rilldata/web-common/components/icons/BarChart.svelte"; +import Donut from "@rilldata/web-common/components/icons/Donut.svelte"; import LineChart from "@rilldata/web-common/components/icons/LineChart.svelte"; import StackedArea from "@rilldata/web-common/components/icons/StackedArea.svelte"; import StackedBar from "@rilldata/web-common/components/icons/StackedBar.svelte"; @@ -7,7 +8,6 @@ import { getRillTheme } from "@rilldata/web-common/components/vega/vega-config"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; -import { PieChart } from "lucide-svelte"; import type { Config } from "vega-lite"; import type { ChartSpec, ChartType } from "./"; import { generateVLAreaChartSpec } from "./cartesian-charts/area/spec"; @@ -50,7 +50,7 @@ export const chartMetadata: ChartMetadata[] = [ icon: StackedBarFull, }, { type: "area_chart", title: "Stacked Area", icon: StackedArea }, - { type: "pie_chart", title: "Pie", icon: PieChart }, + { type: "pie_chart", title: "Pie", icon: Donut }, ]; export function isChartLineLike(chartType: ChartType) { From 6403a429b02fd33a3e0af36cf2fec04593b87db9 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 15:18:53 +0530 Subject: [PATCH 13/62] Support inner radius --- .../components/charts/circular-charts/CircularChart.ts | 6 ++++++ .../canvas/components/charts/circular-charts/pie.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index 0319f13dc73..786c843c585 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -28,6 +28,7 @@ import type { ChartDataQuery } from "../types"; type CircularChartEncoding = { measure?: FieldConfig; color?: FieldConfig; + innerRadius?: number; }; export type CircularChartSpec = BaseChartConfig & CircularChartEncoding; @@ -58,6 +59,10 @@ export class CircularChartComponent extends BaseChart { }, }, }, + innerRadius: { + type: "number", + label: "Inner Radius", + }, }; } @@ -158,6 +163,7 @@ export class CircularChartComponent extends BaseChart { return { metrics_view: metricsViewName, + innerRadius: 0, color: { type: "nominal", field: randomDimension, diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index 403599640d5..a2270878025 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -16,7 +16,7 @@ export function generateVLPieChartSpec( spec.mark = { type: "arc", - innerRadius: 30, + innerRadius: config.innerRadius || 0, }; const theta = createXEncoding(config.measure, data); const color = createColorEncoding(config.color, data); From 06718c11fa48991d3cba69c50c9e041bd7e4ed22 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 16:08:11 +0530 Subject: [PATCH 14/62] add heatmap chart --- .../src/components/icons/Heatmap.svelte | 29 +++ .../canvas/components/charts/builder.ts | 2 +- .../charts/heatmap-charts/HeatmapChart.ts | 209 ++++++++++++++++++ .../components/charts/heatmap-charts/spec.ts | 39 ++++ .../canvas/components/charts/index.ts | 17 +- .../features/canvas/components/charts/util.ts | 11 +- .../src/features/canvas/components/util.ts | 1 + web-common/src/features/canvas/layout-util.ts | 1 + 8 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 web-common/src/components/icons/Heatmap.svelte create mode 100644 web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts create mode 100644 web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts diff --git a/web-common/src/components/icons/Heatmap.svelte b/web-common/src/components/icons/Heatmap.svelte new file mode 100644 index 00000000000..29c237471f8 --- /dev/null +++ b/web-common/src/components/icons/Heatmap.svelte @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index 285766440bd..59dcb347b55 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -28,7 +28,7 @@ export function createMultiLayerBaseSpec() { } export function createSingleLayerBaseSpec( - mark: "line" | "bar" | "point" | "area" | "arc", + mark: "line" | "bar" | "point" | "area" | "arc" | "rect", ): TopLevelUnitSpec { return { $schema: "https://vega.github.io/schema/vega-lite/v5.json", diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts new file mode 100644 index 00000000000..d72837fc63a --- /dev/null +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -0,0 +1,209 @@ +import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; +import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; +import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; +import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; +import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; +import { + createQueryServiceMetricsViewAggregation, + type V1MetricsViewAggregationDimension, + type V1MetricsViewAggregationMeasure, + type V1MetricsViewAggregationSort, + type V1MetricsViewSpec, + type V1Resource, +} from "@rilldata/web-common/runtime-client"; +import { keepPreviousData } from "@tanstack/svelte-query"; +import { derived, get, type Readable } from "svelte/store"; +import type { + CanvasEntity, + ComponentPath, +} from "../../../stores/canvas-entity"; +import { BaseChart, type BaseChartConfig } from "../BaseChart"; +import type { ChartDataQuery, ChartFieldsMap, FieldConfig } from "../types"; + +export type HeatmapChartSpec = BaseChartConfig & { + x?: FieldConfig; + y?: FieldConfig; + color?: FieldConfig; +}; + +export class HeatmapChartComponent extends BaseChart { + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + super(resource, parent, path); + } + + protected getChartSpecificOptions(): Record { + return { + x: { + type: "positional", + label: "X-axis", + meta: { + chartFieldInput: { + type: "dimension", + axisTitleSelector: true, + sortSelector: true, + limitSelector: true, + nullSelector: true, + }, + }, + }, + y: { + type: "positional", + label: "Y-axis", + meta: { + chartFieldInput: { + type: "dimension", + axisTitleSelector: true, + sortSelector: true, + limitSelector: true, + nullSelector: true, + }, + }, + }, + color: { + type: "positional", + label: "Color", + meta: { + chartFieldInput: { + type: "measure", + axisTitleSelector: true, + }, + }, + }, + }; + } + + createChartDataQuery( + ctx: CanvasStore, + timeAndFilterStore: Readable, + ): ChartDataQuery { + const config = get(this.specStore); + + let measures: V1MetricsViewAggregationMeasure[] = []; + let dimensions: V1MetricsViewAggregationDimension[] = []; + + if (config.color?.field) { + measures = [{ name: config.color.field }]; + } + + let xSort: V1MetricsViewAggregationSort | undefined; + let ySort: V1MetricsViewAggregationSort | undefined; + + if (config.x?.field) { + dimensions = [...dimensions, { name: config.x.field }]; + } + + if (config.y?.field) { + dimensions = [...dimensions, { name: config.y.field }]; + } + + return derived( + [ctx.runtime, timeAndFilterStore], + ([runtime, $timeAndFilterStore], set) => { + const { timeRange, where } = $timeAndFilterStore; + + let outerWhere = where; + + // Handle null filtering for both x and y dimensions + if (config.x?.field && !config.x.showNull) { + const excludeNullFilter = createInExpression( + config.x.field, + [null], + true, + ); + outerWhere = mergeFilters(outerWhere, excludeNullFilter); + } + + if (config.y?.field && !config.y.showNull) { + const excludeNullFilter = createInExpression( + config.y.field, + [null], + true, + ); + outerWhere = mergeFilters(outerWhere, excludeNullFilter); + } + + const enabled = !!timeRange?.start && !!timeRange?.end; + + const dataQuery = createQueryServiceMetricsViewAggregation( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions, + sort: [...(xSort ? [xSort] : []), ...(ySort ? [ySort] : [])], + where: outerWhere, + timeRange, + limit: "5000", // Higher limit for heatmap to show more data points + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ctx.queryClient, + ); + + return derived(dataQuery, ($dataQuery) => { + return { + isFetching: $dataQuery.isFetching, + error: $dataQuery.error, + data: $dataQuery?.data?.data, + }; + }).subscribe(set); + }, + ); + } + + static newComponentSpec( + metricsViewName: string, + metricsViewSpec: V1MetricsViewSpec | undefined, + ): HeatmapChartSpec { + // Select two dimensions and one measure if available + const measures = metricsViewSpec?.measures || []; + const dimensions = metricsViewSpec?.dimensions || []; + + const randomMeasure = measures[Math.floor(Math.random() * measures.length)] + ?.name as string; + + // Get two random dimensions + const availableDimensions = [...dimensions]; + const randomDimension1 = availableDimensions.splice( + Math.floor(Math.random() * availableDimensions.length), + 1, + )[0]?.name as string; + const randomDimension2 = availableDimensions[ + Math.floor(Math.random() * availableDimensions.length) + ]?.name as string; + + return { + metrics_view: metricsViewName, + x: { + type: "nominal", + field: randomDimension1, + limit: 20, + }, + y: { + type: "nominal", + field: randomDimension2, + limit: 20, + }, + color: { + type: "quantitative", + field: randomMeasure, + }, + }; + } + + chartTitle(fields: ChartFieldsMap) { + const config = get(this.specStore); + const { x, y, color } = config; + const xLabel = x?.field ? fields[x.field]?.displayName || x.field : ""; + const yLabel = y?.field ? fields[y.field]?.displayName || y.field : ""; + const colorLabel = color?.field + ? fields[color.field]?.displayName || color.field + : ""; + + return `${colorLabel} by ${xLabel} and ${yLabel}`; + } +} diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts new file mode 100644 index 00000000000..003b81cfe09 --- /dev/null +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts @@ -0,0 +1,39 @@ +import type { Field } from "vega-lite/build/src/channeldef"; +import type { TopLevelUnitSpec } from "vega-lite/build/src/spec/unit"; +import { + createColorEncoding, + createDefaultTooltipEncoding, + createSingleLayerBaseSpec, + createXEncoding, + createYEncoding, +} from "../builder"; +import type { ChartDataResult } from "../types"; +import type { HeatmapChartSpec } from "./HeatmapChart"; + +export function generateVLHeatmapSpec( + config: HeatmapChartSpec, + data: ChartDataResult, +): TopLevelUnitSpec { + const spec = createSingleLayerBaseSpec("rect"); + + return { + ...spec, + encoding: { + x: createXEncoding(config.x, data), + y: createYEncoding(config.y, data), + color: createColorEncoding(config.color, data), + tooltip: createDefaultTooltipEncoding( + [config.x, config.y, config.color], + data, + ), + }, + config: { + axis: { grid: true, tickBand: "extent" }, + axisX: { + grid: true, + gridDash: [], + tickBand: "extent", + }, + }, + }; +} diff --git a/web-common/src/features/canvas/components/charts/index.ts b/web-common/src/features/canvas/components/charts/index.ts index 7f69a0bf7c3..fa9edeb4f77 100644 --- a/web-common/src/features/canvas/components/charts/index.ts +++ b/web-common/src/features/canvas/components/charts/index.ts @@ -5,14 +5,22 @@ import { CircularChartComponent, type CircularChartSpec, } from "./circular-charts/CircularChart"; +import { + HeatmapChartComponent, + type HeatmapChartSpec, +} from "./heatmap-charts/HeatmapChart"; export { default as Chart } from "./Chart.svelte"; export type ChartComponent = | typeof CartesianChartComponent - | typeof CircularChartComponent; + | typeof CircularChartComponent + | typeof HeatmapChartComponent; -export type ChartSpec = CartesianChartSpec | CircularChartSpec; +export type ChartSpec = + | CartesianChartSpec + | CircularChartSpec + | HeatmapChartSpec; export type ChartType = | "bar_chart" @@ -20,7 +28,8 @@ export type ChartType = | "area_chart" | "stacked_bar" | "stacked_bar_normalized" - | "pie_chart"; + | "pie_chart" + | "heatmap"; export function getChartComponent( type: ChartType, @@ -34,6 +43,8 @@ export function getChartComponent( return CartesianChartComponent; case "pie_chart": return CircularChartComponent; + case "heatmap": + return HeatmapChartComponent; default: throw new Error(`Unsupported chart type: ${type}`); } diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index fbd59091bdb..2376054c19e 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,5 +1,6 @@ import BarChart from "@rilldata/web-common/components/icons/BarChart.svelte"; import Donut from "@rilldata/web-common/components/icons/Donut.svelte"; +import Heatmap from "@rilldata/web-common/components/icons/Heatmap.svelte"; import LineChart from "@rilldata/web-common/components/icons/LineChart.svelte"; import StackedArea from "@rilldata/web-common/components/icons/StackedArea.svelte"; import StackedBar from "@rilldata/web-common/components/icons/StackedBar.svelte"; @@ -15,7 +16,10 @@ import { generateVLBarChartSpec } from "./cartesian-charts/bar-chart/spec"; import { generateVLLineChartSpec } from "./cartesian-charts/line-chart/spec"; import { generateVLStackedBarChartSpec } from "./cartesian-charts/stacked-bar/default"; import { generateVLStackedBarNormalizedSpec } from "./cartesian-charts/stacked-bar/normalized"; +import type { CircularChartSpec } from "./circular-charts/CircularChart"; import { generateVLPieChartSpec } from "./circular-charts/pie"; +import type { HeatmapChartSpec } from "./heatmap-charts/HeatmapChart"; +import { generateVLHeatmapSpec } from "./heatmap-charts/spec"; import type { ChartDataResult, ChartMetadata } from "./types"; export function generateSpec( @@ -36,7 +40,11 @@ export function generateSpec( case "area_chart": return generateVLAreaChartSpec(rillChartSpec, data); case "pie_chart": - return generateVLPieChartSpec(rillChartSpec, data); + // Type assertion since we know this chart type will only be used with CircularChartSpec + return generateVLPieChartSpec(rillChartSpec as CircularChartSpec, data); + case "heatmap": + // Type assertion since we know this chart type will only be used with HeatmapChartSpec + return generateVLHeatmapSpec(rillChartSpec as HeatmapChartSpec, data); } } @@ -51,6 +59,7 @@ export const chartMetadata: ChartMetadata[] = [ }, { type: "area_chart", title: "Stacked Area", icon: StackedArea }, { type: "pie_chart", title: "Pie", icon: Donut }, + { type: "heatmap", title: "Heatmap", icon: Heatmap }, ]; export function isChartLineLike(chartType: ChartType) { diff --git a/web-common/src/features/canvas/components/util.ts b/web-common/src/features/canvas/components/util.ts index efd56e9acfd..e2b4b8fdd04 100644 --- a/web-common/src/features/canvas/components/util.ts +++ b/web-common/src/features/canvas/components/util.ts @@ -67,6 +67,7 @@ const CHART_TYPES = [ "stacked_bar_normalized", "area_chart", "pie_chart", + "heatmap", ] as const; const NON_CHART_TYPES = [ "markdown", diff --git a/web-common/src/features/canvas/layout-util.ts b/web-common/src/features/canvas/layout-util.ts index e433ac177a9..8c13f1906ab 100644 --- a/web-common/src/features/canvas/layout-util.ts +++ b/web-common/src/features/canvas/layout-util.ts @@ -19,6 +19,7 @@ export const initialHeights: Record = { stacked_bar: 320, stacked_bar_normalized: 320, pie_chart: 320, + heatmap: 320, markdown: 40, kpi_grid: 128, image: 80, From 5ce20c40aae20a0dfb06d84e0113f153728f9668 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 17:20:05 +0530 Subject: [PATCH 15/62] Consolidate chart metadata --- .../canvas/AddComponentDropdown.svelte | 12 +-- .../canvas/components/charts/BaseChart.ts | 2 +- .../canvas/components/charts/index.ts | 80 ++++++++++++++++--- .../canvas/components/charts/types.ts | 17 ++-- .../features/canvas/components/charts/util.ts | 53 +----------- .../inspector/chart/ChartTypeSelector.svelte | 21 ++--- web-common/src/features/canvas/layout-util.ts | 1 + 7 files changed, 103 insertions(+), 83 deletions(-) diff --git a/web-common/src/features/canvas/AddComponentDropdown.svelte b/web-common/src/features/canvas/AddComponentDropdown.svelte index 5bcf6b82230..21f89447e8c 100644 --- a/web-common/src/features/canvas/AddComponentDropdown.svelte +++ b/web-common/src/features/canvas/AddComponentDropdown.svelte @@ -2,8 +2,8 @@ import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; import { Plus, PlusCircle } from "lucide-svelte"; import type { ComponentType, SvelteComponent } from "svelte"; - import type { ChartType } from "./components/charts"; - import { chartMetadata } from "./components/charts/util"; + import { CHART_TYPES } from "./components/charts"; + import type { ChartType } from "./components/charts/types"; import type { CanvasComponentType } from "./components/types"; import BigNumberIcon from "./icons/BigNumberIcon.svelte"; import ChartIcon from "./icons/ChartIcon.svelte"; @@ -18,11 +18,11 @@ // Function to get a random chart type function getRandomChartType(): ChartType { - const chartTypes = chartMetadata - .map((chart) => chart.type) - .filter((t) => t !== "stacked_bar_normalized"); + const chartTypes = CHART_TYPES.filter( + (t) => t !== "stacked_bar_normalized", + ); const randomIndex = Math.floor(Math.random() * chartTypes.length); - return chartTypes[randomIndex]; + return chartTypes[randomIndex] as ChartType; } // Create menu items with a function to get random chart type when clicked diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 83ce8051a4b..cbb7d5c04f9 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -17,11 +17,11 @@ import type { ComponentCommonProperties, ComponentFilterProperties, } from "../types"; -import type { ChartType } from "./"; import Chart from "./Chart.svelte"; import type { ChartDataQuery, ChartFieldsMap, + ChartType, CommonChartProperties, FieldConfig, } from "./types"; diff --git a/web-common/src/features/canvas/components/charts/index.ts b/web-common/src/features/canvas/components/charts/index.ts index fa9edeb4f77..d86f2d1896a 100644 --- a/web-common/src/features/canvas/components/charts/index.ts +++ b/web-common/src/features/canvas/components/charts/index.ts @@ -1,14 +1,31 @@ +import BarChart from "@rilldata/web-common/components/icons/BarChart.svelte"; +import Donut from "@rilldata/web-common/components/icons/Donut.svelte"; +import Heatmap from "@rilldata/web-common/components/icons/Heatmap.svelte"; +import LineChart from "@rilldata/web-common/components/icons/LineChart.svelte"; +import StackedArea from "@rilldata/web-common/components/icons/StackedArea.svelte"; +import StackedBar from "@rilldata/web-common/components/icons/StackedBar.svelte"; +import StackedBarFull from "@rilldata/web-common/components/icons/StackedBarFull.svelte"; import type { BaseCanvasComponentConstructor } from "@rilldata/web-common/features/canvas/components/util"; +import type { ComponentType, SvelteComponent } from "svelte"; +import type { VisualizationSpec } from "svelte-vega"; +import { generateVLAreaChartSpec } from "./cartesian-charts/area/spec"; +import { generateVLBarChartSpec } from "./cartesian-charts/bar-chart/spec"; import type { CartesianChartSpec } from "./cartesian-charts/CartesianChart"; import { CartesianChartComponent } from "./cartesian-charts/CartesianChart"; +import { generateVLLineChartSpec } from "./cartesian-charts/line-chart/spec"; +import { generateVLStackedBarChartSpec } from "./cartesian-charts/stacked-bar/default"; +import { generateVLStackedBarNormalizedSpec } from "./cartesian-charts/stacked-bar/normalized"; import { CircularChartComponent, type CircularChartSpec, } from "./circular-charts/CircularChart"; +import { generateVLPieChartSpec } from "./circular-charts/pie"; import { HeatmapChartComponent, type HeatmapChartSpec, } from "./heatmap-charts/HeatmapChart"; +import { generateVLHeatmapSpec } from "./heatmap-charts/spec"; +import type { ChartDataResult, ChartType } from "./types"; export { default as Chart } from "./Chart.svelte"; @@ -22,15 +39,6 @@ export type ChartSpec = | CircularChartSpec | HeatmapChartSpec; -export type ChartType = - | "bar_chart" - | "line_chart" - | "area_chart" - | "stacked_bar" - | "stacked_bar_normalized" - | "pie_chart" - | "heatmap"; - export function getChartComponent( type: ChartType, ): BaseCanvasComponentConstructor { @@ -49,3 +57,57 @@ export function getChartComponent( throw new Error(`Unsupported chart type: ${type}`); } } + +export interface ChartMetadataConfig { + title: string; + icon: ComponentType; + component: BaseCanvasComponentConstructor; + generateSpec: (config: ChartSpec, data: ChartDataResult) => VisualizationSpec; +} + +export const CHART_CONFIG: Record = { + bar_chart: { + title: "Bar", + icon: BarChart, + component: CartesianChartComponent, + generateSpec: generateVLBarChartSpec, + }, + line_chart: { + title: "Line", + icon: LineChart, + component: CartesianChartComponent, + generateSpec: generateVLLineChartSpec, + }, + area_chart: { + title: "Stacked Area", + icon: StackedArea, + component: CartesianChartComponent, + generateSpec: generateVLAreaChartSpec, + }, + stacked_bar: { + title: "Stacked Bar", + icon: StackedBar, + component: CartesianChartComponent, + generateSpec: generateVLStackedBarChartSpec, + }, + stacked_bar_normalized: { + title: "Stacked Bar Normalized", + icon: StackedBarFull, + component: CartesianChartComponent, + generateSpec: generateVLStackedBarNormalizedSpec, + }, + pie_chart: { + title: "Pie", + icon: Donut, + component: CircularChartComponent, + generateSpec: generateVLPieChartSpec, + }, + heatmap: { + title: "Heatmap", + icon: Heatmap, + component: HeatmapChartComponent, + generateSpec: generateVLHeatmapSpec, + }, +}; + +export const CHART_TYPES = Object.keys(CHART_CONFIG) as ChartType[]; diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index 5c62ad6b3ac..b7cc1322979 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -10,9 +10,16 @@ import { type V1MetricsViewAggregationResponseDataItem, } from "@rilldata/web-common/runtime-client"; import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; -import type { ComponentType, SvelteComponent } from "svelte"; import { type Readable } from "svelte/store"; -import type { ChartType } from "./"; + +export type ChartType = + | "bar_chart" + | "line_chart" + | "area_chart" + | "stacked_bar" + | "stacked_bar_normalized" + | "pie_chart" + | "heatmap"; export type ChartDataQuery = Readable<{ isFetching: boolean; @@ -69,12 +76,6 @@ export interface ChartConfig extends CommonChartProperties { vl_config?: string; } -export interface ChartMetadata { - type: ChartType; - icon: ComponentType; - title: string; -} - /** Temporary solution for the lack of vega lite type exports */ export interface TooltipValue { title?: string; diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 2376054c19e..4515fc6236d 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,26 +1,10 @@ -import BarChart from "@rilldata/web-common/components/icons/BarChart.svelte"; -import Donut from "@rilldata/web-common/components/icons/Donut.svelte"; -import Heatmap from "@rilldata/web-common/components/icons/Heatmap.svelte"; -import LineChart from "@rilldata/web-common/components/icons/LineChart.svelte"; -import StackedArea from "@rilldata/web-common/components/icons/StackedArea.svelte"; -import StackedBar from "@rilldata/web-common/components/icons/StackedBar.svelte"; -import StackedBarFull from "@rilldata/web-common/components/icons/StackedBarFull.svelte"; import { getRillTheme } from "@rilldata/web-common/components/vega/vega-config"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; import type { Config } from "vega-lite"; -import type { ChartSpec, ChartType } from "./"; -import { generateVLAreaChartSpec } from "./cartesian-charts/area/spec"; -import { generateVLBarChartSpec } from "./cartesian-charts/bar-chart/spec"; -import { generateVLLineChartSpec } from "./cartesian-charts/line-chart/spec"; -import { generateVLStackedBarChartSpec } from "./cartesian-charts/stacked-bar/default"; -import { generateVLStackedBarNormalizedSpec } from "./cartesian-charts/stacked-bar/normalized"; -import type { CircularChartSpec } from "./circular-charts/CircularChart"; -import { generateVLPieChartSpec } from "./circular-charts/pie"; -import type { HeatmapChartSpec } from "./heatmap-charts/HeatmapChart"; -import { generateVLHeatmapSpec } from "./heatmap-charts/spec"; -import type { ChartDataResult, ChartMetadata } from "./types"; +import { CHART_CONFIG, type ChartSpec } from "./"; +import type { ChartDataResult, ChartType } from "./types"; export function generateSpec( chartType: ChartType, @@ -28,40 +12,9 @@ export function generateSpec( data: ChartDataResult, ) { if (data.isFetching || data.error) return {}; - switch (chartType) { - case "bar_chart": - return generateVLBarChartSpec(rillChartSpec, data); - case "stacked_bar": - return generateVLStackedBarChartSpec(rillChartSpec, data); - case "stacked_bar_normalized": - return generateVLStackedBarNormalizedSpec(rillChartSpec, data); - case "line_chart": - return generateVLLineChartSpec(rillChartSpec, data); - case "area_chart": - return generateVLAreaChartSpec(rillChartSpec, data); - case "pie_chart": - // Type assertion since we know this chart type will only be used with CircularChartSpec - return generateVLPieChartSpec(rillChartSpec as CircularChartSpec, data); - case "heatmap": - // Type assertion since we know this chart type will only be used with HeatmapChartSpec - return generateVLHeatmapSpec(rillChartSpec as HeatmapChartSpec, data); - } + return CHART_CONFIG[chartType].generateSpec(rillChartSpec, data); } -export const chartMetadata: ChartMetadata[] = [ - { type: "line_chart", title: "Line", icon: LineChart }, - { type: "bar_chart", title: "Bar", icon: BarChart }, - { type: "stacked_bar", title: "Stacked Bar", icon: StackedBar }, - { - type: "stacked_bar_normalized", - title: "Stacked Bar Normalized", - icon: StackedBarFull, - }, - { type: "area_chart", title: "Stacked Area", icon: StackedArea }, - { type: "pie_chart", title: "Pie", icon: Donut }, - { type: "heatmap", title: "Heatmap", icon: Heatmap }, -]; - export function isChartLineLike(chartType: ChartType) { return chartType === "line_chart" || chartType === "area_chart"; } diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index 7b986b1f9e1..c6fe16fbc4e 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -3,10 +3,13 @@ import InputLabel from "@rilldata/web-common/components/forms/InputLabel.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; - import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; + import { + CHART_CONFIG, + CHART_TYPES, + type ChartSpec, + type ChartType, + } from "@rilldata/web-common/features/canvas/components/charts"; import type { BaseChart } from "@rilldata/web-common/features/canvas/components/charts/BaseChart"; - import type { ChartMetadata } from "@rilldata/web-common/features/canvas/components/charts/types"; - import { chartMetadata } from "@rilldata/web-common/features/canvas/components/charts/util"; export let component: BaseChart; @@ -14,27 +17,27 @@ $: type = $chartType; - function selectChartType(chartType: ChartMetadata) { - component.updateChartType(chartType.type); + function selectChartType(chartType: ChartType) { + component.updateChartType(chartType); }
- {#each chartMetadata as chart, i (i)} + {#each CHART_TYPES as chart, i (i)} - {chart.title} + {CHART_CONFIG[chart].title} {/each} diff --git a/web-common/src/features/canvas/layout-util.ts b/web-common/src/features/canvas/layout-util.ts index 8c13f1906ab..fafa0eb5450 100644 --- a/web-common/src/features/canvas/layout-util.ts +++ b/web-common/src/features/canvas/layout-util.ts @@ -12,6 +12,7 @@ import { ResourceKind } from "../entity-management/resource-selectors"; import type { CanvasComponentType } from "./components/types"; import { COMPONENT_CLASS_MAP } from "./components/util"; +// TODO: Move this individual component class export const initialHeights: Record = { line_chart: 320, bar_chart: 320, From c9f806da3616eee0438cd62b08b3579aeab03139 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 17:39:39 +0530 Subject: [PATCH 16/62] lint fix --- .../features/canvas/inspector/chart/ChartTypeSelector.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index c6fe16fbc4e..f6e4f5250c6 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -7,9 +7,9 @@ CHART_CONFIG, CHART_TYPES, type ChartSpec, - type ChartType, } from "@rilldata/web-common/features/canvas/components/charts"; import type { BaseChart } from "@rilldata/web-common/features/canvas/components/charts/BaseChart"; + import type { ChartType } from "@rilldata/web-common/features/canvas/components/charts/types"; export let component: BaseChart; From f277261a4d158af95442eeba08d2777cdb16cf82 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 17:46:59 +0530 Subject: [PATCH 17/62] Fix import --- web-common/src/features/canvas/components/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-common/src/features/canvas/components/types.ts b/web-common/src/features/canvas/components/types.ts index 16120e95e78..ebab2168bf2 100644 --- a/web-common/src/features/canvas/components/types.ts +++ b/web-common/src/features/canvas/components/types.ts @@ -1,7 +1,7 @@ import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; import type { CircularChartSpec } from "@rilldata/web-common/features/canvas/components/charts/circular-charts/CircularChart"; import type { KPIGridSpec } from "@rilldata/web-common/features/canvas/components/kpi-grid"; -import type { ChartType } from "./charts"; +import type { ChartType } from "./charts/types"; import type { ImageSpec } from "./image"; import type { KPISpec } from "./kpi"; import type { LeaderboardSpec } from "./leaderboard"; From a12305a796e8b57d25e7ec765f959d78839682dd Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 18:33:13 +0530 Subject: [PATCH 18/62] Remove limit from heatmap --- .../charts/heatmap-charts/HeatmapChart.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index d72837fc63a..4dce02c9c98 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -7,7 +7,6 @@ import { createQueryServiceMetricsViewAggregation, type V1MetricsViewAggregationDimension, type V1MetricsViewAggregationMeasure, - type V1MetricsViewAggregationSort, type V1MetricsViewSpec, type V1Resource, } from "@rilldata/web-common/runtime-client"; @@ -40,8 +39,6 @@ export class HeatmapChartComponent extends BaseChart { chartFieldInput: { type: "dimension", axisTitleSelector: true, - sortSelector: true, - limitSelector: true, nullSelector: true, }, }, @@ -53,8 +50,6 @@ export class HeatmapChartComponent extends BaseChart { chartFieldInput: { type: "dimension", axisTitleSelector: true, - sortSelector: true, - limitSelector: true, nullSelector: true, }, }, @@ -65,7 +60,6 @@ export class HeatmapChartComponent extends BaseChart { meta: { chartFieldInput: { type: "measure", - axisTitleSelector: true, }, }, }, @@ -85,9 +79,6 @@ export class HeatmapChartComponent extends BaseChart { measures = [{ name: config.color.field }]; } - let xSort: V1MetricsViewAggregationSort | undefined; - let ySort: V1MetricsViewAggregationSort | undefined; - if (config.x?.field) { dimensions = [...dimensions, { name: config.x.field }]; } @@ -101,7 +92,7 @@ export class HeatmapChartComponent extends BaseChart { ([runtime, $timeAndFilterStore], set) => { const { timeRange, where } = $timeAndFilterStore; - let outerWhere = where; + let combinedWhere = where; // Handle null filtering for both x and y dimensions if (config.x?.field && !config.x.showNull) { @@ -110,7 +101,7 @@ export class HeatmapChartComponent extends BaseChart { [null], true, ); - outerWhere = mergeFilters(outerWhere, excludeNullFilter); + combinedWhere = mergeFilters(combinedWhere, excludeNullFilter); } if (config.y?.field && !config.y.showNull) { @@ -119,7 +110,7 @@ export class HeatmapChartComponent extends BaseChart { [null], true, ); - outerWhere = mergeFilters(outerWhere, excludeNullFilter); + combinedWhere = mergeFilters(combinedWhere, excludeNullFilter); } const enabled = !!timeRange?.start && !!timeRange?.end; @@ -130,8 +121,12 @@ export class HeatmapChartComponent extends BaseChart { { measures, dimensions, - sort: [...(xSort ? [xSort] : []), ...(ySort ? [ySort] : [])], - where: outerWhere, + sort: [ + ...(config.x?.field + ? [{ name: config.x.field, desc: true }] + : []), + ], + where: combinedWhere, timeRange, limit: "5000", // Higher limit for heatmap to show more data points }, From edf5581947e446246feb71a13d6db4eaad0dfb95 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 18:57:08 +0530 Subject: [PATCH 19/62] Add initial chart switching logic --- .../canvas/components/charts/BaseChart.ts | 51 ++++++++++++++++--- .../inspector/chart/ChartTypeSelector.svelte | 13 ++++- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index cbb7d5c04f9..2f1c2d80717 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -1,4 +1,5 @@ import { BaseCanvasComponent } from "@rilldata/web-common/features/canvas/components/BaseCanvasComponent"; +import { CHART_CONFIG } from "@rilldata/web-common/features/canvas/components/charts"; import { commonOptions, getFilterOptions, @@ -10,7 +11,10 @@ import type { } from "@rilldata/web-common/features/canvas/inspector/types"; import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; -import type { V1Resource } from "@rilldata/web-common/runtime-client"; +import type { + V1MetricsViewSpec, + V1Resource, +} from "@rilldata/web-common/runtime-client"; import { get, writable, type Readable, type Writable } from "svelte/store"; import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity"; import type { @@ -90,23 +94,58 @@ export abstract class BaseChart< }; } - updateChartType(key: ChartType) { + updateChartType( + key: ChartType, + metricsViewSpec: V1MetricsViewSpec | undefined, + ) { if (!this.parent.fileArtifact) return; - const currentSpec = get(this.specStore); + const currentSpec = get(this.specStore); const parentPath = this.pathInYAML.slice(0, -1); - this.chartType.set(key); const parseDocumentStore = this.parent.parsedContent; const parsedDocument = get(parseDocumentStore); - const { updateEditorContent } = this.parent.fileArtifact; + const newSpecForKey = CHART_CONFIG[key].component.newComponentSpec( + currentSpec.metrics_view, + metricsViewSpec, + ); + + const commonProps = this.extractCommonProperties(currentSpec); + const mergedSpec = { + ...newSpecForKey, + ...commonProps, + }; + + // Preserve the width from the current chart const width = parsedDocument.getIn([...parentPath, "width"]); - parsedDocument.setIn(parentPath, { [key]: currentSpec, width }); + // Update the chart type and spec + this.chartType.set(key); + parsedDocument.setIn(parentPath, { [key]: mergedSpec, width }); updateEditorContent(parsedDocument.toString(), false, true); } + + private extractCommonProperties(spec: TConfig): Partial { + const { + metrics_view, + title, + description, + vl_config, + time_filters, + dimension_filters, + } = spec; + + return { + metrics_view, + title, + description, + vl_config, + time_filters, + dimension_filters, + }; + } } diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index f6e4f5250c6..246d43f48b4 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -13,12 +13,21 @@ export let component: BaseChart; - $: ({ chartType } = component); + $: ({ + parent: { + spec: { getMetricsViewFromName }, + }, + chartType, + specStore, + } = component); + + $: _metricViewSpec = getMetricsViewFromName($specStore.metrics_view); + $: metricsViewSpec = $_metricViewSpec.metricsView; $: type = $chartType; function selectChartType(chartType: ChartType) { - component.updateChartType(chartType); + component.updateChartType(chartType, metricsViewSpec); } From 7a2aaebceb7174de6bf3fd39194ecbee7aac2bb4 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 19:13:24 +0530 Subject: [PATCH 20/62] add color field for cartesian charts --- .../canvas/components/charts/cartesian-charts/CartesianChart.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index c2f6f1c49ba..5438b0bd937 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -225,6 +225,7 @@ export class CartesianChartComponent extends BaseChart { return { metrics_view: metricsViewName, + color: "hsl(246, 66%, 50%)", x: { type: timeDimension ? "temporal" : "nominal", field: timeDimension || randomDimension, From 25913838e01b5f36c16083e0c0cd754e81daeb3b Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 16 Apr 2025 19:27:39 +0530 Subject: [PATCH 21/62] Hide time dimension for donut --- .../components/charts/circular-charts/CircularChart.ts | 1 + .../inspector/chart/PositionalFieldConfig.svelte | 10 ++++++---- web-common/src/features/canvas/inspector/types.ts | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index 786c843c585..f7aad33e524 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -47,6 +47,7 @@ export class CircularChartComponent extends BaseChart { type: "dimension", nullSelector: true, limitSelector: true, + hideTimeDimension: true, }, }, }, diff --git a/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte b/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte index 2b351152957..6f993d1c03c 100644 --- a/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte +++ b/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte @@ -20,7 +20,9 @@ }, } = getCanvasStore(canvasName)); - $: isDimension = config.meta?.chartFieldInput?.type === "dimension"; + $: chartFieldInput = config.meta?.chartFieldInput; + + $: isDimension = chartFieldInput?.type === "dimension"; $: timeDimension = getTimeDimensionForMetricView(metricsView); @@ -58,12 +60,12 @@
- {#if Object.keys(config.meta?.chartFieldInput ?? {}).length > 1} + {#if Object.keys(chartFieldInput ?? {}).length > 1} {/if}
@@ -73,7 +75,7 @@ metricName={metricsView} id={`${key}-field`} type={isDimension ? "dimension" : "measure"} - includeTime + includeTime={!chartFieldInput?.hideTimeDimension} selectedItem={fieldConfig?.field} onSelect={async (field) => { updateFieldConfig(field); diff --git a/web-common/src/features/canvas/inspector/types.ts b/web-common/src/features/canvas/inspector/types.ts index 3e63f312ca3..157a4e20991 100644 --- a/web-common/src/features/canvas/inspector/types.ts +++ b/web-common/src/features/canvas/inspector/types.ts @@ -20,6 +20,7 @@ export type FieldType = "measure" | "dimension" | "time"; export type ChartFieldInput = { type: FieldType; axisTitleSelector?: boolean; + hideTimeDimension?: boolean; originSelector?: boolean; sortSelector?: boolean; limitSelector?: boolean; From cb972b14087d3d15d9cf199ac91b6a097d9a9107 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 17 Apr 2025 17:05:04 +0530 Subject: [PATCH 22/62] Move config as part of chart spec --- .../canvas/components/charts/Chart.svelte | 12 +++------- .../canvas/components/charts/builder.ts | 16 +++++++++++++- .../charts/cartesian-charts/area/spec.ts | 7 +++++- .../charts/cartesian-charts/bar-chart/spec.ts | 12 ++++++++-- .../cartesian-charts/line-chart/spec.ts | 7 +++++- .../cartesian-charts/stacked-bar/default.ts | 14 ++++++++++-- .../stacked-bar/normalized.ts | 7 +++++- .../components/charts/circular-charts/pie.ts | 3 +++ .../components/charts/heatmap-charts/spec.ts | 19 +++++++++------- .../features/canvas/components/charts/util.ts | 22 ++++++++++++------- 10 files changed, 86 insertions(+), 33 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/Chart.svelte b/web-common/src/features/canvas/components/charts/Chart.svelte index 894e9ada9af..038c9da721d 100644 --- a/web-common/src/features/canvas/components/charts/Chart.svelte +++ b/web-common/src/features/canvas/components/charts/Chart.svelte @@ -1,4 +1,5 @@
@@ -117,7 +111,7 @@ {spec} renderer={isChartLineLike(chartType) ? "svg" : "canvas"} {expressionFunctions} - {config} + config={getRillTheme(true)} /> {/if} {/if} diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index 59dcb347b55..67566348d24 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -1,11 +1,16 @@ +import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; import type { FieldConfig, TooltipValue, } from "@rilldata/web-common/features/canvas/components/charts/types"; -import { sanitizeFieldName } from "@rilldata/web-common/features/canvas/components/charts/util"; +import { + mergedVlConfig, + sanitizeFieldName, +} from "@rilldata/web-common/features/canvas/components/charts/util"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; import type { VisualizationSpec } from "svelte-vega"; +import type { Config } from "vega-lite"; import type { ColorDef, Field, @@ -14,6 +19,7 @@ import type { import type { Encoding } from "vega-lite/build/src/encoding"; import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; import type { TopLevelUnitSpec } from "vega-lite/build/src/spec/unit"; +import type { ExprRef, SignalRef } from "vega-typings"; import type { ChartDataResult } from "./types"; export function createMultiLayerBaseSpec() { @@ -162,6 +168,14 @@ export function createDefaultTooltipEncoding( return tooltip; } +export function createConfig( + config: ChartSpec, + chartVLConfig?: Config | undefined, +): Config | undefined { + const userProvidedConfig = config.vl_config; + return mergedVlConfig(userProvidedConfig, chartVLConfig); +} + export function createEncoding( config: CartesianChartSpec, data: ChartDataResult, diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts index dec06b91b5a..ce20e3d2825 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts @@ -4,6 +4,7 @@ import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/ch import type { VisualizationSpec } from "svelte-vega"; import { createColorEncoding, + createConfig, createDefaultTooltipEncoding, createMultiLayerBaseSpec, createXEncoding, @@ -16,6 +17,7 @@ export function generateVLAreaChartSpec( data: ChartDataResult, ): VisualizationSpec { const spec = createMultiLayerBaseSpec(); + const vegaConfig = createConfig(config); const colorField = typeof config.color === "object" ? config.color.field : undefined; @@ -135,5 +137,8 @@ export function generateVLAreaChartSpec( }, ]; - return spec; + return { + ...spec, + ...(vegaConfig && { config: vegaConfig }), + }; } diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts index 7233af2315a..5145459f599 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts @@ -1,5 +1,9 @@ import type { VisualizationSpec } from "svelte-vega"; -import { createEncoding, createSingleLayerBaseSpec } from "../../builder"; +import { + createConfig, + createEncoding, + createSingleLayerBaseSpec, +} from "../../builder"; import type { ChartDataResult } from "../../types"; import type { CartesianChartSpec } from "../CartesianChart"; @@ -9,6 +13,7 @@ export function generateVLBarChartSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("bar"); const baseEncoding = createEncoding(config, data); + const vegaConfig = createConfig(config); if (config.color && typeof config.color === "object" && config.x) { baseEncoding.xOffset = { @@ -18,5 +23,8 @@ export function generateVLBarChartSpec( } spec.encoding = baseEncoding; - return spec; + return { + ...spec, + ...(vegaConfig && { config: vegaConfig }), + }; } diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts index 2a2b989fae5..a139377c2cf 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts @@ -4,6 +4,7 @@ import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/ch import type { VisualizationSpec } from "svelte-vega"; import { createColorEncoding, + createConfig, createDefaultTooltipEncoding, createMultiLayerBaseSpec, createXEncoding, @@ -17,6 +18,7 @@ export function generateVLLineChartSpec( data: ChartDataResult, ): VisualizationSpec { const spec = createMultiLayerBaseSpec(); + const vegaConfig = createConfig(config); const colorField = typeof config.color === "object" ? config.color.field : undefined; @@ -133,5 +135,8 @@ export function generateVLLineChartSpec( }, ]; - return spec; + return { + ...spec, + ...(vegaConfig && { config: vegaConfig }), + }; } diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts index 7b064d1cb94..80b0ba06bcb 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts @@ -1,5 +1,9 @@ import type { VisualizationSpec } from "svelte-vega"; -import { createEncoding, createSingleLayerBaseSpec } from "../../builder"; +import { + createConfig, + createEncoding, + createSingleLayerBaseSpec, +} from "../../builder"; import type { ChartDataResult } from "../../types"; import type { CartesianChartSpec } from "../CartesianChart"; @@ -9,5 +13,11 @@ export function generateVLStackedBarChartSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("bar"); spec.encoding = createEncoding(config, data); - return spec; + + const vegaConfig = createConfig(config); + + return { + ...spec, + ...(vegaConfig && { config: vegaConfig }), + }; } diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts index a891a0cb51f..06d713dcd09 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts @@ -1,6 +1,7 @@ import type { TooltipValue } from "@rilldata/web-common/features/canvas/components/charts/types"; import type { VisualizationSpec } from "svelte-vega"; import { + createConfig, createDefaultTooltipEncoding, createEncoding, createSingleLayerBaseSpec, @@ -14,6 +15,7 @@ export function generateVLStackedBarNormalizedSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("bar"); const baseEncoding = createEncoding(config, data); + const vegaConfig = createConfig(config); if (baseEncoding.y && config.y?.field) { const yField = config.y.field; @@ -77,5 +79,8 @@ export function generateVLStackedBarNormalizedSpec( } spec.encoding = baseEncoding; - return spec; + return { + ...spec, + ...(vegaConfig && { config: vegaConfig }), + }; } diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index a2270878025..148e43a6781 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -1,6 +1,7 @@ import type { VisualizationSpec } from "svelte-vega"; import { createColorEncoding, + createConfig, createDefaultTooltipEncoding, createSingleLayerBaseSpec, createXEncoding, @@ -13,6 +14,7 @@ export function generateVLPieChartSpec( data: ChartDataResult, ): VisualizationSpec { const spec = createSingleLayerBaseSpec("arc"); + const vegaConfig = createConfig(config); spec.mark = { type: "arc", @@ -32,5 +34,6 @@ export function generateVLPieChartSpec( color, tooltip, }, + ...(vegaConfig && { config: vegaConfig }), }; } diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts index 003b81cfe09..443f19c38ba 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts @@ -2,6 +2,7 @@ import type { Field } from "vega-lite/build/src/channeldef"; import type { TopLevelUnitSpec } from "vega-lite/build/src/spec/unit"; import { createColorEncoding, + createConfig, createDefaultTooltipEncoding, createSingleLayerBaseSpec, createXEncoding, @@ -16,6 +17,15 @@ export function generateVLHeatmapSpec( ): TopLevelUnitSpec { const spec = createSingleLayerBaseSpec("rect"); + const vegaConfig = createConfig(config, { + axis: { grid: true, tickBand: "extent" }, + axisX: { + grid: true, + gridDash: [], + tickBand: "extent", + }, + }); + return { ...spec, encoding: { @@ -27,13 +37,6 @@ export function generateVLHeatmapSpec( data, ), }, - config: { - axis: { grid: true, tickBand: "extent" }, - axisX: { - grid: true, - gridDash: [], - tickBand: "extent", - }, - }, + ...(vegaConfig && { config: vegaConfig }), }; } diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 4515fc6236d..3546cbe2961 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,4 +1,3 @@ -import { getRillTheme } from "@rilldata/web-common/components/vega/vega-config"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; @@ -19,23 +18,30 @@ export function isChartLineLike(chartType: ChartType) { return chartType === "line_chart" || chartType === "area_chart"; } -export function mergedVlConfig(config: string): Config { - const defaultConfig = getRillTheme(true); +export function mergedVlConfig( + userProvidedConfig: string | undefined, + specConfig: Config | undefined, +): Config | undefined { + if (!userProvidedConfig) return specConfig; + + const validSpecConfig = specConfig || {}; let parsedConfig: Config; try { - parsedConfig = JSON.parse(config) as Config; + parsedConfig = JSON.parse(userProvidedConfig) as Config; } catch { console.warn("Invalid JSON config"); - return defaultConfig; + return specConfig; } - const reverseArrayMerge = ( + const replaceByClonedSource = ( destinationArray: unknown[], sourceArray: unknown[], - ) => [...sourceArray, ...destinationArray]; + ) => sourceArray; - return merge(defaultConfig, parsedConfig, { arrayMerge: reverseArrayMerge }); + return merge(validSpecConfig, parsedConfig, { + arrayMerge: replaceByClonedSource, + }); } export const timeGrainToVegaTimeUnitMap: Record = { From aea0e780c01222fc608dd497d6a05cc532832cda Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 17 Apr 2025 17:15:41 +0530 Subject: [PATCH 23/62] Update color for heatmap --- web-common/src/components/vega/vega-config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web-common/src/components/vega/vega-config.ts b/web-common/src/components/vega/vega-config.ts index 0648e019fa5..8e04cb0734e 100644 --- a/web-common/src/components/vega/vega-config.ts +++ b/web-common/src/components/vega/vega-config.ts @@ -108,6 +108,9 @@ export const getRillTheme: (isCanvasDashboard: boolean) => Config = ( }, range: { category: COMPARIONS_COLORS, + heatmap: { + scheme: "tealblues", // TODO: Generate this from theme + }, }, numberFormat: "s", tooltipFormat: { From 9b7ba4201d7221e3bb67909432f60efcb3014fdf Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Fri, 18 Apr 2025 15:31:24 +0530 Subject: [PATCH 24/62] Default legend right for pie charts --- .../components/charts/circular-charts/pie.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index 148e43a6781..c08fed148f3 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -1,4 +1,5 @@ import type { VisualizationSpec } from "svelte-vega"; +import type { Config } from "vega-lite"; import { createColorEncoding, createConfig, @@ -9,12 +10,23 @@ import { import type { ChartDataResult } from "../types"; import type { CircularChartSpec } from "./CircularChart"; +/** + * The layout property is not typed in the current version of Vega-Lite. + * This will be fixed when we upgrade to Svelte 5 and subseqent Vega-Lite versions. + */ export function generateVLPieChartSpec( config: CircularChartSpec, data: ChartDataResult, ): VisualizationSpec { const spec = createSingleLayerBaseSpec("arc"); - const vegaConfig = createConfig(config); + const vegaConfig = createConfig(config, { + legend: { + orient: "right", + layout: { + right: { anchor: "middle" }, + }, + }, + } as unknown as Config); spec.mark = { type: "arc", From e88d3481f888bab440f1ac9d9788dce079d0ad3f Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Fri, 18 Apr 2025 20:23:08 +0530 Subject: [PATCH 25/62] Preserve matching properties when switching charts --- .../canvas/components/charts/BaseChart.ts | 43 ++++++++++-- .../charts/cartesian-charts/CartesianChart.ts | 58 ++++++++-------- .../charts/circular-charts/CircularChart.ts | 58 ++++++++-------- .../charts/heatmap-charts/HeatmapChart.ts | 66 ++++++++++--------- .../src/features/canvas/components/util.ts | 2 + 5 files changed, 135 insertions(+), 92 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 2f1c2d80717..2544429c910 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -74,7 +74,7 @@ export abstract class BaseChart< }; } - protected abstract getChartSpecificOptions(): Record< + abstract getChartSpecificOptions(): Record< AllKeys, ComponentInputParam >; @@ -102,7 +102,6 @@ export abstract class BaseChart< const currentSpec = get(this.specStore); const parentPath = this.pathInYAML.slice(0, -1); - this.chartType.set(key); const parseDocumentStore = this.parent.parsedContent; const parsedDocument = get(parseDocumentStore); @@ -113,12 +112,20 @@ export abstract class BaseChart< metricsViewSpec, ); - const commonProps = this.extractCommonProperties(currentSpec); + const commonProps = this.extractCommonProperties( + currentSpec, + this.type, + key, + ); const mergedSpec = { ...newSpecForKey, ...commonProps, }; + this.chartType.set(key); + + // this.parent._rows.refresh(); + // Preserve the width from the current chart const width = parsedDocument.getIn([...parentPath, "width"]); @@ -129,7 +136,11 @@ export abstract class BaseChart< updateEditorContent(parsedDocument.toString(), false, true); } - private extractCommonProperties(spec: TConfig): Partial { + private extractCommonProperties( + spec: TConfig, + sourceType: ChartType, + targetType: ChartType, + ): Partial { const { metrics_view, title, @@ -139,6 +150,29 @@ export abstract class BaseChart< dimension_filters, } = spec; + const sourceChartParams = + CHART_CONFIG[sourceType].component.chartInputParams || {}; + const targetChartParams = + CHART_CONFIG[targetType].component.chartInputParams || {}; + + // Check for common keys and type match first + const commonProps = Object.keys(sourceChartParams).filter((key) => { + const isKeyAndTypeMatch = + targetChartParams?.[key]?.type === sourceChartParams[key]?.type; + const isFieldTypeMatch = + targetChartParams?.[key]?.meta?.chartFieldInput?.type === + sourceChartParams[key]?.meta?.chartFieldInput?.type; + return isKeyAndTypeMatch && isFieldTypeMatch; + }); + + const commonPropsObject = commonProps.reduce( + (acc, key) => { + acc[key] = spec[key]; + return acc; + }, + {} as Record, + ); + return { metrics_view, title, @@ -146,6 +180,7 @@ export abstract class BaseChart< vl_config, time_filters, dimension_filters, + ...commonPropsObject, }; } } diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index 5438b0bd937..c251137c25b 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -38,38 +38,40 @@ export type CartesianChartSpec = BaseChartConfig & { }; export class CartesianChartComponent extends BaseChart { - constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { - super(resource, parent, path); - } - - protected getChartSpecificOptions(): Record { - return { - x: { - type: "positional", - label: "X-axis", - meta: { - chartFieldInput: { - type: "dimension", - axisTitleSelector: true, - sortSelector: true, - limitSelector: true, - nullSelector: true, - }, + static chartInputParams: Record = { + x: { + type: "positional", + label: "X-axis", + meta: { + chartFieldInput: { + type: "dimension", + axisTitleSelector: true, + sortSelector: true, + limitSelector: true, + nullSelector: true, }, }, - y: { - type: "positional", - label: "Y-axis", - meta: { - chartFieldInput: { - type: "measure", - axisTitleSelector: true, - originSelector: true, - }, + }, + y: { + type: "positional", + label: "Y-axis", + meta: { + chartFieldInput: { + type: "measure", + axisTitleSelector: true, + originSelector: true, }, }, - color: { type: "mark", label: "Color", meta: { type: "color" } }, - }; + }, + color: { type: "mark", label: "Color", meta: { type: "color" } }, + }; + + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + super(resource, parent, path); + } + + getChartSpecificOptions(): Record { + return CartesianChartComponent.chartInputParams; } createChartDataQuery( diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index f7aad33e524..abc5df3e360 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -33,38 +33,40 @@ type CircularChartEncoding = { export type CircularChartSpec = BaseChartConfig & CircularChartEncoding; export class CircularChartComponent extends BaseChart { - constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { - super(resource, parent, path); - } - - protected getChartSpecificOptions(): Record { - return { - color: { - type: "positional", - label: "Color", - meta: { - chartFieldInput: { - type: "dimension", - nullSelector: true, - limitSelector: true, - hideTimeDimension: true, - }, + static chartInputParams: Record = { + color: { + type: "positional", + label: "Color", + meta: { + chartFieldInput: { + type: "dimension", + nullSelector: true, + limitSelector: true, + hideTimeDimension: true, }, }, - measure: { - type: "positional", - label: "Measure", - meta: { - chartFieldInput: { - type: "measure", - }, + }, + measure: { + type: "positional", + label: "Measure", + meta: { + chartFieldInput: { + type: "measure", }, }, - innerRadius: { - type: "number", - label: "Inner Radius", - }, - }; + }, + innerRadius: { + type: "number", + label: "Inner Radius", + }, + }; + + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + super(resource, parent, path); + } + + getChartSpecificOptions(): Record { + return CircularChartComponent.chartInputParams; } createChartDataQuery( diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index 4dce02c9c98..71d363e6aa7 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -26,44 +26,46 @@ export type HeatmapChartSpec = BaseChartConfig & { }; export class HeatmapChartComponent extends BaseChart { - constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { - super(resource, parent, path); - } - - protected getChartSpecificOptions(): Record { - return { - x: { - type: "positional", - label: "X-axis", - meta: { - chartFieldInput: { - type: "dimension", - axisTitleSelector: true, - nullSelector: true, - }, + static chartInputParams: Record = { + x: { + type: "positional", + label: "X-axis", + meta: { + chartFieldInput: { + type: "dimension", + axisTitleSelector: true, + nullSelector: true, }, }, - y: { - type: "positional", - label: "Y-axis", - meta: { - chartFieldInput: { - type: "dimension", - axisTitleSelector: true, - nullSelector: true, - }, + }, + y: { + type: "positional", + label: "Y-axis", + meta: { + chartFieldInput: { + type: "dimension", + axisTitleSelector: true, + nullSelector: true, }, }, - color: { - type: "positional", - label: "Color", - meta: { - chartFieldInput: { - type: "measure", - }, + }, + color: { + type: "positional", + label: "Color", + meta: { + chartFieldInput: { + type: "measure", }, }, - }; + }, + }; + + constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { + super(resource, parent, path); + } + + getChartSpecificOptions(): Record { + return HeatmapChartComponent.chartInputParams; } createChartDataQuery( diff --git a/web-common/src/features/canvas/components/util.ts b/web-common/src/features/canvas/components/util.ts index e2b4b8fdd04..a49133f7be4 100644 --- a/web-common/src/features/canvas/components/util.ts +++ b/web-common/src/features/canvas/components/util.ts @@ -92,6 +92,8 @@ export interface BaseCanvasComponentConstructor< path: ComponentPath, ): BaseCanvasComponent; + chartInputParams?: Record; + newComponentSpec( metricsViewName: string, metricsViewSpec?: V1MetricsViewSpec, From 00af02532ee2e6752516b6885f53d59e527d7f48 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Fri, 18 Apr 2025 22:16:49 +0530 Subject: [PATCH 26/62] Optimistically update chart type --- .../canvas/components/BaseCanvasComponent.ts | 16 ++--- .../canvas/components/charts/BaseChart.ts | 25 ++++++-- .../inspector/VisualCanvasEditing.svelte | 17 +++--- .../features/canvas/stores/canvas-entity.ts | 59 +++++++++++++------ .../workspaces/CanvasWorkspace.svelte | 20 +++++-- 5 files changed, 95 insertions(+), 42 deletions(-) diff --git a/web-common/src/features/canvas/components/BaseCanvasComponent.ts b/web-common/src/features/canvas/components/BaseCanvasComponent.ts index fa893681cae..c5f86008b00 100644 --- a/web-common/src/features/canvas/components/BaseCanvasComponent.ts +++ b/web-common/src/features/canvas/components/BaseCanvasComponent.ts @@ -12,19 +12,19 @@ import type { V1Resource, V1TimeRange, } from "@rilldata/web-common/runtime-client"; +import type { ComponentType, SvelteComponent } from "svelte"; import { derived, get, writable, type Writable } from "svelte/store"; -import { CanvasComponentState } from "../stores/canvas-component"; -import type { CanvasEntity, ComponentPath } from "../stores/canvas-entity"; -import type { - ComparisonTimeRangeState, - TimeRangeState, -} from "../../dashboards/time-controls/time-control-store"; +import { mergeFilters } from "../../dashboards/pivot/pivot-merge-filters"; import { buildValidMetricsViewFilter, createAndExpression, } from "../../dashboards/stores/filter-utils"; -import { mergeFilters } from "../../dashboards/pivot/pivot-merge-filters"; -import type { ComponentType, SvelteComponent } from "svelte"; +import type { + ComparisonTimeRangeState, + TimeRangeState, +} from "../../dashboards/time-controls/time-control-store"; +import { CanvasComponentState } from "../stores/canvas-component"; +import type { CanvasEntity, ComponentPath } from "../stores/canvas-entity"; export abstract class BaseCanvasComponent { id: string; diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 2544429c910..a7c0df31cb7 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -2,6 +2,7 @@ import { BaseCanvasComponent } from "@rilldata/web-common/features/canvas/compon import { CHART_CONFIG } from "@rilldata/web-common/features/canvas/components/charts"; import { commonOptions, + createComponent, getFilterOptions, } from "@rilldata/web-common/features/canvas/components/util"; import type { @@ -62,6 +63,7 @@ export abstract class BaseChart< } inputParams(): InputParams { + console.log("inputParams", this.type, this.getChartSpecificOptions()); return { options: { metrics_view: { type: "metrics", label: "Metrics view" }, @@ -122,18 +124,33 @@ export abstract class BaseChart< ...commonProps, }; - this.chartType.set(key); + const newResource = this.parent.createOptimisticResource({ + type: key, + row: this.pathInYAML[1], + column: this.pathInYAML[3], + metricsViewName: currentSpec.metrics_view, + metricsViewSpec, + spec: mergedSpec, + }); + + const newComponent = createComponent( + newResource, + this.parent, + this.pathInYAML, + ); - // this.parent._rows.refresh(); + this.parent.components.set(newComponent.id, newComponent); + this.parent.selectedComponent.set(newComponent.id); + this.parent._rows.refresh(); // Preserve the width from the current chart const width = parsedDocument.getIn([...parentPath, "width"]); - // Update the chart type and spec - this.chartType.set(key); parsedDocument.setIn(parentPath, { [key]: mergedSpec, width }); updateEditorContent(parsedDocument.toString(), false, true); + + this.chartType.set(key); } private extractCommonProperties( diff --git a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte index c398aed3154..39ef1388624 100644 --- a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte +++ b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte @@ -18,6 +18,7 @@ $: ({ editorContent, updateEditorContent, saveLocalContent, path } = fileArtifact); + $: console.log("selectedComponent", $selectedComponent, components); $: parsedDocument = parseDocument($editorContent ?? ""); $: component = components.get($selectedComponent ?? ""); @@ -57,10 +58,12 @@ } - - {#if component} - - {:else} - - {/if} - +{#key component && $selectedComponent} + + {#if component} + + {:else} + + {/if} + +{/key} diff --git a/web-common/src/features/canvas/stores/canvas-entity.ts b/web-common/src/features/canvas/stores/canvas-entity.ts index e30292d7604..7ed66f13b9b 100644 --- a/web-common/src/features/canvas/stores/canvas-entity.ts +++ b/web-common/src/features/canvas/stores/canvas-entity.ts @@ -17,22 +17,22 @@ import { type Readable, type Unsubscriber, } from "svelte/store"; -import { Filters } from "./filters"; -import { CanvasResolvedSpec } from "./spec"; -import { TimeControls } from "./time-control"; +import { parseDocument } from "yaml"; +import type { FileArtifact } from "../../entity-management/file-artifact"; +import { fileArtifacts } from "../../entity-management/file-artifacts"; +import { ResourceKind } from "../../entity-management/resource-selectors"; import type { BaseCanvasComponent } from "../components/BaseCanvasComponent"; +import type { CanvasComponentType, ComponentSpec } from "../components/types"; import { COMPONENT_CLASS_MAP, createComponent, isChartComponentType, isTableComponentType, } from "../components/util"; -import type { FileArtifact } from "../../entity-management/file-artifact"; -import { parseDocument } from "yaml"; -import { fileArtifacts } from "../../entity-management/file-artifacts"; -import type { CanvasComponentType } from "../components/types"; -import { ResourceKind } from "../../entity-management/resource-selectors"; +import { Filters } from "./filters"; import { Grid } from "./grid"; +import { CanvasResolvedSpec } from "./spec"; +import { TimeControls } from "./time-control"; export class CanvasEntity { name: string; @@ -190,6 +190,7 @@ export class CanvasEntity { existingClass.update(newResource, path); } else { createdNewComponent = true; + console.log("createdNewComponent", componentName, newType); this.components.set( componentName, createComponent(newResource, this, path), @@ -210,6 +211,8 @@ export class CanvasEntity { // Calling this function triggers the rows to rerender, ensuring they're up to date // with the components Map, which is not reactive if ((!didUpdateRowCount && createdNewComponent) || this.firstLoad) { + console.log("refreshing rows"); + this.selectedComponent.update((v) => v); this._rows.refresh(); } this.firstLoad = false; @@ -225,13 +228,16 @@ export class CanvasEntity { column: number; metricsViewName: string; metricsViewSpec: V1MetricsViewSpec | undefined; + spec?: ComponentSpec; }): V1Resource => { const { type, row, column, metricsViewName, metricsViewSpec } = options; - const spec = COMPONENT_CLASS_MAP[type].newComponentSpec( - metricsViewName, - metricsViewSpec, - ); + const spec = + options.spec ?? + COMPONENT_CLASS_MAP[type].newComponentSpec( + metricsViewName, + metricsViewSpec, + ); return { meta: { @@ -286,9 +292,28 @@ function areSameType( newType: CanvasComponentType, existingType: CanvasComponentType, ) { - return ( - newType === existingType || - (isTableComponentType(existingType) && isTableComponentType(newType)) || - (isChartComponentType(existingType) && isChartComponentType(newType)) - ); + if (newType === existingType) return true; + + // For chart types, check if they use the same component class + if (isChartComponentType(existingType) && isChartComponentType(newType)) { + const cartesian = [ + "bar_chart", + "line_chart", + "area_chart", + "stacked_bar", + "stacked_bar_normalized", + ]; + + if (cartesian.includes(existingType) && cartesian.includes(newType)) { + return true; + } + + return false; + + // const newComponent = CHART_CONFIG[newType].component; + // const existingComponent = CHART_CONFIG[existingType].component; + // return newComponent.name === existingComponent.name; + } + + return isTableComponentType(existingType) && isTableComponentType(newType); } diff --git a/web-common/src/features/workspaces/CanvasWorkspace.svelte b/web-common/src/features/workspaces/CanvasWorkspace.svelte index 607f3021152..c2051d5c294 100644 --- a/web-common/src/features/workspaces/CanvasWorkspace.svelte +++ b/web-common/src/features/workspaces/CanvasWorkspace.svelte @@ -6,6 +6,7 @@ import VisualCanvasEditing from "@rilldata/web-common/features/canvas/inspector/VisualCanvasEditing.svelte"; import { getNameFromFile } from "@rilldata/web-common/features/entity-management/entity-mappers"; import type { FileArtifact } from "@rilldata/web-common/features/entity-management/file-artifact"; + import { getCanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import { resourceIsLoading, ResourceKind, @@ -41,6 +42,10 @@ hasUnsavedChanges, } = fileArtifact); + $: ({ + canvasEntity: { _rows }, + } = getCanvasStore(canvasName)); + $: resourceQuery = getResource(queryClient, instanceId); $: ({ data } = $resourceQuery); @@ -135,12 +140,15 @@ {/if} - + + {#key $_rows} + + {/key} + {/key} From 3676efd00bf56afdad30770e400cd6d714b7a61e Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Mon, 21 Apr 2025 15:51:59 +0530 Subject: [PATCH 27/62] Remove logs, debug statements --- web-common/src/features/canvas/components/charts/BaseChart.ts | 1 - .../src/features/canvas/inspector/VisualCanvasEditing.svelte | 1 - web-common/src/features/canvas/stores/canvas-entity.ts | 3 --- 3 files changed, 5 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index a7c0df31cb7..fa722aff02e 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -63,7 +63,6 @@ export abstract class BaseChart< } inputParams(): InputParams { - console.log("inputParams", this.type, this.getChartSpecificOptions()); return { options: { metrics_view: { type: "metrics", label: "Metrics view" }, diff --git a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte index 39ef1388624..d105e522d92 100644 --- a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte +++ b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte @@ -18,7 +18,6 @@ $: ({ editorContent, updateEditorContent, saveLocalContent, path } = fileArtifact); - $: console.log("selectedComponent", $selectedComponent, components); $: parsedDocument = parseDocument($editorContent ?? ""); $: component = components.get($selectedComponent ?? ""); diff --git a/web-common/src/features/canvas/stores/canvas-entity.ts b/web-common/src/features/canvas/stores/canvas-entity.ts index 7ed66f13b9b..0bd849a26e4 100644 --- a/web-common/src/features/canvas/stores/canvas-entity.ts +++ b/web-common/src/features/canvas/stores/canvas-entity.ts @@ -190,7 +190,6 @@ export class CanvasEntity { existingClass.update(newResource, path); } else { createdNewComponent = true; - console.log("createdNewComponent", componentName, newType); this.components.set( componentName, createComponent(newResource, this, path), @@ -211,8 +210,6 @@ export class CanvasEntity { // Calling this function triggers the rows to rerender, ensuring they're up to date // with the components Map, which is not reactive if ((!didUpdateRowCount && createdNewComponent) || this.firstLoad) { - console.log("refreshing rows"); - this.selectedComponent.update((v) => v); this._rows.refresh(); } this.firstLoad = false; From 1dfbb3ebfae524438d7e267375141ffd4b98133d Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Mon, 21 Apr 2025 15:59:48 +0530 Subject: [PATCH 28/62] clean up --- .../canvas/components/charts/Chart.svelte | 10 ++-------- .../canvas/inspector/VisualCanvasEditing.svelte | 16 +++++++--------- .../src/features/canvas/stores/canvas-entity.ts | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/Chart.svelte b/web-common/src/features/canvas/components/charts/Chart.svelte index 038c9da721d..f7d674e8ad8 100644 --- a/web-common/src/features/canvas/components/charts/Chart.svelte +++ b/web-common/src/features/canvas/components/charts/Chart.svelte @@ -36,14 +36,8 @@ $: chartSpec = $specStore; - $: ({ - title, - description, - metrics_view, - vl_config, - time_filters, - dimension_filters, - } = chartSpec); + $: ({ title, description, metrics_view, time_filters, dimension_filters } = + chartSpec); $: schemaStore = validateChartSchema(store, chartSpec); diff --git a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte index d105e522d92..c398aed3154 100644 --- a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte +++ b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte @@ -57,12 +57,10 @@ } -{#key component && $selectedComponent} - - {#if component} - - {:else} - - {/if} - -{/key} + + {#if component} + + {:else} + + {/if} + diff --git a/web-common/src/features/canvas/stores/canvas-entity.ts b/web-common/src/features/canvas/stores/canvas-entity.ts index 0bd849a26e4..4c9e3304fb5 100644 --- a/web-common/src/features/canvas/stores/canvas-entity.ts +++ b/web-common/src/features/canvas/stores/canvas-entity.ts @@ -304,9 +304,9 @@ function areSameType( if (cartesian.includes(existingType) && cartesian.includes(newType)) { return true; } - return false; + // FIXME: The below causes a fatal crash through a dependency cycle // const newComponent = CHART_CONFIG[newType].component; // const existingComponent = CHART_CONFIG[existingType].component; // return newComponent.name === existingComponent.name; From 0ba76ec569e5715b05528584c3653464e1cf87bb Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Tue, 22 Apr 2025 21:07:15 +0530 Subject: [PATCH 29/62] default heatmap legend at bottom --- .../features/canvas/components/charts/heatmap-charts/spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts index 443f19c38ba..19d7987c099 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts @@ -18,6 +18,9 @@ export function generateVLHeatmapSpec( const spec = createSingleLayerBaseSpec("rect"); const vegaConfig = createConfig(config, { + legend: { + orient: "bottom", + }, axis: { grid: true, tickBand: "extent" }, axisX: { grid: true, From 35214deec3614eee40ad5e2d5a538889553a1ba2 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 23 Apr 2025 22:30:00 +0530 Subject: [PATCH 30/62] refactor to use query options paradigm --- .../charts/cartesian-charts/CartesianChart.ts | 201 ++++++++---------- .../charts/circular-charts/CircularChart.ts | 44 ++-- .../charts/heatmap-charts/HeatmapChart.ts | 155 ++++++++++---- .../canvas/components/charts/selector.ts | 6 +- .../canvas/components/charts/types.ts | 12 +- .../features/canvas/components/charts/util.ts | 21 +- 6 files changed, 252 insertions(+), 187 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index c251137c25b..72f19e0230f 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -1,24 +1,20 @@ +import { getFilterWithNullHandling } from "@rilldata/web-common/features/canvas/components/charts/util"; import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import { - createQueryServiceMetricsViewAggregation, + getQueryServiceMetricsViewAggregationQueryOptions, type V1Expression, type V1MetricsViewAggregationDimension, type V1MetricsViewAggregationMeasure, - type V1MetricsViewAggregationResponse, type V1MetricsViewAggregationSort, type V1MetricsViewSpec, type V1Resource, } from "@rilldata/web-common/runtime-client"; -import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; -import { - keepPreviousData, - type CreateQueryResult, -} from "@tanstack/svelte-query"; -import { derived, get, readable, type Readable } from "svelte/store"; +import { createQuery, keepPreviousData } from "@tanstack/svelte-query"; +import { derived, get, type Readable } from "svelte/store"; import type { CanvasEntity, ComponentPath, @@ -91,119 +87,108 @@ export class CartesianChartComponent extends BaseChart { let limit: number | undefined; let hasColorDimension = false; - return derived( - [ctx.runtime, timeAndFilterStore], - ([runtime, $timeAndFilterStore], set) => { - const { timeRange, where, timeGrain } = $timeAndFilterStore; + const dimensionName = config.x?.field; - let outerWhere = where; + if (config.x?.type === "nominal" && dimensionName) { + limit = config.x.limit ?? 100; + sort = this.vegaSortToAggregationSort(config.x?.sort, config); + dimensions = [{ name: dimensionName }]; + } else if (config.x?.type === "temporal" && dimensionName) { + dimensions = [{ name: dimensionName }]; + } - if (config.x?.type === "nominal" && config.x?.field) { - limit = config.x.limit; - sort = this.vegaSortToAggregationSort(config.x?.sort, config); - dimensions = [{ name: config.x?.field }]; + if (typeof config.color === "object" && config.color?.field) { + dimensions = [...dimensions, { name: config.color.field }]; + hasColorDimension = true; + } - const showNull = !!config.x.showNull; - if (!showNull) { - const excludeNullFilter = createInExpression( - config.x?.field, - [null], - true, - ); - outerWhere = mergeFilters(where, excludeNullFilter); - } - } else if (config.x?.type === "temporal" && timeGrain) { - dimensions = [{ name: config.x?.field, timeGrain }]; - } + // Create topN query options store + const topNQueryOptionsStore = derived( + [ctx.runtime, timeAndFilterStore], + ([runtime, $timeAndFilterStore]) => { + const { timeRange, where } = $timeAndFilterStore; + const enabled = + !!timeRange?.start && !!timeRange?.end && hasColorDimension; + + const topNWhere = getFilterWithNullHandling(where, config.x); + + return getQueryServiceMetricsViewAggregationQueryOptions( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions: [{ name: dimensionName }], + sort: sort ? [sort] : undefined, + where: topNWhere, + timeRange, + limit: limit?.toString(), + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ); + }, + ); - if (typeof config.color === "object" && config.color?.field) { - dimensions = [...dimensions, { name: config.color.field }]; - hasColorDimension = true; - } + const topNQuery = createQuery(topNQueryOptionsStore); - let topNQuery: - | Readable - | CreateQueryResult = - readable(null); + const queryOptionsStore = derived( + [ctx.runtime, timeAndFilterStore, topNQuery], + ([runtime, $timeAndFilterStore, $topNQuery]) => { + const { timeRange, where, timeGrain } = $timeAndFilterStore; + const topNData = $topNQuery?.data?.data; + const enabled = + !!timeRange?.start && + !!timeRange?.end && + (hasColorDimension ? !!topNData?.length : true); + + let combinedWhere: V1Expression | undefined = getFilterWithNullHandling( + where, + config.x, + ); + if (topNData?.length && dimensionName) { + const topValues = topNData.map((d) => d[dimensionName] as string); + const filterForTopValues = createInExpression( + dimensionName, + topValues, + ); - const enabled = !!timeRange?.start && !!timeRange?.end; + combinedWhere = mergeFilters(where, filterForTopValues); + } - if (limit && hasColorDimension) { - topNQuery = createQueryServiceMetricsViewAggregation( - runtime.instanceId, - config.metrics_view, - { - measures, - dimensions: [{ name: config.x?.field }], - sort: sort ? [sort] : undefined, - where: outerWhere, - timeRange, - limit: limit.toString(), - }, - { - query: { - enabled, - placeholderData: keepPreviousData, - }, - }, - ctx.queryClient, + // Update dimensions with timeGrain if temporal + if (config.x?.type === "temporal" && timeGrain) { + dimensions = dimensions.map((d) => + d.name === dimensionName ? { ...d, timeGrain } : d, ); } - return derived(topNQuery, ($topNQuery, topNSet) => { - if ($topNQuery !== null && !$topNQuery?.data) { - return topNSet({ - isFetching: $topNQuery.isFetching, - error: $topNQuery.error, - data: undefined, - }); - } - - const dimensionName = config.x?.field; - - let combinedWhere: V1Expression | undefined = outerWhere; - if ($topNQuery?.data?.data?.length && dimensionName) { - const topValues = $topNQuery?.data?.data.map( - (d) => d[dimensionName] as string, - ); - const filterForTopValues = createInExpression( - dimensionName, - topValues, - ); - - combinedWhere = mergeFilters(where, filterForTopValues); - } - - const dataQuery = createQueryServiceMetricsViewAggregation( - runtime.instanceId, - config.metrics_view, - { - measures, - dimensions, - sort: sort ? [sort] : undefined, - where: combinedWhere, - timeRange, - limit: hasColorDimension || !limit ? "5000" : limit.toString(), - }, - { - query: { - enabled, - placeholderData: keepPreviousData, - }, + return getQueryServiceMetricsViewAggregationQueryOptions( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions, + sort: sort ? [sort] : undefined, + where: combinedWhere, + timeRange, + limit: hasColorDimension || !limit ? "5000" : limit?.toString(), + }, + { + query: { + enabled, + placeholderData: keepPreviousData, }, - ctx.queryClient, - ); - - return derived(dataQuery, ($dataQuery) => { - return { - isFetching: $dataQuery.isFetching, - error: $dataQuery.error, - data: $dataQuery?.data?.data, - }; - }).subscribe(topNSet); - }).subscribe(set); + }, + ); }, ); + + const query = createQuery(queryOptionsStore); + return query; } static newComponentSpec( diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index abc5df3e360..0fcad1311b4 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -2,21 +2,20 @@ import type { ChartFieldsMap, FieldConfig, } from "@rilldata/web-common/features/canvas/components/charts/types"; +import { getFilterWithNullHandling } from "@rilldata/web-common/features/canvas/components/charts/util"; import type { ComponentInputParam } from "@rilldata/web-common/features/canvas/inspector/types"; import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types"; -import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; -import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import type { V1MetricsViewSpec, V1Resource, } from "@rilldata/web-common/runtime-client"; import { - createQueryServiceMetricsViewAggregation, + getQueryServiceMetricsViewAggregationQueryOptions, type V1MetricsViewAggregationDimension, type V1MetricsViewAggregationMeasure, } from "@rilldata/web-common/runtime-client"; -import { keepPreviousData } from "@tanstack/svelte-query"; +import { createQuery, keepPreviousData } from "@tanstack/svelte-query"; import { derived, get, type Readable } from "svelte/store"; import type { CanvasEntity, @@ -90,29 +89,26 @@ export class CircularChartComponent extends BaseChart { showNull = !!config.color.showNull; } - return derived( + const queryOptionsStore = derived( [ctx.runtime, timeAndFilterStore], - ([runtime, $timeAndFilterStore], set) => { + ([runtime, $timeAndFilterStore]) => { const { timeRange, where } = $timeAndFilterStore; const enabled = !!timeRange?.start && !!timeRange?.end; - let mergedWhere = where; - if (!showNull && config.color?.field) { - const excludeNullFilter = createInExpression( - config.color?.field, - [null], - true, - ); - mergedWhere = mergeFilters(where, excludeNullFilter); - } - - const dataQuery = createQueryServiceMetricsViewAggregation( + const nullHandledWhere = getFilterWithNullHandling(where, config.color); + + const queryOptions = getQueryServiceMetricsViewAggregationQueryOptions( runtime.instanceId, config.metrics_view, { measures, dimensions, - where: mergedWhere, + where: nullHandledWhere, + sort: [ + ...(config.measure?.field + ? [{ name: config.measure.field, desc: true }] + : []), + ], timeRange, limit: limit.toString(), }, @@ -122,18 +118,14 @@ export class CircularChartComponent extends BaseChart { placeholderData: keepPreviousData, }, }, - ctx.queryClient, ); - return derived(dataQuery, ($dataQuery) => { - return { - isFetching: $dataQuery.isFetching, - error: $dataQuery.error, - data: $dataQuery?.data?.data, - }; - }).subscribe(set); + return queryOptions; }, ); + + const query = createQuery(queryOptionsStore); + return query; } chartTitle(fields: ChartFieldsMap) { diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index 71d363e6aa7..c7794eabb79 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -4,13 +4,14 @@ import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/st import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import { - createQueryServiceMetricsViewAggregation, + getQueryServiceMetricsViewAggregationQueryOptions, + type V1Expression, type V1MetricsViewAggregationDimension, type V1MetricsViewAggregationMeasure, type V1MetricsViewSpec, type V1Resource, } from "@rilldata/web-common/runtime-client"; -import { keepPreviousData } from "@tanstack/svelte-query"; +import { createQuery, keepPreviousData } from "@tanstack/svelte-query"; import { derived, get, type Readable } from "svelte/store"; import type { CanvasEntity, @@ -18,6 +19,7 @@ import type { } from "../../../stores/canvas-entity"; import { BaseChart, type BaseChartConfig } from "../BaseChart"; import type { ChartDataQuery, ChartFieldsMap, FieldConfig } from "../types"; +import { getFilterWithNullHandling } from "../util"; export type HeatmapChartSpec = BaseChartConfig & { x?: FieldConfig; @@ -33,6 +35,7 @@ export class HeatmapChartComponent extends BaseChart { meta: { chartFieldInput: { type: "dimension", + limitSelector: true, axisTitleSelector: true, nullSelector: true, }, @@ -44,6 +47,7 @@ export class HeatmapChartComponent extends BaseChart { meta: { chartFieldInput: { type: "dimension", + limitSelector: true, axisTitleSelector: true, nullSelector: true, }, @@ -81,53 +85,127 @@ export class HeatmapChartComponent extends BaseChart { measures = [{ name: config.color.field }]; } - if (config.x?.field) { - dimensions = [...dimensions, { name: config.x.field }]; - } + // Create top level options store for X axis + const xAxisQueryOptionsStore = derived( + [ctx.runtime, timeAndFilterStore], + ([runtime, $timeAndFilterStore]) => { + const { timeRange, where } = $timeAndFilterStore; + const enabled = + !!timeRange?.start && !!timeRange?.end && !!config.x?.field; - if (config.y?.field) { - dimensions = [...dimensions, { name: config.y.field }]; - } + const xWhere = getFilterWithNullHandling(where, config.x); - return derived( + let limit = "100"; + if (config.x?.limit && config.x.type !== "temporal") { + limit = config.x.limit.toString(); + } + + return getQueryServiceMetricsViewAggregationQueryOptions( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions: [{ name: config.x?.field }], + sort: [{ name: config.x?.field, desc: true }], + where: xWhere, + timeRange, + limit, + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ); + }, + ); + + // Create top level options store for Y axis + const yAxisQueryOptionsStore = derived( [ctx.runtime, timeAndFilterStore], - ([runtime, $timeAndFilterStore], set) => { + ([runtime, $timeAndFilterStore]) => { + const { timeRange, where } = $timeAndFilterStore; + const enabled = + !!timeRange?.start && !!timeRange?.end && !!config.y?.field; + + const yWhere = getFilterWithNullHandling(where, config.y); + + let limit = "100"; + if (config.y?.limit && config.y.type !== "temporal") { + limit = config.y.limit.toString(); + } + + return getQueryServiceMetricsViewAggregationQueryOptions( + runtime.instanceId, + config.metrics_view, + { + measures, + dimensions: [{ name: config.y?.field }], + sort: [{ name: config.y?.field, desc: true }], + where: yWhere, + timeRange, + limit, + }, + { + query: { + enabled, + placeholderData: keepPreviousData, + }, + }, + ); + }, + ); + + const xAxisQuery = createQuery(xAxisQueryOptionsStore); + const yAxisQuery = createQuery(yAxisQueryOptionsStore); + + const queryOptionsStore = derived( + [ctx.runtime, timeAndFilterStore, xAxisQuery, yAxisQuery], + ([runtime, $timeAndFilterStore, $xAxisQuery, $yAxisQuery]) => { const { timeRange, where } = $timeAndFilterStore; + const xTopNData = $xAxisQuery?.data?.data; + const yTopNData = $yAxisQuery?.data?.data; - let combinedWhere = where; + const enabled = + !!timeRange?.start && + !!timeRange?.end && + !!xTopNData?.length && + !!yTopNData?.length; - // Handle null filtering for both x and y dimensions - if (config.x?.field && !config.x.showNull) { - const excludeNullFilter = createInExpression( - config.x.field, - [null], - true, - ); - combinedWhere = mergeFilters(combinedWhere, excludeNullFilter); + let combinedWhere: V1Expression | undefined = where; + + if (xTopNData?.length && config.x?.field) { + const xField = config.x.field; + const xTopValues = xTopNData.map((d) => d[xField] as string); + const xFilterForTopValues = createInExpression(xField, xTopValues); + combinedWhere = mergeFilters(combinedWhere, xFilterForTopValues); + } + + if (yTopNData?.length && config.y?.field) { + const yField = config.y.field; + const yTopValues = yTopNData.map((d) => d[yField] as string); + const yFilterForTopValues = createInExpression(yField, yTopValues); + combinedWhere = mergeFilters(combinedWhere, yFilterForTopValues); } - if (config.y?.field && !config.y.showNull) { - const excludeNullFilter = createInExpression( - config.y.field, - [null], - true, - ); - combinedWhere = mergeFilters(combinedWhere, excludeNullFilter); + if (config.x?.field) { + dimensions = [...dimensions, { name: config.x.field }]; } - const enabled = !!timeRange?.start && !!timeRange?.end; + if (config.y?.field) { + dimensions = [...dimensions, { name: config.y.field }]; + } - const dataQuery = createQueryServiceMetricsViewAggregation( + return getQueryServiceMetricsViewAggregationQueryOptions( runtime.instanceId, config.metrics_view, { measures, dimensions, - sort: [ - ...(config.x?.field - ? [{ name: config.x.field, desc: true }] - : []), - ], + sort: config.x?.field + ? [{ name: config.x.field, desc: true }] + : undefined, where: combinedWhere, timeRange, limit: "5000", // Higher limit for heatmap to show more data points @@ -138,18 +216,11 @@ export class HeatmapChartComponent extends BaseChart { placeholderData: keepPreviousData, }, }, - ctx.queryClient, ); - - return derived(dataQuery, ($dataQuery) => { - return { - isFetching: $dataQuery.isFetching, - error: $dataQuery.error, - data: $dataQuery?.data?.data, - }; - }).subscribe(set); }, ); + + return createQuery(queryOptionsStore); } static newComponentSpec( diff --git a/web-common/src/features/canvas/components/charts/selector.ts b/web-common/src/features/canvas/components/charts/selector.ts index 34b38839a6e..28e16d5e1dc 100644 --- a/web-common/src/features/canvas/components/charts/selector.ts +++ b/web-common/src/features/canvas/components/charts/selector.ts @@ -60,9 +60,9 @@ export function getChartData( >, ); return { - data: chartData.data || [], - isFetching: chartData.isFetching, - error: chartData.error, + data: chartData?.data?.data || [], + isFetching: chartData?.isFetching ?? false, + error: chartData?.error, fields: fieldSpecMap, }; }, diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index b7cc1322979..2fa90015de9 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -2,6 +2,7 @@ import type { V1Expression, V1MetricsViewAggregationDimension, V1MetricsViewAggregationMeasure, + V1MetricsViewAggregationResponse, V1MetricsViewAggregationSort, } from "@rilldata/web-common/runtime-client"; import { @@ -10,7 +11,7 @@ import { type V1MetricsViewAggregationResponseDataItem, } from "@rilldata/web-common/runtime-client"; import type { HTTPError } from "@rilldata/web-common/runtime-client/fetchWrapper"; -import { type Readable } from "svelte/store"; +import type { CreateQueryResult } from "@tanstack/svelte-query"; export type ChartType = | "bar_chart" @@ -21,11 +22,10 @@ export type ChartType = | "pie_chart" | "heatmap"; -export type ChartDataQuery = Readable<{ - isFetching: boolean; - error: HTTPError | null; - data: V1MetricsViewAggregationResponseDataItem[] | undefined; -}>; +export type ChartDataQuery = CreateQueryResult< + V1MetricsViewAggregationResponse, + HTTPError +>; export type ChartFieldsMap = Record< string, diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 3546cbe2961..5119c4a233b 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,9 +1,14 @@ +import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; +import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; -import { V1TimeGrain } from "@rilldata/web-common/runtime-client"; +import { + V1TimeGrain, + type V1Expression, +} from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; import type { Config } from "vega-lite"; import { CHART_CONFIG, type ChartSpec } from "./"; -import type { ChartDataResult, ChartType } from "./types"; +import type { ChartDataResult, ChartType, FieldConfig } from "./types"; export function generateSpec( chartType: ChartType, @@ -119,3 +124,15 @@ export function getFieldsByType(spec: ChartSpec): FieldsByType { timeDimensions, }; } + +export function getFilterWithNullHandling( + where: V1Expression | undefined, + fieldConfig: FieldConfig | undefined, +): V1Expression | undefined { + if (!fieldConfig || !fieldConfig.showNull || fieldConfig.type !== "nominal") { + return where; + } + + const excludeNullFilter = createInExpression(fieldConfig.field, [null], true); + return mergeFilters(where, excludeNullFilter); +} From cf9b4e066743c6ee7064a6eaa9f96405f850df82 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 13:25:33 +0530 Subject: [PATCH 31/62] lint fix --- .../canvas/components/charts/circular-charts/CircularChart.ts | 2 -- web-common/src/features/canvas/components/charts/util.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index 0fcad1311b4..dbe7e82e2f9 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -82,11 +82,9 @@ export class CircularChartComponent extends BaseChart { } let limit: number; - let showNull = false; if (config.color?.field) { limit = config.color.limit ?? 20; dimensions = [{ name: config.color.field }]; - showNull = !!config.color.showNull; } const queryOptionsStore = derived( diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 5119c4a233b..1342ef5e568 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -129,7 +129,7 @@ export function getFilterWithNullHandling( where: V1Expression | undefined, fieldConfig: FieldConfig | undefined, ): V1Expression | undefined { - if (!fieldConfig || !fieldConfig.showNull || fieldConfig.type !== "nominal") { + if (!fieldConfig || fieldConfig.showNull || fieldConfig.type !== "nominal") { return where; } From 34251c13528363b573ada66e50006b4acd6646a6 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 14:37:06 +0530 Subject: [PATCH 32/62] Consolidate builder methods --- .../canvas/components/charts/builder.ts | 56 ++++++------------- .../charts/cartesian-charts/CartesianChart.ts | 1 + .../charts/cartesian-charts/area/spec.ts | 7 +-- .../cartesian-charts/line-chart/spec.ts | 7 +-- .../components/charts/circular-charts/pie.ts | 4 +- .../components/charts/heatmap-charts/spec.ts | 7 +-- .../canvas/components/charts/types.ts | 1 + .../src/features/canvas/inspector/types.ts | 1 + 8 files changed, 32 insertions(+), 52 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index 67566348d24..b18caaadc0c 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -46,51 +46,31 @@ export function createSingleLayerBaseSpec( }; } -export function createXEncoding( - xField: FieldConfig | undefined, +export function createPositionEncoding( + field: FieldConfig | undefined, data: ChartDataResult, ): PositionDef { - if (!xField) return {}; - const metaData = data.fields[xField.field]; + if (!field) return {}; + const metaData = data.fields[field.field]; return { - field: sanitizeValueForVega(xField.field), - title: metaData?.displayName || xField.field, - type: xField.type, + field: sanitizeValueForVega(field.field), + title: metaData?.displayName || field.field, + type: field.type, ...(metaData && "timeUnit" in metaData && { timeUnit: metaData.timeUnit }), - ...(xField.sort && xField.type !== "temporal" && { sort: xField.sort }), - axis: { - ...(xField.type === "quantitative" && { - formatType: sanitizeFieldName(xField.field), + ...(field.sort && field.type !== "temporal" && { sort: field.sort }), + ...(field.type === "quantitative" && + field.zeroBasedOrigin !== true && { + scale: { + zero: false, + }, }), - ...(metaData && "format" in metaData && { format: metaData.format }), - ...(!xField.showAxisTitle && { title: null }), - }, - }; -} - -export function createYEncoding( - yField: FieldConfig | undefined, - data: ChartDataResult, -): PositionDef { - if (!yField) return {}; - const metaData = data.fields[yField.field]; - return { - field: sanitizeValueForVega(yField.field), - title: metaData?.displayName || yField.field, - type: yField.type, - ...(yField.zeroBasedOrigin !== true && { - scale: { - zero: false, - }, - }), axis: { - ...(yField.type === "quantitative" && { - formatType: sanitizeFieldName(yField.field), + ...(field.type === "quantitative" && { + formatType: sanitizeFieldName(field.field), }), - ...(!yField.showAxisTitle && { title: null }), ...(metaData && "format" in metaData && { format: metaData.format }), + ...(!field.showAxisTitle && { title: null }), }, - ...(metaData && "timeUnit" in metaData && { timeUnit: metaData.timeUnit }), }; } @@ -181,8 +161,8 @@ export function createEncoding( data: ChartDataResult, ): Encoding { return { - x: createXEncoding(config.x, data), - y: createYEncoding(config.y, data), + x: createPositionEncoding(config.x, data), + y: createPositionEncoding(config.y, data), color: createColorEncoding(config.color, data), tooltip: createDefaultTooltipEncoding( [config.x, config.y, config.color], diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index 72f19e0230f..a42a7faf7dc 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -45,6 +45,7 @@ export class CartesianChartComponent extends BaseChart { sortSelector: true, limitSelector: true, nullSelector: true, + labelAngleSelector: true, }, }, }, diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts index ce20e3d2825..712f2150192 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts @@ -7,8 +7,7 @@ import { createConfig, createDefaultTooltipEncoding, createMultiLayerBaseSpec, - createXEncoding, - createYEncoding, + createPositionEncoding, } from "../../builder"; import type { ChartDataResult } from "../../types"; import type { CartesianChartSpec } from "../CartesianChart"; @@ -47,12 +46,12 @@ export function generateVLAreaChartSpec( multiValueTooltipChannel = multiValueTooltipChannel.slice(0, 50); } - spec.encoding = { x: createXEncoding(config.x, data) }; + spec.encoding = { x: createPositionEncoding(config.x, data) }; spec.layer = [ { encoding: { - y: { ...createYEncoding(config.y, data), stack: "zero" }, + y: { ...createPositionEncoding(config.y, data), stack: "zero" }, color: createColorEncoding(config.color, data), }, layer: [ diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts index a139377c2cf..4b354aa64f9 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts @@ -7,8 +7,7 @@ import { createConfig, createDefaultTooltipEncoding, createMultiLayerBaseSpec, - createXEncoding, - createYEncoding, + createPositionEncoding, } from "../../builder"; import type { ChartDataResult } from "../../types"; import type { CartesianChartSpec } from "../CartesianChart"; @@ -48,12 +47,12 @@ export function generateVLLineChartSpec( multiValueTooltipChannel = multiValueTooltipChannel.slice(0, 50); } - spec.encoding = { x: createXEncoding(config.x, data) }; + spec.encoding = { x: createPositionEncoding(config.x, data) }; spec.layer = [ { encoding: { - y: createYEncoding(config.y, data), + y: createPositionEncoding(config.y, data), color: createColorEncoding(config.color, data), }, layer: [ diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index c08fed148f3..5584c8951a3 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -4,8 +4,8 @@ import { createColorEncoding, createConfig, createDefaultTooltipEncoding, + createPositionEncoding, createSingleLayerBaseSpec, - createXEncoding, } from "../builder"; import type { ChartDataResult } from "../types"; import type { CircularChartSpec } from "./CircularChart"; @@ -32,7 +32,7 @@ export function generateVLPieChartSpec( type: "arc", innerRadius: config.innerRadius || 0, }; - const theta = createXEncoding(config.measure, data); + const theta = createPositionEncoding(config.measure, data); const color = createColorEncoding(config.color, data); const tooltip = createDefaultTooltipEncoding( [config.measure, config.color], diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts index 19d7987c099..0ecd0b440f0 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts @@ -4,9 +4,8 @@ import { createColorEncoding, createConfig, createDefaultTooltipEncoding, + createPositionEncoding, createSingleLayerBaseSpec, - createXEncoding, - createYEncoding, } from "../builder"; import type { ChartDataResult } from "../types"; import type { HeatmapChartSpec } from "./HeatmapChart"; @@ -32,8 +31,8 @@ export function generateVLHeatmapSpec( return { ...spec, encoding: { - x: createXEncoding(config.x, data), - y: createYEncoding(config.y, data), + x: createPositionEncoding(config.x, data), + y: createPositionEncoding(config.y, data), color: createColorEncoding(config.color, data), tooltip: createDefaultTooltipEncoding( [config.x, config.y, config.color], diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index 2fa90015de9..163cb46abc6 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -59,6 +59,7 @@ export interface FieldConfig { sort?: ChartSortDirection; limit?: number; showNull?: boolean; + labelAngle?: number; } export interface CommonChartProperties { diff --git a/web-common/src/features/canvas/inspector/types.ts b/web-common/src/features/canvas/inspector/types.ts index 157a4e20991..35e7e512b29 100644 --- a/web-common/src/features/canvas/inspector/types.ts +++ b/web-common/src/features/canvas/inspector/types.ts @@ -25,6 +25,7 @@ export type ChartFieldInput = { sortSelector?: boolean; limitSelector?: boolean; nullSelector?: boolean; + labelAngleSelector?: boolean; }; export interface ComponentInputParam { From 5628de72bca4c5cfc2e1482d7d0981e9e5b932df Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 14:59:54 +0530 Subject: [PATCH 33/62] Add label angle selector --- .../canvas/components/charts/builder.ts | 1 + .../charts/heatmap-charts/HeatmapChart.ts | 1 + .../chart/FieldConfigDropdown.svelte | 21 +++++++++++++++++++ .../chart/PositionalFieldConfig.svelte | 14 +++++++------ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index b18caaadc0c..8294699bb4a 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -65,6 +65,7 @@ export function createPositionEncoding( }, }), axis: { + ...(field.labelAngle !== undefined && { labelAngle: field.labelAngle }), ...(field.type === "quantitative" && { formatType: sanitizeFieldName(field.field), }), diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index c7794eabb79..78607075e3e 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -38,6 +38,7 @@ export class HeatmapChartComponent extends BaseChart { limitSelector: true, axisTitleSelector: true, nullSelector: true, + labelAngleSelector: true, }, }, }, diff --git a/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte b/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte index 0b1d9002f45..3a94b5cd508 100644 --- a/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte +++ b/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte @@ -20,6 +20,8 @@ $: isMeasure = fieldConfig?.type === "quantitative"; let limit = fieldConfig?.limit || 5000; + let labelAngle = + fieldConfig?.labelAngle ?? (fieldConfig?.type === "temporal" ? 0 : -90); let isDropdownOpen = false; const sortOptions: { label: string; value: ChartSortDirection }[] = [ @@ -34,6 +36,7 @@ $: showSort = chartFieldInput?.sortSelector ?? false; $: showLimit = chartFieldInput?.limitSelector ?? false; $: showNull = chartFieldInput?.nullSelector ?? false; + $: showLabelAngle = chartFieldInput?.labelAngleSelector ?? false; @@ -116,6 +119,24 @@ />
{/if} + {#if showLabelAngle && fieldConfig?.type !== "temporal"} +
+ Label angle + { + onChange("labelAngle", labelAngle); + }} + onEnter={() => { + onChange("labelAngle", labelAngle); + }} + /> +
+ {/if}
diff --git a/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte b/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte index 6f993d1c03c..a84db93b8fb 100644 --- a/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte +++ b/web-common/src/features/canvas/inspector/chart/PositionalFieldConfig.svelte @@ -61,12 +61,14 @@
{#if Object.keys(chartFieldInput ?? {}).length > 1} - + {#key fieldConfig} + + {/key} {/if}
From 883ec991d5b7025ea07d97301e239a54edb974f4 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 15:27:16 +0530 Subject: [PATCH 34/62] Address heatmap container overflow --- web-common/src/components/vega/VegaLiteRenderer.svelte | 2 +- web-common/src/features/canvas/ItemWrapper.svelte | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web-common/src/components/vega/VegaLiteRenderer.svelte b/web-common/src/components/vega/VegaLiteRenderer.svelte index c4d6139f122..762e0697c8a 100644 --- a/web-common/src/components/vega/VegaLiteRenderer.svelte +++ b/web-common/src/components/vega/VegaLiteRenderer.svelte @@ -52,7 +52,7 @@ bind:contentRect class:bg-white={canvasDashboard} class:px-2={canvasDashboard} - class="overflow-hidden size-full flex flex-col items-center justify-center" + class="overflow-y-auto overflow-x-hidden size-full flex flex-col items-center" > {#if error}
From 6b44dade24c6a406e813a7b90b857d40aab9a8af Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 16:51:59 +0530 Subject: [PATCH 35/62] Add E2E test for switching chart types --- .../inspector/chart/ChartTypeSelector.svelte | 1 + web-local/tests/canvas/charts.spec.ts | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 web-local/tests/canvas/charts.spec.ts diff --git a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte index 246d43f48b4..db2648debfd 100644 --- a/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte +++ b/web-common/src/features/canvas/inspector/chart/ChartTypeSelector.svelte @@ -40,6 +40,7 @@ square small type="secondary" + label={CHART_CONFIG[chart].title} selected={type === chart} on:click={() => selectChartType(chart)} > diff --git a/web-local/tests/canvas/charts.spec.ts b/web-local/tests/canvas/charts.spec.ts new file mode 100644 index 00000000000..ab5b131d5b9 --- /dev/null +++ b/web-local/tests/canvas/charts.spec.ts @@ -0,0 +1,26 @@ +import { gotoNavEntry } from "web-local/tests/utils/waitHelpers"; +import { test } from "../setup/base"; + +test.describe("canvas charts", () => { + test.use({ project: "AdBids" }); + + test("switch between charts", async ({ page }) => { + await page.getByLabel("/dashboards").click(); + await gotoNavEntry(page, "/dashboards/AdBids_metrics_canvas.yaml"); + + await page.locator("#AdBids_metrics_canvas--component-1-0 canvas").click(); + + await page.locator(".chart-icons").getByLabel("Heatmap").click(); + + await page + .getByLabel("A rect chart with embedded") + .locator("canvas") + .click(); + + await page.locator(".chart-icons").getByLabel("Pie").click(); + await page + .getByLabel("A arc chart with embedded") + .locator("canvas") + .click(); + }); +}); From 1a37e0dc3649875cdcf687382478a1ecc52cc70e Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 22:54:41 +0530 Subject: [PATCH 36/62] Add initial legend settings --- .../canvas/components/charts/Chart.svelte | 2 + .../canvas/components/charts/builder.ts | 38 +++++++++++++++++++ .../charts/cartesian-charts/area/spec.ts | 4 +- .../charts/cartesian-charts/bar-chart/spec.ts | 6 ++- .../cartesian-charts/line-chart/spec.ts | 4 +- .../cartesian-charts/stacked-bar/default.ts | 4 +- .../stacked-bar/normalized.ts | 4 +- .../components/charts/circular-charts/pie.ts | 3 -- .../canvas/components/charts/types.ts | 23 ++++++++--- .../src/features/canvas/inspector/types.ts | 1 + 10 files changed, 70 insertions(+), 19 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/Chart.svelte b/web-common/src/features/canvas/components/charts/Chart.svelte index f7d674e8ad8..b2175b954b8 100644 --- a/web-common/src/features/canvas/components/charts/Chart.svelte +++ b/web-common/src/features/canvas/components/charts/Chart.svelte @@ -74,6 +74,8 @@ [fieldName]: { fn: (val) => measureFormatters[fieldName](val) }, }; }, {}); + + $: console.log("spec", spec);
diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index 8294699bb4a..b430c402ce7 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -1,6 +1,7 @@ import type { ChartSpec } from "@rilldata/web-common/features/canvas/components/charts"; import type { CartesianChartSpec } from "@rilldata/web-common/features/canvas/components/charts/cartesian-charts/CartesianChart"; import type { + ChartLegend, FieldConfig, TooltipValue, } from "@rilldata/web-common/features/canvas/components/charts/types"; @@ -9,6 +10,7 @@ import { sanitizeFieldName, } from "@rilldata/web-common/features/canvas/components/charts/util"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; +import merge from "deepmerge"; import type { VisualizationSpec } from "svelte-vega"; import type { Config } from "vega-lite"; import type { @@ -149,6 +151,42 @@ export function createDefaultTooltipEncoding( return tooltip; } +export function getLegendConfig( + orientation: ChartLegend, +): Config { + let columns: number | ExprRef = 1; + if (orientation === "top" || orientation === "bottom") { + columns = { expr: "Math.floor(width / 120)" }; + } else if (orientation === "left" || orientation === "right") { + // columns = { expr: "Math.floor(height / 120)" }; + } + return { + legend: { + orient: orientation, + columns: columns, + // layout: { + // right: { anchor: "middle" }, + // left: { anchor: "middle" }, + // }, + }, + }; +} + +export function createConfigWithLegend( + config: ChartSpec, + legendField: FieldConfig | string | undefined, + chartVLConfig?: Config | undefined, +): Config | undefined { + const vlConfig = createConfig(config, chartVLConfig); + + if (!legendField || typeof legendField === "string") { + return vlConfig; + } + const legendConfig = getLegendConfig(legendField.legend ?? "top"); + if (!vlConfig) return legendConfig; + return merge(vlConfig, legendConfig); +} + export function createConfig( config: ChartSpec, chartVLConfig?: Config | undefined, diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts index 712f2150192..f53dce77e79 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/area/spec.ts @@ -4,7 +4,7 @@ import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/ch import type { VisualizationSpec } from "svelte-vega"; import { createColorEncoding, - createConfig, + createConfigWithLegend, createDefaultTooltipEncoding, createMultiLayerBaseSpec, createPositionEncoding, @@ -16,7 +16,7 @@ export function generateVLAreaChartSpec( data: ChartDataResult, ): VisualizationSpec { const spec = createMultiLayerBaseSpec(); - const vegaConfig = createConfig(config); + const vegaConfig = createConfigWithLegend(config, config.color); const colorField = typeof config.color === "object" ? config.color.field : undefined; diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts index 5145459f599..db165c688ba 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/bar-chart/spec.ts @@ -1,6 +1,6 @@ import type { VisualizationSpec } from "svelte-vega"; import { - createConfig, + createConfigWithLegend, createEncoding, createSingleLayerBaseSpec, } from "../../builder"; @@ -13,7 +13,9 @@ export function generateVLBarChartSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("bar"); const baseEncoding = createEncoding(config, data); - const vegaConfig = createConfig(config); + const vegaConfig = createConfigWithLegend(config, config.color); + + console.log("vegaConfig", vegaConfig); if (config.color && typeof config.color === "object" && config.x) { baseEncoding.xOffset = { diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts index 4b354aa64f9..3ed9e673b73 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/line-chart/spec.ts @@ -4,7 +4,7 @@ import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/ch import type { VisualizationSpec } from "svelte-vega"; import { createColorEncoding, - createConfig, + createConfigWithLegend, createDefaultTooltipEncoding, createMultiLayerBaseSpec, createPositionEncoding, @@ -17,7 +17,7 @@ export function generateVLLineChartSpec( data: ChartDataResult, ): VisualizationSpec { const spec = createMultiLayerBaseSpec(); - const vegaConfig = createConfig(config); + const vegaConfig = createConfigWithLegend(config, config.color); const colorField = typeof config.color === "object" ? config.color.field : undefined; diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts index 80b0ba06bcb..196fc2da64e 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/default.ts @@ -1,6 +1,6 @@ import type { VisualizationSpec } from "svelte-vega"; import { - createConfig, + createConfigWithLegend, createEncoding, createSingleLayerBaseSpec, } from "../../builder"; @@ -14,7 +14,7 @@ export function generateVLStackedBarChartSpec( const spec = createSingleLayerBaseSpec("bar"); spec.encoding = createEncoding(config, data); - const vegaConfig = createConfig(config); + const vegaConfig = createConfigWithLegend(config, config.color); return { ...spec, diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts index 06d713dcd09..65b919c1f98 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/stacked-bar/normalized.ts @@ -1,7 +1,7 @@ import type { TooltipValue } from "@rilldata/web-common/features/canvas/components/charts/types"; import type { VisualizationSpec } from "svelte-vega"; import { - createConfig, + createConfigWithLegend, createDefaultTooltipEncoding, createEncoding, createSingleLayerBaseSpec, @@ -15,7 +15,7 @@ export function generateVLStackedBarNormalizedSpec( ): VisualizationSpec { const spec = createSingleLayerBaseSpec("bar"); const baseEncoding = createEncoding(config, data); - const vegaConfig = createConfig(config); + const vegaConfig = createConfigWithLegend(config, config.color); if (baseEncoding.y && config.y?.field) { const yField = config.y.field; diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index 5584c8951a3..143b09dfc50 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -22,9 +22,6 @@ export function generateVLPieChartSpec( const vegaConfig = createConfig(config, { legend: { orient: "right", - layout: { - right: { anchor: "middle" }, - }, }, } as unknown as Config); diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index 163cb46abc6..c4f4ea7b814 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -50,16 +50,27 @@ export interface TimeDimensionDefinition { export type ChartSortDirection = "x" | "y" | "-x" | "-y"; -export interface FieldConfig { - field: string; - showAxisTitle?: boolean; // Default is false - zeroBasedOrigin?: boolean; // Default is false - type: "quantitative" | "ordinal" | "nominal" | "temporal"; - timeUnit?: string; // For temporal fields +export type ChartLegend = "none" | "top" | "bottom" | "left" | "right"; + +interface NominalFieldConfig { sort?: ChartSortDirection; limit?: number; showNull?: boolean; labelAngle?: number; + legend?: ChartLegend; +} + +interface QuantitativeFieldConfig { + zeroBasedOrigin?: boolean; // Default is false +} + +export interface FieldConfig + extends NominalFieldConfig, + QuantitativeFieldConfig { + field: string; + type: "quantitative" | "ordinal" | "nominal" | "temporal"; + showAxisTitle?: boolean; // Default is false + timeUnit?: string; // For temporal fields } export interface CommonChartProperties { diff --git a/web-common/src/features/canvas/inspector/types.ts b/web-common/src/features/canvas/inspector/types.ts index 35e7e512b29..d4ed94efa82 100644 --- a/web-common/src/features/canvas/inspector/types.ts +++ b/web-common/src/features/canvas/inspector/types.ts @@ -26,6 +26,7 @@ export type ChartFieldInput = { limitSelector?: boolean; nullSelector?: boolean; labelAngleSelector?: boolean; + legendSelector?: boolean; }; export interface ComponentInputParam { From 87bdd753d08188899c54ca65ddb54738f4f4bce1 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Thu, 24 Apr 2025 23:03:04 +0530 Subject: [PATCH 37/62] Better query handling for temporal fields --- .../charts/cartesian-charts/CartesianChart.ts | 9 +++++++-- .../charts/heatmap-charts/HeatmapChart.ts | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts index a42a7faf7dc..ea8166e8820 100644 --- a/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts +++ b/web-common/src/features/canvas/components/charts/cartesian-charts/CartesianChart.ts @@ -109,7 +109,10 @@ export class CartesianChartComponent extends BaseChart { ([runtime, $timeAndFilterStore]) => { const { timeRange, where } = $timeAndFilterStore; const enabled = - !!timeRange?.start && !!timeRange?.end && hasColorDimension; + !!timeRange?.start && + !!timeRange?.end && + hasColorDimension && + config.x?.type === "nominal"; const topNWhere = getFilterWithNullHandling(where, config.x); @@ -144,7 +147,9 @@ export class CartesianChartComponent extends BaseChart { const enabled = !!timeRange?.start && !!timeRange?.end && - (hasColorDimension ? !!topNData?.length : true); + (hasColorDimension && config.x?.type === "nominal" + ? !!topNData?.length + : true); let combinedWhere: V1Expression | undefined = getFilterWithNullHandling( where, diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index 78607075e3e..eb3debc0a24 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -107,7 +107,10 @@ export class HeatmapChartComponent extends BaseChart { { measures, dimensions: [{ name: config.x?.field }], - sort: [{ name: config.x?.field, desc: true }], + sort: + config.x?.type === "nominal" + ? [{ name: config.x?.field, desc: true }] + : [], where: xWhere, timeRange, limit, @@ -143,7 +146,10 @@ export class HeatmapChartComponent extends BaseChart { { measures, dimensions: [{ name: config.y?.field }], - sort: [{ name: config.y?.field, desc: true }], + sort: + config.y?.type === "nominal" + ? [{ name: config.y?.field, desc: true }] + : [], where: yWhere, timeRange, limit, @@ -204,9 +210,10 @@ export class HeatmapChartComponent extends BaseChart { { measures, dimensions, - sort: config.x?.field - ? [{ name: config.x.field, desc: true }] - : undefined, + sort: + config.x?.type === "nominal" + ? [{ name: config.x?.field, desc: true }] + : undefined, where: combinedWhere, timeRange, limit: "5000", // Higher limit for heatmap to show more data points From 46d94b6f6b6636c37fbf0a98b355bd67ee6af57a Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Fri, 25 Apr 2025 00:27:57 +0530 Subject: [PATCH 38/62] Fix local timezone offset for chart data --- .../canvas/components/charts/selector.ts | 23 +++++++++++++++---- .../features/canvas/components/charts/util.ts | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/selector.ts b/web-common/src/features/canvas/components/charts/selector.ts index 28e16d5e1dc..ec7c96ae0c2 100644 --- a/web-common/src/features/canvas/components/charts/selector.ts +++ b/web-common/src/features/canvas/components/charts/selector.ts @@ -9,7 +9,11 @@ import { } from "@rilldata/web-common/runtime-client"; import { derived, type Readable } from "svelte/store"; import type { ChartDataResult, TimeDimensionDefinition } from "./types"; -import { getFieldsByType, timeGrainToVegaTimeUnitMap } from "./util"; +import { + adjustDataForTimeZone, + getFieldsByType, + timeGrainToVegaTimeUnitMap, +} from "./util"; export function getChartData( ctx: CanvasStore, @@ -44,8 +48,8 @@ export function getChartData( }); return derived( - [chartDataQuery, ...fieldReadableMap], - ([chartData, ...fieldMap]) => { + [chartDataQuery, timeAndFilterStore, ...fieldReadableMap], + ([chartData, $timeAndFilterStore, ...fieldMap]) => { const fieldSpecMap = allFields.reduce( (acc, field, index) => { acc[field.field] = fieldMap?.[index]; @@ -59,8 +63,19 @@ export function getChartData( | undefined >, ); + + let data = chartData?.data?.data; + + if (timeDimensions?.length && $timeAndFilterStore.timeGrain) { + data = adjustDataForTimeZone( + data, + timeDimensions, + $timeAndFilterStore.timeGrain, + $timeAndFilterStore.timeRange.timeZone || "UTC", + ); + } return { - data: chartData?.data?.data || [], + data: data || [], isFetching: chartData?.isFetching ?? false, error: chartData?.error, fields: fieldSpecMap, diff --git a/web-common/src/features/canvas/components/charts/util.ts b/web-common/src/features/canvas/components/charts/util.ts index 1342ef5e568..ff78cf08ee6 100644 --- a/web-common/src/features/canvas/components/charts/util.ts +++ b/web-common/src/features/canvas/components/charts/util.ts @@ -1,9 +1,12 @@ import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import { sanitizeValueForVega } from "@rilldata/web-common/features/templates/charts/utils"; +import { adjustOffsetForZone } from "@rilldata/web-common/lib/convertTimestampPreview"; +import { timeGrainToDuration } from "@rilldata/web-common/lib/time/grains"; import { V1TimeGrain, type V1Expression, + type V1MetricsViewAggregationResponseDataItem, } from "@rilldata/web-common/runtime-client"; import merge from "deepmerge"; import type { Config } from "vega-lite"; @@ -136,3 +139,23 @@ export function getFilterWithNullHandling( const excludeNullFilter = createInExpression(fieldConfig.field, [null], true); return mergeFilters(where, excludeNullFilter); } + +export function adjustDataForTimeZone( + data: V1MetricsViewAggregationResponseDataItem[] | undefined, + timeFields: string[], + timeGrain: V1TimeGrain, + selectedTimezone: string, +) { + if (!data) return data; + + return data.map((datum) => { + timeFields.forEach((timeField) => { + datum[timeField] = adjustOffsetForZone( + datum[timeField] as string, + selectedTimezone, + timeGrainToDuration(timeGrain), + ); + }); + return datum; + }); +} From c0aadd74e0dd716d0e55385fbbd11792c3397730 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Mon, 28 Apr 2025 16:04:55 +0530 Subject: [PATCH 39/62] Add legend orientation selector for heatmap and pie --- .../canvas/components/charts/builder.ts | 39 +++++++++++++------ .../charts/circular-charts/CircularChart.ts | 1 + .../components/charts/circular-charts/pie.ts | 18 ++++----- .../charts/heatmap-charts/HeatmapChart.ts | 1 + .../components/charts/heatmap-charts/spec.ts | 24 ++++++------ .../canvas/components/charts/types.ts | 2 +- .../chart/FieldConfigDropdown.svelte | 24 ++++++++++++ .../src/features/canvas/inspector/types.ts | 7 +++- 8 files changed, 81 insertions(+), 35 deletions(-) diff --git a/web-common/src/features/canvas/components/charts/builder.ts b/web-common/src/features/canvas/components/charts/builder.ts index b430c402ce7..71bc9b55a12 100644 --- a/web-common/src/features/canvas/components/charts/builder.ts +++ b/web-common/src/features/canvas/components/charts/builder.ts @@ -21,7 +21,7 @@ import type { import type { Encoding } from "vega-lite/build/src/encoding"; import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; import type { TopLevelUnitSpec } from "vega-lite/build/src/spec/unit"; -import type { ExprRef, SignalRef } from "vega-typings"; +import type { ExprRef, Layout, SignalRef } from "vega-typings"; import type { ChartDataResult } from "./types"; export function createMultiLayerBaseSpec() { @@ -155,34 +155,51 @@ export function getLegendConfig( orientation: ChartLegend, ): Config { let columns: number | ExprRef = 1; + let layout: Layout | undefined = undefined; if (orientation === "top" || orientation === "bottom") { - columns = { expr: "Math.floor(width / 120)" }; - } else if (orientation === "left" || orientation === "right") { - // columns = { expr: "Math.floor(height / 120)" }; + columns = { expr: "floor(width / 140)" }; + } + /** + * The layout property is not typed in the current version of Vega-Lite. + * This will be fixed when we upgrade to Svelte 5 and subseqent Vega-Lite versions. + */ + if (orientation === "right" || orientation === "left") { + layout = { + right: { anchor: "middle" }, + left: { anchor: "middle" }, + } as unknown as Layout; + } + if (orientation === "none") { + return { + legend: { + disable: true, + }, + }; } return { legend: { orient: orientation, columns: columns, - // layout: { - // right: { anchor: "middle" }, - // left: { anchor: "middle" }, - // }, + labelLimit: 140, + ...(layout && { layout }), }, - }; + } as unknown as Config; } export function createConfigWithLegend( config: ChartSpec, legendField: FieldConfig | string | undefined, - chartVLConfig?: Config | undefined, + chartVLConfig: Config | undefined = undefined, + defaultLegendPosition: ChartLegend = "top", ): Config | undefined { const vlConfig = createConfig(config, chartVLConfig); if (!legendField || typeof legendField === "string") { return vlConfig; } - const legendConfig = getLegendConfig(legendField.legend ?? "top"); + const legendConfig = getLegendConfig( + legendField.legendOrientation ?? defaultLegendPosition, + ); if (!vlConfig) return legendConfig; return merge(vlConfig, legendConfig); } diff --git a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts index dbe7e82e2f9..f5437804125 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/CircularChart.ts @@ -42,6 +42,7 @@ export class CircularChartComponent extends BaseChart { nullSelector: true, limitSelector: true, hideTimeDimension: true, + defaultLegendOrientation: "right", }, }, }, diff --git a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts index 143b09dfc50..5546e4a9133 100644 --- a/web-common/src/features/canvas/components/charts/circular-charts/pie.ts +++ b/web-common/src/features/canvas/components/charts/circular-charts/pie.ts @@ -1,8 +1,7 @@ import type { VisualizationSpec } from "svelte-vega"; -import type { Config } from "vega-lite"; import { createColorEncoding, - createConfig, + createConfigWithLegend, createDefaultTooltipEncoding, createPositionEncoding, createSingleLayerBaseSpec, @@ -10,20 +9,17 @@ import { import type { ChartDataResult } from "../types"; import type { CircularChartSpec } from "./CircularChart"; -/** - * The layout property is not typed in the current version of Vega-Lite. - * This will be fixed when we upgrade to Svelte 5 and subseqent Vega-Lite versions. - */ export function generateVLPieChartSpec( config: CircularChartSpec, data: ChartDataResult, ): VisualizationSpec { const spec = createSingleLayerBaseSpec("arc"); - const vegaConfig = createConfig(config, { - legend: { - orient: "right", - }, - } as unknown as Config); + const vegaConfig = createConfigWithLegend( + config, + config.color, + undefined, + "right", + ); spec.mark = { type: "arc", diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts index eb3debc0a24..2076ad0369f 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/HeatmapChart.ts @@ -60,6 +60,7 @@ export class HeatmapChartComponent extends BaseChart { meta: { chartFieldInput: { type: "measure", + defaultLegendOrientation: "right", }, }, }, diff --git a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts index 0ecd0b440f0..beb2067a74c 100644 --- a/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts +++ b/web-common/src/features/canvas/components/charts/heatmap-charts/spec.ts @@ -2,7 +2,7 @@ import type { Field } from "vega-lite/build/src/channeldef"; import type { TopLevelUnitSpec } from "vega-lite/build/src/spec/unit"; import { createColorEncoding, - createConfig, + createConfigWithLegend, createDefaultTooltipEncoding, createPositionEncoding, createSingleLayerBaseSpec, @@ -16,17 +16,19 @@ export function generateVLHeatmapSpec( ): TopLevelUnitSpec { const spec = createSingleLayerBaseSpec("rect"); - const vegaConfig = createConfig(config, { - legend: { - orient: "bottom", + const vegaConfig = createConfigWithLegend( + config, + config.color, + { + axis: { grid: true, tickBand: "extent" }, + axisX: { + grid: true, + gridDash: [], + tickBand: "extent", + }, }, - axis: { grid: true, tickBand: "extent" }, - axisX: { - grid: true, - gridDash: [], - tickBand: "extent", - }, - }); + "right", + ); return { ...spec, diff --git a/web-common/src/features/canvas/components/charts/types.ts b/web-common/src/features/canvas/components/charts/types.ts index c4f4ea7b814..7df3992c73e 100644 --- a/web-common/src/features/canvas/components/charts/types.ts +++ b/web-common/src/features/canvas/components/charts/types.ts @@ -57,7 +57,7 @@ interface NominalFieldConfig { limit?: number; showNull?: boolean; labelAngle?: number; - legend?: ChartLegend; + legendOrientation?: ChartLegend; } interface QuantitativeFieldConfig { diff --git a/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte b/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte index 3a94b5cd508..9e178893663 100644 --- a/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte +++ b/web-common/src/features/canvas/inspector/chart/FieldConfigDropdown.svelte @@ -6,6 +6,7 @@ import Switch from "@rilldata/web-common/components/forms/Switch.svelte"; import SettingsSlider from "@rilldata/web-common/components/icons/SettingsSlider.svelte"; import type { + ChartLegend, ChartSortDirection, FieldConfig, } from "@rilldata/web-common/features/canvas/components/charts/types"; @@ -31,12 +32,21 @@ { label: "Y-axis descending", value: "-y" }, ]; + const legendOptions: { label: string; value: ChartLegend }[] = [ + { label: "Top", value: "top" }, + { label: "Right", value: "right" }, + { label: "Bottom", value: "bottom" }, + { label: "Left", value: "left" }, + { label: "None", value: "none" }, + ]; + $: showAxisTitle = chartFieldInput?.axisTitleSelector ?? false; $: showOrigin = chartFieldInput?.originSelector ?? false; $: showSort = chartFieldInput?.sortSelector ?? false; $: showLimit = chartFieldInput?.limitSelector ?? false; $: showNull = chartFieldInput?.nullSelector ?? false; $: showLabelAngle = chartFieldInput?.labelAngleSelector ?? false; + $: showLegend = chartFieldInput?.defaultLegendOrientation ?? false; @@ -137,6 +147,20 @@ />
{/if} + {#if showLegend} +
+ Legend orientation + {/if} {#if showLimit} -
+
Limit {/if} {#if showLabelAngle && fieldConfig?.type !== "temporal"} -
+
Label angle {/if} {#if showLegend} -
+
Legend orientation