@bicharts/chart-mcp 0.6.90 → 0.6.92

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-mcp",
3
- "version": "0.6.90",
3
+ "version": "0.6.92",
4
4
  "description": "MCP (Model Context Protocol) stdio server for BIC AI charts: profile a dataset locally, then generate chart code that has passed the BIC backend's render gates. Works with any MCP-capable client (Claude Code, Claude Desktop, Cursor, Copilot Studio). An account pays to author charts; the code it writes is yours and runs with no licence.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -47,8 +47,8 @@
47
47
  },
48
48
  "// dependencies": "NONE, deliberately. dist/ bundles every module the server uses, so a runtime dependency installs nothing it needs - it only makes npx fetch that package (and its tree) before the first start. Everything the bundle carries is a devDependency; scripts/verify-package.mjs fails the release if a dependency appears or a shipped file imports anything but Node's built-ins.",
49
49
  "devDependencies": {
50
- "@bicharts/chart-host": "^0.6.90",
51
- "@bicharts/shape-core": "^0.6.90",
50
+ "@bicharts/chart-host": "^0.6.92",
51
+ "@bicharts/shape-core": "^0.6.92",
52
52
  "@modelcontextprotocol/sdk": "^1.0.0",
53
53
  "@types/node": "^20.0.0",
54
54
  "@types/papaparse": "^5.3.14",
@@ -33,6 +33,7 @@ on the same picture, not a looser one.
33
33
  | Bivariate World Choropleth | 1 categorical, 2 continuous, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 400x300 px | D3 | Best for two country-level measures that might disagree: wealth vs inequality, revenue vs margin, cases vs deaths - at least 24 countries and two measures that do not simply track each other |
34
34
  | Box plot | 1 continuous | needs 460x340 px | D3, Matplotlib, Plotly, Seaborn | Requires one continuous variable (plus optional category) |
35
35
  | Bubble chart | 3 continuous | needs 520x380 px | D3, Matplotlib, Plotly, Seaborn | Three continuous variables (x, y, size); one mark per row on continuous axes. If you want both axes grouped into bands with one aggregated bubble per cell, pick Binned bubble matrix instead |
36
+ | Budget allocator | 1 categorical, 2 continuous | needs 560x380 px | D3 | One spend measure and one outcome by channel and period - monthly spend and signups for five marketing channels, budget and sales by region - with at least eight periods per channel |
36
37
  | Bullet chart | 1 continuous, a reference value | needs 220x120 px | D3, PLOTLY | Best for actual-vs-target KPIs with thresholds (revenue vs goal, SLA vs target) — a denser, clearer replacement for gauges |
37
38
  | Bump chart | 1 categorical, 1 continuous, 3+ time points | <=15 categories, needs 320x280 px | D3, PLOTLY | Best for leaderboard/position dynamics: how a handful of regions, products, or teams change rank quarter to quarter |
38
39
  | Calendar heatmap | 1 continuous, 60+ time points | needs 600x420 px | D3, Matplotlib, Plotly, Seaborn | Requires daily datetime data with associated numeric values; best for activity/contribution tracking over time |
@@ -46,6 +47,7 @@ on the same picture, not a looser one.
46
47
  | Clustered heatmap | 2 categorical, 1 continuous | <=50 categories, needs 320x320 px | Plotly | Requires two variables for matrix values |
47
48
  | Connected scatterplot | 2 continuous, 6+ time points | needs 300x300 px | D3, PLOTLY | Best for the joint trajectory of two related metrics over time (e.g. units vs revenue, unemployment vs inflation) where the path shape is the insight |
48
49
  | Custom small multiples | 1 categorical, 1 continuous | <=16 categories, needs 320x240 px | D3, Matplotlib, Plotly, Seaborn | Best when comparing patterns across categories |
50
+ | Cutoff tuner | 1 continuous | needs 560x360 px | D3 | One row per case with a model score or rating and the actual yes/no outcome - churn, fraud, default, conversion - when the question is where to draw the line and what each choice costs |
49
51
  | Cycle plot | 1 continuous, 24+ time points | needs 320x280 px | D3, PLOTLY | Best for multi-year monthly/quarterly/weekly data where the question is seasonality vs trend ('is Q4 always highest, and is each Q4 growing?') |
50
52
  | Delta KPI | 2 continuous | needs 200x120 px | D3 | Two comparable measures, or one measure at two points in time - this period against last, actual against plan, after against before |
51
53
  | Dendrogram (tree diagram) | 2 categorical, 1 continuous | <=60 leaves, needs 320x240 px | D3, Plotly | Best for 2-4 hierarchy levels with ≤ ~80 leaves |
@@ -42,7 +42,7 @@ those which defaulted, paying attention to credit score"* wants `LoanId`, `Defau
42
42
  `CreditScore`, filtered to six months.
43
43
 
44
44
  **A highlight is a COLUMN, not a styling note.** "Highlighting those which defaulted" means
45
- `Defaulted` travels as its own categorical column. There is no instruction channel that can colour
45
+ `Defaulted` travels as its own categorical column. There is no instruction channel that can color
46
46
  rows the data does not distinguish.
47
47
 
48
48
  **GRAIN is a choice, and the default is usually wrong for these charts.** Almost every BI query
@@ -59,7 +59,7 @@ question, and nothing downstream will flag it. Ask yourself which you meant:
59
59
 
60
60
  **Aim with `CHART-SHAPES.md`** (beside this file). It lists every chart the engine can build and
61
61
  what each one needs — minimum columns by role, category ceilings, draw caps, viewport floors,
62
- which renderers can draw it. It is GENERATED from the chart catalogue, so it cannot drift from what
62
+ which renderers can draw it. It is GENERATED from the chart catalog, so it cannot drift from what
63
63
  the engine will actually accept. Read the LIMITS column carefully: **the units differ** — a sankey
64
64
  is capped on EDGES and a chord on NODES, so `<=80 edges` is the tighter constraint on the same
65
65
  picture, not the looser one.
@@ -118,7 +118,7 @@ npm install -D esbuild
118
118
  `npm init -y` line again before retrying.
119
119
  - **`global.json`** pins the build to the newest installed .NET 10 SDK. Without it a machine that
120
120
  also has a newer or preview SDK builds with that one.
121
- - These blocks are written against `@bicharts/chart-host` 0.6.33 or later; every name they import
121
+ - These blocks are written against `@bicharts/chart-host` 0.6.86 or later; every name they import
122
122
  is exported there.
123
123
  - npm 11 may warn that esbuild's postinstall is "not yet covered by allowScripts". The build doesn't
124
124
  need it: esbuild's platform binary arrives as an ordinary dependency, and the bundling target runs.
@@ -169,7 +169,7 @@ correction:
169
169
  | Parameter | Use it when |
170
170
  | --- | --- |
171
171
  | `rows_policy: "all"` | every source row must appear — a row-per-entity table, a point per location. Without it the engine may legitimately show a Top-N with an "Other" rollup. |
172
- | `required_columns: [...]` | a field must be visible because your app's interactions depend on it (e.g. the column you colour or filter by). |
172
+ | `required_columns: [...]` | a field must be visible because your app's interactions depend on it (e.g. the column you color or filter by). |
173
173
  | `forbid_derived_columns: true` | your app owns the analytics; the chart is only the display. Stops per-capita / share-of-total / rank columns being added. |
174
174
 
175
175
  Asking for these in `prompt` prose instead is the most expensive mistake available here: it
@@ -189,7 +189,7 @@ engine picked, what was dropped), for a client that shows only the text. `engine
189
189
  the server reported what it picked; when it is `unavailable` the engine may have picked where a row
190
190
  reads `default`, so never tell the user it picked nothing. Most settings are
191
191
  passed only when the user has said them in their own words: a title, data labels, a legend position,
192
- a limit, a date grain, an overlay, a font. A few are worth one question when they matter and the user
192
+ a limit, a date grain, an overlay, a font. Three are worth one question whenever the user
193
193
  hasn't said: `chart_style` (which family of charts to consider: the familiar forms or the other types), `level_of_detail` (how much of
194
194
  the data they want to see: every observation, a distribution, or one mark per category) and
195
195
  `culture` (their language and region), and, once the chart is drawn, the live options
@@ -226,10 +226,13 @@ Import the generated code as raw text and the sample payload as JSON:
226
226
  ```tsx
