diff --git a/.dockerignore b/.dockerignore
index 4c3e47098f0..b2785009de0 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,6 @@
+.git
.babel_cache
-target/*
+target
.circleci
.cpcache
.devcontainer
@@ -15,6 +16,7 @@ test_config/*
test_modules/*
test_resources/*
node_modules
+**/node_modules
**metabase.jar
*.db
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 09d5a6faaab..5f3c90bb2e7 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -547,6 +547,13 @@ const configs = [
"no-console": "off",
},
},
+ {
+ // Standalone Node service — console logging is appropriate here.
+ files: ["frontend/src/static-viz-server/**/*.ts"],
+ rules: {
+ "no-console": "off",
+ },
+ },
{
files: ["frontend/src/metabase/**/*"],
plugins: {
diff --git a/frontend/lint/module-boundaries.mjs b/frontend/lint/module-boundaries.mjs
index e0f17991ce5..b0196aff22d 100644
--- a/frontend/lint/module-boundaries.mjs
+++ b/frontend/lint/module-boundaries.mjs
@@ -179,6 +179,7 @@ const elements = [
"frontend/src/metabase/app-embed-mcp-public-path.ts",
"frontend/src/metabase/app-embed-mcp-public-path.unit.spec.ts",
"frontend/src/metabase/app-public.ts",
+ "frontend/src/metabase/app-static-viz.ts",
"frontend/src/metabase/AppComponent.tsx",
"frontend/src/metabase/App.styled.tsx",
"frontend/src/metabase/AppKBarProvider.tsx",
diff --git a/frontend/src/metabase-shared/color_selector.js b/frontend/src/metabase-shared/color_selector.js
deleted file mode 100644
index 8d8534ff80c..00000000000
--- a/frontend/src/metabase-shared/color_selector.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// This runs in the Nashorn JavaScript engine and there are some limitations
-//
-// 1. This is not currently automatically built with the rest of the application, please run `bun run build-shared` after modifying
-// 2. Avoid including unnecessary libraries as the JS engine takes a long time to parse and execute them
-// 3. Related to #2, we aren't currently including `core-js` so don't use features that require it, e.x. iterables / for-of
-
-import { makeCellBackgroundGetter } from "metabase/visualizations/lib/table_format";
-
-global.console = {
- log: print,
- warn: print,
- error: print,
-};
-
-function buildGetter(rowsJSON, colsJSON, settingsJSON) {
- const rows = JSON.parse(rowsJSON);
- const cols = JSON.parse(colsJSON);
- const settings = settingsJSON ? JSON.parse(settingsJSON) : {};
- try {
- return makeCellBackgroundGetter(
- rows,
- cols,
- settings?.["table.column_formatting"] ?? [],
- settings?.["table.pivot"],
- );
- } catch (e) {
- print("ERROR", e);
- return () => null;
- }
-}
-
-// Colors many cells in one host call, so the JVM side never holds a context-bound function value
-// across a render. `cellsJSON` is an array of [value, rowIndex, colName] triples; returns a JSON
-// array of color strings (or null), positionally.
-global.getCellBackgroundColors = function (
- rowsJSON,
- colsJSON,
- settingsJSON,
- cellsJSON,
-) {
- const getter = buildGetter(rowsJSON, colsJSON, settingsJSON);
- const cells = JSON.parse(cellsJSON);
- return JSON.stringify(cells.map((cell) => getter(cell[0], cell[1], cell[2])));
-};
diff --git a/frontend/src/metabase/app-static-viz.ts b/frontend/src/metabase/app-static-viz.ts
new file mode 100644
index 00000000000..339ffbc7ea4
--- /dev/null
+++ b/frontend/src/metabase/app-static-viz.ts
@@ -0,0 +1,14 @@
+import "metabase/static-viz/polyfill";
+
+import {
+ getCellBackgroundColors as getCellBackgroundColorsImpl,
+ renderChart as renderChartImpl,
+} from "metabase/static-viz";
+
+export function renderChart(inputJSON: string): string {
+ return JSON.stringify(renderChartImpl(JSON.parse(inputJSON)));
+}
+
+export function getCellBackgroundColors(inputJSON: string): string {
+ return JSON.stringify(getCellBackgroundColorsImpl(JSON.parse(inputJSON)));
+}
diff --git a/frontend/src/metabase/static-viz/index.tsx b/frontend/src/metabase/static-viz/index.tsx
index eaa3d69a003..6904a5cd252 100644
--- a/frontend/src/metabase/static-viz/index.tsx
+++ b/frontend/src/metabase/static-viz/index.tsx
@@ -1,6 +1,3 @@
-import "./mock-environment";
-import "fast-text-encoding";
-
import { setPlatformAPI } from "echarts/core";
import ReactDOMServer from "react-dom/server";
@@ -17,11 +14,11 @@ import { LegacyStaticChart } from "metabase/static-viz/containers/LegacyStaticCh
import type { LegacyStaticChartType } from "metabase/static-viz/containers/LegacyStaticChart/LegacyStaticChart";
import { createStaticRenderingContext } from "metabase/static-viz/lib/rendering-context";
import { measureTextEChartsAdapter } from "metabase/static-viz/lib/text";
-import type { ColorPalette } from "metabase/ui/colors/types";
import { updateStartOfWeek } from "metabase/utils/i18n";
import MetabaseSettings from "metabase/utils/settings";
import { extractRemappings, isCartesianChart } from "metabase/visualizations";
import { extendCardWithDashcardSettings } from "metabase/visualizations/lib/settings/typed-utils";
+import { makeCellBackgroundGetter } from "metabase/visualizations/lib/table_format";
import {
createDataSource,
getVisualizationColumns,
@@ -34,44 +31,36 @@ import type {
DashCardVisualizationSettings,
Dataset,
DatasetData,
- DayOfWeekId,
- FormattingSettings,
GeoJSONData,
RawSeries,
SettingKey,
- TokenFeatures,
VisualizerDataSourceId,
VisualizerVizDefinition,
} from "metabase-types/api";
+import type {
+ CellBackgroundColorsInput,
+ RenderChartDashcardSettings,
+ RenderChartInput,
+ RenderChartOptions,
+ RenderedChart,
+} from "./types";
+
+export type {
+ CellBackgroundColorsInput,
+ RenderChartInput,
+ RenderChartOptions,
+ RenderedChart,
+} from "./types";
+
setPlatformAPI({
measureText: measureTextEChartsAdapter,
});
-export type RenderChartOptions = {
- tokenFeatures: TokenFeatures;
- applicationColors: ColorPalette;
- customFormatting: FormattingSettings;
- startOfWeek: DayOfWeekId | null | undefined;
- // Explicit pixel dimensions for the chart. Use fitWithinBounds to have height include
- // chart legends
- width?: number;
- height?: number;
- // When true, width/height are treated as the exact output box
- fitWithinBounds?: boolean;
-};
-
-type RenderChartDashcardSettings = DashCardVisualizationSettings & {
- visualization?: VisualizerVizDefinition;
-};
-
/**
* @deprecated use RenderChart instead
*/
-export function LegacyRenderChart(
- type: LegacyStaticChartType,
- options: unknown,
-) {
+function LegacyRenderChart(type: LegacyStaticChartType, options: unknown) {
return ReactDOMServer.renderToStaticMarkup(
,
);
@@ -125,7 +114,7 @@ function getVisualizerRawSeries(
];
}
-export function RenderChart(
+function RenderChart(
rawSeries: RawSeries,
dashcardSettings: RenderChartDashcardSettings,
options: RenderChartOptions,
@@ -219,3 +208,59 @@ export function RenderChart(
/>,
);
}
+
+export function renderChart(input: RenderChartInput): RenderedChart {
+ let content: string;
+ switch (input.kind) {
+ case "funnel":
+ content = LegacyRenderChart("funnel", {
+ data: input.data,
+ settings: input.settings,
+ tokenFeatures: input.tokenFeatures,
+ });
+ break;
+ case "gauge":
+ content = LegacyRenderChart("gauge", {
+ card: input.card,
+ data: input.data,
+ tokenFeatures: input.tokenFeatures,
+ });
+ break;
+ default:
+ content = RenderChart(
+ input.rawSeries,
+ input.dashcardSettings,
+ input.options,
+ );
+ }
+ return { type: content.startsWith("