@databrainhq/mcp-server 0.3.0-rc.4 → 0.3.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +56 -1
  2. package/dist/apps/server.d.ts +6 -6
  3. package/dist/apps/server.js +3 -2
  4. package/dist/apps/stdio.js +4 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/{chunk-CX3CS62P.js → chunk-3GF7WSKK.js} +4 -2
  7. package/dist/chunk-3GF7WSKK.js.map +1 -0
  8. package/dist/{chunk-NZGQJBJV.js → chunk-4DYPD273.js} +8 -6
  9. package/dist/{chunk-NZGQJBJV.js.map → chunk-4DYPD273.js.map} +1 -1
  10. package/dist/{chunk-I35U2LBX.js → chunk-5IQ7MVUH.js} +953 -1118
  11. package/dist/chunk-5IQ7MVUH.js.map +1 -0
  12. package/dist/chunk-PRE4CGOV.js +1903 -0
  13. package/dist/chunk-PRE4CGOV.js.map +1 -0
  14. package/dist/{chunk-3JFMWXRO.js → chunk-V64ZQINV.js} +2 -2
  15. package/dist/cli.d.ts +15 -0
  16. package/dist/cli.js +495 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/custom-chart/runtime-shim.js +146 -0
  19. package/dist/custom-chart/template/AGENTS.md +374 -0
  20. package/dist/custom-chart/template/README.md +374 -0
  21. package/dist/custom-chart/template/chart.css +28 -0
  22. package/dist/custom-chart/template/chart.ts +192 -0
  23. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  24. package/dist/custom-chart/template/icon.svg +1 -0
  25. package/dist/custom-chart/template/manifest.json +23 -0
  26. package/dist/custom-chart/template/package.json +14 -0
  27. package/dist/custom-chart/template/sample-data.json +18 -0
  28. package/dist/custom-chart/template/tsconfig.json +12 -0
  29. package/dist/index.js +11 -7
  30. package/dist/index.js.map +1 -1
  31. package/dist/{main-QHUNQWID.js → main-Q6XT4O46.js} +34 -8
  32. package/dist/main-Q6XT4O46.js.map +1 -0
  33. package/dist/{test-only-B4EVUNFU.js → test-only-TJ3DQUYW.js} +2 -2
  34. package/package.json +6 -3
  35. package/dist/chunk-CX3CS62P.js.map +0 -1
  36. package/dist/chunk-I35U2LBX.js.map +0 -1
  37. package/dist/main-QHUNQWID.js.map +0 -1
  38. /package/dist/{chunk-3JFMWXRO.js.map → chunk-V64ZQINV.js.map} +0 -0
  39. /package/dist/{test-only-B4EVUNFU.js.map → test-only-TJ3DQUYW.js.map} +0 -0
