From 933f81cf69c87d3e32d8e229dfd7044e7e556780 Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Tue, 26 May 2026 12:26:05 +0530 Subject: [PATCH 1/2] feat: add screenshot capture to measure charts --- package-lock.json | 8 + web-common/package.json | 1 + .../MetricsTimeSeriesCharts.svelte | 113 ++++++++---- .../time-series/ScreenshotContainer.svelte | 174 ++++++++++++++++++ 4 files changed, 262 insertions(+), 34 deletions(-) create mode 100644 web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte diff --git a/package-lock.json b/package-lock.json index 78d4b9eba1a..fc7d30bd968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19785,6 +19785,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -36113,6 +36120,7 @@ "fflate": "^0.8.2", "globals": "^16.0.0", "gridstack": "^11.3.0", + "html-to-image": "^1.11.13", "jsdom": "^26.0.0", "json-schema": "^0.4.0", "lucide-svelte": "^0.298.0", diff --git a/web-common/package.json b/web-common/package.json index cb7343d0923..fbc8a1d5ece 100644 --- a/web-common/package.json +++ b/web-common/package.json @@ -73,6 +73,7 @@ "fflate": "^0.8.2", "globals": "^16.0.0", "gridstack": "^11.3.0", + "html-to-image": "^1.11.13", "jsdom": "^26.0.0", "json-schema": "^0.4.0", "lucide-svelte": "^0.298.0", diff --git a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte index 559305143cd..d3c5fc48026 100644 --- a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte +++ b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte @@ -33,7 +33,7 @@ import { type MetricsViewSpecMeasure } from "@rilldata/web-common/runtime-client/gen/index.schemas"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { DateTime, Interval } from "luxon"; - import { Button } from "../../../components/button"; + import { Button, IconButton } from "../../../components/button"; import Pivot from "../../../components/icons/Pivot.svelte"; import { TIME_GRAIN } from "../../../lib/time/config"; import { DashboardState_ActivePage } from "../../../proto/gen/rill/ui/v1/dashboard_pb"; @@ -44,6 +44,8 @@ import MeasureChart from "./measure-chart/MeasureChart.svelte"; import MeasureChartXAxis from "./measure-chart/MeasureChartXAxis.svelte"; import { ScrubController } from "./measure-chart/ScrubController"; + import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte"; + import ScreenshotContainer from "@rilldata/web-common/features/dashboards/time-series/ScreenshotContainer.svelte"; const { rillTime } = featureFlags; @@ -179,6 +181,9 @@ $: annotationsEnabled = !!$exploreValidSpec.data?.metricsView?.annotations?.length; + let screenshotDialogOpen = false; + let screenshotDialogMeasure: MetricsViewSpecMeasure | undefined = undefined; + // Pan handler function handlePan(direction: "left" | "right") { const panRange = $getNewPanRange(direction); @@ -253,6 +258,11 @@ measureSelection.clear(); } } + + function openScreenshotDialog(measure: MetricsViewSpecMeasure) { + screenshotDialogMeasure = measure; + screenshotDialogOpen = true; + } @@ -392,39 +402,54 @@ /> {#if activeTimeGrain} - handlePan("left")} - onPanRight={() => handlePan("right")} - {showComparison} - {showTimeDimensionDetail} - dynamicYAxis={dynamicYAxisScale} - onScrub={handleScrub} - onScrubClear={() => { - metricsExplorerStore.setSelectedScrubRange( - exploreName, - undefined, - ); - }} - /> +
+ handlePan("left")} + onPanRight={() => handlePan("right")} + {showComparison} + {showTimeDimensionDetail} + dynamicYAxis={dynamicYAxisScale} + onScrub={handleScrub} + onScrubClear={() => { + metricsExplorerStore.setSelectedScrubRange( + exploreName, + undefined, + ); + }} + /> + + + + + + + openScreenshotDialog(measure)} + > + Download as PNG + + + +
{:else}
@@ -442,3 +467,23 @@ }} onReplace={createPivot} /> + +{#if screenshotDialogMeasure} + +{/if} diff --git a/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte new file mode 100644 index 00000000000..bf9c46f9253 --- /dev/null +++ b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte @@ -0,0 +1,174 @@ + + + { + url = ""; + }} +> + + + Export chart + + +
+
+ +

+ {measure.displayName || measure.name} +

+ {#if measure.description} +

{measure.description}

+ {/if} +
+
+ +
+ {#if timeGranularity} +
+
+ +
+ {/if} + + + + {#if timeGranularity} + + {/if} +
+ +
+ + Generated {new Date().toLocaleString()} + Rill + +
+
+ + {#if url} +
+ Screenshot +
+ {/if} + + + + + +
+
From 9d8e73c29c77f59771d9e2cbad29cc4bc9ac900f Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Tue, 26 May 2026 16:36:12 +0530 Subject: [PATCH 2/2] apply theme --- .../MetricsTimeSeriesCharts.svelte | 2 +- .../time-series/ScreenshotContainer.svelte | 148 +++++++++--------- web-common/src/features/themes/selectors.ts | 1 + 3 files changed, 80 insertions(+), 71 deletions(-) diff --git a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte index d3c5fc48026..a814d0ed0a5 100644 --- a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte +++ b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte @@ -33,7 +33,7 @@ import { type MetricsViewSpecMeasure } from "@rilldata/web-common/runtime-client/gen/index.schemas"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { DateTime, Interval } from "luxon"; - import { Button, IconButton } from "../../../components/button"; + import { Button } from "../../../components/button"; import Pivot from "../../../components/icons/Pivot.svelte"; import { TIME_GRAIN } from "../../../lib/time/config"; import { DashboardState_ActivePage } from "../../../proto/gen/rill/ui/v1/dashboard_pb"; diff --git a/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte index bf9c46f9253..c85f1906e96 100644 --- a/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte +++ b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte @@ -7,12 +7,16 @@ V1Expression, V1TimeGrain, } from "@rilldata/web-common/runtime-client"; - import { getFontEmbedCSS, toPng } from "html-to-image"; - import { Interval } from "luxon"; + import { toPng } from "html-to-image"; + import { DateTime, Interval } from "luxon"; import MeasureBigNumber from "../big-number/MeasureBigNumber.svelte"; import MeasureChart from "./measure-chart/MeasureChart.svelte"; import MeasureChartXAxis from "./measure-chart/MeasureChartXAxis.svelte"; import { ScrubController } from "./measure-chart/ScrubController"; + import { prettyFormatTimeRange } from "@rilldata/web-common/lib/time/ranges/formatter.ts"; + import ExploreFilterChipsReadOnly from "@rilldata/web-common/features/dashboards/filters/ExploreFilterChipsReadOnly.svelte"; + import ThemeProvider from "@rilldata/web-common/features/dashboards/ThemeProvider.svelte"; + import { activeDashboardTheme } from "@rilldata/web-common/features/themes/active-dashboard-theme.ts"; export let open = false; export let measure: MetricsViewSpecMeasure; @@ -35,7 +39,14 @@ let captureNode: HTMLDivElement; let downloading = false; - let url = ""; + + $: formattedTimeRange = interval + ? prettyFormatTimeRange(interval, timeGranularity) + : ""; + $: generatedTime = prettyFormatTimeRange( + Interval.fromDateTimes(DateTime.now(), DateTime.now()), + timeGranularity, + ); const SVG_PROPS = [ "fill", @@ -68,97 +79,94 @@ try { inlineSvgStyles(captureNode); await document.fonts.ready; - const fontEmbedCSS = await getFontEmbedCSS(captureNode); - url = await toPng(captureNode, { fontEmbedCSS, cacheBust: true }); - // const link = document.createElement("a"); - // link.download = `${measure.name ?? "chart"}.png`; - // link.href = url; - // link.click(); + const url = await toPng(captureNode, { cacheBust: true }); + const link = document.createElement("a"); + link.download = `${measure.name ?? "chart"}_${formattedTimeRange || generatedTime}.png`; + link.href = url; + link.click(); } finally { downloading = false; } } - { - url = ""; - }} -> + Export chart -
-
- -

- {measure.displayName || measure.name} -

- {#if measure.description} -

{measure.description}

- {/if} -
-
- -
- {#if timeGranularity} -
-
- + +
+
+
+

+ {measure.displayName || measure.name} +

+ {#if measure.description} +

{measure.description}

+ {/if}
- {/if} +
+
{formattedTimeRange}
+
- - {#if timeGranularity} - + {#if timeGranularity} +
+
+ +
+ {/if} + + - {/if} -
-
- - Generated {new Date().toLocaleString()} - Rill - -
-
+ {#if timeGranularity} + + {/if} +
- {#if url} -
- Screenshot +
+ Rill + Generated {generatedTime} +
- {/if} + diff --git a/web-common/src/features/themes/selectors.ts b/web-common/src/features/themes/selectors.ts index d05ab43a6a2..5a55017bddc 100644 --- a/web-common/src/features/themes/selectors.ts +++ b/web-common/src/features/themes/selectors.ts @@ -72,6 +72,7 @@ export function createResolvedThemeStore( queryClient, ); return themeQuery.subscribe(($themeQuery) => { + console.log(name, $themeQuery.data); if ($themeQuery.data?.theme?.spec) { set(new Theme($themeQuery.data.theme.spec)); } else {