diff --git a/_docs/v0.62/README.md b/_docs/v0.62/README.md index 310310f046..16f9a7c29d 100644 --- a/_docs/v0.62/README.md +++ b/_docs/v0.62/README.md @@ -106,6 +106,7 @@ Metabase's reference documentation. - [Visualizing data](./questions/visualizations/visualizing-results) - [Box plots](./questions/visualizations/box-plot) - [Combo charts](./questions/visualizations/combo-chart) +- [Custom visualizations](./questions/visualizations/custom) - [Detail](./questions/visualizations/detail) - [Funnel charts](./questions/visualizations/funnel) - [Gauge charts](./questions/visualizations/gauge) diff --git a/_docs/v0.62/configuring-metabase/fonts.md b/_docs/v0.62/configuring-metabase/fonts.md index 29ce0bc08d..e49faf6fbe 100644 --- a/_docs/v0.62/configuring-metabase/fonts.md +++ b/_docs/v0.62/configuring-metabase/fonts.md @@ -135,6 +135,12 @@ raw.githubusercontent.com/${user}/${repo}/${branch}/${path} Note that in the raw link, there is no `/blob/` directory in the URL. +### Custom fonts and Content Security Policy + +When you add a custom font hosted on another domain, Metabase automatically allows that domain in the browser's Content Security Policy for fonts. You don't need to configure anything for the font to work. + +If you don't add any custom fonts, Metabase only allows fonts served from your own instance. + ### Supporting multiple languages To support multiple character sets, for example both Latin and Cyrillic, you'll need to merge font files. diff --git a/_docs/v0.62/configuring-metabase/settings.md b/_docs/v0.62/configuring-metabase/settings.md index ee6b338cdb..2e12e74f00 100644 --- a/_docs/v0.62/configuring-metabase/settings.md +++ b/_docs/v0.62/configuring-metabase/settings.md @@ -100,9 +100,11 @@ When on, Metabase restricts the browser's Content Security Policy so images can By default, images from any domain are allowed. +You must turn on this setting to enable [Custom visualizations](../questions/visualizations/custom). While custom visualizations are enabled, you can't turn it back off. + ## Allowed domains for images -When the [Restrict image domains](#restrict-image-domains) setting is on, Metabase will only allow images served from this Metabase instance, and any domains listed here. +When the [Restrict image domains](#restrict-image-domains) setting is on, Metabase will only allow images served from this Metabase instance, and any domains listed on this page. Leave this input empty to only allow images hosted by your Metabase instance. diff --git a/_docs/v0.62/configuring-metabase/start.md b/_docs/v0.62/configuring-metabase/start.md index 7f8060cb47..056758fed9 100644 --- a/_docs/v0.62/configuring-metabase/start.md +++ b/_docs/v0.62/configuring-metabase/start.md @@ -69,6 +69,10 @@ Cache query results for faster loading times. Upload custom maps to your Metabase. +## [Custom visualizations](../questions/visualizations/custom) + +Add your own chart types by uploading visualization plugins. + ## [Customizing the Metabase Jetty webserver](./customizing-jetty-webserver) Set SSL and port settings for the Jetty webserver. diff --git a/_docs/v0.62/developers-guide/custom-visualizations.md b/_docs/v0.62/developers-guide/custom-visualizations.md new file mode 100644 index 0000000000..27cd59fa62 --- /dev/null +++ b/_docs/v0.62/developers-guide/custom-visualizations.md @@ -0,0 +1,438 @@ +--- +version: v0.62 +has_magic_breadcrumbs: true +show_category_breadcrumb: true +show_title_breadcrumb: true +category: 'Developers Guide' +title: 'Building custom visualizations' +source_url: 'https://github.com/metabase/metabase/blob/master/docs/developers-guide/custom-visualizations.md' +layout: new-docs +summary: 'Use the Custom Visualizations SDK to build, develop, and package your own chart types for Metabase.' +--- + +# Building custom visualizations + +{% include plans-blockquote.html feature="Custom visualizations" %} + +You can create a custom chart type for Metabase that you build with React and TypeScript and ship as a plugin. + +You scaffold a project with the `@metabase/custom-viz` package, write your visualization, and package it into a `.tgz` bundle. An admin uploads the plugin to Metabase (see [Custom visualizations](../questions/visualizations/custom)), and you're in business. + +## Overview of a custom visualization + +A custom visualization is a small React app that Metabase renders in place of a built-in chart. + +Building a custom viz from scaffolding to adding it to your Metabase looks something like: + +1. **Scaffold** a project with the `@metabase/custom-viz` CLI. The command sets up the build, the manifest, and a working starter visualization. +2. **Develop** against a locally running Metabase with hot reload while you write your component and settings. +3. **Handle the data**: read query results from `series`, wire up clicks and tooltips, and add any settings your chart needs. +4. **Match the look** with Metabase's formatters, theme variables, and color scheme. +5. **Build and package** the project into a `.tgz` bundle. +6. **Add it to your Metabase**: an admin uploads the bundle, and your chart type becomes available in your Metabase. + +## Prerequisites + +- Node.js 22 or newer. +- Familiarity with React and TypeScript. +- A Metabase on a [Pro or Enterprise plan](/pricing/) to load your plugin into. + +## Scaffold a custom visualization project + +Generate a new project with the `@metabase/custom-viz` CLI: + +``` +npx @metabase/custom-viz init my-viz +``` + +Then install dependencies and start the dev server: + +``` +cd my-viz +npm install +npm run dev +``` + +`npm run dev` runs in watch mode and rebuilds your plugin on every change. + +### Project structure + +``` +src/ + index.tsx # Your visualization code — start here +metabase-plugin.json # Plugin manifest (name, icon, version) +public/ + assets/ + icon.svg # Visualization icon (shown in the chart type picker) +package.json +vite.config.ts # Build configuration — don't edit +pack.mjs # Packages the build into a .tgz — don't edit +tsconfig.json +``` + +Only `index.tsx` has to export the factory. For a more sophisticated plugin, you'd want to split the component, settings, types, and helpers into their own modules (check out the [calendar-heatmap example](#example-plugins), which keeps the definition in `index.tsx`, the React component in `Visualization.tsx`, and chart configuration and utilities under `src/`). + +### The starter visualization + +The scaffold ships a complete, working example: a chart that shows a thumbs-up emoji (👍) when a single numeric result meets a `threshold` setting, and a thumbs-down (👎) otherwise. + +## Develop against a running Metabase + +To develop your plugin against a live Metabase with hot reload: + +1. Start Metabase with the `MB_CUSTOM_VIZ_PLUGIN_DEV_MODE_ENABLED` environment variable set to `true`. Dev mode is meant for local development, so you can only turn it on with this environment variable. Like any Metabase that runs custom visualizations, this local instance needs a [Pro or Enterprise](/pricing/) token. +2. Run `npm run dev` in your project. By default, the dev server listens on `http://localhost:5174`. +3. In Metabase, go to **Admin** > **Settings** > **Custom visualizations** > **Development** and set the **Dev server URL** to your dev server's address. + +Your plugin shows up in the **Custom visualizations** section of the visualization sidebar (alongside any installed plugins) and is labeled as a dev visualization. + +If you're running Metabase in a Docker container, you'll need to set the **Dev server URL** to: + +``` +http://host.docker.internal:5174 +``` + +## The plugin manifest + +Every plugin includes a `metabase-plugin.json` file at the root of the project: + +```json +{ + "name": "my-viz", + "icon": "icon.svg", + "metabase": { + "version": ">=1.62.0" + } +} +``` + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Unique identifier for the plugin. Metabase registers your visualization under this name and uses it to match replacement bundles. | +| `icon` | Path to the visualization icon (SVG recommended). Metabase serves the icon automatically. It's the only file Metabase serves alongside your bundle. See [Bundling assets](#bundling-assets). | +| `metabase.version` | Semver range of Metabase versions the plugin supports (for example, `">=1.62.0"`, `"^1.62"`, `">=1.62 <1.64"`). | + +## Defining a visualization + +`src/index.tsx` exports a factory function. Metabase calls the function with two helpers: `defineSetting` (for declaring settings) and the current `locale`. The factory function should return the result of `defineConfig`, which wraps your `VisualizationComponent`. + +```tsx +import { + defineConfig, + type CreateCustomVisualization, + type CustomVisualizationProps, +} from "@metabase/custom-viz"; + +type Settings = { + threshold?: number; +}; + +const createVisualization: CreateCustomVisualization = ({ + defineSetting, + locale, +}) => { + const VisualizationComponent = ({ + series, + settings, + width, + height, + }: CustomVisualizationProps) => { + // Render your visualization with React + return
{/* ... */}
; + }; + + return defineConfig({ + id: "my-viz", + getName: () => "My visualization", + minSize: { width: 2, height: 2 }, + defaultSize: { width: 6, height: 4 }, + checkRenderable(series, settings) { + // Throw if the visualization can't render with this data or these settings + if (series.length === 0) { + throw new Error("No data"); + } + }, + settings: { + threshold: defineSetting({ + id: "threshold", + title: "Threshold", + widget: "number", + }), + }, + VisualizationComponent, + }); +}; + +export default createVisualization; +``` + +### Visualization definition properties + +| Property | Type | Description | +| ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------- | +| `id` | `string` | Identifier for the visualization definition. | +| `getName()` | `() => string` | Display name for the visualization. | +| `minSize` | `{ width, height }` | Minimum size on a dashboard grid. | +| `defaultSize` | `{ width, height }` | Default size on a dashboard grid. | +| `noHeader` | `boolean` | When `true`, hides the default card title and description header. | +| `canSavePng` | `boolean` | Set to `true` to enable PNG export of the live, interactive chart. Disabled by default. | +| `checkRenderable` | `(series, settings) => void` | Let people know the chart doesn't work with the current data or settings. | +| `settings` | `Record` | Map of setting definitions created with `defineSetting()`. | +| `VisualizationComponent` | `React.ComponentType` | The interactive React component that renders the visualization in questions and dashboards. | + +### Props passed to your component + +| Prop | Type | Description | +| ------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- | +| `series` | `Series` | Query results — an array of series; each has `data.rows` and `data.cols`. | +| `settings` | `CustomVisualizationSettings` | The resolved visualization settings. | +| `width` | `number \| null` | Container width in pixels. `null` until the first measure — render `null` to avoid a flash. | +| `height` | `number \| null` | Container height in pixels. `null` until the first measure. | +| `colorScheme` | `"light" \| "dark"` | Metabase's current color scheme. | +| `onClick` | `(clickObject) => void` | Call to trigger drill-through actions on a data point. | +| `onHover` | `(hoverObject?) => void` | Call to show a tooltip on a data point. | + +## Handling query results + +`series` is an array of result sets, with one entry per series on the chart. A single question produces one entry; a dashboard card with [multiple series](../dashboards/multiple-series) produces several entries. Each entry has a `data` object: + +- `data.rows`: an array of rows; each row is an array of cell values in column order. Row order is preserved, so when you map rows to chart points one-to-one, a point's index maps straight back to `data.rows[i]`. Useful for grabbing the whole row, not just the clicked cell. +- `data.cols`: an array of column objects describing each value. The fields you'll reach for most: `name` (database column name), `display_name` (label shown in the UI), `base_type` (Metabase type, for example `"type/Integer"`), and `semantic_type` (for example `"type/Currency"` or `"type/Latitude"`). + +```tsx +const [{ data }] = series; +const total = data.rows.reduce((sum, [value]) => sum + Number(value), 0); +``` + +To classify a column without matching type strings by hand, use the column-type predicates the SDK exports: `isNumeric`, `isDate`, `isString`, `isBoolean`, `isCurrency`, `isLatitude`, `isCoordinate`, `isFK`, `isPK`, `isCategory`, `isURL`. These predicates take a `Column` and resolve type metadata from the host, so they only work inside a running Metabase. See [Formatting and theming](#formatting-and-theming). + +```tsx +import { isNumeric } from "@metabase/custom-viz"; + +const numericColumns = data.cols.filter(isNumeric); +``` + +## Clicks and tooltips + +Your component receives `onClick` and `onHover`. Call them with an object that identifies the data point being interacted with. Metabase positions popovers from it, and for clicks it offers the matching drill-through actions (filter by this value, view these rows, and so on). + +```tsx + + onClick({ + value: row[1], + column: cols[1], + dimensions: [{ value: row[0], column: cols[0] }], + event: event.nativeEvent, + element: event.currentTarget, + }) + } + onMouseMove={(event) => + onHover({ + element: event.currentTarget, + data: cols.map((col, i) => ({ + col, + value: row[i], + key: col.display_name, + })), + }) + } + onMouseLeave={() => onHover(null)} +/> +``` + +Pass `null` to `onHover` to dismiss the tooltip. `onClick` also takes an `origin: { row, cols }` when a drill-through needs the whole row, not just the clicked cell. It can take a `data` array of `{ col, value }` pairs (one per column) when an action needs every column's value. You can include `settings` (the current resolved settings) in the click object too, so dashboard click behaviors configured against your visualization have what they need. + +The hover object accepts more than `element` and `data`. Optional fields like `index` and `seriesIndex` (to highlight a series in the legend) and `value`, `column`, `dimensions`, and `event` (for a simpler single-point tooltip) are available when you need them. + +## Settings and widgets + +Define settings with the `defineSetting()` helper. Each setting shows up in the visualization settings sidebar. + +```tsx +settings: { + threshold: defineSetting({ + id: "threshold", + title: "Threshold", + getSection: () => "Display", + widget: "number", + getDefault: () => 0, + getProps: () => ({ + placeholder: "Enter threshold", + options: { isNonNegative: true }, + }), + }), +}, +``` + +### Setting definition properties + +| Property | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------------ | +| `id` | Unique key — has to match the key in your `Settings` type. | +| `title` | Label shown in the sidebar. | +| `getSection()` | Function returning the section the setting appears under (for example, `"Data"` or `"Display"`). | +| `group` | Sub-heading within a section for grouping related settings. | +| `index` | Display order within a group. | +| `inline` | When `true`, renders the widget on the same line as `title` (handy for `"toggle"`). | +| `widget` | A [built-in widget](#built-in-widgets) name, or a [custom React component](#custom-widgets). | +| `getDefault(series, settings)` | Computes the default value when none is stored. | +| `getValue(series, settings)` | Always-computed value — overrides the stored value on every render. | +| `getProps(series, settings)` | Returns widget-specific props. | +| `isValid(series, settings)` | Return `false` to discard a stored value and fall back to `getDefault`. | +| `readDependencies` | Setting IDs that have to resolve before this one. | +| `writeDependencies` | Setting IDs whose current values are persisted when this setting changes. | +| `eraseDependencies` | Setting IDs reset to `null` when this setting changes. | +| `persistDefault` | When `true`, writes the value from `getDefault` to stored settings on first render. | + +### Built-in widgets + +Widgets for the settings UI. + +| Widget | `getProps()` return type | Description | +| -------------------- | -------------------------------------------------------------------------- | ------------------------ | +| `"input"` | `{ placeholder? }` | Text input | +| `"number"` | `{ placeholder?, options?: { isInteger?, isNonNegative? } }` | Numeric input | +| `"toggle"` | _(none — omit `getProps`)_ | Boolean toggle | +| `"radio"` | `{ options: { name, value }[] }` | Radio button group | +| `"select"` | `{ options: { name, value }[], placeholder?, placeholderNoOptions? }` | Dropdown | +| `"segmentedControl"` | `{ options: { name, value }[] }` | Segmented button control | +| `"color"` | `{ title? }` | Color picker | +| `"multiselect"` | `{ options: { label, value }[], placeholder?, placeholderNoOptions? }` | Multi-select dropdown | +| `"field"` | `{ columns, options: { name, value }[], showColumnSetting? }` | Single column picker | +| `"fields"` | `{ columns, options: { name, value }[], addAnother?, showColumnSetting? }` | Multi-column picker | + +### Custom widgets + +When the built-in widgets don't fit, set `widget` to your own React component instead of a built-in name. Metabase renders the component in the settings sidebar, inside the same [sandbox](#sandbox-restrictions) as your visualization. A widget that reaches for a blocked API is removed, so keep widgets to plain inputs and display. + +Metabase injects these props into your widget component (import the type with `BaseWidgetProps`): + +| Prop | Type | Description | +| ------------------ | --------------------- | --------------------------------------- | +| `id` | `string` | The setting's `id`. | +| `value` | `TValue \| undefined` | The setting's current value. | +| `onChange` | `(value?) => void` | Update this setting's value. | +| `onChangeSettings` | `(settings) => void` | Update other settings at the same time. | + +Add any extra props your component needs with `getProps()`. Its return type is your component's own props, minus the base props Metabase injects. + +```tsx +import { defineConfig, type BaseWidgetProps } from "@metabase/custom-viz"; + +type Settings = { label?: string }; + +function LabelWidget({ value, onChange }: BaseWidgetProps) { + return ( + onChange(e.target.value)} /> + ); +} + +// ...in your visualization's settings: +settings: { + label: defineSetting({ + id: "label", + title: "Label", + widget: LabelWidget, + }), +}, +``` + +## Formatting and theming + +Render numbers, dates, and currencies the way the rest of Metabase does with `formatValue`. Pass the cell's column to pick up that column's formatting settings, or override with options like `currency`, `decimals`, `compact`, or `date_style`: + +```tsx +import { formatValue } from "@metabase/custom-viz"; + +formatValue(row[1], { column: cols[1] }); +formatValue(0.084, { number_style: "percent", decimals: 1 }); // "8.4%" +``` + +`formatValue` and the column-type predicates (like `isNumeric` and `isDate`) read formatting and type metadata from Metabase. If you call them outside of Metabase, like in a unit test, they'll throw `Metabase Viz API not initialized`. + +For layout math (like fitting labels or sizing axes), `measureText(text, { size, family, weight })` returns `{ width, height }` in pixels. There's also `measureTextWidth` and `measureTextHeight` if you only need one dimension. + +To match Metabase's look (and follow [dark mode](../people-and-groups/account-settings#theme)), you have two paths. For anything you render as DOM or SVG, you can style with Metabase's CSS variables: `var(--mb-color-brand)` and the other `--mb-color-*` variables, and the theme follows automatically. + +Canvas-based charting libraries (like ECharts and Chart.js) can't read CSS variables, so in those cases you branch on the `colorScheme` prop (`"light"` or `"dark"`) and pass explicit colors. See the [calendar-heatmap example](#example-plugins) for one built with ECharts. + +## Bundling assets + +The build produces a single JavaScript bundle (`dist/index.js`), and the [icon](#the-visualization-icon) is the only file Metabase serves alongside it. Metabase doesn't serve arbitrary static files, so bundling images into your plugin is the most reliable approach. The [sandbox](#sandbox-restrictions) blocks scripted network access like `fetch` and `XMLHttpRequest`, but it doesn't stop the browser from loading an `` or CSS `url()`: an external image still loads as long as its domain is allowed by the image-domains Content Security Policy (see below). + +Bundled images always render, including when an admin has turned on [Restrict image domains](../configuring-metabase/settings#restrict-image-domains). That Content Security Policy setting limits which external hosts images can load from, but inline and `data:` images ship inside your bundle, so they're never blocked. + +Your `npm` dependencies are bundled in too. You can pull in a charting library (the calendar-heatmap example bundles [ECharts](https://echarts.apache.org/)), but everything ships in that single `dist/index.js`, so your code and its dependencies all count toward the packaged plugin's [size limits](#build-and-package-the-plugin). + +So anything your visualization renders has to live inside that bundle. For images, you have a few options: + +- **Inline SVG or emoji.** What the starter visualization does (it renders 👍 / 👎). Drop the SVG markup straight into your JSX. +- **Import the image.** Import an image from `src/` and the bundler inlines small files as a base64 data URL. Vite inlines assets below its `assetsInlineLimit` (4 KB by default); larger files are emitted as separate assets that won't ship in the single bundle, so keep imported images small or raise the limit. +- **Embed a data URL directly.** Paste a `data:image/png;base64,...` string into your component's `src`. + +```tsx +import logo from "./logo.svg"; // inlined as a data URL at build time + +const VisualizationComponent = () => ; +``` + +## The visualization icon + +The icon shows up in the chart type picker and elsewhere in the Metabase UI. + +- Declare it with `"icon"` in `metabase-plugin.json`. The default location is `public/assets/icon.svg`. +- Use `currentColor` for fills and strokes so the icon adapts to light and dark themes, as well as to hover and active states (like when it's highlighted in a menu): + +```svg + + + +``` + +- For more control, you can use Metabase's CSS variables inside an inline SVG, like `fill="var(--mb-color-brand)"`. +- Keep the icon simple and monochromatic. Skip gradients and multiple colors. + +## Build and package the plugin + +Run: + +``` +npm run build +``` + +This compiles `src/` to `dist/` and packages the result into `-.tgz` at the project root. The archive contains `metabase-plugin.json`, `dist/index.js`, and the whitelisted icon under `dist/assets/`, and has to come in under 5 MiB. The packaging step also rejects an archive whose uncompressed contents exceed 25 MiB. You don't need to commit `dist/`. + +For uploading and managing plugins, see [Custom visualizations](../questions/visualizations/custom). + +## Versioning and compatibility + +The Custom Visualizations SDK works with Metabase 1.62 and newer. Declare the versions your plugin supports with `metabase.version` in `metabase-plugin.json`, using [npm semver range](https://github.com/npm/node-semver#ranges) syntax — `">=1.62.0"`, `"^1.62"`, `">=1.62 <1.64"`. Write the range against the full version number (`">=1.62.0"`), not a bare major version (`">=62"`), which won't match. + +If you upload a bundle to a Metabase outside the plugin's declared range, Metabase rejects the upload. + +## Sandbox restrictions + +Metabase runs plugin code in an isolated sandbox, so a visualization works only from the `series` and `settings` it's given. The sandbox blocks: + +- **Network access**: `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `Worker`, `SharedWorker`, `RTCPeerConnection`, `WebTransport`, `BroadcastChannel`, `navigator.sendBeacon`, and `FontFace.load`. You can't call Metabase's APIs or any other service. +- **Browser storage and cookies**: `localStorage`, `sessionStorage`, `indexedDB`, the Cache API, `document.cookie`, and the `CookieStore` API. +- **Device and credential APIs**: clipboard, geolocation, camera and microphone, service workers, the Credentials and Permissions APIs, USB, Bluetooth, HID, serial, WebXR, and Web Share. +- **Browser UI**: `window.open`, dialogs (`alert`, `confirm`, `prompt`, `print`), notifications, modal dialogs, fullscreen, and payment requests. +- **Navigation and the rest of the app**: history changes, the host page's URL and referrer, and any DOM outside the plugin's own container. +- **Unsafe DOM and timing APIs**: `document.write`, `execCommand`, constructable stylesheets, raw HTML parsers (`DOMParser`, `setHTMLUnsafe`, `XSLTProcessor`), and resource-timing APIs that expose other requests the page has made. + +### Custom visualizations only render in the live app + +Custom visualizations only render in the live, interactive app. Static renders, like dashboard subscriptions sent by [email](../dashboards/subscriptions), Slack, or webhook, fall back to a table for any card that uses a custom visualization. The same goes for [embedded](../embedding/introduction) questions and dashboards: a card that uses a custom visualization falls back to a table. + +## Example plugins + +- [Calendar heatmap](https://github.com/metabase/custom-viz-calendar-heatmap). Read through `src/` for an example of `checkRenderable`, settings, and rendering against `series` data. +- [Thumbs](https://github.com/metabase/custom-viz-thumbs). Thumbs up or down depending on a threshold. + +## Further reading + +- [Custom visualizations](../questions/visualizations/custom) +- [`@metabase/custom-viz` on npm](https://www.npmjs.com/package/@metabase/custom-viz) +- [Visualization overview](../questions/visualizations/visualizing-results) diff --git a/_docs/v0.62/developers-guide/images/custom-viz-dev.png b/_docs/v0.62/developers-guide/images/custom-viz-dev.png new file mode 100644 index 0000000000..85d1e9f3ae Binary files /dev/null and b/_docs/v0.62/developers-guide/images/custom-viz-dev.png differ diff --git a/_docs/v0.62/developers-guide/start.md b/_docs/v0.62/developers-guide/start.md index 9176a874fd..1ebfe5959a 100644 --- a/_docs/v0.62/developers-guide/start.md +++ b/_docs/v0.62/developers-guide/start.md @@ -50,6 +50,10 @@ This guide contains detailed information on how to work on Metabase codebase. - [Community drivers](./community-drivers) - [Guide to writing a driver](drivers/start) +## Customizing Metabase + +- [Building custom visualizations](./custom-visualizations) + ## Metabase documentation - [Developing Metabase documentation](./docs) diff --git a/_docs/v0.62/questions/images/custom-viz-calendar-heatmap.png b/_docs/v0.62/questions/images/custom-viz-calendar-heatmap.png new file mode 100644 index 0000000000..66f4350f58 Binary files /dev/null and b/_docs/v0.62/questions/images/custom-viz-calendar-heatmap.png differ diff --git a/_docs/v0.62/questions/images/gondola-line-chart.png b/_docs/v0.62/questions/images/gondola-line-chart.png new file mode 100644 index 0000000000..a5c7947078 Binary files /dev/null and b/_docs/v0.62/questions/images/gondola-line-chart.png differ diff --git a/_docs/v0.62/questions/visualizations/custom.md b/_docs/v0.62/questions/visualizations/custom.md new file mode 100644 index 0000000000..bd0c07ceda --- /dev/null +++ b/_docs/v0.62/questions/visualizations/custom.md @@ -0,0 +1,94 @@ +--- +version: v0.62 +has_magic_breadcrumbs: true +show_category_breadcrumb: true +show_title_breadcrumb: true +category: Questions +title: 'Custom visualizations' +source_url: 'https://github.com/metabase/metabase/blob/master/docs/questions/visualizations/custom.md' +layout: new-docs +summary: 'Add your own chart types to Metabase by uploading visualization plugins built with the Custom Visualizations SDK.' +--- + +# Custom visualizations + +{% include plans-blockquote.html feature="Custom visualizations" %} + +You can build new chart types and add them to Metabase. Here's a calendar heatmap: + +![Calendar heatmap custom visualization](../images/custom-viz-calendar-heatmap.png) + +Here's the [code for that calendar heatmap viz](https://github.com/metabase/custom-viz-calendar-heatmap). + +This page covers how to add a custom visualization to your Metabase. To _create_ a new custom visualization, see [developing a custom visualization](../../developers-guide/custom-visualizations). + +## Enabling custom visualizations + +### Restrict image domains first + +Before you can turn on custom visualizations, you need to enable [Restrict image domains](../../configuring-metabase/settings#restrict-image-domains). A custom visualization runs third-party JavaScript in your Metabase. By restricting image (and font) domains, you limit where that code can load assets from, which narrows the ways a plugin could leak data through outbound asset requests. See [Only add plugins you trust](#only-add-plugins-you-trust). + +While custom visualizations are enabled, you can't turn **Restrict image domains** back off. You'll need to first disable custom visualizations. + +### Turn on custom visualizations + +To turn on custom visualizations, go to **Admin** > **Settings** > **Custom visualizations** and click **Enable custom visualizations**. + +You can also enable (or disable) custom visualizations with the [`MB_CUSTOM_VIZ_ENABLED`](../../configuring-metabase/environment-variables#mb_custom_viz_enabled) environment variable, or with the `custom-viz-enabled` key in a [configuration file](../../configuring-metabase/config-file). + +## Adding a custom visualization + +Once you've [built the custom visualization](../../developers-guide/custom-visualizations): + +1. In Metabase, go to **Admin** > **Settings** > **Custom visualizations** > **Manage visualizations**. +2. Click **Add** and drag the `.tgz` file into the upload area (or click to browse for it). +3. Click **Add visualization**. + +- Bundles must be smaller than 5 MiB. +- Each plugin lists the Metabase versions it supports (for example, "Requires Metabase >=1.62"). If your Metabase version isn't in that range, Metabase rejects the upload and tells you which version the plugin needs. +- The **Manage visualizations** page shows each plugin's icon, name, the first eight characters of the bundle's hash, and its required Metabase version range, so you can tell which version is installed. + +## Using a custom visualization + +On a question, dashboard or document card, open the visualization sidebar (the **Visualization** button), and look for the **Custom visualizations** section. Pick your visualization the same way you'd pick a line chart or a table, and voilà, there's that gondola line chart you needed: + +![Gondola line chart](../images/gondola-line-chart.png) + +If a custom visualization can't render the current query results (for example, if the query is missing a column the visualization needs), Metabase shows the error message from the plugin so you can adjust the query or pick a different chart. + +Custom visualizations behave like built-in charts in most places: + +- **Settings.** Click the **gear** icon in the visualization sidebar to change the visualization's settings. A plugin defines its own setting tabs: each setting names the section it belongs to. +- **Dark mode.** Plugins that use Metabase's colors adapt to [dark mode](../../people-and-groups/account-settings#theme) automatically. +- **Icons.** A custom visualization shows its own icon in the visualization picker, and questions that use it show that icon in collections and bookmarks. + +## Managing custom visualizations + +_Admin > Settings > Custom visualizations > Manage visualizations_ + +- **Disable a visualization.** Any question, dashboard card, or document card that used the visualization falls back to the default visualization for that query's results. If you re-enable the plugin, those cards will go back to using the custom visualization. +- **Replace a bundle.** Upload a new `.tgz` to ship an updated version of a plugin. The new bundle's manifest `name` _must_ match the existing plugin's identifier, so questions that already use the visualization keep working. +- **Remove a visualization.** Cards that used the custom viz fall back to the default visualization. + +## Exports + +- **Dashboard subscriptions and alerts don't use custom visualizations**. Cards that use custom visualizations will fall back to a default visualization for the card's data shape. +- **PDF exports of dashboards include custom visualizations**. +- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that plugin. PNG export is off by default. + +## Only add plugins you trust + +A custom visualization plugin runs JavaScript in your Metabase. Only upload plugins from sources you trust (like plugins you've built yourself, or have vetted). + +Metabase runs custom visualizations in a sandbox to limit what a plugin can do: + +- A plugin renders inside an isolated container and can't reach the rest of the Metabase app. +- A plugin can't call Metabase's APIs or make network requests. + +While this sandboxing limits the damage a plugin can do, you still need to review the code. + +## Further reading + +- [Building custom visualizations](../../developers-guide/custom-visualizations) +- [Visualization overview](./visualizing-results) +- [Appearance](../../configuring-metabase/appearance) diff --git a/_docs/v0.62/questions/visualizations/visualizing-results.md b/_docs/v0.62/questions/visualizations/visualizing-results.md index b6e2fb8cdd..2029fb19a8 100644 --- a/_docs/v0.62/questions/visualizations/visualizing-results.md +++ b/_docs/v0.62/questions/visualizations/visualizing-results.md @@ -175,6 +175,10 @@ If you have a bar chart like Count of Users by Age, where the x-axis is a number ![Scatter](../images/scatter.png) +## Custom visualizations + +On [Pro and Enterprise plans](/pricing/), admins can add [custom visualizations](./custom): chart types you build with the Custom Visualizations SDK and upload to Metabase. Once a custom visualization is installed, it shows up in the visualization picker alongside the built-in charts. + ## Styling and formatting data in charts ![Chart formatting options](../images/chart-formatting-options.png) @@ -188,5 +192,6 @@ See also [Formatting defaults](../../data-modeling/formatting). ## Further reading - [Charts with multiple series](../../dashboards/multiple-series) +- [Custom visualizations](./custom) - [Appearance](../../configuring-metabase/appearance) - [BI dashboard best practices](/learn/metabase-basics/querying-and-dashboards/dashboards/bi-dashboard-best-practices) diff --git a/_site/docs/v0.62/configuring-metabase/fonts.html b/_site/docs/v0.62/configuring-metabase/fonts.html index ef1211643e..d8f22707de 100644 --- a/_site/docs/v0.62/configuring-metabase/fonts.html +++ b/_site/docs/v0.62/configuring-metabase/fonts.html @@ -6361,6 +6361,12 @@

Hosting fonts on GitHub

Note that in the raw link, there is no /blob/ directory in the URL.

+

Custom fonts and Content Security Policy

+ +

When you add a custom font hosted on another domain, Metabase automatically allows that domain in the browser’s Content Security Policy for fonts. You don’t need to configure anything for the font to work.

+ +

If you don’t add any custom fonts, Metabase only allows fonts served from your own instance.

+

Supporting multiple languages

To support multiple character sets, for example both Latin and Cyrillic, you’ll need to merge font files.

diff --git a/_site/docs/v0.62/configuring-metabase/settings.html b/_site/docs/v0.62/configuring-metabase/settings.html index 19f75623af..c3f642b223 100644 --- a/_site/docs/v0.62/configuring-metabase/settings.html +++ b/_site/docs/v0.62/configuring-metabase/settings.html @@ -6324,9 +6324,11 @@

Restrict image domains

By default, images from any domain are allowed.

+

You must turn on this setting to enable Custom visualizations. While custom visualizations are enabled, you can’t turn it back off.

+

Allowed domains for images

-

When the Restrict image domains setting is on, Metabase will only allow images served from this Metabase instance, and any domains listed here.

+

When the Restrict image domains setting is on, Metabase will only allow images served from this Metabase instance, and any domains listed on this page.

Leave this input empty to only allow images hosted by your Metabase instance.

diff --git a/_site/docs/v0.62/configuring-metabase/start.html b/_site/docs/v0.62/configuring-metabase/start.html index 5a26b0f0f7..2e1b54574e 100644 --- a/_site/docs/v0.62/configuring-metabase/start.html +++ b/_site/docs/v0.62/configuring-metabase/start.html @@ -6268,6 +6268,10 @@

Custom maps

Upload custom maps to your Metabase.

+

Custom visualizations

+ +

Add your own chart types by uploading visualization plugins.

+

Customizing the Metabase Jetty webserver

Set SSL and port settings for the Jetty webserver.

diff --git a/_site/docs/v0.62/developers-guide/custom-visualizations.html b/_site/docs/v0.62/developers-guide/custom-visualizations.html new file mode 100644 index 0000000000..efaf153fbc --- /dev/null +++ b/_site/docs/v0.62/developers-guide/custom-visualizations.html @@ -0,0 +1,7272 @@ + + + + + + + + + + + + + + + + + + + + + + +Building custom visualizations | Metabase Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ + + + +
+
+ + + +
+
+
+ +
+ + + + + + + + + +
+ + + +
+ + +
+ +
+ +
+ + + +
+ + + v0.62 + + + + + + + + +
+ + + + + What’s new + + + + + +
+ + + + +
+
+ + +
+ + +
+ + +
+ These are the docs for Metabase v0.62. + Check out the docs for the current stable version, Metabase v0.61. +
+ + +
+ + +

Building custom visualizations

+ +
+
+ + + + + + + + + + + +

+ Custom visualizations is only available on + Pro and + Enterprise + plans (both self-hosted and on Metabase Cloud). +

+
+
+ +

You can create a custom chart type for Metabase that you build with React and TypeScript and ship as a plugin.

+ +

You scaffold a project with the @metabase/custom-viz package, write your visualization, and package it into a .tgz bundle. An admin uploads the plugin to Metabase (see Custom visualizations), and you’re in business.

+ +

Overview of a custom visualization

+ +

A custom visualization is a small React app that Metabase renders in place of a built-in chart.

+ +

Building a custom viz from scaffolding to adding it to your Metabase looks something like:

+ +
    +
  1. Scaffold a project with the @metabase/custom-viz CLI. The command sets up the build, the manifest, and a working starter visualization.
  2. +
  3. Develop against a locally running Metabase with hot reload while you write your component and settings.
  4. +
  5. Handle the data: read query results from series, wire up clicks and tooltips, and add any settings your chart needs.
  6. +
  7. Match the look with Metabase’s formatters, theme variables, and color scheme.
  8. +
  9. Build and package the project into a .tgz bundle.
  10. +
  11. Add it to your Metabase: an admin uploads the bundle, and your chart type becomes available in your Metabase.
  12. +
+ +

Prerequisites

+ +
    +
  • Node.js 22 or newer.
  • +
  • Familiarity with React and TypeScript.
  • +
  • A Metabase on a Pro or Enterprise plan to load your plugin into.
  • +
+ +

Scaffold a custom visualization project

+ +

Generate a new project with the @metabase/custom-viz CLI:

+ +
npx @metabase/custom-viz init my-viz
+
+ +

Then install dependencies and start the dev server:

+ +
cd my-viz
+npm install
+npm run dev
+
+ +

npm run dev runs in watch mode and rebuilds your plugin on every change.

+ +

Project structure

+ +
src/
+  index.tsx             # Your visualization code — start here
+metabase-plugin.json    # Plugin manifest (name, icon, version)
+public/
+  assets/
+    icon.svg            # Visualization icon (shown in the chart type picker)
+package.json
+vite.config.ts          # Build configuration — don't edit
+pack.mjs                # Packages the build into a .tgz — don't edit
+tsconfig.json
+
+ +

Only index.tsx has to export the factory. For a more sophisticated plugin, you’d want to split the component, settings, types, and helpers into their own modules (check out the calendar-heatmap example, which keeps the definition in index.tsx, the React component in Visualization.tsx, and chart configuration and utilities under src/).

+ +

The starter visualization

+ +

The scaffold ships a complete, working example: a chart that shows a thumbs-up emoji (👍) when a single numeric result meets a threshold setting, and a thumbs-down (👎) otherwise.

+ +

Develop against a running Metabase

+ +

To develop your plugin against a live Metabase with hot reload:

+ +
    +
  1. Start Metabase with the MB_CUSTOM_VIZ_PLUGIN_DEV_MODE_ENABLED environment variable set to true. Dev mode is meant for local development, so you can only turn it on with this environment variable. Like any Metabase that runs custom visualizations, this local instance needs a Pro or Enterprise token.
  2. +
  3. Run npm run dev in your project. By default, the dev server listens on http://localhost:5174.
  4. +
  5. In Metabase, go to Admin > Settings > Custom visualizations > Development and set the Dev server URL to your dev server’s address.
  6. +
+ +

Your plugin shows up in the Custom visualizations section of the visualization sidebar (alongside any installed plugins) and is labeled as a dev visualization.

+ +

If you’re running Metabase in a Docker container, you’ll need to set the Dev server URL to:

+ +
http://host.docker.internal:5174
+
+ +

The plugin manifest

+ +

Every plugin includes a metabase-plugin.json file at the root of the project:

+ +
{
+  "name": "my-viz",
+  "icon": "icon.svg",
+  "metabase": {
+    "version": ">=1.62.0"
+  }
+}
+
+ + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
nameUnique identifier for the plugin. Metabase registers your visualization under this name and uses it to match replacement bundles.
iconPath to the visualization icon (SVG recommended). Metabase serves the icon automatically. It’s the only file Metabase serves alongside your bundle. See Bundling assets.
metabase.versionSemver range of Metabase versions the plugin supports (for example, ">=1.62.0", "^1.62", ">=1.62 <1.64").
+ +

Defining a visualization

+ +

src/index.tsx exports a factory function. Metabase calls the function with two helpers: defineSetting (for declaring settings) and the current locale. The factory function should return the result of defineConfig, which wraps your VisualizationComponent.

+ +
import {
+  defineConfig,
+  type CreateCustomVisualization,
+  type CustomVisualizationProps,
+} from "@metabase/custom-viz";
+
+type Settings = {
+  threshold?: number;
+};
+
+const createVisualization: CreateCustomVisualization<Settings> = ({
+  defineSetting,
+  locale,
+}) => {
+  const VisualizationComponent = ({
+    series,
+    settings,
+    width,
+    height,
+  }: CustomVisualizationProps<Settings>) => {
+    // Render your visualization with React
+    return <div>{/* ... */}</div>;
+  };
+
+  return defineConfig<Settings>({
+    id: "my-viz",
+    getName: () => "My visualization",
+    minSize: { width: 2, height: 2 },
+    defaultSize: { width: 6, height: 4 },
+    checkRenderable(series, settings) {
+      // Throw if the visualization can't render with this data or these settings
+      if (series.length === 0) {
+        throw new Error("No data");
+      }
+    },
+    settings: {
+      threshold: defineSetting({
+        id: "threshold",
+        title: "Threshold",
+        widget: "number",
+      }),
+    },
+    VisualizationComponent,
+  });
+};
+
+export default createVisualization;
+
+ +

Visualization definition properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
idstringIdentifier for the visualization definition.
getName()() => stringDisplay name for the visualization.
minSize{ width, height }Minimum size on a dashboard grid.
defaultSize{ width, height }Default size on a dashboard grid.
noHeaderbooleanWhen true, hides the default card title and description header.
canSavePngbooleanSet to true to enable PNG export of the live, interactive chart. Disabled by default.
checkRenderable(series, settings) => voidLet people know the chart doesn’t work with the current data or settings.
settingsRecord<string, SettingDefinition>Map of setting definitions created with defineSetting().
VisualizationComponentReact.ComponentTypeThe interactive React component that renders the visualization in questions and dashboards.
+ +

Props passed to your component

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropTypeDescription
seriesSeriesQuery results — an array of series; each has data.rows and data.cols.
settingsCustomVisualizationSettings<TSettings>The resolved visualization settings.
widthnumber \| nullContainer width in pixels. null until the first measure — render null to avoid a flash.
heightnumber \| nullContainer height in pixels. null until the first measure.
colorScheme"light" \| "dark"Metabase’s current color scheme.
onClick(clickObject) => voidCall to trigger drill-through actions on a data point.
onHover(hoverObject?) => voidCall to show a tooltip on a data point.
+ +

Handling query results

+ +

series is an array of result sets, with one entry per series on the chart. A single question produces one entry; a dashboard card with multiple series produces several entries. Each entry has a data object:

+ +
    +
  • data.rows: an array of rows; each row is an array of cell values in column order. Row order is preserved, so when you map rows to chart points one-to-one, a point’s index maps straight back to data.rows[i]. Useful for grabbing the whole row, not just the clicked cell.
  • +
  • data.cols: an array of column objects describing each value. The fields you’ll reach for most: name (database column name), display_name (label shown in the UI), base_type (Metabase type, for example "type/Integer"), and semantic_type (for example "type/Currency" or "type/Latitude").
  • +
+ +
const [{ data }] = series;
+const total = data.rows.reduce((sum, [value]) => sum + Number(value), 0);
+
+ +

To classify a column without matching type strings by hand, use the column-type predicates the SDK exports: isNumeric, isDate, isString, isBoolean, isCurrency, isLatitude, isCoordinate, isFK, isPK, isCategory, isURL. These predicates take a Column and resolve type metadata from the host, so they only work inside a running Metabase. See Formatting and theming.

+ +
import { isNumeric } from "@metabase/custom-viz";
+
+const numericColumns = data.cols.filter(isNumeric);
+
+ +

Clicks and tooltips

+ +

Your component receives onClick and onHover. Call them with an object that identifies the data point being interacted with. Metabase positions popovers from it, and for clicks it offers the matching drill-through actions (filter by this value, view these rows, and so on).

+ +
<rect
+  onClick={(event) =>
+    onClick({
+      value: row[1],
+      column: cols[1],
+      dimensions: [{ value: row[0], column: cols[0] }],
+      event: event.nativeEvent,
+      element: event.currentTarget,
+    })
+  }
+  onMouseMove={(event) =>
+    onHover({
+      element: event.currentTarget,
+      data: cols.map((col, i) => ({
+        col,
+        value: row[i],
+        key: col.display_name,
+      })),
+    })
+  }
+  onMouseLeave={() => onHover(null)}
+/>
+
+ +

Pass null to onHover to dismiss the tooltip. onClick also takes an origin: { row, cols } when a drill-through needs the whole row, not just the clicked cell. It can take a data array of { col, value } pairs (one per column) when an action needs every column’s value. You can include settings (the current resolved settings) in the click object too, so dashboard click behaviors configured against your visualization have what they need.

+ +

The hover object accepts more than element and data. Optional fields like index and seriesIndex (to highlight a series in the legend) and value, column, dimensions, and event (for a simpler single-point tooltip) are available when you need them.

+ +

Settings and widgets

+ +

Define settings with the defineSetting() helper. Each setting shows up in the visualization settings sidebar.

+ +
settings: {
+  threshold: defineSetting({
+    id: "threshold",
+    title: "Threshold",
+    getSection: () => "Display",
+    widget: "number",
+    getDefault: () => 0,
+    getProps: () => ({
+      placeholder: "Enter threshold",
+      options: { isNonNegative: true },
+    }),
+  }),
+},
+
+ +

Setting definition properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
idUnique key — has to match the key in your Settings type.
titleLabel shown in the sidebar.
getSection()Function returning the section the setting appears under (for example, "Data" or "Display").
groupSub-heading within a section for grouping related settings.
indexDisplay order within a group.
inlineWhen true, renders the widget on the same line as title (handy for "toggle").
widgetA built-in widget name, or a custom React component.
getDefault(series, settings)Computes the default value when none is stored.
getValue(series, settings)Always-computed value — overrides the stored value on every render.
getProps(series, settings)Returns widget-specific props.
isValid(series, settings)Return false to discard a stored value and fall back to getDefault.
readDependenciesSetting IDs that have to resolve before this one.
writeDependenciesSetting IDs whose current values are persisted when this setting changes.
eraseDependenciesSetting IDs reset to null when this setting changes.
persistDefaultWhen true, writes the value from getDefault to stored settings on first render.
+ +

Built-in widgets

+ +

Widgets for the settings UI.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WidgetgetProps() return typeDescription
"input"{ placeholder? }Text input
"number"{ placeholder?, options?: { isInteger?, isNonNegative? } }Numeric input
"toggle"(none — omit getProps)Boolean toggle
"radio"{ options: { name, value }[] }Radio button group
"select"{ options: { name, value }[], placeholder?, placeholderNoOptions? }Dropdown
"segmentedControl"{ options: { name, value }[] }Segmented button control
"color"{ title? }Color picker
"multiselect"{ options: { label, value }[], placeholder?, placeholderNoOptions? }Multi-select dropdown
"field"{ columns, options: { name, value }[], showColumnSetting? }Single column picker
"fields"{ columns, options: { name, value }[], addAnother?, showColumnSetting? }Multi-column picker
+ +

Custom widgets

+ +

When the built-in widgets don’t fit, set widget to your own React component instead of a built-in name. Metabase renders the component in the settings sidebar, inside the same sandbox as your visualization. A widget that reaches for a blocked API is removed, so keep widgets to plain inputs and display.

+ +

Metabase injects these props into your widget component (import the type with BaseWidgetProps<TValue, TSettings>):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropTypeDescription
idstringThe setting’s id.
valueTValue \| undefinedThe setting’s current value.
onChange(value?) => voidUpdate this setting’s value.
onChangeSettings(settings) => voidUpdate other settings at the same time.
+ +

Add any extra props your component needs with getProps(). Its return type is your component’s own props, minus the base props Metabase injects.

+ +
import { defineConfig, type BaseWidgetProps } from "@metabase/custom-viz";
+
+type Settings = { label?: string };
+
+function LabelWidget({ value, onChange }: BaseWidgetProps<string, Settings>) {
+  return (
+    <input value={value ?? ""} onChange={(e) => onChange(e.target.value)} />
+  );
+}
+
+// ...in your visualization's settings:
+settings: {
+  label: defineSetting({
+    id: "label",
+    title: "Label",
+    widget: LabelWidget,
+  }),
+},
+
+ +

Formatting and theming

+ +

Render numbers, dates, and currencies the way the rest of Metabase does with formatValue. Pass the cell’s column to pick up that column’s formatting settings, or override with options like currency, decimals, compact, or date_style:

+ +
import { formatValue } from "@metabase/custom-viz";
+
+formatValue(row[1], { column: cols[1] });
+formatValue(0.084, { number_style: "percent", decimals: 1 }); // "8.4%"
+
+ +

formatValue and the column-type predicates (like isNumeric and isDate) read formatting and type metadata from Metabase. If you call them outside of Metabase, like in a unit test, they’ll throw Metabase Viz API not initialized.

+ +

For layout math (like fitting labels or sizing axes), measureText(text, { size, family, weight }) returns { width, height } in pixels. There’s also measureTextWidth and measureTextHeight if you only need one dimension.

+ +

To match Metabase’s look (and follow dark mode), you have two paths. For anything you render as DOM or SVG, you can style with Metabase’s CSS variables: var(--mb-color-brand) and the other --mb-color-* variables, and the theme follows automatically.

+ +

Canvas-based charting libraries (like ECharts and Chart.js) can’t read CSS variables, so in those cases you branch on the colorScheme prop ("light" or "dark") and pass explicit colors. See the calendar-heatmap example for one built with ECharts.

+ +

Bundling assets

+ +

The build produces a single JavaScript bundle (dist/index.js), and the icon is the only file Metabase serves alongside it. Metabase doesn’t serve arbitrary static files, so bundling images into your plugin is the most reliable approach. The sandbox blocks scripted network access like fetch and XMLHttpRequest, but it doesn’t stop the browser from loading an <img> or CSS url(): an external image still loads as long as its domain is allowed by the image-domains Content Security Policy (see below).

+ +

Bundled images always render, including when an admin has turned on Restrict image domains. That Content Security Policy setting limits which external hosts images can load from, but inline and data: images ship inside your bundle, so they’re never blocked.

+ +

Your npm dependencies are bundled in too. You can pull in a charting library (the calendar-heatmap example bundles ECharts), but everything ships in that single dist/index.js, so your code and its dependencies all count toward the packaged plugin’s size limits.

+ +

So anything your visualization renders has to live inside that bundle. For images, you have a few options:

+ +
    +
  • Inline SVG or emoji. What the starter visualization does (it renders 👍 / 👎). Drop the SVG markup straight into your JSX.
  • +
  • Import the image. Import an image from src/ and the bundler inlines small files as a base64 data URL. Vite inlines assets below its assetsInlineLimit (4 KB by default); larger files are emitted as separate assets that won’t ship in the single bundle, so keep imported images small or raise the limit.
  • +
  • Embed a data URL directly. Paste a data:image/png;base64,... string into your component’s src.
  • +
+ +
import logo from "./logo.svg"; // inlined as a data URL at build time
+
+const VisualizationComponent = () => <img src={logo} alt="" />;
+
+ +

The visualization icon

+ +

The icon shows up in the chart type picker and elsewhere in the Metabase UI.

+ +
    +
  • Declare it with "icon" in metabase-plugin.json. The default location is public/assets/icon.svg.
  • +
  • Use currentColor for fills and strokes so the icon adapts to light and dark themes, as well as to hover and active states (like when it’s highlighted in a menu):
  • +
+ +
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="..." stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+</svg>
+
+ +
    +
  • For more control, you can use Metabase’s CSS variables inside an inline SVG, like fill="var(--mb-color-brand)".
  • +
  • Keep the icon simple and monochromatic. Skip gradients and multiple colors.
  • +
+ +

Build and package the plugin

+ +

Run:

+ +
npm run build
+
+ +

This compiles src/ to dist/ and packages the result into <name>-<version>.tgz at the project root. The archive contains metabase-plugin.json, dist/index.js, and the whitelisted icon under dist/assets/, and has to come in under 5 MiB. The packaging step also rejects an archive whose uncompressed contents exceed 25 MiB. You don’t need to commit dist/.

+ +

For uploading and managing plugins, see Custom visualizations.

+ +

Versioning and compatibility

+ +

The Custom Visualizations SDK works with Metabase 1.62 and newer. Declare the versions your plugin supports with metabase.version in metabase-plugin.json, using npm semver range syntax — ">=1.62.0", "^1.62", ">=1.62 <1.64". Write the range against the full version number (">=1.62.0"), not a bare major version (">=62"), which won’t match.

+ +

If you upload a bundle to a Metabase outside the plugin’s declared range, Metabase rejects the upload.

+ +

Sandbox restrictions

+ +

Metabase runs plugin code in an isolated sandbox, so a visualization works only from the series and settings it’s given. The sandbox blocks:

+ +
    +
  • Network access: fetch, XMLHttpRequest, WebSocket, EventSource, Worker, SharedWorker, RTCPeerConnection, WebTransport, BroadcastChannel, navigator.sendBeacon, and FontFace.load. You can’t call Metabase’s APIs or any other service.
  • +
  • Browser storage and cookies: localStorage, sessionStorage, indexedDB, the Cache API, document.cookie, and the CookieStore API.
  • +
  • Device and credential APIs: clipboard, geolocation, camera and microphone, service workers, the Credentials and Permissions APIs, USB, Bluetooth, HID, serial, WebXR, and Web Share.
  • +
  • Browser UI: window.open, dialogs (alert, confirm, prompt, print), notifications, modal dialogs, fullscreen, and payment requests.
  • +
  • Navigation and the rest of the app: history changes, the host page’s URL and referrer, and any DOM outside the plugin’s own container.
  • +
  • Unsafe DOM and timing APIs: document.write, execCommand, constructable stylesheets, raw HTML parsers (DOMParser, setHTMLUnsafe, XSLTProcessor), and resource-timing APIs that expose other requests the page has made.
  • +
+ +

Custom visualizations only render in the live app

+ +

Custom visualizations only render in the live, interactive app. Static renders, like dashboard subscriptions sent by email, Slack, or webhook, fall back to a table for any card that uses a custom visualization. The same goes for embedded questions and dashboards: a card that uses a custom visualization falls back to a table.

+ +

Example plugins

+ +
    +
  • Calendar heatmap. Read through src/ for an example of checkRenderable, settings, and rendering against series data.
  • +
  • Thumbs. Thumbs up or down depending on a threshold.
  • +
+ +

Further reading

+ + + + +
+
+ +

+ Read docs for other versions of Metabase. +

+ +
+
+ +
+ +
+
+
Was this helpful?
+ + +
+ +
+

+ +
+ + + +
+
+ +
+
Thanks for your feedback!
+
+ + + + Want to improve these docs? Propose a change. + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_site/docs/v0.62/developers-guide/images/custom-viz-dev.png b/_site/docs/v0.62/developers-guide/images/custom-viz-dev.png new file mode 100644 index 0000000000..85d1e9f3ae Binary files /dev/null and b/_site/docs/v0.62/developers-guide/images/custom-viz-dev.png differ diff --git a/_site/docs/v0.62/developers-guide/start.html b/_site/docs/v0.62/developers-guide/start.html index a34878a8d7..f2315ce887 100644 --- a/_site/docs/v0.62/developers-guide/start.html +++ b/_site/docs/v0.62/developers-guide/start.html @@ -6261,6 +6261,12 @@

Database drivers

  • Guide to writing a driver
  • +

    Customizing Metabase

    + + +

    Metabase documentation