From 22725a83b9fbd9b23e6b524e5074e7e017bdb645 Mon Sep 17 00:00:00 2001 From: Alexander Polyankin Date: Thu, 9 Jul 2026 11:26:13 -0400 Subject: [PATCH] Extract a StaticVizRenderer protocol for static-viz rendering (#77315) --- .dockerignore | 4 +- eslint.config.mjs | 7 + frontend/lint/module-boundaries.mjs | 1 + .../src/metabase-shared/color_selector.js | 44 ---- frontend/src/metabase/app-static-viz.ts | 14 ++ frontend/src/metabase/static-viz/index.tsx | 103 +++++--- .../{mock-environment.ts => polyfill.ts} | 10 +- frontend/src/metabase/static-viz/types.ts | 80 +++++++ .../visualizations/lib/table_format.ts | 3 +- jest.config.js | 1 + package.json | 1 - resources/frontend_shared/color_selector.js | 1 - .../frontend_shared/static_viz_interface.js | 52 ----- rspack.shared.config.js | 24 -- rspack.static-viz.config.js | 6 +- src/metabase/channel/render/js/color.clj | 28 +-- src/metabase/channel/render/js/engine.clj | 94 -------- src/metabase/channel/render/js/graal.clj | 221 ++++++++++++++++++ src/metabase/channel/render/js/pool.clj | 160 ------------- src/metabase/channel/render/js/protocol.clj | 18 ++ src/metabase/channel/render/js/renderer.clj | 13 ++ src/metabase/channel/render/js/svg.clj | 80 +++---- .../channel/render/js/engine_test.clj | 27 --- .../metabase/channel/render/js/graal_test.clj | 28 +++ test/metabase/channel/render/js/pool_test.clj | 37 --- 25 files changed, 515 insertions(+), 542 deletions(-) delete mode 100644 frontend/src/metabase-shared/color_selector.js create mode 100644 frontend/src/metabase/app-static-viz.ts rename frontend/src/metabase/static-viz/{mock-environment.ts => polyfill.ts} (98%) create mode 100644 frontend/src/metabase/static-viz/types.ts delete mode 100644 resources/frontend_shared/color_selector.js delete mode 100644 resources/frontend_shared/static_viz_interface.js delete mode 100644 rspack.shared.config.js delete mode 100644 src/metabase/channel/render/js/engine.clj create mode 100644 src/metabase/channel/render/js/graal.clj delete mode 100644 src/metabase/channel/render/js/pool.clj create mode 100644 src/metabase/channel/render/js/protocol.clj create mode 100644 src/metabase/channel/render/js/renderer.clj delete mode 100644 test/metabase/channel/render/js/engine_test.clj create mode 100644 test/metabase/channel/render/js/graal_test.clj delete mode 100644 test/metabase/channel/render/js/pool_test.clj 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(" +) { + try { + return makeCellBackgroundGetter(...args); + } catch (e) { + console.error("Error building cell background getter", e); + return () => null; + } +} + +export function getCellBackgroundColors({ + rows, + cols, + settings, + cells, +}: CellBackgroundColorsInput): (string | null)[] { + const getter = buildCellBackgroundGetter( + rows, + cols, + settings?.["table.column_formatting"] ?? [], + Boolean(settings?.["table.pivot"]), + ); + return cells.map( + ([value, rowIndex, columnName]) => + getter(value, rowIndex, columnName) ?? null, + ); +} diff --git a/frontend/src/metabase/static-viz/mock-environment.ts b/frontend/src/metabase/static-viz/polyfill.ts similarity index 98% rename from frontend/src/metabase/static-viz/mock-environment.ts rename to frontend/src/metabase/static-viz/polyfill.ts index edc1a70c3ac..073516cfe3f 100644 --- a/frontend/src/metabase/static-viz/mock-environment.ts +++ b/frontend/src/metabase/static-viz/polyfill.ts @@ -1,6 +1,11 @@ +import "fast-text-encoding"; + import ResizeObserver from "resize-observer-polyfill"; const defineGlobal = (name: PropertyKey, value: unknown): void => { + if (Reflect.has(globalThis, name)) { + return; + } Object.defineProperty(globalThis, name, { value, writable: true, @@ -296,8 +301,3 @@ defineGlobal( } }, ); - -class MockTextEncoder { - encode(): void {} -} -defineGlobal("TextEncoder", MockTextEncoder); diff --git a/frontend/src/metabase/static-viz/types.ts b/frontend/src/metabase/static-viz/types.ts new file mode 100644 index 00000000000..b6b0e9c0641 --- /dev/null +++ b/frontend/src/metabase/static-viz/types.ts @@ -0,0 +1,80 @@ +import type { ColorPalette } from "metabase/ui/colors/types"; +import type { + ColumnFormattingSetting, + DashCardVisualizationSettings, + DatasetColumn, + DayOfWeekId, + FormattingSettings, + RawSeries, + RowValue, + RowValues, + TokenFeatures, + VisualizerVizDefinition, +} from "metabase-types/api"; + +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; +}; + +export type RenderChartDashcardSettings = DashCardVisualizationSettings & { + visualization?: VisualizerVizDefinition; +}; + +// The default (isomorphic) path — anything StaticVisualization can render. +export type IsomorphicChartInput = { + kind?: undefined; + rawSeries: RawSeries; + dashcardSettings: RenderChartDashcardSettings; + options: RenderChartOptions; +}; + +// Legacy funnel/gauge charts render through LegacyStaticChart, whose options are untyped. +export type FunnelChartInput = { + kind: "funnel"; + data: unknown; + settings: unknown; + tokenFeatures: TokenFeatures; +}; + +export type GaugeChartInput = { + kind: "gauge"; + card: unknown; + data: unknown; + tokenFeatures: TokenFeatures; +}; + +export type RenderChartInput = + | IsomorphicChartInput + | FunnelChartInput + | GaugeChartInput; + +export type RenderedChart = { + type: "svg" | "html"; + content: string; +}; + +// One cell to color: its value, its row index, and its column name. +export type CellToColor = [ + value: RowValue, + rowIndex: number, + columnName: string, +]; + +export type CellBackgroundColorsInput = { + rows: RowValues[]; + cols: DatasetColumn[]; + settings: { + "table.column_formatting"?: ColumnFormattingSetting[]; + "table.pivot"?: boolean; + }; + cells: CellToColor[]; +}; diff --git a/frontend/src/metabase/visualizations/lib/table_format.ts b/frontend/src/metabase/visualizations/lib/table_format.ts index 4a7ea5243d2..878792e901a 100644 --- a/frontend/src/metabase/visualizations/lib/table_format.ts +++ b/frontend/src/metabase/visualizations/lib/table_format.ts @@ -1,5 +1,6 @@ // NOTE: this file is used on the frontend and backend and there are some -// limitations. See frontend/src/metabase-shared/color_selector for details +// limitations. On the backend it is bundled into the static-viz bundle and +// called via getCellBackgroundColors; see frontend/src/metabase/static-viz/index.tsx. import Color from "color"; diff --git a/jest.config.js b/jest.config.js index 8b6470eaabd..3df9509f7cc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -66,6 +66,7 @@ const baseConfig = { "sdk-iframe-embedding-ee-plugins": "/frontend/src/metabase/utils/noop.ts", "ee-plugins": "/frontend/src/metabase/utils/noop.ts", + "ee-overrides": "/frontend/src/metabase/utils/noop.ts", /** * Imports which are only applicable to the embedding sdk. * As we use SDK components in new iframe embedding, we need to import them here. diff --git a/package.json b/package.json index 23640793940..a7b314c2662 100644 --- a/package.json +++ b/package.json @@ -479,7 +479,6 @@ "build-release:static-viz": "WEBPACK_BUNDLE=production rspack --config rspack.static-viz.config.js", "build-release:embedding-sdk-package": "WEBPACK_BUNDLE=production rspack --config rspack.embedding-sdk-package.config.js", "build-release:embedding-sdk-bundle": "WEBPACK_BUNDLE=production rspack --config rspack.embedding-sdk-bundle.config.js", - "build-shared": "bun install && rspack --config rspack.shared.config.js", "build-static-viz": "bun install && bun run build-release:cljs && WEBPACK_BUNDLE=production rspack --config rspack.static-viz.config.js", "build-static-viz:watch": "rspack --config rspack.static-viz.config.js --watch", "build-static-viz:watch-wait": "bun run wait:cljs; bun run build-static-viz:watch", diff --git a/resources/frontend_shared/color_selector.js b/resources/frontend_shared/color_selector.js deleted file mode 100644 index e57355db1ee..00000000000 --- a/resources/frontend_shared/color_selector.js +++ /dev/null @@ -1 +0,0 @@ -var shared;!function(){"use strict";var r,n,e,t,a={};a.d=function(r,n){for(var e in n)a.o(n,e)&&!a.o(r,e)&&Object.defineProperty(r,e,{enumerable:!0,get:n[e]})},a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(r){if("object"==typeof window)return window}}(),a.o=function(r,n){return Object.prototype.hasOwnProperty.call(r,n)},a.r=function(r){"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(r,"__esModule",{value:!0})};var o={};a.r(o),a.d(o,{VERSION:function(){return N},after:function(){return nV},all:function(){return n9},allKeys:function(){return rq},any:function(){return er},assign:function(){return rQ},before:function(){return nH},bind:function(){return nI},bindAll:function(){return nz},chain:function(){return nj},chunk:function(){return eC},clone:function(){return r5},collect:function(){return n5},compact:function(){return e_},compose:function(){return nU},constant:function(){return ry},contains:function(){return en},countBy:function(){return eg},create:function(){return r2},debounce:function(){return nB},default:function(){return eB},defaults:function(){return r0},defer:function(){return nD},delay:function(){return nC},detect:function(){return n0},difference:function(){return ej},drop:function(){return eM},each:function(){return n2},escape:function(){return ng},every:function(){return n9},extend:function(){return rZ},extendOwn:function(){return rQ},filter:function(){return n8},find:function(){return n0},findIndex:function(){return nK},findKey:function(){return nW},findLastIndex:function(){return nX},findWhere:function(){return n1},first:function(){return ew},flatten:function(){return eN},foldl:function(){return n6},foldr:function(){return n4},forEach:function(){return n2},functions:function(){return rG},get:function(){return r7},groupBy:function(){return eb},has:function(){return r9},head:function(){return ew},identity:function(){return nr},include:function(){return en},includes:function(){return en},indexBy:function(){return ed},indexOf:function(){return nZ},initial:function(){return ex},inject:function(){return n6},intersection:function(){return eI},invert:function(){return rX},invoke:function(){return ee},isArguments:function(){return rp},isArray:function(){return rb},isArrayBuffer:function(){return ra},isBoolean:function(){return X},isDataView:function(){return rh},isDate:function(){return rr},isElement:function(){return G},isEmpty:function(){return r$},isEqual:function(){return rz},isError:function(){return re},isFinite:function(){return rm},isFunction:function(){return rl},isMap:function(){return rV},isMatch:function(){return rS},isNaN:function(){return rv},isNull:function(){return J},isNumber:function(){return Q},isObject:function(){return W},isRegExp:function(){return rn},isSet:function(){return rL},isString:function(){return Z},isSymbol:function(){return rt},isTypedArray:function(){return r_},isUndefined:function(){return K},isWeakMap:function(){return rH},isWeakSet:function(){return rW},iteratee:function(){return no},keys:function(){return rO},last:function(){return eA},lastIndexOf:function(){return nQ},map:function(){return n5},mapObject:function(){return nl},matcher:function(){return nn},matches:function(){return nn},max:function(){return eo},memoize:function(){return nq},methods:function(){return rG},min:function(){return ei},mixin:function(){return eR},negate:function(){return nT},noop:function(){return nu},now:function(){return nh},object:function(){return ez},omit:function(){return ek},once:function(){return nL},pairs:function(){return rK},partial:function(){return nS},partition:function(){return ep},pick:function(){return ey},pluck:function(){return et},property:function(){return ne},propertyOf:function(){return nc},random:function(){return nf},range:function(){return eq},reduce:function(){return n6},reduceRight:function(){return n4},reject:function(){return n7},rest:function(){return eM},restArguments:function(){return L},result:function(){return nA},sample:function(){return ec},select:function(){return n8},shuffle:function(){return es},size:function(){return em},some:function(){return er},sortBy:function(){return ef},sortedIndex:function(){return nG},tail:function(){return eM},take:function(){return ew},tap:function(){return r3},template:function(){return nM},templateSettings:function(){return nm},throttle:function(){return nR},times:function(){return ns},toArray:function(){return eu},toPath:function(){return r6},transpose:function(){return eE},unescape:function(){return np},union:function(){return eS},uniq:function(){return e$},unique:function(){return e$},uniqueId:function(){return nN},unzip:function(){return eE},values:function(){return rJ},where:function(){return ea},without:function(){return eO},wrap:function(){return nP},zip:function(){return eF}});let i={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]};for(let r in i)Object.freeze(i[r]);var l=Object.freeze(i);let u=Object.create(null);for(let r in l)Object.hasOwn(l,r)&&(u[l[r]]=r);let c={to:{},get:{}};function s(r,n,e){return Math.min(Math.max(n,r),e)}function f(r){let n=Math.round(r).toString(16).toUpperCase();return n.length<2?"0"+n:n}c.get=function(r){let n,e;switch(r.slice(0,3).toLowerCase()){case"hsl":n=c.get.hsl(r),e="hsl";break;case"hwb":n=c.get.hwb(r),e="hwb";break;default:n=c.get.rgb(r),e="rgb"}return n?{model:e,value:n}:null},c.get.rgb=function(r){let n,e,t;if(!r)return null;let a=[0,0,0,1];if(n=r.match(/^#([a-f\d]{6})([a-f\d]{2})?$/i)){for(t=n[2],n=n[1],e=0;e<3;e++){let r=2*e;a[e]=Number.parseInt(n.slice(r,r+2),16)}t&&(a[3]=Number.parseInt(t,16)/255)}else if(n=r.match(/^#([a-f\d]{3,4})$/i)){for(t=(n=n[1])[3],e=0;e<3;e++)a[e]=Number.parseInt(n[e]+n[e],16);t&&(a[3]=Number.parseInt(t+t,16)/255)}else if(n=r.match(/^rgba?\(\s*([+-]?(?:\d*\.)?\d+(?:e\d+)?)(?=[\s,])\s*(?:,\s*)?([+-]?(?:\d*\.)?\d+(?:e\d+)?)(?=[\s,])\s*(?:,\s*)?([+-]?(?:\d*\.)?\d+(?:e\d+)?)\s*(?:[\s,|/]\s*([+-]?(?:\d*\.)?\d+(?:e\d+)?)(%?)\s*)?\)$/i)){for(e=0;e<3;e++)a[e]=Number.parseFloat(n[e+1]);n[4]&&(a[3]=n[5]?.01*Number.parseFloat(n[4]):Number.parseFloat(n[4]))}else if(n=r.match(/^rgba?\(\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[\s,|/]\s*([+-]?[\d.]+)(%?)\s*)?\)$/i)){for(e=0;e<3;e++)a[e]=Math.round(2.55*Number.parseFloat(n[e+1]));n[4]&&(a[3]=n[5]?.01*Number.parseFloat(n[4]):Number.parseFloat(n[4]))}else if(!(n=r.toLowerCase().match(/^(\w+)$/)))return null;else return"transparent"===n[1]?[0,0,0,0]:Object.hasOwn(l,n[1])?((a=l[n[1]].slice())[3]=1,a):null;for(e=0;e<3;e++)a[e]=s(a[e],0,255);return a[3]=s(a[3],0,1),a},c.get.hsl=function(r){if(!r)return null;let n=r.match(/^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[,|/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:e[+-]?\d+)?)\s*)?\)$/i);if(n){let r=Number.parseFloat(n[4]);return[(Number.parseFloat(n[1])%360+360)%360,s(Number.parseFloat(n[2]),0,100),s(Number.parseFloat(n[3]),0,100),s(Number.isNaN(r)?1:r,0,1)]}return null},c.get.hwb=function(r){if(!r)return null;let n=r.match(/^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*[\s,]\s*([+-]?[\d.]+)%\s*[\s,]\s*([+-]?[\d.]+)%\s*(?:[\s,]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:e[+-]?\d+)?)\s*)?\)$/i);if(n){let r=Number.parseFloat(n[4]);return[(Number.parseFloat(n[1])%360+360)%360,s(Number.parseFloat(n[2]),0,100),s(Number.parseFloat(n[3]),0,100),s(Number.isNaN(r)?1:r,0,1)]}return null},c.to.hex=function(...r){return"#"+f(r[0])+f(r[1])+f(r[2])+(r[3]<1?f(Math.round(255*r[3])):"")},c.to.rgb=function(...r){return r.length<4||1===r[3]?"rgb("+Math.round(r[0])+", "+Math.round(r[1])+", "+Math.round(r[2])+")":"rgba("+Math.round(r[0])+", "+Math.round(r[1])+", "+Math.round(r[2])+", "+r[3]+")"},c.to.rgb.percent=function(...r){let n=Math.round(r[0]/255*100),e=Math.round(r[1]/255*100),t=Math.round(r[2]/255*100);return r.length<4||1===r[3]?"rgb("+n+"%, "+e+"%, "+t+"%)":"rgba("+n+"%, "+e+"%, "+t+"%, "+r[3]+")"},c.to.hsl=function(...r){return r.length<4||1===r[3]?"hsl("+r[0]+", "+r[1]+"%, "+r[2]+"%)":"hsla("+r[0]+", "+r[1]+"%, "+r[2]+"%, "+r[3]+")"},c.to.hwb=function(...r){let n="";return r.length>=4&&1!==r[3]&&(n=", "+r[3]),"hwb("+r[0]+", "+r[1]+"%, "+r[2]+"%"+n+")"},c.to.keyword=function(...r){return u[r.slice(0,3)]};let h={};for(let r of Object.keys(l))h[l[r]]=r;let b={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},oklab:{channels:3,labels:["okl","oka","okb"]},lch:{channels:3,labels:"lch"},oklch:{channels:3,labels:["okl","okc","okh"]},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}},d=(6/29)**3;function g(r){return Math.min(Math.max(0,r>.0031308?1.055*r**(1/2.4)-.055:12.92*r),1)}function p(r){return r>.04045?((r+.055)/1.055)**2.4:r/12.92}for(let r of Object.keys(b)){if(!("channels"in b[r]))throw Error("missing channels property: "+r);if(!("labels"in b[r]))throw Error("missing channel labels property: "+r);if(b[r].labels.length!==b[r].channels)throw Error("channel and label counts mismatch: "+r);let{channels:n,labels:e}=b[r];delete b[r].channels,delete b[r].labels,Object.defineProperty(b[r],"channels",{value:n}),Object.defineProperty(b[r],"labels",{value:e})}b.rgb.hsl=function(r){let n,e=r[0]/255,t=r[1]/255,a=r[2]/255,o=Math.min(e,t,a),i=Math.max(e,t,a),l=i-o;switch(i){case o:n=0;break;case e:n=(t-a)/l;break;case t:n=2+(a-e)/l;break;case a:n=4+(e-t)/l}(n=Math.min(60*n,360))<0&&(n+=360);let u=(o+i)/2;return[n,100*(i===o?0:u<=.5?l/(i+o):l/(2-i-o)),100*u]},b.rgb.hsv=function(r){let n,e,t,a,o,i=r[0]/255,l=r[1]/255,u=r[2]/255,c=Math.max(i,l,u),s=c-Math.min(i,l,u),f=function(r){return(c-r)/6/s+.5};if(0===s)a=0,o=0;else{switch(o=s/c,n=f(i),e=f(l),t=f(u),c){case i:a=t-e;break;case l:a=1/3+n-t;break;case u:a=2/3+e-n}a<0?a+=1:a>1&&(a-=1)}return[360*a,100*o,100*c]},b.rgb.hwb=function(r){let n=r[0],e=r[1],t=r[2];return[b.rgb.hsl(r)[0],1/255*Math.min(n,Math.min(e,t))*100,100*(t=1-1/255*Math.max(n,Math.max(e,t)))]},b.rgb.oklab=function(r){let n=p(r[0]/255),e=p(r[1]/255),t=p(r[2]/255),a=Math.cbrt(.4122214708*n+.5363325363*e+.0514459929*t),o=Math.cbrt(.2119034982*n+.6806995451*e+.1073969566*t),i=Math.cbrt(.0883024619*n+.2817188376*e+.6299787005*t);return[100*(.2104542553*a+.793617785*o-.0040720468*i),100*(1.9779984951*a-2.428592205*o+.4505937099*i),100*(.0259040371*a+.7827717662*o-.808675766*i)]},b.rgb.cmyk=function(r){let n=r[0]/255,e=r[1]/255,t=r[2]/255,a=Math.min(1-n,1-e,1-t);return[100*((1-n-a)/(1-a)||0),100*((1-e-a)/(1-a)||0),100*((1-t-a)/(1-a)||0),100*a]},b.rgb.keyword=function(r){let n,e=h[r];if(e)return e;let t=1/0;for(let e of Object.keys(l)){let a=l[e],o=(r[0]-a[0])**2+(r[1]-a[1])**2+(r[2]-a[2])**2;od?e**(1/3):7.787*e+16/116,[116*(t=t>d?t**(1/3):7.787*t+16/116)-16,500*(e-t),200*(t-(a=a>d?a**(1/3):7.787*a+16/116))]},b.hsl.rgb=function(r){let n,e,t=r[0]/360,a=r[1]/100,o=r[2]/100;if(0===a)return[e=255*o,e,e];let i=o<.5?o*(1+a):o+a-o*a,l=2*o-i,u=[0,0,0];for(let r=0;r<3;r++)(n=t+-(1/3*(r-1)))<0&&n++,n>1&&n--,e=6*n<1?l+(i-l)*6*n:2*n<1?i:3*n<2?l+(i-l)*(2/3-n)*6:l,u[r]=255*e;return u},b.hsl.hsv=function(r){let n=r[0],e=r[1]/100,t=r[2]/100,a=e,o=Math.max(t,.01);t*=2,e*=t<=1?t:2-t,a*=o<=1?o:2-o;let i=(t+e)/2;return[n,100*(0===t?2*a/(o+a):2*e/(t+e)),100*i]},b.hsv.rgb=function(r){let n=r[0]/60,e=r[1]/100,t=r[2]/100,a=Math.floor(n)%6,o=n-Math.floor(n),i=255*t*(1-e),l=255*t*(1-e*o),u=255*t*(1-e*(1-o));switch(t*=255,a){case 0:return[t,u,i];case 1:return[l,t,i];case 2:return[i,t,u];case 3:return[i,l,t];case 4:return[u,i,t];case 5:return[t,i,l]}},b.hsv.hsl=function(r){let n,e,t=r[0],a=r[1]/100,o=r[2]/100,i=Math.max(o,.01);e=(2-a)*o;let l=(2-a)*i;return[t,100*(n=(n=a*i/(l<=1?l:2-l))||0),100*(e/=2)]},b.hwb.rgb=function(r){let n,e,t,a,o=r[0]/360,i=r[1]/100,l=r[2]/100,u=i+l;u>1&&(i/=u,l/=u);let c=Math.floor(6*o),s=1-l;n=6*o-c,(1&c)!=0&&(n=1-n);let f=i+n*(s-i);switch(c){default:case 6:case 0:e=s,t=f,a=i;break;case 1:e=f,t=s,a=i;break;case 2:e=i,t=s,a=f;break;case 3:e=i,t=f,a=s;break;case 4:e=f,t=i,a=s;break;case 5:e=s,t=i,a=f}return[255*e,255*t,255*a]},b.cmyk.rgb=function(r){let n=r[0]/100,e=r[1]/100,t=r[2]/100,a=r[3]/100;return[255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,t*(1-a)+a))]},b.xyz.rgb=function(r){let n,e,t,a=r[0]/100,o=r[1]/100,i=r[2]/100;return n=3.2404542*a+-1.5371385*o+-.4985314*i,e=-.969266*a+1.8760108*o+.041556*i,t=.0556434*a+-.2040259*o+1.0572252*i,n=g(n),[255*n,255*(e=g(e)),255*(t=g(t))]},b.xyz.lab=function(r){let n=r[0],e=r[1],t=r[2];return n/=95.047,e/=100,t/=108.883,n=n>d?n**(1/3):7.787*n+16/116,[116*(e=e>d?e**(1/3):7.787*e+16/116)-16,500*(n-e),200*(e-(t=t>d?t**(1/3):7.787*t+16/116))]},b.xyz.oklab=function(r){let n=r[0]/100,e=r[1]/100,t=r[2]/100,a=Math.cbrt(.8189330101*n+.3618667424*e-.1288597137*t),o=Math.cbrt(.0329845436*n+.9293118715*e+.0361456387*t),i=Math.cbrt(.0482003018*n+.2643662691*e+.633851707*t);return[100*(.2104542553*a+.793617785*o-.0040720468*i),100*(1.9779984951*a-2.428592205*o+.4505937099*i),100*(.0259040371*a+.7827717662*o-.808675766*i)]},b.oklab.oklch=function(r){return b.lab.lch(r)},b.oklab.xyz=function(r){let n=r[0]/100,e=r[1]/100,t=r[2]/100,a=(.999999998*n+.396337792*e+.215803758*t)**3,o=(1.000000008*n-.105561342*e-.063854175*t)**3,i=(1.000000055*n-.089484182*e-1.291485538*t)**3;return[100*(1.227013851*a-.55779998*o+.281256149*i),100*(-.040580178*a+1.11225687*o-.071676679*i),100*(-.076381285*a-.421481978*o+1.58616322*i)]},b.oklab.rgb=function(r){let n=r[0]/100,e=r[1]/100,t=r[2]/100,a=(n+.3963377774*e+.2158037573*t)**3,o=(n-.1055613458*e-.0638541728*t)**3,i=(n-.0894841775*e-1.291485548*t)**3;return[255*g(4.0767416621*a-3.3077115913*o+.2309699292*i),255*g(-1.2684380046*a+2.6097574011*o-.3413193965*i),255*g(-.0041960863*a-.7034186147*o+1.707614701*i)]},b.oklch.oklab=function(r){return b.lch.lab(r)},b.lab.xyz=function(r){let n,e,t,a=r[0],o=r[1],i=r[2];n=o/500+(e=(a+16)/116),t=e-i/200;let l=e**3,u=n**3,c=t**3;return e=(l>d?l:(e-16/116)/7.787)*100,[n=(u>d?u:(n-16/116)/7.787)*95.047,e,t=(c>d?c:(t-16/116)/7.787)*108.883]},b.lab.lch=function(r){let n,e=r[0],t=r[1],a=r[2];return(n=360*Math.atan2(a,t)/2/Math.PI)<0&&(n+=360),[e,Math.sqrt(t*t+a*a),n]},b.lch.lab=function(r){let n=r[0],e=r[1],t=r[2]/360*2*Math.PI;return[n,e*Math.cos(t),e*Math.sin(t)]},b.rgb.ansi16=function(r,n=null){let[e,t,a]=r,o=null===n?b.rgb.hsv(r)[2]:n;if(0===(o=Math.round(o/50)))return 30;let i=30+(Math.round(a/255)<<2|Math.round(t/255)<<1|Math.round(e/255));return 2===o&&(i+=60),i},b.hsv.ansi16=function(r){return b.rgb.ansi16(b.hsv.rgb(r),r[2])},b.rgb.ansi256=function(r){let n=r[0],e=r[1],t=r[2];return n>>4==e>>4&&e>>4==t>>4?n<8?16:n>248?231:Math.round((n-8)/247*24)+232:16+36*Math.round(n/255*5)+6*Math.round(e/255*5)+Math.round(t/255*5)},b.ansi16.rgb=function(r){let n=(r=r[0])%10;if(0===n||7===n)return r>50&&(n+=3.5),[n=n/10.5*255,n,n];let e=(Math.trunc(r>50)+1)*.5;return[(1&n)*e*255,(n>>1&1)*e*255,(n>>2&1)*e*255]},b.ansi256.rgb=function(r){let n;if((r=r[0])>=232){let n=(r-232)*10+8;return[n,n,n]}return[Math.floor((r-=16)/36)/5*255,Math.floor((n=r%36)/6)/5*255,n%6/5*255]},b.rgb.hex=function(r){let n=(((255&Math.round(r[0]))<<16)+((255&Math.round(r[1]))<<8)+(255&Math.round(r[2]))).toString(16).toUpperCase();return"000000".slice(n.length)+n},b.hex.rgb=function(r){let n=r.toString(16).match(/[a-f\d]{6}|[a-f\d]{3}/i);if(!n)return[0,0,0];let e=n[0];3===n[0].length&&(e=[...e].map(r=>r+r).join(""));let t=Number.parseInt(e,16);return[t>>16&255,t>>8&255,255&t]},b.rgb.hcg=function(r){let n=r[0]/255,e=r[1]/255,t=r[2]/255,a=Math.max(Math.max(n,e),t),o=Math.min(Math.min(n,e),t),i=a-o;return[360*((i<=0?0:a===n?(e-t)/i%6:a===e?2+(t-n)/i:4+(n-e)/i)/6%1),100*i,100*(i<1?o/(1-i):0)]},b.hsl.hcg=function(r){let n=r[1]/100,e=r[2]/100,t=e<.5?2*n*e:2*n*(1-e),a=0;return t<1&&(a=(e-.5*t)/(1-t)),[r[0],100*t,100*a]},b.hsv.hcg=function(r){let n=r[1]/100,e=r[2]/100,t=n*e,a=0;return t<1&&(a=(e-t)/(1-t)),[r[0],100*t,100*a]},b.hcg.rgb=function(r){let n=r[0]/360,e=r[1]/100,t=r[2]/100;if(0===e)return[255*t,255*t,255*t];let a=[0,0,0],o=n%1*6,i=o%1,l=1-i,u=0;switch(Math.floor(o)){case 0:a[0]=1,a[1]=i,a[2]=0;break;case 1:a[0]=l,a[1]=1,a[2]=0;break;case 2:a[0]=0,a[1]=1,a[2]=i;break;case 3:a[0]=0,a[1]=l,a[2]=1;break;case 4:a[0]=i,a[1]=0,a[2]=1;break;default:a[0]=1,a[1]=0,a[2]=l}return u=(1-e)*t,[(e*a[0]+u)*255,(e*a[1]+u)*255,(e*a[2]+u)*255]},b.hcg.hsv=function(r){let n=r[1]/100,e=n+r[2]/100*(1-n),t=0;return e>0&&(t=n/e),[r[0],100*t,100*e]},b.hcg.hsl=function(r){let n=r[1]/100,e=r[2]/100*(1-n)+.5*n,t=0;return e>0&&e<.5?t=n/(2*e):e>=.5&&e<1&&(t=n/(2*(1-e))),[r[0],100*t,100*e]},b.hcg.hwb=function(r){let n=r[1]/100,e=n+r[2]/100*(1-n);return[r[0],(e-n)*100,(1-e)*100]},b.hwb.hcg=function(r){let n=r[1]/100,e=1-r[2]/100,t=e-n,a=0;return t<1&&(a=(e-t)/(1-t)),[r[0],100*t,100*a]},b.apple.rgb=function(r){return[r[0]/65535*255,r[1]/65535*255,r[2]/65535*255]},b.rgb.apple=function(r){return[r[0]/255*65535,r[1]/255*65535,r[2]/255*65535]},b.gray.rgb=function(r){return[r[0]/100*255,r[0]/100*255,r[0]/100*255]},b.gray.hsl=function(r){return[0,0,r[0]]},b.gray.hsv=b.gray.hsl,b.gray.hwb=function(r){return[0,100,r[0]]},b.gray.cmyk=function(r){return[0,0,0,r[0]]},b.gray.lab=function(r){return[r[0],0,0]},b.gray.hex=function(r){let n=255&Math.round(r[0]/100*255),e=((n<<16)+(n<<8)+n).toString(16).toUpperCase();return"000000".slice(e.length)+e},b.rgb.gray=function(r){return[(r[0]+r[1]+r[2])/3/255*100]};let m={};for(let r of Object.keys(b)){m[r]={},Object.defineProperty(m[r],"channels",{value:b[r].channels}),Object.defineProperty(m[r],"labels",{value:b[r].labels});let n=function(r){let n=function(r){let n=function(){let r={},n=Object.keys(b);for(let{length:e}=n,t=0;t0;){let r=e.pop(),t=Object.keys(b[r]);for(let{length:a}=t,o=0;o1&&(n=e);let t=r(n);if("object"==typeof t)for(let{length:r}=t,n=0;n1&&(n=e),r(n))};return"conversion"in r&&(n.conversion=r.conversion),n}(t)}}let v=["keyword","gray","hex"],y={};for(let r of Object.keys(m))y[[...m[r].labels].sort().join("")]=r;let k={};function x(r,n){let e,t;if(!(this instanceof x))return new x(r,n);if(n&&n in v&&(n=null),n&&!(n in m))throw Error("Unknown model: "+n);if(null==r)this.model="rgb",this.color=[0,0,0],this.valpha=1;else if(r instanceof x)this.model=r.model,this.color=[...r.color],this.valpha=r.valpha;else if("string"==typeof r){let n=c.get(r);if(null===n)throw Error("Unable to parse color from string: "+r);this.model=n.model,t=m[this.model].channels,this.color=n.value.slice(0,t),this.valpha="number"==typeof n.value[t]?n.value[t]:1}else if(r.length>0){this.model=n||"rgb",t=m[this.model].channels;let e=Array.prototype.slice.call(r,0,t);this.color=A(e,t),this.valpha="number"==typeof r[t]?r[t]:1}else if("number"==typeof r)this.model="rgb",this.color=[r>>16&255,r>>8&255,255&r],this.valpha=1;else{this.valpha=1;let n=Object.keys(r);"alpha"in r&&(n.splice(n.indexOf("alpha"),1),this.valpha="number"==typeof r.alpha?r.alpha:0);let t=n.sort().join("");if(!(t in y))throw Error("Unable to parse color from object: "+JSON.stringify(r));this.model=y[t];let{labels:a}=m[this.model],o=[];for(e=0;e(r%360+360)%360),saturationl:w("hsl",1,M(100)),lightness:w("hsl",2,M(100)),saturationv:w("hsv",1,M(100)),value:w("hsv",2,M(100)),chroma:w("hcg",1,M(100)),gray:w("hcg",2,M(100)),white:w("hwb",1,M(100)),wblack:w("hwb",2,M(100)),cyan:w("cmyk",0,M(100)),magenta:w("cmyk",1,M(100)),yellow:w("cmyk",2,M(100)),black:w("cmyk",3,M(100)),x:w("xyz",0,M(95.047)),y:w("xyz",1,M(100)),z:w("xyz",2,M(108.833)),l:w("lab",0,M(100)),a:w("lab",1),b:w("lab",2),keyword(r){return void 0!==r?new x(r):m[this.model].keyword(this.color)},hex(r){return void 0!==r?new x(r):c.to.hex(...this.rgb().round().color)},hexa(r){if(void 0!==r)return new x(r);let n=this.rgb().round().color,e=Math.round(255*this.valpha).toString(16).toUpperCase();return 1===e.length&&(e="0"+e),c.to.hex(...n)+e},rgbNumber(){let r=this.rgb().color;return(255&r[0])<<16|(255&r[1])<<8|255&r[2]},luminosity(){let r=this.rgb().color,n=[];for(let[e,t]of r.entries()){let r=t/255;n[e]=r<=.04045?r/12.92:((r+.055)/1.055)**2.4}return .2126*n[0]+.7152*n[1]+.0722*n[2]},contrast(r){let n=this.luminosity(),e=r.luminosity();return n>e?(n+.05)/(e+.05):(e+.05)/(n+.05)},level(r){let n=this.contrast(r);return n>=7?"AAA":n>=4.5?"AA":""},isDark(){let r=this.rgb().color;return(2126*r[0]+7152*r[1]+722*r[2])/1e4<128},isLight(){return!this.isDark()},negate(){let r=this.rgb();for(let n=0;n<3;n++)r.color[n]=255-r.color[n];return r},lighten(r){let n=this.hsl();return n.color[2]+=n.color[2]*r,n},darken(r){let n=this.hsl();return n.color[2]-=n.color[2]*r,n},saturate(r){let n=this.hsl();return n.color[1]+=n.color[1]*r,n},desaturate(r){let n=this.hsl();return n.color[1]-=n.color[1]*r,n},whiten(r){let n=this.hwb();return n.color[1]+=n.color[1]*r,n},blacken(r){let n=this.hwb();return n.color[2]+=n.color[2]*r,n},grayscale(){let r=this.rgb().color,n=.3*r[0]+.59*r[1]+.11*r[2];return x.rgb(n,n,n)},fade(r){return this.alpha(this.valpha-this.valpha*r)},opaquer(r){return this.alpha(this.valpha+this.valpha*r)},rotate(r){let n=this.hsl(),e=n.color[0];return e=(e=(e+r)%360)<0?360+e:e,n.color[0]=e,n},mix(r,n){if(!r||!r.rgb)throw Error('Argument to "mix" was not a Color instance, but rather an instance of '+typeof r);let e=r.rgb(),t=this.rgb(),a=void 0===n?.5:n,o=2*a-1,i=e.alpha()-t.alpha(),l=((o*i==-1?o:(o+i)/(1+o*i))+1)/2,u=1-l;return x.rgb(l*e.red()+u*t.red(),l*e.green()+u*t.green(),l*e.blue()+u*t.blue(),e.alpha()*a+t.alpha()*(1-a))}},Object.keys(m))){if(v.includes(r))continue;let{channels:n}=m[r];x.prototype[r]=function(...n){var e;return this.model===r?new x(this):n.length>0?new x(n,r):new x([...Array.isArray(e=m[this.model][r].raw(this.color))?e:[e],this.valpha],r)},x[r]=function(...e){let t=e[0];return"number"==typeof t&&(t=A(e,n)),new x(t,r)}}function w(r,n,e){for(let t of r=Array.isArray(r)?r:[r])(k[t]||=[])[n]=e;return r=r[0],function(t){let a;return void 0!==t?(e&&(t=e(t)),(a=this[r]()).color[n]=t):(a=this[r]().color[n],e&&(a=e(a))),a}}function M(r){return function(n){return Math.max(0,Math.min(r,n))}}function A(r,n){for(let e=0;etypeof Symbol?Symbol.prototype:null,I=O.push,E=O.slice,F=$.toString,z=$.hasOwnProperty,q="u">typeof ArrayBuffer,C="u">typeof DataView,D=Array.isArray,R=Object.keys,B=Object.create,P=q&&ArrayBuffer.isView,T=isNaN,U=isFinite,V=!({toString:null}).propertyIsEnumerable("toString"),H=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];function L(r,n){return n=null==n?r.length-1:+n,function(){for(var e=Math.max(arguments.length-n,0),t=Array(e),a=0;atypeof Map&&ru(new Map),rf=Y("DataView"),rh=rc?function(r){return null!=r&&rl(r.getInt8)&&ra(r.buffer)}:rf,rb=D||Y("Array");function rd(r,n){return null!=r&&z.call(r,n)}var rg=Y("Arguments");!function(){rg(arguments)||(rg=function(r){return rd(r,"callee")})}();var rp=rg;function rm(r){return!rt(r)&&U(r)&&!isNaN(parseFloat(r))}function rv(r){return Q(r)&&T(r)}function ry(r){return function(){return r}}function rk(r){return function(n){var e=r(n);return"number"==typeof e&&e>=0&&e<=0x1fffffffffffff}}function rx(r){return function(n){return null==n?void 0:n[r]}}var rw=rx("byteLength"),rM=rk(rw),rA=/\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/,r_=q?function(r){return P?P(r)&&!rh(r):rM(r)&&rA.test(F.call(r))}:ry(!1),rN=rx("length");function rj(r,n){n=function(r){for(var n={},e=r.length,t=0;t=0))if(t.push(r),a.push(n),e.push(!0),u){if((f=r.length)!==n.length)return!1;for(;f--;)e.push({a:r[f],b:n[f]})}else{var h,b=rO(r);if(f=b.length,rO(n).length!==f)return!1;for(;f--;){if(!rd(n,h=b[f]))return!1;e.push({a:r[h],b:n[h]})}}}return!0}function rq(r){if(!W(r))return[];var n=[];for(var e in r)n.push(e);return V&&rj(r,n),n}function rC(r){var n=rN(r);return function(e){if(null==e||rN(rq(e)))return!1;for(var t=0;t":">",'"':""","'":"'","`":"`"},ng=nb(nd),np=nb(rX(nd)),nm=rI.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},nv=/(.)^/,ny={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},nk=/\\|'|\r|\n|\u2028|\u2029/g;function nx(r){return"\\"+ny[r]}var nw=/^\s*(\w|\$)+\s*$/;function nM(r,n,e){!n&&e&&(n=e);var t,a=RegExp([((n=r0({},n,rI.templateSettings)).escape||nv).source,(n.interpolate||nv).source,(n.evaluate||nv).source].join("|")+"|$","g"),o=0,i="__p+='";r.replace(a,function(n,e,t,a,l){return i+=r.slice(o,l).replace(nk,nx),o=l+n.length,e?i+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?i+="'+\n((__t=("+t+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),n}),i+="';\n";var l=n.variable;if(l){if(!nw.test(l))throw Error("variable is not a bare identifier: "+l)}else i="with(obj||{}){\n"+i+"}\n",l="obj";i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{t=Function(l,"_",i)}catch(r){throw r.source=i,r}var u=function(r){return t.call(this,r,rI)};return u.source="function("+l+"){\n"+i+"}",u}function nA(r,n,e){var t=(n=r4(n)).length;if(!t)return rl(e)?e.call(r):e;for(var a=0;a=i){if(!l.length)break;var u=l.pop();o=u.i,i=rN(r=u.v);continue}var c=r[o++];l.length>=n?t[a++]=c:nE(c)&&(rb(c)||rp(c))?(l.push({i:o,v:r}),o=0,i=rN(r=c)):e||(t[a++]=c)}return t}var nz=L(function(r,n){var e=(n=nF(n,!1,!1)).length;if(e<1)throw Error("bindAll must be passed function names");for(;e--;){var t=n[e];r[t]=nI(r[t],r)}return r});function nq(r,n){var e=function(t){var a=e.cache,o=""+(n?n.apply(this,arguments):t);return rd(a,o)||(a[o]=r.apply(this,arguments)),a[o]};return e.cache={},e}var nC=L(function(r,n,e){return setTimeout(function(){return r.apply(null,e)},n)}),nD=nS(nC,rI,1);function nR(r,n,e){var t,a,o,i,l=0;e||(e={});var u=function(){l=!1===e.leading?0:nh(),t=null,i=r.apply(a,o),t||(a=o=null)},c=function(){var c=nh();l||!1!==e.leading||(l=c);var s=n-(c-l);return a=this,o=arguments,s<=0||s>n?(t&&(clearTimeout(t),t=null),l=c,i=r.apply(a,o),t||(a=o=null)):t||!1===e.trailing||(t=setTimeout(u,s)),i};return c.cancel=function(){clearTimeout(t),l=0,t=a=o=null},c}function nB(r,n,e){var t,a,o,i,l,u=function(){var c=nh()-a;n>c?t=setTimeout(u,n-c):(t=null,e||(i=r.apply(l,o)),t||(o=l=null))},c=L(function(c){return l=this,o=c,a=nh(),!t&&(t=setTimeout(u,n),e&&(i=r.apply(l,o))),i});return c.cancel=function(){clearTimeout(t),t=o=l=null},c}function nP(r,n){return nS(n,r)}function nT(r){return function(){return!r.apply(this,arguments)}}function nU(){var r=arguments,n=r.length-1;return function(){for(var e=n,t=r[n].apply(this,arguments);e--;)t=r[e].call(this,t);return t}}function nV(r,n){return function(){if(--r<1)return n.apply(this,arguments)}}function nH(r,n){var e;return function(){return--r>0&&(e=n.apply(this,arguments)),r<=1&&(n=null),e}}var nL=nS(nH,2);function nW(r,n,e){n=ni(n,e);for(var t,a=rO(r),o=0,i=a.length;o0?0:a-1;o>=0&&o0?i=o>=0?o:Math.max(o+l,i):l=o>=0?Math.min(o+1,l):o+l+1;else if(e&&o&&l)return o=e(t,a),t[o]===a?o:-1;if(a!=a)return(o=n(E.call(t,i,l),rv))>=0?o+i:-1;for(o=r>0?i:l-1;o>=0&&o0?0:i-1;for(a||(t=n[o?o[l]:l],l+=r);l>=0&&l=3;return n(r,nt(e,a,4),t,o)}}var n6=n3(1),n4=n3(-1);function n8(r,n,e){var t=[];return n=ni(n,e),n2(r,function(r,e,a){n(r,e,a)&&t.push(r)}),t}function n7(r,n,e){return n8(r,nT(ni(n)),e)}function n9(r,n,e){n=ni(n,e);for(var t=!nE(r)&&rO(r),a=(t||r).length,o=0;o=0}var ee=L(function(r,n,e){var t,a;return rl(n)?a=n:(t=(n=r4(n)).slice(0,-1),n=n[n.length-1]),n5(r,function(r){var o=a;if(!o){if(t&&t.length&&(r=r8(r,t)),null==r)return;o=r[n]}return null==o?o:o.apply(r,e)})});function et(r,n){return n5(r,ne(n))}function ea(r,n){return n8(r,nn(n))}function eo(r,n,e){var t,a,o=-1/0,i=-1/0;if(null==n||"number"==typeof n&&"object"!=typeof r[0]&&null!=r){r=nE(r)?r:rJ(r);for(var l=0,u=r.length;lo&&(o=t)}else n=ni(n,e),n2(r,function(r,e,t){((a=n(r,e,t))>i||a===-1/0&&o===-1/0)&&(o=r,i=a)});return o}function ei(r,n,e){var t,a,o=1/0,i=1/0;if(null==n||"number"==typeof n&&"object"!=typeof r[0]&&null!=r){r=nE(r)?r:rJ(r);for(var l=0,u=r.length;lt||void 0===e)return 1;if(e1&&(t=nt(t,n[1])),n=rq(r)):(t=ev,n=nF(n,!1,!1),r=Object(r));for(var a=0,o=n.length;a1&&(e=n[1])):(n=n5(nF(n,!1,!1),String),t=function(r,e){return!en(n,e)}),ey(r,t,e)});function ex(r,n,e){return E.call(r,0,Math.max(0,r.length-(null==n||e?1:n)))}function ew(r,n,e){return null==r||r.length<1?null==n||e?void 0:[]:null==n||e?r[0]:ex(r,r.length-n)}function eM(r,n,e){return E.call(r,null==n||e?1:n)}function eA(r,n,e){return null==r||r.length<1?null==n||e?void 0:[]:null==n||e?r[r.length-1]:eM(r,Math.max(0,r.length-n))}function e_(r){return n8(r,Boolean)}function eN(r,n){return nF(r,n,!1)}var ej=L(function(r,n){return n=nF(n,!0,!0),n8(r,function(r){return!en(n,r)})}),eO=L(function(r,n){return ej(r,n)});function e$(r,n,e,t){X(n)||(t=e,e=n,n=!1),null!=e&&(e=ni(e,t));for(var a=[],o=[],i=0,l=rN(r);i[r,n,e]);function eV(r){let n={};return r.slice(0,9).forEach((r,e)=>{if(!r)return;let t=eT[e];if("string"==typeof r){n[t.base]=r,n[t.tint]=eH(r),n[t.shade]=eL(r);return}"object"==typeof r&&(n[t.base]=r.base,n[t.tint]=r.tint??eH(r.base),n[t.shade]=r.shade??eL(r.base))}),n}function eH(r){let n=x(r);return n.lightness(n.lightness()+12.5).hex()}function eL(r){let n=x(r);return n.lightness(n.lightness()-12.5).hex()}let eW=["metabase-brand","core-metabase_brand","admin-navbar","admin-navbar-secondary","admin-navbar-inverse","navbar-admin","navbar-admin-secondary","navbar-admin-inverse","accent0","accent1","accent2","accent3","accent4","accent5","accent6","accent7","accent-gray","accent-gray-dark","accent-gray-light"],eJ=["#509EE3","#88BF4D","#A989C5","#EF8C8C","#F9D45C","#F2A86F","#98D9D9","#7172AD"],eK=[...eJ,{base:_.orion[10],tint:_.orion[5],shade:_.orion[20]}],eX=[...eJ,{base:_.orion[80],tint:_.orion[80],shade:_.orion[110]}],eG={"admin-navbar":_.octopus[80],"admin-navbar-secondary":_.octopus[60],"admin-navbar-inverse":_.octopus[60],"background-brand":_.brand[90],"background-filter":_.filter[90],"background-disabled":_.orionAlphaInverse[10],"background-disabled-inverse":_.orionAlpha[10],"background-error-secondary":_.lobster[90],"background-highlight":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background-hover":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background-selected":"color-mix(in srgb, var(--mb-color-core-brand) 21%, transparent)","background-primary":_.orion[100],"background-secondary":_.orion[110],"background-tertiary":_.orion[80],"background-primary-inverse":_.orion[20],"background-secondary-inverse":_.orion[30],"background-tertiary-inverse":_.orion[70],overlay:_.orionAlpha[70],"background-error":_.lobster[90],"background-success":_.palm[90],"background-success-secondary":_.palm[70],brand:_.blue[40],"brand-hover":_.brand[30],danger:_.lobster[50],error:_.lobster[50],filter:_.octopus[40],focus:_.blue[70],"icon-brand":"var(--mb-color-text-brand)","icon-primary":"var(--mb-color-text-primary)","icon-disabled":"var(--mb-color-text-disabled)","icon-secondary":"var(--mb-color-text-secondary)","illustration-brand-secondary":_.brand[80],"illustration-brand-tertiary":_.brand[90],"metabase-brand":_.blue[40],"metabase-brand-hover":_.blue[50],"saturated-blue":_.ocean[40],"saturated-green":_.palm[40],"saturated-purple":_.octopus[40],"saturated-red":_.lobster[40],"saturated-yellow":_.dubloon[30],shadow:`color-mix(in srgb, ${_.orion[110]} 20%, transparent)`,"success-secondary":_.palm[40],success:_.palm[50],summarize:_.palm[40],"syntax-parameters-active":_.mango[90],"syntax-parameters":_.mango[40],"text-tertiary":_.orionAlphaInverse[40],"text-tertiary-inverse":_.orionAlpha[40],"text-hover":_.brand[30],"text-secondary-opaque":_.orion[20],warning:_.dubloon[30],"background-warning":_.dubloon[90],"background-warning-secondary":_.dubloon[70],info:_.orion[50],"background-info":_.orion[90],white:_.orion[110],border:_.orionAlphaInverse[20],"border-strong":_.orionAlphaInverse[30],"border-stronger":_.orionAlphaInverse[50],"border-subtle":_.orionAlphaInverse[10],copper:"#B87333",bronze:"#CD7F32",silver:"#C0C0C0",gold:"#FFD700","upsell-primary":_.ocean[30],"upsell-secondary":_.ocean[80],"upsell-gem":"#00d4ff"},eY={"background_page-filter":_.filter[90],"background_page-highlighted":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background_page-primary":_.orion[100],"background_page-primary-inverse":"white","background_page-secondary":"color-mix(in srgb, var(--mb-color-background_page-primary), black 25%)","background_page-secondary-inverse":_.orion[5],"background_page-tertiary":"color-mix(in srgb, var(--mb-color-background_page-primary), black 50%)","background_page-tertiary-inverse":_.orion[10],"background_surface-brand-strong":_.brand[20],"background_surface-brand-strong-hover":_.brand[10],"background_surface-brand-strong-pressed":_.brand[20],"background_surface-brand-subtle":"color-mix(in srgb, var(--mb-color-core-brand) 10%, transparent)","background_surface-brand-subtle-hover":"color-mix(in srgb, var(--mb-color-core-brand), black 50%)","background_surface-brand-subtle-pressed":"color-mix(in srgb, var(--mb-color-core-brand), black 75%)","background_surface-disabled":_.orionAlphaInverse[10],"background_surface-disabled-inverse":_.orionAlpha[10],"background_surface-error":_.lobster[90],"background_surface-error-subtle":_.lobster[90],"background_surface-hover":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background_surface-primary":"var(--mb-color-background_page-primary)","background_surface-primary-hover":_.orionAlphaInverse[20],"background_surface-primary-inverse":"var(--mb-color-background_page-primary-inverse)","background_surface-primary-inverse-hover":_.orionAlpha[10],"background_surface-primary-inverse-pressed":_.orionAlpha[20],"background_surface-primary-pressed":_.orionAlphaInverse[10],"background_surface-secondary":_.orionAlphaInverse[10],"background_surface-secondary-hover":_.orionAlphaInverse[20],"background_surface-secondary-inverse":_.orionAlpha[10],"background_surface-secondary-inverse-hover":_.orionAlpha[20],"background_surface-secondary-inverse-pressed":_.orionAlpha[30],"background_surface-secondary-pressed":_.orionAlphaInverse[10],"background_surface-selected":"color-mix(in srgb, var(--mb-color-core-brand) 21%, transparent)","background_surface-success":_.palm[90],"background_surface-warning":_.dubloon[90],"background_surface-warning-strong":_.dubloon[70],"border-brand":_.brand[80],"border-filter":_.filter[80],"border-neutral":_.orionAlphaInverse[20],"border-neutral-strong":_.orionAlphaInverse[30],"border-neutral-strongest":_.orionAlphaInverse[50],"border-neutral-subtle":_.orionAlphaInverse[10],"core-blue-saturated":_.ocean[40],"core-brand-hover":_.brand[30],"core-gold":"#FFD700","core-green-saturated":_.palm[40],"core-info":_.orion[50],"core-metabase_brand":_.blue[40],"core-metabase_brand-hover":_.blue[50],"core-purple-saturated":_.octopus[40],"core-red-saturated":_.lobster[40],"core-white":_.orion[110],"core-white_constant":"white","core-yellow-saturated":_.dubloon[30],"feedback-negative":_.lobster[50],"feedback-negative-strong":_.lobster[30],"feedback-neutral-strong":_.orionAlphaInverse[30],"feedback-positive":_.palm[50],"feedback-positive-selected":_.palm[40],"feedback-positive-strong":_.palm[20],"feedback-warning":_.dubloon[30],"feedback-warning-strong":_.dubloon[20],"illustration-secondary-brand":_.brand[80],"illustration-tertiary-brand":_.brand[90],"input-focus":_.blue[70],"modal-overlay":_.orionAlpha[70],"navbar-admin":_.octopus[80],"navbar-admin-inverse":_.octopus[60],"navbar-admin-secondary":_.octopus[60],"shadow-default":`color-mix(in srgb, ${_.orion[110]} 20%, transparent)`,"switch-off":_.orionAlphaInverse[20],"text-brand":_.brand[40],"text-brand-hover":_.brand[30],"text-brand-strong":_.brand[20],"text-brand-strong-hover":_.brand[10],"text-disabled":_.orionAlphaInverse[40],"text-disabled-inverse":_.orionAlpha[40],"text-filter":_.filter[30],"text-primary":_.orionAlphaInverse[80],"text-primary-inverse":_.orionAlpha[80],"text-secondary":_.orionAlphaInverse[60],"text-secondary-inverse":_.orionAlpha[60],"text-selected":_.white,"text-syntax-parameter":_.mango[40],"text-syntax-parameter-active":_.mango[90],"tooltip-background":_.orion[70],"tooltip-background-focused":`color-mix(in srgb, ${_.orion[70]} 50%, #000)`,"tooltip-text":_.orionAlphaInverse[80],"tooltip-text-secondary":_.orionAlphaInverse[60]},eZ={version:2,chartColors:eX,colors:{...eG,...eY}},eQ={"admin-navbar":_.octopus[60],"admin-navbar-secondary":_.octopus[40],"admin-navbar-inverse":_.octopus[80],"background-brand":_.brand[10],"background-filter":_.filter[5],"background-disabled":_.orionAlpha[10],"background-disabled-inverse":_.orionAlphaInverse[10],"background-error-secondary":_.lobster[5],"background-highlight":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background-hover":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background-selected":"color-mix(in srgb, var(--mb-color-core-brand) 21%, transparent)","background-primary":_.white,"background-secondary":_.orion[5],"background-tertiary":_.orion[10],"background-primary-inverse":_.orion[80],"background-secondary-inverse":_.orion[70],"background-tertiary-inverse":_.orion[40],overlay:_.orionAlpha[60],"background-error":_.lobster[10],"background-success":_.palm[5],"background-success-secondary":_.palm[20],brand:_.blue[40],"brand-hover":_.brand[50],danger:_.lobster[50],error:_.lobster[50],filter:_.octopus[50],focus:_.blue[20],"icon-brand":"var(--mb-color-text-brand)","icon-primary":"var(--mb-color-text-primary)","icon-disabled":"var(--mb-color-text-disabled)","icon-secondary":"var(--mb-color-text-secondary)","illustration-brand-secondary":_.brand[10],"illustration-brand-tertiary":_.brand[5],"metabase-brand":_.blue[40],"metabase-brand-hover":_.blue[30],"saturated-blue":_.ocean[60],"saturated-green":_.palm[60],"saturated-purple":_.octopus[60],"saturated-red":_.lobster[60],"saturated-yellow":_.dubloon[30],shadow:_.orionAlpha[20],"success-secondary":_.palm[60],success:_.palm[50],summarize:_.palm[50],"syntax-parameters-active":_.mango[10],"syntax-parameters":_.mango[60],"text-tertiary":_.orionAlpha[40],"text-tertiary-inverse":_.orionAlphaInverse[40],"text-hover":_.brand[60],"text-secondary-opaque":_.orion[60],warning:_.dubloon[30],"background-warning":_.dubloon[5],"background-warning-secondary":_.dubloon[20],info:_.orion[40],"background-info":_.orion[10],white:_.white,border:_.orion[20],"border-strong":_.orionAlpha[30],"border-stronger":_.orionAlpha[50],"border-subtle":_.orionAlpha[10],copper:"#B87333",bronze:"#CD7F32",silver:"#C0C0C0",gold:"#FFD700","upsell-primary":_.ocean[70],"upsell-secondary":_.ocean[20],"upsell-gem":"#00d4ff"},e0={"background_page-filter":_.filter[5],"background_page-highlighted":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background_page-primary":_.white,"background_page-primary-inverse":_.orion[80],"background_page-secondary":_.orion[5],"background_page-secondary-inverse":`color-mix(in srgb, ${_.orion[100]}, black 25%)`,"background_page-tertiary":_.orion[10],"background_page-tertiary-inverse":`color-mix(in srgb, ${_.orion[100]}, black 50%)`,"background_surface-brand-strong":_.brand[60],"background_surface-brand-strong-hover":_.brand[70],"background_surface-brand-strong-pressed":_.brand[80],"background_surface-brand-subtle":"color-mix(in srgb, var(--mb-color-core-brand) 10%, transparent)","background_surface-brand-subtle-hover":"color-mix(in srgb, var(--mb-color-core-brand) 30%, transparent)","background_surface-brand-subtle-pressed":"color-mix(in srgb, var(--mb-color-core-brand) 60%, transparent)","background_surface-disabled":_.orionAlpha[10],"background_surface-disabled-inverse":_.orionAlphaInverse[10],"background_surface-error":_.lobster[10],"background_surface-error-subtle":_.lobster[5],"background_surface-hover":"color-mix(in srgb, var(--mb-color-core-brand) 7%, transparent)","background_surface-primary":"var(--mb-color-background_page-primary)","background_surface-primary-hover":_.orionAlpha[10],"background_surface-primary-inverse":"var(--mb-color-background_page-primary-inverse)","background_surface-primary-inverse-hover":_.orionAlphaInverse[20],"background_surface-primary-inverse-pressed":_.orionAlphaInverse[10],"background_surface-primary-pressed":_.orionAlpha[20],"background_surface-secondary":_.orionAlpha[10],"background_surface-secondary-hover":_.orionAlpha[20],"background_surface-secondary-inverse":_.orionAlphaInverse[10],"background_surface-secondary-inverse-hover":_.orionAlphaInverse[20],"background_surface-secondary-inverse-pressed":_.orionAlphaInverse[10],"background_surface-secondary-pressed":_.orionAlpha[30],"background_surface-selected":"color-mix(in srgb, var(--mb-color-core-brand) 21%, transparent)","background_surface-success":_.palm[5],"background_surface-warning":_.dubloon[5],"background_surface-warning-strong":_.dubloon[20],"border-brand":_.brand[10],"border-filter":_.filter[20],"border-neutral":_.orion[20],"border-neutral-strong":_.orionAlpha[30],"border-neutral-strongest":_.orionAlpha[50],"border-neutral-subtle":_.orionAlpha[10],"core-blue-saturated":_.ocean[60],"core-brand-hover":_.brand[50],"core-gold":"#FFD700","core-green-saturated":_.palm[60],"core-info":_.orion[40],"core-metabase_brand":_.blue[40],"core-metabase_brand-hover":_.blue[30],"core-purple-saturated":_.octopus[60],"core-red-saturated":_.lobster[60],"core-white":_.white,"core-white_constant":"white","core-yellow-saturated":_.dubloon[30],"feedback-negative":_.lobster[50],"feedback-negative-strong":_.lobster[60],"feedback-neutral-strong":_.orionAlpha[60],"feedback-positive":_.palm[50],"feedback-positive-selected":_.palm[60],"feedback-positive-strong":_.palm[60],"feedback-warning":_.dubloon[30],"feedback-warning-strong":_.mango[60],"illustration-secondary-brand":_.brand[10],"illustration-tertiary-brand":_.brand[5],"input-focus":_.blue[20],"modal-overlay":_.orionAlpha[60],"navbar-admin":_.octopus[60],"navbar-admin-inverse":_.octopus[80],"navbar-admin-secondary":_.octopus[40],"shadow-default":_.orionAlpha[20],"switch-off":_.orionAlpha[20],"text-brand":_.brand[50],"text-brand-hover":_.brand[60],"text-brand-strong":_.brand[60],"text-brand-strong-hover":_.brand[70],"text-disabled":_.orionAlpha[40],"text-disabled-inverse":_.orionAlphaInverse[40],"text-filter":_.filter[50],"text-primary":_.orionAlpha[80],"text-primary-inverse":_.orionAlphaInverse[80],"text-secondary":_.orionAlpha[60],"text-secondary-inverse":_.orionAlphaInverse[60],"text-selected":_.orionAlpha[80],"text-syntax-parameter":_.mango[60],"text-syntax-parameter-active":_.mango[10],"tooltip-background":_.orion[80],"tooltip-background-focused":`color-mix(in srgb, ${_.orion[80]} 50%, #000)`,"tooltip-text":_.white,"tooltip-text-secondary":_.orionAlphaInverse[60]},e1={version:2,chartColors:eK,colors:{...eQ,...e0}},e2="u">typeof window?window:{},e5=!!(e2.MetabaseBootstrap?.["token-features"]??{}).whitelabel&&e2.MetabaseBootstrap?.["application-colors"]||{},e3=r=>(function({colorScheme:r,whitelabelColors:n,embeddingThemeOverride:e}){var t;let a,o,i="dark"===r?eZ:e1,l=eP.omit(e?.colors,...eW),u={...i.colors,...eV(i.chartColors),...(a=Object.fromEntries(Object.entries(t=n??{}).filter(([r])=>r.startsWith("accent"))),o=Object.fromEntries(Object.entries(t).filter(([r])=>!r.startsWith("accent"))),{...Object.entries(a).reduce((r,[n,e])=>{let t,[a,o]=n.split("-");if("light"===o){let r=eL(e);t={[a]:r,[`${a}-dark`]:eL(r)}}else if("dark"===o){let r=eH(e);t={[a]:r,[`${a}-light`]:eH(r)}}else t={[`${a}-light`]:eH(e),[`${a}-dark`]:eL(e)};return{...t,...r}},a),...o}),...l,...eV(e?.chartColors??[])};return u["core-brand"]=u.brand,u["core-filter"]=u.filter,u["core-summarize"]=u.summarize,{version:2,colors:u}})({colorScheme:"light",whitelabelColors:r}).colors,e6=e3(e5);_.orion[80],_.orion[60],_.orion[40],e3();let e4={dashboard:r=>e8("core-brand",r),document:r=>e8("core-brand",r),nav:r=>e8("background_page-primary",r),content:r=>e8("background_page-secondary",r),database:r=>e8("accent2",r),pulse:r=>e8("accent4",r),"text-primary":r=>e8("text-primary",r),"text-secondary":r=>e8("text-secondary",r),"text-tertiary":r=>e8("text-tertiary",r),background:r=>e8("white",r),"background-disbaled":r=>e8("accent-gray",r),focus:r=>tr("core-brand",r),"accent0-light":r=>e7(e8("accent0",r)),"accent1-light":r=>e7(e8("accent1",r)),"accent2-light":r=>e7(e8("accent2",r)),"accent3-light":r=>e7(e8("accent3",r)),"accent4-light":r=>e7(e8("accent4",r)),"accent5-light":r=>e7(e8("accent5",r)),"accent6-light":r=>e7(e8("accent6",r)),"accent7-light":r=>e7(e8("accent7",r)),"accent0-dark":r=>e9(e8("accent0",r)),"accent1-dark":r=>e9(e8("accent1",r)),"accent2-dark":r=>e9(e8("accent2",r)),"accent3-dark":r=>e9(e8("accent3",r)),"accent4-dark":r=>e9(e8("accent4",r)),"accent5-dark":r=>e9(e8("accent5",r)),"accent6-dark":r=>e9(e8("accent6",r)),"accent7-dark":r=>e9(e8("accent7",r))};function e8(r,n=e6){let e={...e6,...n};return r in e?e[r]:r in e4?e4[r](n):r}let e7=(r,n=.125)=>{let e=x(e8(r));return e.lightness(e.lightness()+100*n).hex()},e9=(r,n=.125)=>{let e=x(e8(r));return e.lightness(e.lightness()-100*n).hex()},tr=(r,n=e6)=>((r,n=.5)=>x(e8(r)).lighten(n).string())(e8(r,n),.465);_.orionAlphaInverse[80],_.orionAlpha[80],Object.keys(e1.colors).concat(eU).concat(["core-brand","core-filter","core-summarize"]);let tn=String.raw`-?\d+(?:\.\d+)?(?:e[+-]?\d+)?`,te=RegExp(`rgba\\(\\s*(?${tn})\\s*,\\s*(?${tn})\\s*,\\s*(?${tn})\\s*,\\s*(?${tn})\\s*\\)`,"i"),tt=Math.sqrt(50),ta=Math.sqrt(10),to=Math.sqrt(2);function ti(r,n,e){let t,a,o,i=(n-r)/Math.max(0,e),l=Math.floor(Math.log10(i)),u=i/Math.pow(10,l),c=u>=tt?10:u>=ta?5:u>=to?2:1;return(l<0?(t=Math.round(r*(o=Math.pow(10,-l)/c)),a=Math.round(n*o),t/on&&--a,o=-o):(t=Math.round(r/(o=Math.pow(10,l)*c)),a=Math.round(n/o),t*on&&--a),an?1:r>=n?0:NaN}function tc(r,n){return null==r||null==n?NaN:nr?1:n>=r?0:NaN}function ts(r){let n,e,t;function a(r,t,o=0,i=r.length){if(o>>1;0>e(r[n],t)?o=n+1:i=n}while(otu(r(n),e),t=(n,e)=>r(n)-e):(n=r===tu||r===tc?r:tf,e=r,t=r),{left:a,center:function(r,n,e=0,o=r.length){let i=a(r,n,e,o-1);return i>e&&t(r[i-1],n)>-t(r[i],n)?i-1:i},right:function(r,t,a=0,o=r.length){if(a>>1;0>=e(r[n],t)?a=n+1:o=n}while(a>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?tE(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?tE(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=tx.exec(r))?new tz(n[1],n[2],n[3],1):(n=tw.exec(r))?new tz(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=tM.exec(r))?tE(n[1],n[2],n[3],n[4]):(n=tA.exec(r))?tE(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=t_.exec(r))?tP(n[1],n[2]/100,n[3]/100,1):(n=tN.exec(r))?tP(n[1],n[2]/100,n[3]/100,n[4]):tj.hasOwnProperty(r)?tI(tj[r]):"transparent"===r?new tz(NaN,NaN,NaN,0):null}function tI(r){return new tz(r>>16&255,r>>8&255,255&r,1)}function tE(r,n,e,t){return t<=0&&(r=n=e=NaN),new tz(r,n,e,t)}function tF(r,n,e,t){var a;return 1==arguments.length?((a=r)instanceof tp||(a=tS(a)),a)?new tz((a=a.rgb()).r,a.g,a.b,a.opacity):new tz:new tz(r,n,e,null==t?1:t)}function tz(r,n,e,t){this.r=+r,this.g=+n,this.b=+e,this.opacity=+t}function tq(){return`#${tB(this.r)}${tB(this.g)}${tB(this.b)}`}function tC(){let r=tD(this.opacity);return`${1===r?"rgb(":"rgba("}${tR(this.r)}, ${tR(this.g)}, ${tR(this.b)}${1===r?")":`, ${r})`}`}function tD(r){return isNaN(r)?1:Math.max(0,Math.min(1,r))}function tR(r){return Math.max(0,Math.min(255,Math.round(r)||0))}function tB(r){return((r=tR(r))<16?"0":"")+r.toString(16)}function tP(r,n,e,t){return t<=0?r=n=e=NaN:e<=0||e>=1?r=n=NaN:n<=0&&(r=NaN),new tU(r,n,e,t)}function tT(r){if(r instanceof tU)return new tU(r.h,r.s,r.l,r.opacity);if(r instanceof tp||(r=tS(r)),!r)return new tU;if(r instanceof tU)return r;var n=(r=r.rgb()).r/255,e=r.g/255,t=r.b/255,a=Math.min(n,e,t),o=Math.max(n,e,t),i=NaN,l=o-a,u=(o+a)/2;return l?(i=n===o?(e-t)/l+(e0&&u<1?0:i,new tU(i,l,u,r.opacity)}function tU(r,n,e,t){this.h=+r,this.s=+n,this.l=+e,this.opacity=+t}function tV(r){return(r=(r||0)%360)<0?r+360:r}function tH(r){return Math.max(0,Math.min(1,r||0))}function tL(r,n,e){return(r<60?n+(e-n)*r/60:r<180?e:r<240?n+(e-n)*(240-r)/60:n)*255}function tW(r,n,e,t,a){var o=r*r,i=o*r;return((1-3*r+3*o-i)*n+(4-6*o+3*i)*e+(1+3*r+3*o-3*i)*t+i*a)/6}td(tp,tS,{copy(r){return Object.assign(new this.constructor,this,r)},displayable(){return this.rgb().displayable()},hex:tO,formatHex:tO,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return tT(this).formatHsl()},formatRgb:t$,toString:t$}),td(tz,tF,tg(tp,{brighter(r){return r=null==r?1.4285714285714286:Math.pow(1.4285714285714286,r),new tz(this.r*r,this.g*r,this.b*r,this.opacity)},darker(r){return r=null==r?.7:Math.pow(.7,r),new tz(this.r*r,this.g*r,this.b*r,this.opacity)},rgb(){return this},clamp(){return new tz(tR(this.r),tR(this.g),tR(this.b),tD(this.opacity))},displayable(){return -.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:tq,formatHex:tq,formatHex8:function(){return`#${tB(this.r)}${tB(this.g)}${tB(this.b)}${tB((isNaN(this.opacity)?1:this.opacity)*255)}`},formatRgb:tC,toString:tC})),td(tU,function(r,n,e,t){return 1==arguments.length?tT(r):new tU(r,n,e,null==t?1:t)},tg(tp,{brighter(r){return r=null==r?1.4285714285714286:Math.pow(1.4285714285714286,r),new tU(this.h,this.s,this.l*r,this.opacity)},darker(r){return r=null==r?.7:Math.pow(.7,r),new tU(this.h,this.s,this.l*r,this.opacity)},rgb(){var r=this.h%360+(this.h<0)*360,n=isNaN(r)||isNaN(this.s)?0:this.s,e=this.l,t=e+(e<.5?e:1-e)*n,a=2*e-t;return new tz(tL(r>=240?r-240:r+120,a,t),tL(r,a,t),tL(r<120?r+240:r-120,a,t),this.opacity)},clamp(){return new tU(tV(this.h),tH(this.s),tH(this.l),tD(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){let r=tD(this.opacity);return`${1===r?"hsl(":"hsla("}${tV(this.h)}, ${100*tH(this.s)}%, ${100*tH(this.l)}%${1===r?")":`, ${r})`}`}}));var tJ=r=>()=>r;function tK(r,n){var e=n-r;return e?function(n){return r+n*e}:tJ(isNaN(r)?n:r)}var tX=function r(n){var e,t=1==(e=+n)?tK:function(r,n){var t,a,o;return n-r?(t=r,a=n,t=Math.pow(t,o=e),a=Math.pow(a,o)-t,o=1/o,function(r){return Math.pow(t+r*a,o)}):tJ(isNaN(r)?n:r)};function a(r,n){var e=t((r=tF(r)).r,(n=tF(n)).r),a=t(r.g,n.g),o=t(r.b,n.b),i=tK(r.opacity,n.opacity);return function(n){return r.r=e(n),r.g=a(n),r.b=o(n),r.opacity=i(n),r+""}}return a.gamma=r,a}(1);function tG(r){return function(n){var e,t,a=n.length,o=Array(a),i=Array(a),l=Array(a);for(e=0;e=1?(e=1,n-1):Math.floor(e*n),a=r[t],o=r[t+1],i=t>0?r[t-1]:2*a-o,l=t=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function t9(r){var n;if(!(n=t7.exec(r)))throw Error("invalid format: "+r);return new ar({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function ar(r){this.fill=void 0===r.fill?" ":r.fill+"",this.align=void 0===r.align?">":r.align+"",this.sign=void 0===r.sign?"-":r.sign+"",this.symbol=void 0===r.symbol?"":r.symbol+"",this.zero=!!r.zero,this.width=void 0===r.width?void 0:+r.width,this.comma=!!r.comma,this.precision=void 0===r.precision?void 0:+r.precision,this.trim=!!r.trim,this.type=void 0===r.type?"":r.type+""}function an(r,n){if((e=(r=n?r.toExponential(n-1):r.toExponential()).indexOf("e"))<0)return null;var e,t=r.slice(0,e);return[t.length>1?t[0]+t.slice(2):t,+r.slice(e+1)]}function ae(r){return(r=an(Math.abs(r)))?r[1]:NaN}function at(r,n){var e=an(r,n);if(!e)return r+"";var t=e[0],a=e[1];return a<0?"0."+Array(-a).join("0")+t:t.length>a+1?t.slice(0,a+1)+"."+t.slice(a+1):t+Array(a-t.length+2).join("0")}t9.prototype=ar.prototype,ar.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var aa={"%":(r,n)=>(100*r).toFixed(n),b:r=>Math.round(r).toString(2),c:r=>r+"",d:function(r){return Math.abs(r=Math.round(r))>=1e21?r.toLocaleString("en").replace(/,/g,""):r.toString(10)},e:(r,n)=>r.toExponential(n),f:(r,n)=>r.toFixed(n),g:(r,n)=>r.toPrecision(n),o:r=>Math.round(r).toString(8),p:(r,n)=>at(100*r,n),r:at,s:function(n,e){var t=an(n,e);if(!t)return n+"";var a=t[0],o=t[1],i=o-(r=3*Math.max(-8,Math.min(8,Math.floor(o/3))))+1,l=a.length;return i===l?a:i>l?a+Array(i-l+1).join("0"):i>0?a.slice(0,i)+"."+a.slice(i):"0."+Array(1-i).join("0")+an(n,Math.max(0,e+i-1))[0]},X:r=>Math.round(r).toString(16).toUpperCase(),x:r=>Math.round(r).toString(16)};function ao(r){return r}var ai=Array.prototype.map,al=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];e=(n=function(n){var e,t,a,o=void 0===n.grouping||void 0===n.thousands?ao:(e=ai.call(n.grouping,Number),t=n.thousands+"",function(r,n){for(var a=r.length,o=[],i=0,l=e[0],u=0;a>0&&l>0&&(u+l+1>n&&(l=Math.max(1,n-u)),o.push(r.substring(a-=l,a+l)),!((u+=l+1)>n));)l=e[i=(i+1)%e.length];return o.reverse().join(t)}),i=void 0===n.currency?"":n.currency[0]+"",l=void 0===n.currency?"":n.currency[1]+"",u=void 0===n.decimal?".":n.decimal+"",c=void 0===n.numerals?ao:(a=ai.call(n.numerals,String),function(r){return r.replace(/[0-9]/g,function(r){return a[+r]})}),s=void 0===n.percent?"%":n.percent+"",f=void 0===n.minus?"−":n.minus+"",h=void 0===n.nan?"NaN":n.nan+"";function b(n){var e=(n=t9(n)).fill,t=n.align,a=n.sign,b=n.symbol,d=n.zero,g=n.width,p=n.comma,m=n.precision,v=n.trim,y=n.type;"n"===y?(p=!0,y="g"):aa[y]||(void 0===m&&(m=12),v=!0,y="g"),(d||"0"===e&&"="===t)&&(d=!0,e="0",t="=");var k="$"===b?i:"#"===b&&/[boxX]/.test(y)?"0"+y.toLowerCase():"",x="$"===b?l:/[%p]/.test(y)?s:"",w=aa[y],M=/[defgprs%]/.test(y);function A(n){var i,l,s,b=k,A=x;if("c"===y)A=w(n)+A,n="";else{var _=(n*=1)<0||1/n<0;if(n=isNaN(n)?h:w(Math.abs(n),m),v&&(n=function(r){r:for(var n,e=r.length,t=1,a=-1;t0&&(a=0)}return a>0?r.slice(0,a)+r.slice(n+1):r}(n)),_&&0==+n&&"+"!==a&&(_=!1),b=(_?"("===a?a:f:"-"===a||"("===a?"":a)+b,A=("s"===y?al[8+r/3]:"")+A+(_&&"("===a?")":""),M){for(i=-1,l=n.length;++i(s=n.charCodeAt(i))||s>57){A=(46===s?u+n.slice(i+1):n.slice(i))+A,n=n.slice(0,i);break}}}p&&!d&&(n=o(n,1/0));var N=b.length+n.length+A.length,j=N>1)+b+n+A+j.slice(N);break;default:n=j+b+n+A}return c(n)}return m=void 0===m?6:/[gprs]/.test(y)?Math.max(1,Math.min(21,m)):Math.max(0,Math.min(20,m)),A.toString=function(){return n+""},A}return{format:b,formatPrefix:function(r,n){var e=b(((r=t9(r)).type="f",r)),t=3*Math.max(-8,Math.min(8,Math.floor(ae(n)/3))),a=Math.pow(10,-t),o=al[8+t/3];return function(r){return e(a*r)+o}}}}({thousands:",",grouping:[3],currency:["$",""]})).format,t=n.formatPrefix;let au=r=>"string"==typeof r&&r.length>0,ac={"<":(r,n)=>e=>"number"==typeof r&&"number"==typeof e&&ee=>"number"==typeof r&&"number"==typeof e&&e<=r?n:null,">=":(r,n)=>e=>"number"==typeof r&&"number"==typeof e&&e>=r?n:null,">":(r,n)=>e=>"number"==typeof r&&"number"==typeof e&&e>r?n:null,"=":(r,n)=>e=>e===r?n:null,"!=":(r,n)=>e=>"string"==typeof r&&0===r.length||e===r?null:n,"is-null":(r,n)=>r=>null===r?n:null,"not-null":(r,n)=>r=>null!==r?n:null,contains:(r,n)=>e=>au(r)&&au(e)&&e.indexOf(r)>=0?n:null,"does-not-contain":(r,n)=>e=>au(r)&&au(e)&&0>e.indexOf(r)?n:null,"starts-with":(r,n)=>e=>au(r)&&au(e)&&e.startsWith(r)?n:null,"ends-with":(r,n)=>e=>au(r)&&au(e)&&e.endsWith(r)?n:null,"is-true":(r,n)=>r=>r?n:null,"is-false":(r,n)=>r=>r?null:n};function as(r,n=null,a=null,o=!1){if("single"===r.type){let n,{operator:e,value:t,color:a}=r;n=a,a=x(e8(n)).alpha(o?.2:.65).string();let i=ac[e];return i?i(t,a):(console.error("Unsupported formatting operator:",e),()=>null)}if("range"!==r.type)return console.warn("Unknown format type",r.type),()=>null;{let o=r=>"string"==typeof r?a?.[r]?.[0]:void 0,i=r=>"string"==typeof r?a?.[r]?.[1]:void 0,l="custom"===r.min_type?parseFloat(String(r.min_value)):"all"===r.min_type?Math.min(...r.columns.map(o).filter(r=>"number"==typeof r)):o(n),u="custom"===r.max_type?parseFloat(String(r.max_value)):"all"===r.max_type?Math.max(...r.columns.map(i).filter(r=>"number"==typeof r)):i(n);if("number"!=typeof u||"number"!=typeof l)return console.warn("Invalid range min/max",l,u),()=>null;let c=Math.min(l,u),s=Math.max(l,u),f=((r,n)=>{let[a,o]=r;return function r(){var n,a=(function(){var r,n,e,t,a,o,i=t2,l=t2,u=function r(n,e){var t,a,o=typeof e;return null==e||"boolean"===o?tJ(e):("number"===o?tY:"string"===o?(a=tS(e))?(e=a,tX):function(r,n){var e,t,a,o,i,l=tZ.lastIndex=tQ.lastIndex=0,u=-1,c=[],s=[];for(r+="",n+="";(a=tZ.exec(r))&&(o=tQ.exec(n));)(i=o.index)>l&&(i=n.slice(l,i),c[u]?c[u]+=i:c[++u]=i),(a=a[0])===(o=o[0])?c[u]?c[u]+=o:c[++u]=o:(c[++u]=null,s.push({i:u,x:tY(a,o)})),l=tQ.lastIndex;return ln&&(e=r,r=n,n=e),c=function(e){return Math.max(r,Math.min(n,e))}),t=u>2?t4:t6,a=o=null,f}function f(n){return null==n||isNaN(n*=1)?e:(a||(a=t(i.map(r),l,u)))(r(c(n)))}return f.invert=function(e){return c(n((o||(o=t(l,i.map(r),tY)))(e)))},f.domain=function(r){return arguments.length?(i=Array.from(r,t1),s()):i.slice()},f.range=function(r){return arguments.length?(l=Array.from(r),s()):l.slice()},f.rangeRound=function(r){return l=Array.from(r),u=t0,s()},f.clamp=function(r){return arguments.length?(c=!!r||t5,s()):c!==t5},f.interpolate=function(r){return arguments.length?(u=r,s()):u},f.unknown=function(r){return arguments.length?(e=r,f):e},function(e,t){return r=e,n=t,s()}})()(t5,t5);return a.copy=function(){return r().domain(a.domain()).range(a.range()).interpolate(a.interpolate()).clamp(a.clamp()).unknown(a.unknown())},t8.apply(a,arguments),n=a.domain,a.ticks=function(r){var e=n();return function(r,n,e){if(n*=1,r*=1,!((e*=1)>0))return[];if(r===n)return[r];let t=n=a))return[];let l=o-a+1,u=Array(l);if(t)if(i<0)for(let r=0;r0;){if((t=tl(u,c,r))===e)return o[i]=u,o[l]=c,n(o);if(t>0)u=Math.floor(u/t)*t,c=Math.ceil(c/t)*t;else if(t<0)u=Math.ceil(u*t)/t,c=Math.floor(c*t)/t;else break;e=t}return a},a}(3===n.length?[a,a+(o-a)/2,o]:[a,o],n)})([l,u],r.colors.map(r=>{var n;let e=x(r),t=e.alpha();return e.alpha(0===(n=t)?0:Math.min(.75,Math.max(1e-6,n))).toString()})).clamp(!0);return r=>{if("number"!=typeof r||rs)return null;let n=f(r);return n?n.replace(te,(...r)=>{let n=r.at(-1),e=parseFloat(n.r),t=parseFloat(n.g),a=parseFloat(n.b),o=Math.min(1,Math.max(0,Math.round(1e6*parseFloat(n.a))/1e6));return`rgba(${Math.round(e)}, ${Math.round(t)}, ${Math.round(a)}, ${o})`}):null}}}a.g.console={log:print,warn:print,error:print},a.g.getCellBackgroundColors=function(r,n,e,t){let a=function(r,n,e){let t=JSON.parse(r),a=JSON.parse(n),o=e?JSON.parse(e):{};try{return function(r,n,e,t){let a={},o=[],i=function(r){let n={};for(let e=0;e{r.columns.forEach(r=>{if(!n[r]){let e=c[r];n[r]=function(r,n){let e=1/0,t=-1/0,a=r.length;for(let o=0;ot&&(t=a)}return[e,t]}(u,e)}})}),n);s=e,f=d,t={},s.forEach(r=>{r.columns.forEach(n=>{t[n]=t[n]||[],t[n].push(as(r,n,f,!1))})}),a=t,h=e,b=[],h.filter(r=>"single"===r.type&&r.highlight_row).forEach(r=>{let n=as(r,null,null,!0);n&&r.columns.forEach(r=>{b.push((e,t)=>n(e[t[r]]))})}),o=b}catch(r){console.error("Unexpected error compiling column formatters: ",r)}return 0===Object.keys(a).length&&0===o.length?()=>null:function(n,e,l){if(a[l])for(let r=0;rnull}}(r,n,e);return JSON.stringify(JSON.parse(t).map(r=>a(r[0],r[1],r[2])))},shared={}}(); \ No newline at end of file diff --git a/resources/frontend_shared/static_viz_interface.js b/resources/frontend_shared/static_viz_interface.js deleted file mode 100644 index be8554372b3..00000000000 --- a/resources/frontend_shared/static_viz_interface.js +++ /dev/null @@ -1,52 +0,0 @@ -const toJSArray = (a) => { - const jsArray = []; - for (let i = 0; i < a.length; i++) { - jsArray[i] = a[i]; - } - return jsArray; -}; - -function toJSMap(m) { - const o = {}; - for (let i = 0; i < m.length; i++) { - o[m[i][0]] = m[i][1]; - } - return o; -} - -/** - * @deprecated use javascript_visualization instead - */ -function gauge(card, data, tokenFeatures) { - return StaticViz.LegacyRenderChart("gauge", { - card: JSON.parse(card), - data: JSON.parse(data), - tokenFeatures: JSON.parse(tokenFeatures), - }); -} - -/** - * @deprecated use javascript_visualization instead - */ -function funnel(data, settings, tokenFeatures) { - return StaticViz.LegacyRenderChart("funnel", { - data: JSON.parse(data), - settings: JSON.parse(settings), - tokenFeatures: JSON.parse(tokenFeatures), - }); -} - - -function javascript_visualization(rawSeries, dashcardSettings, options) { - const content = StaticViz.RenderChart( - JSON.parse(rawSeries), - JSON.parse(dashcardSettings), - JSON.parse(options), - ); - const type = content.startsWith(" { entry: { "lib-static-viz": { - import: "./static-viz/index.tsx", + import: "./app-static-viz.ts", library: { - name: "StaticViz", + name: "MetabaseStaticViz", type: "var", }, }, @@ -43,7 +43,7 @@ module.exports = (env) => { path: BUILD_PATH + "/app/dist", filename: "[name].bundle.js", publicPath: "/app/dist", - globalObject: "{}", + globalObject: "globalThis", }, module: { diff --git a/src/metabase/channel/render/js/color.clj b/src/metabase/channel/render/js/color.clj index 784a6cd2ee9..762097d2f3e 100644 --- a/src/metabase/channel/render/js/color.clj +++ b/src/metabase/channel/render/js/color.clj @@ -1,12 +1,10 @@ (ns metabase.channel.render.js.color - "Determines the background colors of pulse table cells by delegating to the shared color-selector javascript (loaded - into every pooled static-viz context — see `metabase.channel.render.js.pool`). All the colors for a table are - computed in a single batched JS call, so no context-bound value ever escapes the pool." + "Determines the background colors of pulse table cells by delegating to the shared color-selector javascript through + [[metabase.channel.render.js.protocol]]. All the colors for a table are computed in a single batched JS call." (:require - [metabase.channel.render.js.engine :as js.engine] - [metabase.channel.render.js.pool :as js.pool] + [metabase.channel.render.js.protocol :as js.protocol] + [metabase.channel.render.js.renderer :as renderer] [metabase.formatter.core :as formatter] - [metabase.util.json :as json] [metabase.util.malli :as mu])) (set! *warn-on-reflection* true) @@ -65,13 +63,11 @@ (empty? (or (:table.column_formatting viz-settings) (get viz-settings "table.column_formatting")))) (vec (repeat (count cells) nil)) - (js.pool/with-static-viz-context context - (-> (js.engine/execute-fn-name context "getCellBackgroundColors" - (json/encode rows) - (json/encode cols) - (json/encode viz-settings) - (json/encode (mapv (fn [[value row-index column-name]] - [(->js-value value) row-index column-name]) - cells))) - .asString - json/decode)))) + (js.protocol/cell-background-colors + (renderer/renderer) + {:rows rows + :cols cols + :settings viz-settings + :cells (mapv (fn [[value row-index column-name]] + [(->js-value value) row-index column-name]) + cells)}))) diff --git a/src/metabase/channel/render/js/engine.clj b/src/metabase/channel/render/js/engine.clj deleted file mode 100644 index 6a5a893118c..00000000000 --- a/src/metabase/channel/render/js/engine.clj +++ /dev/null @@ -1,94 +0,0 @@ -(ns metabase.channel.render.js.engine - "Graal polyglot context suitable for executing javascript code. - - We run the js in interpreted mode and turn off the warning with the `(option \"engine.WarnInterpreterOnly\" - \"false\")`. Ideally we would compile the javascript but this is difficult when using the graal ecosystem in a non - graal jdk. See https://github.com/oracle/graaljs/blob/master/docs/user/RunOnJDK.md for more information. - - Javadocs: https://www.graalvm.org/truffle/javadoc/overview-summary.html" - (:require - [clojure.java.io :as io] - [metabase.util.i18n :refer [trs]]) - (:import - (org.graalvm.polyglot Context Engine HostAccess Source Value))) - -(set! *warn-on-reflection* true) - -;; A singleton (one shared instance, not rebuilt per context) so all contexts sharing the `Engine` have identical -;; host-access config, as GraalVM requires. - -(def ^:private no-host-class-lookup - "Predicate blocking all host class lookup. A singleton so all contexts sharing an engine have identical config." - (reify java.util.function.Predicate - (test [_ _] false))) - -(defn- create-engine - "Build a JS `Engine` to be shared across contexts. We run JS interpreted (no Graal compiler on a stock JDK), so - `engine.WarnInterpreterOnly` is silenced here on the engine (it's an engine-level option)." - ^Engine [] - ;; https://github.com/oracle/graaljs/blob/master/docs/user/RunOnJDK.md - (.. (Engine/newBuilder) - (option "engine.WarnInterpreterOnly" "false") - (build))) - -(defonce ^:private - ^{:doc "The process-lifetime GraalVM `Engine` shared by every sandboxed JS context. The engine owns the Truffle - runtime and the parsed-source code cache, so contexts (and pool recycles) reuse one parsed copy of any - `Source` instance they share instead of each standing up their own. Intentionally never closed."} - shared-sandboxed-js-engine - (delay (create-engine))) - -(defn create-context - "Create a sandboxed org.graalvm.polyglot.Context (on the process-shared engine) for evaluating javascript. No host - access, no class lookup, no filesystem I/O. All data must be passed as JSON strings and parsed in JS." - ^Context [] - (.. (Context/newBuilder (into-array String ["js"])) - (engine ^Engine @shared-sandboxed-js-engine) - (option "js.intl-402" "true") - (allowHostAccess HostAccess/NONE) - (allowHostClassLookup no-host-class-lookup) - (out System/out) - (err System/err) - (allowIO false) - (build))) - -(defn load-js-string - "Load a string literal source into the js context." - [^Context context ^String string-src ^String src-name] - (.eval context (.buildLiteral (Source/newBuilder "js" string-src src-name)))) - -(defn build-source - "Build a `Source` from a classpath resource path. Evaluating the SAME `Source` instance in several contexts that - share an engine makes the engine's code cache reuse one parsed copy; a fresh instance per context makes the engine - parse (and retain) a separate copy each time — so callers that create many contexts should build once and share." - ^Source [source-path] - (let [resource (io/resource source-path)] - (when (nil? resource) - (throw (ex-info (trs "Javascript resource not found: {0}" source-path) - {:source source-path}))) - (.build (Source/newBuilder "js" ^java.net.URL resource)))) - -(defn eval-source - "Evaluate an already-built `Source` in the js context." - [^Context context ^Source source] - (.eval context source)) - -(defn execute-fn-name - "Executes `js-fn-name` in js context with args" - ^Value [^Context context js-fn-name & args] - ;; TODO: locking context is not ideal, but contexts are currently being shared with all threads and GraalVM doesn't - ;; support concurrent execution for js. - ;; There is a couple of idea we can try: - ;; - put a thread pool around context initialization - ;; - init a new context for each thread - (locking context - (let [fn-ref (.eval context "js" js-fn-name) - args (into-array Object args)] - (assert (.canExecute fn-ref) (str "cannot execute " js-fn-name)) - (.execute fn-ref args)))) - -(defn execute-fn - "fn-ref should be an executable org.graalvm.polyglot.Value return from a js engine. Invoke this function with args." - ^Value [^Value fn-ref & args] - (assert (.canExecute fn-ref) "cannot execute function reference") - (.execute fn-ref (object-array args))) diff --git a/src/metabase/channel/render/js/graal.clj b/src/metabase/channel/render/js/graal.clj new file mode 100644 index 00000000000..a3ad14e1468 --- /dev/null +++ b/src/metabase/channel/render/js/graal.clj @@ -0,0 +1,221 @@ +(ns metabase.channel.render.js.graal + "The GraalVM [[metabase.channel.render.js.protocol/StaticVizRenderer]]: runs the static-viz JS + in-process on a pool of sandboxed GraalVM contexts (up to two by default). + + We run the JS interpreted (no Graal compiler on a stock JDK) and silence the interpreter warning with + the engine-level `engine.WarnInterpreterOnly` option. See + https://github.com/oracle/graaljs/blob/master/docs/user/RunOnJDK.md. + + The pooled contexts share one `Engine` and one parsed bundle `Source`: the engine (and its + `Source`-keyed code cache) is created with the first context and closed with the last, and each context + evaluates the shared source into its own realm. So one parsed copy of the bundle is held regardless of + how many contexts there are, which makes raising the pool's max from 1 to 2 or 3 a one-line change. A + context is held exclusively per render (so renders serialize per context); the utilization controller + has min 0, so when idle the pool shrinks to 0 and the last `destroy` closes the engine (GraalVM reclaims + neither context nor engine on GC). The first render after an idle gap rebuilds them." + (:require + [clojure.java.io :as io] + [metabase.channel.render.js.protocol :as js.protocol] + [metabase.config.core :as config] + [metabase.util.i18n :refer [trs]] + [metabase.util.json :as json] + [metabase.util.malli :as mu]) + (:import + (io.aleph.dirigiste IPool$Generator Pool Pools) + (java.util.concurrent TimeUnit) + (org.graalvm.polyglot Context Engine HostAccess Source Value))) + +(set! *warn-on-reflection* true) + +;;; --------------------------------------------- engine / context ---------------------------------------- + +(def ^:private no-host-class-lookup + "Predicate blocking all host class lookup." + (reify java.util.function.Predicate + (test [_ _] false))) + +(defn- create-engine + "Build a JS `Engine`. We run JS interpreted (no Graal compiler on a stock JDK), so + `engine.WarnInterpreterOnly` is silenced here (it's an engine-level option)." + ^Engine [] + (.. (Engine/newBuilder) + (option "engine.WarnInterpreterOnly" "false") + (build))) + +(defn create-context + "Create a sandboxed org.graalvm.polyglot.Context for evaluating javascript — on a fresh engine (no args) + or on the given `engine`, so several contexts can share one engine and its parsed-source code cache. No + host access, no class lookup, no filesystem I/O; all data must be passed as JSON strings and parsed in JS." + (^Context [] (create-context (create-engine))) + (^Context [^Engine engine] + (.. (Context/newBuilder (into-array String ["js"])) + (engine engine) + (option "js.intl-402" "true") + (allowHostAccess HostAccess/NONE) + (allowHostClassLookup no-host-class-lookup) + (out System/out) + (err System/err) + (allowIO false) + (build)))) + +(defn load-js-string + "Load a string literal source into the js context." + [^Context context ^String string-src ^String src-name] + (.eval context (.buildLiteral (Source/newBuilder "js" string-src src-name)))) + +(defn- build-source + "Build a `Source` from a classpath resource path." + ^Source [source-path] + (let [resource (io/resource source-path)] + (when (nil? resource) + (throw (ex-info (trs "Javascript resource not found: {0}" source-path) + {:source source-path}))) + (.build (Source/newBuilder "js" ^java.net.URL resource)))) + +(defn- eval-source + "Evaluate an already-built `Source` in the js context." + [^Context context ^Source source] + (.eval context source)) + +(defn execute-fn-name + "Execute the global js function named `js-fn-name` in `context` with `args`. Not thread-safe on its own + — a context is held exclusively per render by the pool (see [[call-js]])." + ^Value [^Context context js-fn-name & args] + (let [fn-ref (.eval context "js" js-fn-name)] + (assert (.canExecute fn-ref) (str "cannot execute " js-fn-name)) + (.execute fn-ref (into-array Object args)))) + +(defn execute-fn + "fn-ref should be an executable org.graalvm.polyglot.Value returned from a js engine. Invoke it with args." + ^Value [^Value fn-ref & args] + (assert (.canExecute fn-ref) "cannot execute function reference") + (.execute fn-ref (object-array args))) + +;;; ---------------------------------------- shared engine + contexts ------------------------------------- + +(def ^:private bundle-resource-path "frontend_client/app/dist/lib-static-viz.bundle.js") + +(defn- assert-tests-not-initializing! + "Guard against loading the static-viz bundle as a side effect of loading namespaces: it might not have + been built yet. If it hasn't, we want a meaningful error (see the fixture in + [[metabase.channel.render.js.svg-test]]) rather than a meaningless compilation error at test-runner + startup." + [] + (when config/tests-available? + ((requiring-resolve 'mb.hawk.init/assert-tests-are-not-initializing) "(mt/id ...) or (data/id ...)"))) + +(def ^:private engine-lock (Object.)) + +(def ^:private shared-engine + "Atom holding `{:engine , :source , :refs }`, or nil when no context + is live. Guarded by [[engine-lock]]. The engine and parsed bundle are shared by every pooled context; + they're created with the first context ([[acquire-engine!]]) and closed with the last + ([[release-engine!]])." + (atom nil)) + +(defn- acquire-engine! + "Return the shared `{:engine, :source}`, creating them (parsing the bundle) with the first context. + Bumps the ref count." + [] + (locking engine-lock + (let [state (or @shared-engine + {:source (build-source bundle-resource-path) + :engine (create-engine) + :refs 0})] + (reset! shared-engine (update state :refs inc)) + state))) + +(defn- release-engine! + "Drop a ref on the shared engine, closing it once the last context is gone." + [] + (locking engine-lock + (let [{:keys [^Engine engine refs]} @shared-engine] + (if (<= refs 1) + (do (try (.close engine) (catch Exception _)) + (reset! shared-engine nil)) + (swap! shared-engine update :refs dec))))) + +(defn- generate-context! + "Build a context on the shared engine and evaluate the bundle into it (creating the engine + parsing the + bundle if this is the first context)." + ^Context [] + (assert-tests-not-initializing!) + (let [{:keys [^Engine engine ^Source source]} (acquire-engine!)] + (try + (doto (create-context engine) + (eval-source source)) + (catch Throwable t + (release-engine!) + (throw t))))) + +(defn- destroy-context! + "Close a context and drop its ref on the shared engine (closing the engine if it was the last context)." + [^Context context] + (try (.close context true) (catch Exception _)) + (release-engine!)) + +;;; ------------------------------------------------ context pool ----------------------------------------- + +(def ^:private pool-key + "Dirigiste pools are keyed; the key itself is arbitrary, it just has to be the same for every operation." + :static-viz) + +(def ^:private ^Pool static-viz-context-pool + "A pool of up to two static-viz contexts (change the controller's max to run 1 or 3), each held + exclusively from acquire to release, so at most two renders run at once — one per context, on the + shared engine. + + The utilization controller targets 100% utilization with a max of 2 and a min of 0, so when nothing is + rendering it shrinks to 0 and the generator's `destroy` closes the context (and, on the last one, the + shared engine). It rechecks once a minute, so an idle context lingers up to ~1 minute before being + reaped (keeping it warm through short gaps between renders). The other constructor args (queue size, + sampling interval) don't matter much." + (let [max-pool-size 2 + max-queued-acquires 65000 + sample-period-ms (.toMillis TimeUnit/MILLISECONDS 25) + control-period-ms (.toMillis TimeUnit/MINUTES 1)] + (Pool. (reify IPool$Generator + (generate [_ _] + (generate-context!)) + (destroy [_ _ context] + (destroy-context! context))) + (Pools/utilizationController 1.0 max-pool-size max-pool-size) + max-queued-acquires + sample-period-ms + control-period-ms + TimeUnit/MILLISECONDS))) + +(defn- do-with-static-viz-context + "Borrow a pooled static-viz context and call `f` with it, held exclusively for the call (never let it — + or a context-bound `Value` — escape). In dev, builds and closes a throwaway context per call so a fresh + `bun run build-static-viz` is picked up without a REPL restart." + [f] + (if config/is-dev? + (let [context (generate-context!)] + (try (f context) + (finally (destroy-context! context)))) + (let [context (.acquire static-viz-context-pool pool-key)] + (try (f context) + (finally (.release static-viz-context-pool pool-key context)))))) + +;;; ------------------------------------------------ backend ---------------------------------------------- + +(mu/defn- call-js :- :string + "Execute static-viz bundle function `fn-name` (a `MetabaseStaticViz.*` global) with the already-JSON-encoded + string `args` on the pooled context." + [fn-name :- :string + args :- [:sequential :string]] + (do-with-static-viz-context + (fn [^Context context] + (.asString ^Value (apply execute-fn-name context (str "MetabaseStaticViz." fn-name) args))))) + +(defn renderer + "The GraalVM [[metabase.channel.render.js.protocol/StaticVizRenderer]] — runs the static-viz JS + in-process on the pooled GraalVM context. Each method JSON-encodes its `input` map for the bundle and + decodes the bundle's JSON result back into Clojure data." + [] + (reify js.protocol/StaticVizRenderer + (chart [_ input] + (json/decode+kw (call-js "renderChart" [(json/encode input)]))) + (cell-background-colors [_ input] + (json/decode (call-js "getCellBackgroundColors" [(json/encode input)]))))) diff --git a/src/metabase/channel/render/js/pool.clj b/src/metabase/channel/render/js/pool.clj deleted file mode 100644 index 5a2d074afff..00000000000 --- a/src/metabase/channel/render/js/pool.clj +++ /dev/null @@ -1,160 +0,0 @@ -(ns metabase.channel.render.js.pool - "The shared pool of sandboxed GraalVM JS contexts used for static-viz rendering — charts - (`metabase.channel.render.js.svg`) and table-cell color selection (`metabase.channel.render.js.color`). Every - pooled context has the static-viz bundle, the static-viz interface, and the color-selector sources loaded. - - ### Memory model (GraalJS 25, interpreter mode — measured, see GHY-4077) - - - The engine plus the parsed sources cost ~220 MB retained, steady, paid once per process. All contexts share the - process-lifetime engine and evaluate the same `Source` instances (one `defonce` per resource), so the engine's - code cache — which is keyed on the `Source` instance — holds ONE parsed copy of the static-viz bundle. Building a - fresh `Source` per context (the previous behavior) made the engine retain a separate parsed copy for every pool - generation, and made each generation reparse the 27 MB bundle: a multi-hundred-MB allocation spike every context - recycle. - - Evaluating the sources in a fresh context (realm setup against the warm code cache) churns through ~500 MB of - allocation in ~1 s. That is collectable garbage, not footprint: each extra live context retains only ~16 MB. The - very first context in the JVM additionally pays the cold parse — ~1.2 GB / ~5 s. - - A render itself allocates on the order of ~1 MB per row of chart data, also transient. - - ### Why context creation is serialized - - Creation being ~500 MB of churn a pop, letting the pool grow 1→3 concurrently during a render burst stacks - ~1–1.5 GB of transient allocation on top of the in-flight renders — on a 2 GB heap (the Cloud default) that - outruns the collector and drives GC thrash / OOM. So creation takes a private monitor - (see [[context-creation-lock]]). A time-based throttle (\"no two creations within X seconds\") is the wrong shape: - a burst of concurrent renders genuinely needs multiple contexts, so we let the pool grow to meet demand — it just - warms one context per ~1 s instead of all at once, bounding the worst case to a single creation's churn. Waiting - acquires simply queue on the pool. Pooled contexts expire after ~10 minutes with jitter so members created - together don't all regenerate in the same window. - - Consumers borrow a context within [[with-static-viz-context]] and return plain data — never the context or a - context-bound `Value`. Never acquire a context while already holding one: with a small pool, nested acquires can - deadlock." - (:require - [metabase.channel.render.js.engine :as js.engine] - [metabase.config.core :as config]) - (:import - (io.aleph.dirigiste IPool$Controller IPool$Generator Pool Pools Stats) - (java.util.concurrent TimeUnit) - (org.graalvm.polyglot Context))) - -(set! *warn-on-reflection* true) - -;; Each source is built exactly once per process (defonce + delay) and shared by every context: the engine's code -;; cache is keyed on the `Source` instance, so this is what makes it hold ONE parsed copy of each resource. Building -;; a `Source` anywhere else reintroduces the leak/reparse this exists to fix — after editing a resource in dev, -;; re-evaluate its defonce (or restart the REPL) to pick the change up. - -;; goes through webpack; changes require a `bun run build-static-viz` -(defonce ^:private bundle-source - (delay (js.engine/build-source "frontend_client/app/dist/lib-static-viz.bundle.js"))) - -;; does not go through a build step, edit freely -(defonce ^:private interface-source - (delay (js.engine/build-source "frontend_shared/static_viz_interface.js"))) - -;; built by `bun run build-shared` -(defonce ^:private color-selector-source - (delay (js.engine/build-source "frontend_shared/color_selector.js"))) - -(defn- static-viz-sources [] - [@bundle-source @interface-source @color-selector-source]) - -(defn- assert-tests-not-initializing! [] - ;; make sure people don't try to load the static viz bundle as a side-effect of loading namespaces, because it might - ;; not have been built! If it's not built, we want to be able to give people a meaningful error (see the fixture - ;; in [[metabase.channel.render.js.svg-test]]) rather than have the test runner fail to start with a meaningless - ;; compilation error. - (when config/tests-available? - ((requiring-resolve 'mb.hawk.init/assert-tests-are-not-initializing) "(mt/id ...) or (data/id ...)"))) - -(defn- create-static-viz-context - "A fresh sandboxed context with the static-viz sources loaded." - ^Context [] - (assert-tests-not-initializing!) - (let [ctx (js.engine/create-context)] - (doseq [source (static-viz-sources)] - (js.engine/eval-source ctx source)) - ctx)) - -(def ^:private pool-key - "Dirigiste pools are keyed. The key itself is arbitrary — it just has to be the same for every pool operation." - :engines) - -(def ^:private context-creation-lock - "Monitor serializing pooled context creation. Even with the shared-`Source` code cache, evaluating the bundle in a - fresh context allocates ~500 MB, so letting the pool grow 1→3 concurrently during a render burst stacks ~1–1.5 GB - of transient allocation on top of the in-flight renders — enough to blow a 2 GB heap. Holding creations to one at a - time bounds the worst case to a single creation (~1 s each); waiting acquires just queue on the pool meanwhile. - Nothing else may lock this monitor." - (Object.)) - -(defn- expiry-timestamp - "Expiry timestamp for a pooled context: now + 10 minutes ± up to 3 minutes of jitter. The jitter keeps pool members - created in the same burst from all expiring — and serially regenerating — in the same window." - ^long [] - (+ (System/nanoTime) - (.toNanos TimeUnit/MINUTES 7) - (long (rand (.toNanos TimeUnit/MINUTES 6))))) - -(defn- generate-pool-entry - "Create a `[context expiry-timestamp]` tuple for the pool. At most one context creation runs at a time (see - [[context-creation-lock]]); acquisition/use is already exclusive per context, so this only bounds pool *growth*." - [] - (locking context-creation-lock - [(create-static-viz-context) (expiry-timestamp)])) - -(def ^:private ^Pool static-viz-context-pool - "Pool of Truffle JS contexts. They are not thread-safe, so access is exclusive from acquire to release. Generating a - context is cheap — realm setup plus top-level eval against the engine's code cache, no reparse — and per-context - memory is a realm, not a full parsed copy of the bundle. The pool targets 100% utilization with a maximum of 3 - contexts (to bound memory; renders hold a context exclusively, so that is also the render concurrency), but at - least 1 context is always kept in the pool to pick up. Each pooled tuple carries a (jittered, ~10-minute) expiry - timestamp so a context is recycled regardless, bounding per-context leak accumulation. Context creation is - serialized (see [[context-creation-lock]]) so pool growth never runs multiple ~500 MB creations concurrently." - (let [base-controller (Pools/utilizationController 1.0 3 3)] - (Pool. (reify IPool$Generator - (generate [_ _] - (generate-pool-entry)) - (destroy [_ _ [^Context ctx _expiry]] - ;; Close the context when it's disposed from the pool (expiry/idle shrink/shutdown). Without this, each - ;; disposed static-viz context leaks its memory: GraalVM only releases it on `close`, not on GC. - (try - (.close ctx true) ;; force close - can't wait for running code - (catch Exception _)))) - ;; Wrap the utilization controller with a modification that doesn't allow the pool to go below 1 instance. - (reify IPool$Controller - (shouldIncrement [_ k a b] (.shouldIncrement base-controller k a b)) - (adjustment [_ stats] - (let [adj (.adjustment base-controller stats) - n (some-> ^Stats (get stats pool-key) .getNumWorkers) - engines-adj (get adj pool-key)] - (if (and n engines-adj (<= (+ n engines-adj) 0)) - ;; If the adjustment is going to bring the pool to 0 engines, return empty adjustment instead. - {} - adj)))) - 65000 ;; Queue size - doesn't matter much. - 25 ;; Sampling interval - doesn't matter much. - 10000 ;; Recheck every 10 seconds - TimeUnit/MILLISECONDS))) - -(defn do-with-static-viz-context - "Impl for [[with-static-viz-context]]." - [f] - (if config/is-dev? - (with-open [ctx (create-static-viz-context)] - (f ctx)) - (loop [] - (let [[context expiry-ts :as tuple] (.acquire static-viz-context-pool pool-key)] - (if (>= (System/nanoTime) expiry-ts) - (do (.dispose static-viz-context-pool pool-key tuple) - (recur)) - (try (f context) - (finally (.release static-viz-context-pool pool-key tuple)))))))) - -(defmacro with-static-viz-context - "Execute `body` with `binding-name` bound to a pooled static-viz JS context (in dev mode, a fresh context closed - afterwards). The context is borrowed for the dynamic extent of `body`: do not let it — or any context-bound - `Value` — escape, and do not acquire another context inside `body` (nested acquires can deadlock the pool)." - [binding-name & body] - `(do-with-static-viz-context (fn [~binding-name] ~@body))) diff --git a/src/metabase/channel/render/js/protocol.clj b/src/metabase/channel/render/js/protocol.clj new file mode 100644 index 00000000000..099cfb87683 --- /dev/null +++ b/src/metabase/channel/render/js/protocol.clj @@ -0,0 +1,18 @@ +;; The protocol implemented by the static-viz renderers. +(ns metabase.channel.render.js.protocol + "The strongly-typed protocol implemented by the static-viz renderers (currently + [[metabase.channel.render.js.graal]]). Callers get the implementation from + [[metabase.channel.render.js.renderer/renderer]] and invoke these methods on it.") + +(set! *warn-on-reflection* true) + +(defprotocol StaticVizRenderer + "Renders static visualizations. Each method takes a plain map of arguments and returns the corresponding + static-viz bundle function's result parsed from JSON into Clojure data." + (chart [renderer input] + "Render one chart. `input` is a map: the default isomorphic path takes + `{:rawSeries .. :dashcardSettings .. :options ..}`; legacy charts take `{:kind \"funnel\"|\"gauge\" ..}` + plus that kind's fields. Returns a keywordized `{:type .. :content ..}` map.") + (cell-background-colors [renderer input] + "Compute pulse table cell background colors. `input` is `{:rows .. :cols .. :settings .. :cells ..}`. + Returns a vector of color strings (or nil), positionally.")) diff --git a/src/metabase/channel/render/js/renderer.clj b/src/metabase/channel/render/js/renderer.clj new file mode 100644 index 00000000000..4649718883a --- /dev/null +++ b/src/metabase/channel/render/js/renderer.clj @@ -0,0 +1,13 @@ +(ns metabase.channel.render.js.renderer + "Hands callers the static-viz [[metabase.channel.render.js.protocol/StaticVizRenderer]] implementation: + the in-process GraalVM renderer. Kept behind the protocol so an alternative backend can be slotted in + without touching the callers ([[metabase.channel.render.js.svg]], [[metabase.channel.render.js.color]])." + (:require + [metabase.channel.render.js.graal :as graal])) + +(set! *warn-on-reflection* true) + +(defn renderer + "The static-viz renderer." + [] + (graal/renderer)) diff --git a/src/metabase/channel/render/js/svg.clj b/src/metabase/channel/render/js/svg.clj index 1815375e959..2615cca5a0a 100644 --- a/src/metabase/channel/render/js/svg.clj +++ b/src/metabase/channel/render/js/svg.clj @@ -1,18 +1,17 @@ (ns metabase.channel.render.js.svg - "Functions to render charts as svg strings by using graal's js engine. A bundle is built by `bun run build-static-viz` - which has charting library. This namespace has some wrapper functions to invoke those functions. Interop is very - strange, as the jvm datastructures, not just serialized versions are used. This is why we have the `toJSArray` and - `toJSMap` functions to turn Clojure's normal datastructures into js native structures." + "Functions to render charts as svg strings by invoking the static-viz JavaScript bundle (built by `bun run + build-static-viz`) through [[metabase.channel.render.js.protocol]], which runs the JS on a pooled in-process + GraalVM context. This namespace has wrapper functions that build the argument map, call the corresponding + protocol function, and rasterize the resulting SVG to PNG bytes via Batik." (:require [clojure.string :as str] [metabase.appearance.core :as appearance] [metabase.channel.render.image-buffer :as image-buffer] - [metabase.channel.render.js.engine :as js.engine] - [metabase.channel.render.js.pool :as js.pool] + [metabase.channel.render.js.protocol :as js.protocol] + [metabase.channel.render.js.renderer :as renderer] [metabase.channel.render.style :as style] [metabase.lib-be.core :as lib-be] - [metabase.premium-features.core :as premium-features] - [metabase.util.json :as json]) + [metabase.premium-features.core :as premium-features]) (:import (java.io ByteArrayInputStream ByteArrayOutputStream) (java.nio.charset StandardCharsets) @@ -168,53 +167,40 @@ frontend/src/metabase/static-viz/components/FunnelChart/types.ts for the actual format options. Returns a byte array of a png file." [data settings] - (let [svg-string (js.pool/with-static-viz-context context - (.asString (js.engine/execute-fn-name context "funnel" (json/encode data) - (json/encode settings) - (json/encode (premium-features/token-features)))))] - (svg-string->bytes svg-string))) + (-> (js.protocol/chart (renderer/renderer) + {:kind "funnel" + :data data + :settings settings + :tokenFeatures (premium-features/token-features)}) + :content + svg-string->bytes)) (defn ^:dynamic *javascript-visualization* "Clojure entrypoint to render javascript visualizations. This functions is dynanic only for testing purposes." [cards-with-data dashcard-viz-settings] - (let [response (js.pool/with-static-viz-context context - (.asString (js.engine/execute-fn-name context "javascript_visualization" - (json/encode cards-with-data) - (json/encode dashcard-viz-settings) - (json/encode (cond-> {:applicationColors (appearance/application-colors) - :startOfWeek (lib-be/start-of-week) - :customFormatting (appearance/custom-formatting) - :tokenFeatures (premium-features/token-features)} - *chart-size* - (assoc :width (:width *chart-size*) - :height (:height *chart-size*) - :fitWithinBounds (boolean (:fit-within? *chart-size*))))))))] - (-> response - json/decode+kw - (update :type (fnil keyword "unknown"))))) + (-> (js.protocol/chart + (renderer/renderer) + {:rawSeries cards-with-data + :dashcardSettings dashcard-viz-settings + :options (cond-> {:applicationColors (appearance/application-colors) + :startOfWeek (lib-be/start-of-week) + :customFormatting (appearance/custom-formatting) + :tokenFeatures (premium-features/token-features)} + *chart-size* + (assoc :width (:width *chart-size*) + :height (:height *chart-size*) + :fitWithinBounds (boolean (:fit-within? *chart-size*))))}) (update :type (fnil keyword "unknown")))) (defn gauge "Clojure entrypoint to render a gauge chart. Returns a byte array of a png file" [card data] - (js.pool/with-static-viz-context context - (let [js-res (js.engine/execute-fn-name context "gauge" - (json/encode card) - (json/encode data) - (json/encode (premium-features/token-features))) - svg-string (.asString js-res)] - (svg-string->bytes svg-string)))) - -(defn progress - "Clojure entrypoint to render a progress bar. Returns a byte array of a png file" - [value goal settings] - (js.pool/with-static-viz-context context - (let [js-res (js.engine/execute-fn-name context "progress" - (json/encode {:value value :goal goal}) - (json/encode settings) - (json/encode (appearance/application-colors)) - (json/encode (premium-features/token-features))) - svg-string (.asString js-res)] - (svg-string->bytes svg-string)))) + (-> (js.protocol/chart (renderer/renderer) + {:kind "gauge" + :card card + :data data + :tokenFeatures (premium-features/token-features)}) + :content + svg-string->bytes)) (def ^:private icon-paths {:dashboard "M32 28a4 4 0 0 1-4 4H4a4.002 4.002 0 0 1-3.874-3H0V4a4 4 0 0 1 4-4h25a3 3 0 0 1 3 3v25zm-4 0V8H4v20h24zM7.273 18.91h10.182v4.363H7.273v-4.364zm0-6.82h17.454v4.365H7.273V12.09zm13.09 6.82h4.364v4.363h-4.363v-4.364z" diff --git a/test/metabase/channel/render/js/engine_test.clj b/test/metabase/channel/render/js/engine_test.clj deleted file mode 100644 index d70749e5629..00000000000 --- a/test/metabase/channel/render/js/engine_test.clj +++ /dev/null @@ -1,27 +0,0 @@ -(ns metabase.channel.render.js.engine-test - (:require - [clojure.test :refer :all] - [metabase.channel.render.js.engine :as js] - [metabase.test :as mt])) - -(set! *warn-on-reflection* true) - -(deftest make-context-test - (testing "can make a context that evaluates javascript" - (let [context (js/create-context)] - (js/load-js-string context "function plus (x, y) { return x + y }" "plus test") - (is (= 3 (.asLong (js/execute-fn-name context "plus" 1 2)))))) - (testing "can invoke closures return from that javascript" - (let [context (js/create-context)] - (js/load-js-string context "function curry_plus (x) { return function (y) { return x + y}}" - "curried function test") - (let [curried (js/execute-fn-name context "curry_plus" 1)] - (is (= 3 (.asLong (js/execute-fn curried 2)))))))) - -(deftest thread-safe-execute-fn-name-test - (testing "execute-fn-name is thread safe" - (let [context (js/create-context)] - (js/load-js-string context "function plus (x, y) { return x + y }" "plus test") - (is (= (repeat 10 2) - (mt/repeat-concurrently 10 - #(.asLong (js/execute-fn-name context "plus" 1 1)))))))) diff --git a/test/metabase/channel/render/js/graal_test.clj b/test/metabase/channel/render/js/graal_test.clj new file mode 100644 index 00000000000..30e514f1ff3 --- /dev/null +++ b/test/metabase/channel/render/js/graal_test.clj @@ -0,0 +1,28 @@ +(ns metabase.channel.render.js.graal-test + (:require + [clojure.test :refer :all] + [metabase.channel.render.js.graal :as graal] + [metabase.test :as mt])) + +(set! *warn-on-reflection* true) + +(deftest make-context-test + (testing "can make a context that evaluates javascript" + (let [context (graal/create-context)] + (graal/load-js-string context "function plus (x, y) { return x + y }" "plus test") + (is (= 3 (.asLong (graal/execute-fn-name context "plus" 1 2)))))) + (testing "can invoke closures return from that javascript" + (let [context (graal/create-context)] + (graal/load-js-string context "function curry_plus (x) { return function (y) { return x + y}}" + "curried function test") + (let [curried (graal/execute-fn-name context "curry_plus" 1)] + (is (= 3 (.asLong (graal/execute-fn curried 2)))))))) + +(deftest concurrent-execution-with-locking-test + (testing "concurrent execution on a shared context is safe when callers hold its monitor" + (let [context (graal/create-context)] + (graal/load-js-string context "function plus (x, y) { return x + y }" "plus test") + (is (= (repeat 10 2) + (mt/repeat-concurrently 10 + #(locking context + (.asLong (graal/execute-fn-name context "plus" 1 1))))))))) diff --git a/test/metabase/channel/render/js/pool_test.clj b/test/metabase/channel/render/js/pool_test.clj deleted file mode 100644 index 95f7f3a6500..00000000000 --- a/test/metabase/channel/render/js/pool_test.clj +++ /dev/null @@ -1,37 +0,0 @@ -(ns metabase.channel.render.js.pool-test - (:require - [clojure.test :refer :all] - [metabase.channel.render.js.pool :as js.pool]) - (:import - (java.util.concurrent TimeUnit))) - -(set! *warn-on-reflection* true) - -(deftest generate-pool-entry-serializes-context-creation-test - (testing "no two static-viz context creations run concurrently (#GHY-4077)" - (let [in-flight (atom 0) - max-in-flight (atom 0) - fake-context (Object.)] - (with-redefs [js.pool/create-static-viz-context - (fn [] - (let [n (swap! in-flight inc)] - (swap! max-in-flight max n) - (Thread/sleep 50) - (swap! in-flight dec) - fake-context))] - (let [entries (->> (repeatedly 4 #(future (#'js.pool/generate-pool-entry))) - doall - (mapv deref))] - (is (= 1 @max-in-flight)) - (testing "every entry is a [context expiry-timestamp] tuple" - (doseq [[ctx expiry-ts] entries] - (is (identical? fake-context ctx)) - (is (int? expiry-ts))))))))) - -(deftest expiry-timestamp-jitter-test - (testing "expiry is ~10 minutes out with ±3 minutes of jitter" - (let [now (System/nanoTime) - expiries (repeatedly 100 #(#'js.pool/expiry-timestamp))] - (doseq [expiry expiries - :let [delta (- expiry now)]] - (is (<= (.toNanos TimeUnit/MINUTES 7) delta (.toNanos TimeUnit/MINUTES 14)))))))