@@ -0,0 +1,146 @@
1
+ /*
2
+ * Databrain custom chart runtime shim (DAT-925).
3
+ *
4
+ * The CLI (`databrain custom-chart build`) prepends this file, byte for byte,
5
+ * to the author's bundled IIFE. It is plain ES2020 with no imports so it runs
6
+ * inside the sandbox frame exactly as written. The plugin keeps a copy at
7
+ * `packages/@databrainhq/plugin/src/customChart/runtimeShim.js` and tests it;
8
+ * `contract/runtime-shim.js` in the fleet folder is the same bytes.
9
+ *
10
+ * Contract with the author bundle (see the authoring guide):
11
+ * - The bundle assigns `globalThis.__dbnCustomChart = { render, resize? }`.
12
+ * - The CLI may prepend `globalThis.__dbnCustomChartSlots = [...slot ids]`
13
+ * so every manifest slot is present on `ctx.slots`, unbound ones as `[]`.
14
+ * - `render(ctx)` runs on the first data message and on every data change.
15
+ * - `resize(ctx)` runs on window resize when the author defines it;
16
+ * otherwise `render(ctx)` runs again (one call per animation frame).
17
+ * - Inside the frame, `dbnChart.click(rowIndex, columnId)`,
18
+ * `dbnChart.filter(columnId, values)` and `dbnChart.resize(height)` map
19
+ * onto the sandbox protocol. There is no other host access.
20
+ * - A throw (or a rejected promise) inside render/resize is reported to the
21
+ * host once as `error/runtime`; the chart stops receiving data after it.
22
+ */
23
+ (() => {
24
+ const sandbox = globalThis.dbnSandbox;
25
+ if (!sandbox || typeof sandbox.onData !== 'function') return;
26
+
27
+ const DEFAULT_THEME = Object.freeze({
28
+ fontFamily: 'system-ui, sans-serif',
29
+ fontSize: 12,
30
+ textColor: 'CanvasText',
31
+ textSecondaryColor: 'GrayText',
32
+ background: 'Canvas',
33
+ surfaceColor: 'Canvas',
34
+ borderColor: 'GrayText',
35
+ borderRadius: 8,
36
+ primaryColor: 'Highlight',
37
+ palette: [],
38
+ });
39
+
40
+ Object.defineProperty(globalThis, 'dbnChart', {
41
+ value: Object.freeze({
42
+ click(rowIndex, columnId) {
43
+ sandbox.click(rowIndex, columnId);
44
+ },
45
+ filter(columnId, values) {
46
+ sandbox.filter(columnId, Array.isArray(values) ? values : []);
47
+ },
48
+ resize(height) {
49
+ sandbox.resize(height);
50
+ },
51
+ }),
52
+ writable: false,
53
+ configurable: false,
54
+ });
55
+
56
+ let failed = false;
57
+ let latest = null;
58
+ let frame = 0;
59
+
60
+ const report = (error) => {
61
+ if (failed) return;
62
+ failed = true;
63
+ latest = null;
64
+ try {
65
+ console.error('[dbn custom chart] render failed:', error);
66
+ } catch {}
67
+ const message =
68
+ error && typeof error.message === 'string'
69
+ ? error.message
70
+ : String(error);
71
+ // The sandbox bootstrap listens for window "error" and reports it to the
72
+ // host as error/runtime. One event, then the chart is inert.
73
+ globalThis.dispatchEvent(new ErrorEvent('error', { error, message }));
74
+ };
75
+
76
+ const chart = () => {
77
+ const value = globalThis.__dbnCustomChart;
78
+ if (!value || typeof value.render !== 'function') {
79
+ throw new TypeError(
80
+ 'The chart bundle did not assign globalThis.__dbnCustomChart = { render }.'
81
+ );
82
+ }
83
+ return value;
84
+ };
85
+
86
+ const slotsOf = (columns) => {
87
+ const slots = {};
88
+ const declared = globalThis.__dbnCustomChartSlots;
89
+ if (Array.isArray(declared)) {
90
+ for (const id of declared) if (typeof id === 'string') slots[id] = [];
91
+ }
92
+ columns.forEach((column, index) => {
93
+ const id = column?.slotId;
94
+ if (typeof id !== 'string') return;
95
+ if (!Array.isArray(slots[id])) slots[id] = [];
96
+ slots[id].push(index);
97
+ });
98
+ return slots;
99
+ };
100
+
101
+ const context = (data) => ({
102
+ container: document.getElementById('chart'),
103
+ data: { columns: data.columns, rows: data.rows },
104
+ slots: slotsOf(data.columns),
105
+ options: data.options,
106
+ theme: data.theme || DEFAULT_THEME,
107
+ language: typeof data.language === 'string' ? data.language : 'en',
108
+ dimensions: {
109
+ width: globalThis.innerWidth,
110
+ height: globalThis.innerHeight,
111
+ },
112
+ });
113
+
114
+ const run = (method, data) => {
115
+ if (failed) return;
116
+ let result;
117
+ try {
118
+ const value = chart();
119
+ const fn =
120
+ method === 'resize' && typeof value.resize === 'function'
121
+ ? value.resize
122
+ : value.render;
123
+ result = fn.call(value, context(data));
124
+ } catch (error) {
125
+ report(error);
126
+ return;
127
+ }
128
+ if (result && typeof result.then === 'function') {
129
+ result.then(undefined, report);
130
+ }
131
+ };
132
+
133
+ sandbox.onData((data) => {
134
+ latest = data;
135
+ run('render', data);
136
+ });
137
+
138
+ globalThis.addEventListener('resize', () => {
139
+ if (!latest || failed) return;
140
+ if (frame) return;
141
+ frame = globalThis.requestAnimationFrame(() => {
142
+ frame = 0;
143
+ if (latest) run('resize', latest);
144
+ });
145
+ });
146
+ })();
@@ -0,0 +1,374 @@
1
+ # Databrain custom chart: author guide
2
+
3
+ This folder is one custom chart type for Databrain. When it is uploaded and
4
+ published, every builder in your company can pick it in the metric editor as
5
+ chart type `custom:<key>` and put it on dashboards. The same guide is shipped
6
+ as `AGENTS.md` (for coding agents) and `README.md` (for people); the two files
7
+ are identical, and the `custom-chart` skill in `@databrainhq/agent-skills`
8
+ carries a byte identical copy as `references/authoring-guide.md`.
9
+
10
+ Read this file to the end before you change `chart.ts`.
11
+
12
+ ## 1. What a custom chart is
13
+
14
+ A custom chart is a small package: a manifest that declares the data the chart
15
+ needs and the settings it exposes, a TypeScript module that draws into a DOM
16
+ element, an optional stylesheet and an icon. The CLI compiles the module into
17
+ one self contained `dist/index.js`. Databrain stores that file, serves it to
18
+ dashboards, resolves the metric's data onto the slots you declared, and runs
19
+ the bundle inside a sandboxed frame. The chart never sees a token, a query or
20
+ a network. It only sees rows, options, a theme and a container.
21
+
22
+ The lifecycle:
23
+
24
+ 1. `databrain custom-chart init my-chart` writes this folder.
25
+ 2. You edit `manifest.json` and `chart.ts`.
26
+ 3. `databrain custom-chart validate` checks the manifest and the icon.
27
+ 4. `databrain custom-chart build` writes `dist/`.
28
+ 5. `databrain custom-chart upload` creates the chart in your company as a
29
+ private chart, version 1. A second upload creates version 2 in preview
30
+ while live dashboards keep the published version.
31
+ 6. Publish (with `--publish`, the `publish_custom_chart` tool, or Settings >
32
+ Developers > Custom charts) makes a version live for every builder.
33
+
34
+ ## 2. Project structure
35
+
36
+ ```
37
+ my-chart/
38
+ manifest.json the contract: id, name, slots, options
39
+ chart.ts export function render(ctx), optional resize(ctx)
40
+ chart.css optional, uploaded next to the bundle; the host injects it into the frame
41
+ custom-chart.d.ts the CustomChartContext types (do not edit)
42
+ icon.svg required, at most 16 KiB, no script or external references
43
+ sample-data.json optional, used by `dev` and by the in app preview
44
+ package.json scripts: validate, build, dev, upload
45
+ tsconfig.json editor settings so chart.ts type checks
46
+ AGENTS.md this guide
47
+ README.md this guide
48
+ dist/ written by build, never edit by hand
49
+ index.js the shim plus your compiled module (upload this)
50
+ manifest.json the validated manifest
51
+ icon.svg the sanitized icon
52
+ index.css the raw chart.css, uploaded as bundleCss
53
+ bundle.zip the four files above, for drag and drop upload in the app
54
+ ```
55
+
56
+ Only `chart.ts` may import other files. You can add more `.ts` files next to
57
+ it and import them with a relative path. You can also `npm install` a browser
58
+ library and import it; the build bundles it. There is no `external`: whatever
59
+ you import ends up inside `index.js`, and the whole file must stay under 2 MiB.
60
+
61
+ ## 3. The manifest
62
+
63
+ `manifest.json` is checked with the same rules in the CLI, in the MCP tools, on
64
+ the server and in the dashboard runtime. Unknown keys are refused at every
65
+ level. Every error carries a code shaped `manifest.<path>.<reason>`, a JSON
66
+ path and a sentence that says what to change.
67
+
68
+ ```json
69
+ {
70
+ "schemaVersion": 2,
71
+ "id": "revenue-bars",
72
+ "name": "Revenue bars",
73
+ "version": "1.2.0",
74
+ "description": "Bars with a target line.",
75
+ "slots": [
76
+ { "id": "category", "label": "Category", "role": "dimension", "types": ["string"], "required": true,
77
+ "description": "One bar per value." },
78
+ { "id": "value", "label": "Value", "role": "measure", "types": ["number"], "required": true },
79
+ { "id": "target", "label": "Target", "role": "measure", "types": ["number"], "required": false }
80
+ ],
81
+ "options": [
82
+ { "id": "show_labels", "label": "Show labels", "type": "boolean", "default": true, "group": "Labels" },
83
+ { "id": "bar_radius", "label": "Corner radius", "type": "number", "default": 4, "min": 0, "max": 20, "step": 1 },
84
+ { "id": "subtitle", "label": "Subtitle", "type": "string", "default": "", "maxLength": 120 },
85
+ { "id": "mode", "label": "Mode", "type": "select", "default": "grouped",
86
+ "choices": [ { "value": "grouped", "label": "Grouped" }, { "value": "stacked", "label": "Stacked" } ] },
87
+ { "id": "bar_color", "label": "Bar colour", "type": "color", "default": "#3366cc" }
88
+ ],
89
+ "translations": {
90
+ "fr": {
91
+ "name": "Barres de revenus",
92
+ "slots": { "category": { "label": "Categorie" } },
93
+ "options": { "mode": { "label": "Mode", "choices": { "grouped": "Groupe", "stacked": "Empile" } } }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### 3.1 Top level keys
100
+
101
+ | Key | Required | Rule |
102
+ | --- | --- | --- |
103
+ | `schemaVersion` | yes | `1` or `2`. Use `2`. Version 1 is the older sandbox manifest and does not allow `version`, `description`, `translations`, the `color` type, `step`, `group` or slot descriptions. |
104
+ | `id` | yes | The chart key. `^[a-z][a-z0-9_-]{0,63}$`. The chart type becomes `custom:<id>`. Changing it later creates a different chart. |
105
+ | `name` | yes | 1 to 256 characters. Shown in the chart picker. |
106
+ | `version` | no | Semver such as `1.2.0`. A label for you; Databrain numbers versions itself (1, 2, 3). |
107
+ | `description` | no | Up to 500 characters. |
108
+ | `slots` | yes | 1 to 16 slots with unique ids. See 3.2. |
109
+ | `options` | yes | 0 to 32 options with unique ids. Use `[]` for none. See 3.3. |
110
+ | `translations` | no | Per language overrides. See 3.4. |
111
+
112
+ The whole file must be at most 32 KiB as JSON.
113
+
114
+ ### 3.2 Slots
115
+
116
+ A slot is one place where a builder binds a column of the metric. In the
117
+ metric editor each slot becomes a select: dimension slots list the metric's
118
+ dimension columns, measure slots list its measure columns.
119
+
120
+ | Field | Required | Rule |
121
+ | --- | --- | --- |
122
+ | `id` | yes | `^[a-z][a-z0-9_-]{0,63}$`, unique across slots. This is the key you read in `ctx.slots`. |
123
+ | `label` | yes | 1 to 256 characters. |
124
+ | `role` | yes | `"dimension"` (a category, a date, a flag) or `"measure"` (a number to aggregate). |
125
+ | `types` | yes | 1 to 3 unique values from `"string"`, `"number"`, `"boolean"`. A measure slot must be exactly `["number"]`. Dates arrive as ISO strings, so a date slot is `["string"]`. |
126
+ | `required` | yes | `true` when the chart cannot draw without it. The editor marks required slots and refuses to save without them. |
127
+ | `description` | no | Up to 256 characters, shown as help text. |
128
+
129
+ One slot binds one column. If a chart needs several series, declare several
130
+ slots (`value_1`, `value_2`) or one measure slot and one dimension slot that
131
+ splits the series. There is no "multiple" slot.
132
+
133
+ ### 3.3 Options
134
+
135
+ Options are the settings a builder changes in the chart settings panel. Their
136
+ values reach the chart as `ctx.options[id]`, already merged with the defaults,
137
+ so you never need to check for a missing option.
138
+
139
+ Every option has `id` (`^[a-z][a-z0-9_-]{0,63}$`, unique), `label` (1 to 256
140
+ characters), `type`, `default` and an optional `group` (up to 64 characters;
141
+ options with the same group render as one section).
142
+
143
+ | `type` | Extra fields | Default rule | Rendered as |
144
+ | --- | --- | --- | --- |
145
+ | `boolean` | none | `true` or `false` | a switch |
146
+ | `number` | `min`, `max` (required, finite, `min <= max`), `step` (optional, positive, at most `max - min`) | a finite number inside `[min, max]` | a number input |
147
+ | `string` | `maxLength` (1 to 1024) | a string no longer than `maxLength` | a text input |
148
+ | `select` | `choices`: 1 to 32 of `{ "value", "label" }`, values unique and shaped like ids | one of the choice values | a select |
149
+ | `color` | none | `#rrggbb` (six hex digits) | the colour picker |
150
+
151
+ The id `theme` and any id that starts with `theme.` are reserved and refused
152
+ with `manifest.option.reserved`. The host theme arrives on `ctx.theme`, not as
153
+ an option.
154
+
155
+ ### 3.4 Translations
156
+
157
+ `translations` is an object keyed by language code (`^[a-z]{2}(-[A-Z]{2})?$`,
158
+ for example `fr` or `pt-BR`). Each entry may override `name`, `description`,
159
+ `slots.<slotId>.label`, `slots.<slotId>.description`, `options.<optionId>.label`,
160
+ `options.<optionId>.group` and `options.<optionId>.choices.<value>`. Every
161
+ referenced slot, option and choice must exist in the manifest, and every
162
+ string is 1 to 256 characters. The editor uses the builder's language;
163
+ `ctx.language` tells the chart which language the viewer has.
164
+
165
+ ### 3.5 Error codes you will meet
166
+
167
+ | Code | Meaning |
168
+ | --- | --- |
169
+ | `manifest.schemaVersion.unsupported` | `schemaVersion` is not 1 or 2. |
170
+ | `manifest.unknownKey` | A key that is not in the table above (or a v2 key on a v1 manifest). |
171
+ | `manifest.id.invalid` | The id does not match the pattern. Lowercase, no spaces. |
172
+ | `manifest.slots.empty`, `manifest.slots.tooMany` | Declare 1 to 16 slots. |
173
+ | `manifest.slot.types` | A measure slot must be exactly `["number"]`, or a type is not string, number or boolean. |
174
+ | `manifest.slot.duplicate` | Two slots share an id. |
175
+ | `manifest.option.type` | Unknown option type, or `color` on schemaVersion 1. |
176
+ | `manifest.option.reserved` | The option id is `theme` or starts with `theme.`. |
177
+ | `manifest.option.default` | The default is outside the range, longer than `maxLength` or not a choice. |
178
+ | `manifest.option.step` | `step` is not positive or is larger than `max - min`. |
179
+ | `manifest.option.color` | The colour default is not `#rrggbb`. |
180
+ | `manifest.option.duplicate` | Two options share an id. |
181
+ | `manifest.translations.language` | A key is not a language code. |
182
+ | `manifest.translations.slot`, `.option`, `.choice` | A translation names something the manifest does not declare. |
183
+ | `manifest.size` | The manifest is over 32 KiB as JSON. Reported once every other rule passes. |
184
+ | `bundle.icon.required`, `bundle.icon.size`, `bundle.icon.notSvg`, `bundle.icon.unsafe` | The icon is missing, over 16 KiB, not one `<svg>` element, or breaks a rule from section 7 (the message names the rule). |
185
+ | `bundle.js.size` | `dist/index.js` is over 2 MiB. |
186
+ | `bundle.css.size` | `chart.css` is over 256 KiB. |
187
+ | `bundle.build.failed` | esbuild could not compile `chart.ts`; the message is the compiler's. An import that does not resolve inside the project (a node builtin, a path outside the folder) is this too. |
188
+ | `bundle.build.noRender` | `chart.ts` is not an ES module with a named `render` export. `module.exports` and `export default` are not read. |
189
+
190
+ ## 4. The data your chart receives
191
+
192
+ Databrain calls `render(ctx)` with one `CustomChartContext` (see
193
+ `custom-chart.d.ts`):
194
+
195
+ ```ts
196
+ import type { CustomChartContext } from '@databrainhq/custom-chart-types';
197
+
198
+ export function render({ container, data, slots, options, theme, language, dimensions }: CustomChartContext): void {
199
+ // draw into container
200
+ }
201
+
202
+ export function resize(ctx: CustomChartContext): void {
203
+ // optional; without it render runs again on every size change
204
+ }
205
+ ```
206
+
207
+ | Field | What it is |
208
+ | --- | --- |
209
+ | `container` | The `HTMLElement` to draw into. It fills the tile. Clear it before drawing again (`container.replaceChildren()`); render runs again on every data update. |
210
+ | `data.columns` | One entry per bound column, in manifest slot order: `{ id, label, type, filterable, slotId }`. `id` is what you pass to `dbnChart.click` and `dbnChart.filter`. `filterable` is true on dimension columns. |
211
+ | `data.rows` | An array of rows, each an array of cells aligned with `data.columns`. Cells are `string`, `number`, `boolean` or `null`. Row order is the metric's order. Rows are already tenant scoped. At most 5000 rows and 64 columns reach the chart. |
212
+ | `slots` | `slotId -> number[]`, the indexes into `data.columns` bound to that slot. An optional slot that is not bound gives `[]`. Read columns through slots, never by position. |
213
+ | `options` | The option values, defaults merged with what the builder saved. |
214
+ | `theme` | The flat host theme (section 5). |
215
+ | `language` | The viewer's language code, for example `en` or `fr`. |
216
+ | `dimensions` | `{ width, height }` of the container in CSS pixels at the time of the call. |
217
+
218
+ A typical start of `render`:
219
+
220
+ ```ts
221
+ const categoryIndex = ctx.slots.category[0];
222
+ const valueIndex = ctx.slots.value[0];
223
+ const targetIndex = ctx.slots.target[0]; // undefined when the optional slot is empty
224
+ for (const row of ctx.data.rows) {
225
+ const label = String(row[categoryIndex] ?? '');
226
+ const value = typeof row[valueIndex] === 'number' ? row[valueIndex] : 0;
227
+ }
228
+ ```
229
+
230
+ Numbers arrive as numbers, not strings. Dates arrive as ISO strings; parse
231
+ them with `new Date(value)` if you need a time axis. Booleans arrive as
232
+ booleans. Null means no value: draw a gap, not a zero, unless the chart says
233
+ otherwise.
234
+
235
+ `sample-data.json` uses the same shape (`columns`, `rows`, optional
236
+ `options`) and is what `databrain custom-chart dev` and the in app preview
237
+ feed to the chart. Keep it small and realistic.
238
+
239
+ ## 5. Theme
240
+
241
+ `ctx.theme` is derived from the dashboard's resolved theme so a custom chart
242
+ looks like the charts around it. Every key is always present; when a host
243
+ sends no theme at all, the shim supplies a neutral default with an empty
244
+ palette, so fall back to `primaryColor` when `palette` is empty.
245
+
246
+ | Key | Type | Use it for |
247
+ | --- | --- | --- |
248
+ | `fontFamily` | string | Text. |
249
+ | `fontSize` | number (px) | The base text size. Axis labels are usually `fontSize * 0.9`. |
250
+ | `textColor` | string | Titles, values, primary text. |
251
+ | `textSecondaryColor` | string | Axis labels, subtitles, hints. |
252
+ | `background` | string | The tile background. Set it on the container if you paint one. |
253
+ | `surfaceColor` | string | Cards, tooltips, legends. |
254
+ | `borderColor` | string | Axis lines, gridlines, borders. |
255
+ | `borderRadius` | number (px) | Corners. |
256
+ | `primaryColor` | string | The accent colour. |
257
+ | `palette` | string[] | Series colours in order. At least one entry, at most 32. |
258
+
259
+ Use the theme first and your own colours second. A `color` option is the
260
+ right way to let a builder override one colour; a hard coded `#ff0000` in
261
+ `chart.ts` is not. Do not read `prefers-color-scheme`: the host decides.
262
+
263
+ `chart.css` is uploaded next to the bundle (`bundleCss`) and the host injects
264
+ it into the frame as a style element, so class names cannot clash with the
265
+ host page. It cannot load fonts, images or any `url(...)`; the sandbox blocks
266
+ network access.
267
+
268
+ ## 6. Interactions
269
+
270
+ Inside the chart a global `dbnChart` is available (typed in
271
+ `custom-chart.d.ts`). Every call is an event to the host; the host decides
272
+ what happens, and nothing in the chart can navigate, fetch or write.
273
+
274
+ | Call | Effect on the dashboard |
275
+ | --- | --- |
276
+ | `dbnChart.click(rowIndex, columnId)` | The tile's data point click. `rowIndex` indexes `data.rows`; the host reads the row's slot values and passes them to the embedding app's `onDataPointClick`. |
277
+ | `dbnChart.filter(columnId, values)` | Cross filter the dashboard on that column when it is `filterable` and the values are in the current rows. At most 100 values. An empty list clears the filter. Ignored otherwise. |
278
+ | `dbnChart.resize(height)` | Ask the host for a different tile height, 80 to 1200 pixels. Width always belongs to the host. |
279
+
280
+ Bind them to real user actions (click, keyboard), never call them in a loop.
281
+ The host caps events at 60 per second and drops the rest.
282
+
283
+ ## 7. Icon
284
+
285
+ `icon.svg` shows in the chart picker and in Settings. Rules, checked at build
286
+ and again on upload:
287
+
288
+ - One `<svg>` element, at most 16 KiB.
289
+ - No `<script>`, no `<foreignObject>`, no `<image>`, no `on*=` attributes.
290
+ - No `url(...)` in styles or attributes except `url(#id)`, no `href` that does not start with `#`.
291
+ - No comments, no DOCTYPE, no CDATA, no animation elements, and only the standard SVG
292
+ drawing attributes (an editor's `inkscape:` or `sodipodi:` attributes are refused).
293
+ The build strips the XML declaration and comments for you; the upload does not.
294
+ - Use `currentColor` for strokes and fills so the icon follows the UI theme.
295
+
296
+ The template icon is a 24 by 24 outline on a 2 pixel stroke. Keep that size.
297
+
298
+ ## 8. Build, upload, publish
299
+
300
+ ```
301
+ databrain custom-chart validate # manifest, icon, css. No compile.
302
+ databrain custom-chart build # esbuild, writes dist/
303
+ databrain custom-chart dev # local page on http://127.0.0.1:4174
304
+ databrain custom-chart upload # build, server validate, plan, confirm, upload
305
+ databrain custom-chart upload --publish # same, then publish that version
306
+ databrain custom-chart upload --yes # no confirmation prompt (CI)
307
+ ```
308
+
309
+ `build` compiles `chart.ts` with esbuild (`format: iife`, `target: es2020`,
310
+ minified, everything bundled), prepends the Databrain runtime shim and the
311
+ list of slot ids, checks the manifest and the icon, enforces 2 MiB and writes
312
+ `dist/`. The shim is the only code between your module and the sandbox: it
313
+ receives the data message, builds the context, calls your `render` and
314
+ `resize` (once per animation frame on resize), and maps `dbnChart` to the
315
+ sandbox protocol. If `render` throws or returns a rejected promise, the shim
316
+ reports it to the host once and the chart stops receiving data. Do not edit
317
+ `dist/index.js`.
318
+
319
+ `upload` needs `DATABRAIN_SERVICE_TOKEN` (Settings > Service Tokens) and, on
320
+ self hosted instances, `DATABRAIN_API_URL`. It builds, asks the server to
321
+ validate, prints the plan (`will create chart` or `will create version n`),
322
+ waits for `y`, then uploads. The first upload creates the chart with status
323
+ Private and version 1. Every later upload with different content creates the
324
+ next version in preview; the same content is recognised by its hash and
325
+ creates nothing. Live dashboards keep rendering the published version until
326
+ someone publishes the preview.
327
+
328
+ Publishing is the one step that changes what customers see. Do it with
329
+ `--publish`, with the `publish_custom_chart` MCP tool (dry run first, then
330
+ `confirm: "APPLY_TO_PRODUCTION"`), or in Settings > Developers > Custom charts.
331
+
332
+ From an agent, the same steps are the MCP tools `custom_chart_scaffold`,
333
+ `custom_chart_validate`, `custom_chart_upload` (dry run, then apply with the
334
+ confirmation string) and `publish_custom_chart`. The tools never write to disk
335
+ and never publish without the confirmation.
336
+
337
+ ## 9. Writing a good chart
338
+
339
+ - Draw with plain DOM or SVG when you can. A dependency is fine when it earns
340
+ its bytes; check the size line that `build` prints.
341
+ - Read columns through `ctx.slots`, never by position, and handle an empty
342
+ optional slot (`[]`).
343
+ - Clear the container at the top of `render`. The host calls it again on
344
+ every data update and, without `resize`, on every size change.
345
+ - Use `ctx.dimensions` for the drawing size and `ctx.theme` for every colour
346
+ and font you do not expose as an option.
347
+ - Format numbers with `Intl.NumberFormat(ctx.language)`.
348
+ - Show a short message when the required data is missing instead of throwing.
349
+ A thrown error is reported to the host once, the tile shows a generic
350
+ failure and the chart receives no more data.
351
+ - Keep `render` under a few milliseconds for 5000 rows. Avoid layout reads in
352
+ a loop.
353
+ - Do not touch `window.parent`, `document.cookie`, `fetch`, `localStorage` or
354
+ timers that outlive a render. The sandbox blocks them and a reviewer will
355
+ reject the chart.
356
+
357
+ ## 10. Troubleshooting
358
+
359
+ | Symptom | Cause and fix |
360
+ | --- | --- |
361
+ | `manifest.unknownKey` at `/version` | The manifest says `schemaVersion: 1`. Set it to `2`. |
362
+ | `manifest.slot.types` at `/slots/N/types` | A measure slot lists something other than `["number"]`. Change the role to `dimension` or the types to `["number"]`. |
363
+ | `manifest.option.reserved` | Rename the option; `theme` is the host theme. |
364
+ | `bundle.build.failed` with a TypeScript message | esbuild reports the file and line. It does not type check; run `npx tsc -p tsconfig.json` in the project for types. |
365
+ | `bundle.js.size` | A dependency is too big. Import only what you use, or draw it yourself. |
366
+ | `bundle.icon.unsafe` | Export the icon again without links, images, styles with `url(...)` or scripts. Inline the paths. |
367
+ | The chart is blank in `dev` | The events panel shows `error` with code `runtime` and the browser console shows `[dbn custom chart] render failed:` with the error. Fix it and reload. |
368
+ | The chart draws once and never updates | `render` does not clear the container, or it caches the first rows. Clear and redraw from `ctx` each time. |
369
+ | Labels are unreadable on a dark dashboard | Colours are hard coded. Use `ctx.theme.textColor` and `ctx.theme.palette`. |
370
+ | Clicks do nothing on the dashboard | `dbnChart.click` was given a row index that is not in `data.rows`, or the embedding app has no `onDataPointClick`. |
371
+ | `upload` says `DATABRAIN_SERVICE_TOKEN is not set` | Export the token in the shell that runs the command. Never write it into a file in the project. |
372
+ | `upload` says the server refused the bundle | The server runs the same rules; the printed problems name the path to fix. Rebuild and upload again. |
373
+ | Dashboards still show the old chart after upload | Uploads land in preview. Publish the version. |
374
+ | `This tile uses a custom chart (custom:key) that is not published.` | The dashboard references a key that is private or missing in this company. Publish it, or pick another chart type. |