227
227
  import * as d3 from "d3";
228
228
  import { BicChart, BicChartGroup } from "@bicharts/chart-host/react";
229
+ import { createLegendFitPass } from "@bicharts/chart-host/legend-fit"; // chart-host 0.6.86 or later
229
230
  import mapCode from "./charts/map/chart.js?raw";
230
231
  import tableCode from "./charts/table/chart.js?raw";
231
232
  import payload from "./charts/map/data.sample.json";
232
233
 
234
+ const LAYOUT_PASS = createLegendFitPass(); // once, at module scope: a new function on each render rebuilds the host
235
+
233
236
  // data.sample.json rows are POSITIONAL arrays; the group wants row OBJECTS.
234
237
  const columns = payload.columns;
235
238
  const rows = payload.rows.map(r =>
@@ -240,8 +243,8 @@ const rows = payload.rows.map(r =>
240
243
 
241
244
  ```tsx
242
245
  <BicChartGroup columns={columns} rows={rows} point={{ city: "City", state: "StateOrProvince" }}>
243
- <BicChart id="map" code={mapCode} d3={d3} geoKind="north-america" respondsWith="highlight" />
244
- <BicChart id="table" code={tableCode} d3={d3} />
246
+ <BicChart id="map" code={mapCode} d3={d3} geoKind="north-america" respondsWith="highlight" layoutPass={LAYOUT_PASS} />
247
+ <BicChart id="table" code={tableCode} d3={d3} layoutPass={LAYOUT_PASS} />
245
248
  </BicChartGroup>
246
249
  ```
247
250
 
@@ -277,9 +280,10 @@ Ctrl/Cmd/Shift-click adds or removes one mark, and a click on empty canvas clear
277
280
  ```js
278
281
  import * as d3base from "d3";
279
282
  import { assembleD3, createChartGroup, createChartHost, loadGeo } from "@bicharts/chart-host";
283
+ import { createLegendFitPass } from "@bicharts/chart-host/legend-fit";
280
284
 
281
285
  const d3 = assembleD3(d3base); // + any plugins a chart's contract names
282
- const at = (path) => new URL(path, document.baseURI); // honours <base href> on a sub-path host
286
+ const at = (path) => new URL(path, document.baseURI); // honors <base href> on a sub-path host
283
287
  async function text(path) {
284
288
  const r = await fetch(at(path));
285
289
  if (!r.ok) throw new Error(`${path}: HTTP ${r.status}`);
@@ -310,14 +314,21 @@ export async function createGroup(sampleUrl, dotnet) {
310
314
  const group = createChartGroup(cols, rows);
311
315
  const off = group.onChange(sel => dotnet.invokeMethodAsync("OnSelectionChanged", sel.sourceId, [...sel.rows]));
312
316
  return {
313
- async mount(el, spec) { // spec: { id, dir, height, respondsWith, geoKind }
317
+ async mount(el, spec) { // spec: { id, dir, height, respondsWith, geoKind, lasso }
314
318
  const code = await text(`${spec.dir}/chart.js`); // TEXT: chart-host compiles it and injects d3
315
319
  if (spec.geoKind) await loadGeo(spec.geoKind); // BEFORE the first render: render() is synchronous
316
320
  const opts = spec.respondsWith ? { respondsWith: spec.respondsWith } : {};
321
+ // The host lasso, for a chart whose generate_chart result carries capabilities.lasso (spec.lasso): a drag around
322
+ // marks selects the ones inside, through the group's own selection, so there is no handler to write. `capable` is
323
+ // the server's word for this chart; without it there is no lasso.
324
+ const lasso = spec.lasso
325
+ ? { capable: true, enabled: true, ...(spec.lasso.rowCap > 0 ? { rowCap: spec.lasso.rowCap } : {}) }
326
+ : null;
317
327
  const host = createChartHost(el, {
318
328
  code, d3, geoKind: spec.geoKind || undefined,
319
329
  data: group.memberPayload(spec.id, opts).payload,
320
- options: { width: Math.floor(el.getBoundingClientRect().width), height: spec.height, ...theme(el) },
330
+ options: { width: Math.floor(el.getBoundingClientRect().width), height: spec.height, ...theme(el), ...(lasso ? { lasso } : {}) },
331
+ layoutPass: createLegendFitPass(), // after every render: a legend slid clear of the plot, text brought inside the frame
321
332
  });
322
333
  host.render();
323
334
  const member = group.attach(spec.id, host, opts); // its clicks publish SOURCE rows
@@ -423,6 +434,10 @@ export async function createGroup(sampleUrl, dotnet) {
423
434
  [Parameter] public string? RespondsWith { get; set; }
424
435
  /// <summary>A map's geo.kind from the generate_chart result (e.g. "us-state-name").</summary>
425
436
  [Parameter] public string? GeoKind { get; set; }
437
+ /// <summary>True when the generate_chart result carries capabilities.lasso: a drag around marks selects the ones inside.</summary>
438
+ [Parameter] public bool Lasso { get; set; }
439
+ /// <summary>capabilities.lasso.rowCap from the same result, when it gives one: the most rows in view that still get a lasso.</summary>
440
+ [Parameter] public int? LassoRowCap { get; set; }
426
441
 
427
442
  ElementReference el;
428
443
  IJSObjectReference? chart;
@@ -432,7 +447,8 @@ export async function createGroup(sampleUrl, dotnet) {
432
447
  if (!firstRender) return;
433
448
  var group = await Group.Ready;
434
449
  chart = await group.InvokeAsync<IJSObjectReference>("mount", el,
435
- new { id = Id, dir = Dir, height = Height, respondsWith = RespondsWith, geoKind = GeoKind });
450
+ new { id = Id, dir = Dir, height = Height, respondsWith = RespondsWith, geoKind = GeoKind,
451
+ lasso = Lasso ? new { rowCap = LassoRowCap } : null });
436
452
  }
437
453
 
438
454
  /// <summary>A live restyle: merged into the chart's options and repainted.</summary>
@@ -475,7 +491,7 @@ Add `@using MyCharts.Components` to `_Imports.razor`, then on a page:
475
491
  the tool resolved.
476
492
  - **Restyle** - a chart honours only the options its code reads, so a live restyle control has to
477
493
  set one of those. The generate result's `optionsRead` lists them for that chart, and its
478
- `integrationContract` says what each does. The common ones: `palette` (a categorical colour
494
+ `integrationContract` says what each does. The common ones: `palette` (a categorical color
479
495
  list), `colorScaleLow`/`colorScaleHigh` (a sequential ramp's ends, what a choropleth reads),
480
496
  `maxMapPoints` (a point map's cap). An option not in `optionsRead` does nothing on that chart.
481
497
  - **Hover drop lines** - a 3D scatter can draw, on hover, a line from a point to each back wall of
@@ -485,6 +501,9 @@ Add `@using MyCharts.Components` to `_Imports.razor`, then on a page:
485
501
  `dropLines` says which applies: `ask` carries the question, `on`/`off` means the user already
486
502
  answered (the client asked them) and the generated component applies it. Keep the answer when
487
503
  the chart is regenerated; never ask again.
504
+ - **Lasso** - when the result's `capabilities.lasso` is present, pass `Lasso="true"` (and `LassoRowCap` with its
505
+ `rowCap`): a drag around marks then selects the ones inside through the group's own selection, with no handler to
506
+ write. A chart whose result has none takes neither parameter and has no lasso.
488
507
  - **GeoKind** - the result's `geo.kind` (the text reply's `Geo:` line). Without it a map draws
489
508
  its marks over no land. `loadGeo` takes that geometry from chart-host itself, so the
490
509
  `data.geo.json` the tool also writes beside a map is the same shapes as a file: the page never
@@ -498,7 +517,7 @@ Add `@using MyCharts.Components` to `_Imports.razor`, then on a page:
498
517
  `.bic-chart` its width in CSS (a block element in a sized column); the height is the
499
518
  `Height` parameter. A `display:none` container measures 0 and draws nothing.
500
519
  - **Light and dark** - `theme(el)` reads the background the chart sits on, so the chart follows the
501
- PAGE. For a page that follows the reader, switch the page's own colours in CSS
520
+ PAGE. For a page that follows the reader, switch the page's own colors in CSS
502
521
  (`@media (prefers-color-scheme: dark) { ... }` on `body` or the chart's card) and the charts follow
503
522
  on the next `change` of the media query.
504
523
 
@@ -559,7 +578,7 @@ chart's grain.
559
578
  **4. Ask before choosing, then choose what answers the question.** `list_eligible_charts` on that
560
579
  data. Pass `chart_type` to `generate_chart` only for a type the person named; when they only asked for a chart, leave it
561
580
  unset and the engine chooses from what fits. A type the person described without naming it counts as named once you have
562
- matched it to the catalogue. The catalogue goes past the template's own charts, and these are the ones people describe
581
+ matched it to the catalog. The catalog goes past the template's own charts, and these are the ones people describe
563
582
  without naming:
564
583
 
565
584
  | the request sounds like | ask for |
@@ -581,7 +600,7 @@ slider sets the rate anyway.
581
600
  queries at run time (`useSemanticModelQuery`), so the payload is rebuilt from live rows every time -
582
601
  never hand-built. **With `out_dir`, a D3 chart arrives with `chart.component.tsx` beside `chart.js`: a component that
583
602
  already does all of this** - the columns, the binding (`geo`, `point`, `destination`), the geometry kind, the
584
- d3 plugins the chart calls, and `rowsFromTable`, which maps a DAX result's `Table[Column]` and `[Measure]`
603
+ d3 plugins the chart calls, the host's legend and frame fit (`layoutPass`), and `rowsFromTable`, which maps a DAX result's `Table[Column]` and `[Measure]`
585
604
  headers onto the chart's plain names. Use it as it is:
586
605
 
587
606
  ```tsx
@@ -608,6 +627,7 @@ host by hand instead - `generate_chart` returns `binding`; pass all three:
608
627
  ```tsx
609
628
  import * as d3 from "d3";
610
629
  import { BicChart, BicChartGroup } from "@bicharts/chart-host/react";
630
+ import { createLegendFitPass } from "@bicharts/chart-host/legend-fit";
611
631
  import code from "@/charts/occupancy-map/chart.js?raw";
612
632
  // Saved from the generate_chart result: columns = data.columns, binding = binding, geoKind = geo.kind.
613
633
  import { columns, binding, geoKind } from "@/charts/occupancy-map/meta";
@@ -616,6 +636,8 @@ import { columns, binding, geoKind } from "@/charts/occupancy-map/meta";
616
636
  const RENAME = { "Geography[CountryIso3]": "CountryIso3", "Geography[CountryName]": "CountryName",
617
637
  "[Average Daily Rate]": "AverageDailyRate", "[Occupancy %]": "Occupancy" };
618
638
 
639
+ const LAYOUT_PASS = createLegendFitPass(); // once, at module scope: a new function on each render rebuilds the host
640
+
619
641
  function CountryMap({ table, onPick }) { // table = the live query result
620
642
  const rows = table.rows.map(r => Object.fromEntries(
621
643
  table.columns.map((c, i) => [RENAME[c.name] ?? c.name, r[i]])));
@@ -623,7 +645,7 @@ function CountryMap({ table, onPick }) { // table = the live query resu
623
645
  <BicChartGroup columns={columns} rows={rows}
624
646
  geo={binding.geo} point={binding.point} destination={binding.destination}>
625
647
  <BicChart id="map" code={code} d3={d3} geoKind={geoKind}
626
- options={{ width, height }}
648
+ options={{ width, height }} layoutPass={LAYOUT_PASS}
627
649
  onSelect={idxs => onPick(idxs.length ? rows[idxs[0]].CountryIso3 : null)} />
628
650
  </BicChartGroup>
629
651
  );
@@ -636,6 +658,14 @@ function CountryMap({ table, onPick }) { // table = the live query resu
636
658
  basemap but no join, so its `binding.geo` is null.
637
659
  - **`<BicChartGroup destination>` needs `@bicharts/chart-host` 0.6.50 or later.** Before it, a flow
638
660
  map's far end is not built.
661
+ - **`LAYOUT_PASS` is built once, at module scope, and needs `@bicharts/chart-host` 0.6.86 or later.** After
662
+ every render it slides a legend clear of the plot and brings text that runs past the frame back inside; the
663
+ chart component `generate_chart` writes does the same, and a new function on each render would rebuild the host.
664
+ - **The result's `capabilities.lasso` means the chart takes chart-host's lasso (`@bicharts/chart-host` 0.6.84 or later).**
665
+ Pass `lasso: { capable: true, enabled: true }` in that chart's `options` (add `rowCap` when the result gives one), and a
666
+ drag around marks selects the ones inside through the same `onSelect` / `selects` a click uses: the gesture and the
667
+ selection are the host's, so there is no handler to write. The component `generate_chart` writes passes it already.
668
+ Without `capabilities.lasso` the chart has no lasso, and `lasso: { enabled: false }` turns it off.
639
669
 
640
670
  **Layout: a chart is never clipped or squashed.** Charts sit in cards, and a card is where they get cut off.
641
671
  - Render every chart inside `BicChartPanel`. It sizes the chart from its cell, never from the chart.
@@ -708,7 +738,7 @@ Anything saved against a mark - a note, a flag - is keyed the same way, never by
708
738
  More the page gets from the same filters, each one line:
709
739
  - A link that opens the page as it was: `useBicUrlState()` in the page component (after its `useBicFilter` /
710
740
  `useBicControls` calls) keeps every filter and every chart's controls in `?view=`. `useBicView()` gives `save()` /
711
- `restore(view)` for a saved view of your own (a user's favourite), and `token()` for a link.
741
+ `restore(view)` for a saved view of your own (a user's favorite), and `token()` for a link.
712
742
  - Linked hover: `useBicFilter("CountryIso3", { hover: true })` - hovering a mark lights the same key's marks on every
713
743
  chart bound to that filter (a glow, never a selection).
714
744
  - A filter that narrows a query to nothing: `<BicChartPanel ... onClearFilters={country.clear}>` says so with a Clear,
@@ -835,7 +865,7 @@ chart in a `BicChartPanel`), draws every chart together, and compares the stack
835
865
  the line and the call to use instead; fix the ERRORs in the app's own files before deploying.
836
866
  You don't need to deploy to check a chart. The template's `validate:visual` checks `VegaVisual` factories; a
837
867
  RegiaBI chart's check is `check_chart` - don't list it there. `check_chart` and `check_app` check the charts and how
838
- the app uses them; the template's app-validation in the portal embed still applies. Then `npx tsc -b` and the tests. To test the page's behaviour,
868
+ the app uses them; the template's app-validation in the portal embed still applies. Then `npx tsc -b` and the tests. To test the page's behavior,
839
869
  `@bicharts/chart-host/testing` has a reader's gestures - `clickMark`, `clickEmpty`, `selectedRows` - for the checks
840
870
  worth keeping: a second click on a mark clears its filter, and a page Clear clears the chart's marks. Deploying (`npx rayfin up`), on Rayfin 1.36.1:
841
871
  - Signed in? `npx rayfin login status` (a subcommand, not a `--status` flag); `npx rayfin login` if not.