@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/README.md +4 -1
- package/dist/check-chart.mjs +86 -86
- package/dist/index.mjs +111 -109
- package/package.json +3 -3
- package/skills/bic-charts/CHART-SHAPES.md +2 -0
- package/skills/bic-charts/SKILL.md +48 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-mcp",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
51
|
-
"@bicharts/shape-core": "^0.6.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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); //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|