Skip to content

Commit 5bb0898

Browse files
authored
feat: allow multiple chart types, add donut, heatmap (#7142)
* Add abstract base chart class * Move to index file * Use base chart reference * Reorganize files and params * Use updated references, add different chart types * Consolidate chart types * Move data query inside chart component * Add pie chart component * Reorganize types and title * Standardize chart input params * By default hide nulls * Add icon for donut * Support inner radius * add heatmap chart * Consolidate chart metadata * lint fix * Fix import * Remove limit from heatmap * Add initial chart switching logic * add color field for cartesian charts * Hide time dimension for donut * Move config as part of chart spec * Update color for heatmap * Default legend right for pie charts * Preserve matching properties when switching charts * Optimistically update chart type * Remove logs, debug statements * clean up * default heatmap legend at bottom * refactor to use query options paradigm * lint fix * Consolidate builder methods * Add label angle selector * Address heatmap container overflow * Add E2E test for switching chart types * Better query handling for temporal fields * Fix local timezone offset for chart data
1 parent 58ab3b4 commit 5bb0898

38 files changed

+1943
-797
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script>
2+
export let size = "18px";
3+
export let primaryColor = "#3524C7";
4+
export let secondaryColor = "#9CABFF";
5+
</script>
6+
7+
<svg
8+
width={size}
9+
height={size}
10+
viewBox="0 0 24 24"
11+
fill="none"
12+
xmlns="http://www.w3.org/2000/svg"
13+
>
14+
<path
15+
fill-rule="evenodd"
16+
clip-rule="evenodd"
17+
d="M12 22C17.5229 22 22 17.5228 22 12C22 6.47715 17.5229 2 12 2C6.47716 2 2.00001 6.47715 2.00001 12C2.00001 17.5228 6.47716 22 12 22ZM12 17C14.7614 17 17 14.7614 17 12C17 9.23858 14.7614 7 12 7C9.23858 7 7.00001 9.23858 7.00001 12C7.00001 14.7614 9.23858 17 12 17Z"
18+
fill={primaryColor}
19+
/>
20+
<path
21+
fill-rule="evenodd"
22+
clip-rule="evenodd"
23+
d="M17.5557 3.68531C15.9112 2.58649 13.9778 2 12 2V7C14.7614 7 17 9.23858 17 12C17 13.3807 16.4404 14.6307 15.5355 15.5355L19.0711 19.0711C20.4696 17.6725 21.422 15.8907 21.8079 13.9509C22.1937 12.0111 21.9957 10.0004 21.2388 8.17317C20.4819 6.34591 19.2002 4.78412 17.5557 3.68531Z"
24+
fill={secondaryColor}
25+
/>
26+
</svg>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
export let size = "24";
3+
export let color = "currentColor";
4+
</script>
5+
6+
<svg
7+
width={size}
8+
height={size}
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
xmlns="http://www.w3.org/2000/svg"
12+
>
13+
<rect x="2" y="2" width="4" height="4" fill={color} opacity="0.2" />
14+
<rect x="7" y="2" width="4" height="4" fill={color} opacity="0.4" />
15+
<rect x="12" y="2" width="4" height="4" fill={color} opacity="0.6" />
16+
<rect x="17" y="2" width="4" height="4" fill={color} opacity="0.8" />
17+
<rect x="2" y="7" width="4" height="4" fill={color} opacity="0.3" />
18+
<rect x="7" y="7" width="4" height="4" fill={color} opacity="0.5" />
19+
<rect x="12" y="7" width="4" height="4" fill={color} opacity="0.7" />
20+
<rect x="17" y="7" width="4" height="4" fill={color} opacity="0.9" />
21+
<rect x="2" y="12" width="4" height="4" fill={color} opacity="0.4" />
22+
<rect x="7" y="12" width="4" height="4" fill={color} opacity="0.6" />
23+
<rect x="12" y="12" width="4" height="4" fill={color} opacity="0.8" />
24+
<rect x="17" y="12" width="4" height="4" fill={color} />
25+
<rect x="2" y="17" width="4" height="4" fill={color} opacity="0.5" />
26+
<rect x="7" y="17" width="4" height="4" fill={color} opacity="0.7" />
27+
<rect x="12" y="17" width="4" height="4" fill={color} opacity="0.9" />
28+
<rect x="17" y="17" width="4" height="4" fill={color} />
29+
</svg>

web-common/src/components/vega/VegaLiteRenderer.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
bind:contentRect
5353
class:bg-white={canvasDashboard}
5454
class:px-2={canvasDashboard}
55-
class="overflow-hidden size-full flex flex-col items-center justify-center"
55+
class="overflow-y-auto overflow-x-hidden size-full flex flex-col items-center"
5656
>
5757
{#if error}
5858
<div

web-common/src/components/vega/vega-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ export const getRillTheme: (isCanvasDashboard: boolean) => Config = (
108108
},
109109
range: {
110110
category: COMPARIONS_COLORS,
111+
heatmap: {
112+
scheme: "tealblues", // TODO: Generate this from theme
113+
},
111114
},
112115
numberFormat: "s",
113116
tooltipFormat: {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts">
22
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu";
3-
import { chartMetadata } from "@rilldata/web-common/features/canvas/components/charts/util";
43
import { Plus, PlusCircle } from "lucide-svelte";
54
import type { ComponentType, SvelteComponent } from "svelte";
5+
import { CHART_TYPES } from "./components/charts";
66
import type { ChartType } from "./components/charts/types";
77
import type { CanvasComponentType } from "./components/types";
88
import BigNumberIcon from "./icons/BigNumberIcon.svelte";
@@ -18,11 +18,11 @@
1818
1919
// Function to get a random chart type
2020
function getRandomChartType(): ChartType {
21-
const chartTypes = chartMetadata
22-
.map((chart) => chart.type)
23-
.filter((t) => t !== "stacked_bar_normalized");
21+
const chartTypes = CHART_TYPES.filter(
22+
(t) => t !== "stacked_bar_normalized",
23+
);
2424
const randomIndex = Math.floor(Math.random() * chartTypes.length);
25-
return chartTypes[randomIndex];
25+
return chartTypes[randomIndex] as ChartType;
2626
}
2727
2828
// Create menu items with a function to get random chart type when clicked

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
export let type: string | undefined = undefined;
66
77
$: expandable =
8-
type === "kpi_grid" || type === "markdown" || type === "leaderboard";
8+
type === "kpi_grid" ||
9+
type === "markdown" ||
10+
type === "leaderboard" ||
11+
type === "heatmap";
912
$: minHeight = getInitialHeight(type) + "px";
1013
</script>
1114

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ import type {
1212
V1Resource,
1313
V1TimeRange,
1414
} from "@rilldata/web-common/runtime-client";
15+
import type { ComponentType, SvelteComponent } from "svelte";
1516
import { derived, get, writable, type Writable } from "svelte/store";
16-
import { CanvasComponentState } from "../stores/canvas-component";
17-
import type { CanvasEntity, ComponentPath } from "../stores/canvas-entity";
18-
import type {
19-
ComparisonTimeRangeState,
20-
TimeRangeState,
21-
} from "../../dashboards/time-controls/time-control-store";
17+
import { mergeFilters } from "../../dashboards/pivot/pivot-merge-filters";
2218
import {
2319
buildValidMetricsViewFilter,
2420
createAndExpression,
2521
} from "../../dashboards/stores/filter-utils";
26-
import { mergeFilters } from "../../dashboards/pivot/pivot-merge-filters";
27-
import type { ComponentType, SvelteComponent } from "svelte";
22+
import type {
23+
ComparisonTimeRangeState,
24+
TimeRangeState,
25+
} from "../../dashboards/time-controls/time-control-store";
26+
import { CanvasComponentState } from "../stores/canvas-component";
27+
import type { CanvasEntity, ComponentPath } from "../stores/canvas-entity";
2828

2929
export abstract class BaseCanvasComponent<T = ComponentSpec> {
3030
id: string;
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { BaseCanvasComponent } from "@rilldata/web-common/features/canvas/components/BaseCanvasComponent";
2+
import { CHART_CONFIG } from "@rilldata/web-common/features/canvas/components/charts";
3+
import {
4+
commonOptions,
5+
createComponent,
6+
getFilterOptions,
7+
} from "@rilldata/web-common/features/canvas/components/util";
8+
import type {
9+
AllKeys,
10+
ComponentInputParam,
11+
InputParams,
12+
} from "@rilldata/web-common/features/canvas/inspector/types";
13+
import type { CanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers";
14+
import type { TimeAndFilterStore } from "@rilldata/web-common/features/canvas/stores/types";
15+
import type {
16+
V1MetricsViewSpec,
17+
V1Resource,
18+
} from "@rilldata/web-common/runtime-client";
19+
import { get, writable, type Readable, type Writable } from "svelte/store";
20+
import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity";
21+
import type {
22+
ComponentCommonProperties,
23+
ComponentFilterProperties,
24+
} from "../types";
25+
import Chart from "./Chart.svelte";
26+
import type {
27+
ChartDataQuery,
28+
ChartFieldsMap,
29+
ChartType,
30+
CommonChartProperties,
31+
FieldConfig,
32+
} from "./types";
33+
34+
// Base interface for all chart configurations
35+
export type BaseChartConfig = ComponentFilterProperties &
36+
ComponentCommonProperties &
37+
CommonChartProperties;
38+
39+
export abstract class BaseChart<
40+
TConfig extends BaseChartConfig,
41+
> extends BaseCanvasComponent<TConfig> {
42+
minSize = { width: 4, height: 4 };
43+
defaultSize = { width: 6, height: 4 };
44+
resetParams = [];
45+
type: ChartType;
46+
chartType: Writable<ChartType>;
47+
component = Chart;
48+
49+
constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) {
50+
const baseSpec: BaseChartConfig = {
51+
metrics_view: "",
52+
title: "",
53+
description: "",
54+
};
55+
super(resource, parent, path, baseSpec as TConfig);
56+
57+
this.type = resource.component?.state?.validSpec?.renderer as ChartType;
58+
this.chartType = writable(this.type);
59+
}
60+
61+
isValid(spec: TConfig): boolean {
62+
return typeof spec.metrics_view === "string";
63+
}
64+
65+
inputParams(): InputParams<TConfig> {
66+
return {
67+
options: {
68+
metrics_view: { type: "metrics", label: "Metrics view" },
69+
tooltip: { type: "tooltip", label: "Tooltip", showInUI: false },
70+
vl_config: { type: "config", showInUI: false },
71+
...this.getChartSpecificOptions(),
72+
...commonOptions,
73+
},
74+
filter: getFilterOptions(false),
75+
};
76+
}
77+
78+
abstract getChartSpecificOptions(): Record<
79+
AllKeys<TConfig>,
80+
ComponentInputParam
81+
>;
82+
83+
abstract createChartDataQuery(
84+
ctx: CanvasStore,
85+
timeAndFilterStore: Readable<TimeAndFilterStore>,
86+
): ChartDataQuery;
87+
88+
abstract chartTitle(fields: ChartFieldsMap): string;
89+
90+
protected getDefaultFieldConfig(): Partial<FieldConfig> {
91+
return {
92+
showAxisTitle: true,
93+
zeroBasedOrigin: true,
94+
showNull: false,
95+
};
96+
}
97+
98+
updateChartType(
99+
key: ChartType,
100+
metricsViewSpec: V1MetricsViewSpec | undefined,
101+
) {
102+
if (!this.parent.fileArtifact) return;
103+
104+
const currentSpec = get(this.specStore);
105+
const parentPath = this.pathInYAML.slice(0, -1);
106+
107+
const parseDocumentStore = this.parent.parsedContent;
108+
const parsedDocument = get(parseDocumentStore);
109+
const { updateEditorContent } = this.parent.fileArtifact;
110+
111+
const newSpecForKey = CHART_CONFIG[key].component.newComponentSpec(
112+
currentSpec.metrics_view,
113+
metricsViewSpec,
114+
);
115+
116+
const commonProps = this.extractCommonProperties(
117+
currentSpec,
118+
this.type,
119+
key,
120+
);
121+
const mergedSpec = {
122+
...newSpecForKey,
123+
...commonProps,
124+
};
125+
126+
const newResource = this.parent.createOptimisticResource({
127+
type: key,
128+
row: this.pathInYAML[1],
129+
column: this.pathInYAML[3],
130+
metricsViewName: currentSpec.metrics_view,
131+
metricsViewSpec,
132+
spec: mergedSpec,
133+
});
134+
135+
const newComponent = createComponent(
136+
newResource,
137+
this.parent,
138+
this.pathInYAML,
139+
);
140+
141+
this.parent.components.set(newComponent.id, newComponent);
142+
this.parent.selectedComponent.set(newComponent.id);
143+
this.parent._rows.refresh();
144+
145+
// Preserve the width from the current chart
146+
const width = parsedDocument.getIn([...parentPath, "width"]);
147+
148+
parsedDocument.setIn(parentPath, { [key]: mergedSpec, width });
149+
150+
updateEditorContent(parsedDocument.toString(), false, true);
151+
152+
this.chartType.set(key);
153+
}
154+
155+
private extractCommonProperties(
156+
spec: TConfig,
157+
sourceType: ChartType,
158+
targetType: ChartType,
159+
): Partial<BaseChartConfig> {
160+
const {
161+
metrics_view,
162+
title,
163+
description,
164+
vl_config,
165+
time_filters,
166+
dimension_filters,
167+
} = spec;
168+
169+
const sourceChartParams =
170+
CHART_CONFIG[sourceType].component.chartInputParams || {};
171+
const targetChartParams =
172+
CHART_CONFIG[targetType].component.chartInputParams || {};
173+
174+
// Check for common keys and type match first
175+
const commonProps = Object.keys(sourceChartParams).filter((key) => {
176+
const isKeyAndTypeMatch =
177+
targetChartParams?.[key]?.type === sourceChartParams[key]?.type;
178+
const isFieldTypeMatch =
179+
targetChartParams?.[key]?.meta?.chartFieldInput?.type ===
180+
sourceChartParams[key]?.meta?.chartFieldInput?.type;
181+
return isKeyAndTypeMatch && isFieldTypeMatch;
182+
});
183+
184+
const commonPropsObject = commonProps.reduce(
185+
(acc, key) => {
186+
acc[key] = spec[key];
187+
return acc;
188+
},
189+
{} as Record<string, unknown>,
190+
);
191+
192+
return {
193+
metrics_view,
194+
title,
195+
description,
196+
vl_config,
197+
time_filters,
198+
dimension_filters,
199+
...commonPropsObject,
200+
};
201+
}
202+
}

0 commit comments

Comments
 (0)