@bicharts/chart-mcp 0.6.52 → 0.6.55
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 +7 -1
- package/dist/check-chart.mjs +305 -0
- package/dist/index.mjs +92 -77
- package/package.json +11 -7
- package/skills/bic-charts/CHART-SHAPES.md +5 -4
- package/skills/bic-charts/SKILL.md +193 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.55",
|
|
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). Requires a BIC trial or paid account.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -41,17 +41,21 @@
|
|
|
41
41
|
"prestart": "node build.mjs",
|
|
42
42
|
"start": "node dist/index.mjs"
|
|
43
43
|
},
|
|
44
|
-
"dependencies":
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
46
|
-
"papaparse": "^5.4.1",
|
|
47
|
-
"zod": "^3.23.0"
|
|
48
|
-
},
|
|
44
|
+
"// 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
45
|
"devDependencies": {
|
|
50
46
|
"@bicharts/chart-host": "^0.6.52",
|
|
51
47
|
"@bicharts/shape-core": "^0.6.52",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
52
49
|
"@types/node": "^20.0.0",
|
|
53
50
|
"@types/papaparse": "^5.3.14",
|
|
51
|
+
"d3": "^7.9.0",
|
|
52
|
+
"d3-hexbin": "^0.2.2",
|
|
53
|
+
"d3-sankey": "^0.12.3",
|
|
54
|
+
"d3-voronoi-treemap": "^1.1.2",
|
|
54
55
|
"esbuild": "^0.28.1",
|
|
55
|
-
"
|
|
56
|
+
"happy-dom": "^20.14.5",
|
|
57
|
+
"papaparse": "^5.4.1",
|
|
58
|
+
"typescript": "^5.4.0",
|
|
59
|
+
"zod": "^3.23.0"
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -59,7 +59,7 @@ on the same picture, not a looser one.
|
|
|
59
59
|
| Flippable multi-card | 1 continuous | <=12 categories, needs 180x120 px | D3 | Best when more headline numbers compete for one tile than fit at once: several measures side by side, or one or two measures paged through a small category, with the details on hover |
|
|
60
60
|
| Funnel chart | 1 continuous | <=12 categories, needs 460x340 px | D3, PLOTLY | Best for conversion/attrition pipelines: signup funnels, sales pipeline stages, checkout flow drop-off |
|
|
61
61
|
| Gantt chart | 1 categorical, 2 temporal | <=60 categories, needs 420x260 px | D3 | Best for project and phase schedules: tasks with a start and an end date, optionally grouped by phase, workstream or owner |
|
|
62
|
-
| Gauge | 1 continuous, a bounded scale | needs 140x140 px | D3 |
|
|
62
|
+
| Gauge | 1 continuous, a bounded scale | needs 140x140 px | D3 | A value against a ceiling the data states - a percentage, a rate, or a measure that has a target - overall or one dial per category; a constant value is a valid reading |
|
|
63
63
|
| Grouped bar chart | 2 categorical, 1 continuous | needs 700x480 px | D3, Matplotlib, Plotly, Seaborn | Requires one categorical, one subcategory, and one numeric variable |
|
|
64
64
|
| Heatmap | 2 categorical, 1 continuous | <=50 categories, needs 460x340 px | D3, Matplotlib, Plotly, Seaborn | Requires two categorical or ordinal fields for the rows and columns, and one measure for the cell colour |
|
|
65
65
|
| Hexbin plot | 2 continuous | needs 280x280 px | D3, Matplotlib, Plotly, Seaborn | Requires two continuous variables |
|
|
@@ -71,7 +71,7 @@ on the same picture, not a looser one.
|
|
|
71
71
|
| KDE plot (1D & 2D) | 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires one continuous variable (or two for 2D KDE) |
|
|
72
72
|
| Lag plot (scatter of value vs lag) | 1 continuous, 20+ time points | needs 280x280 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires a datetime series with one continuous variable |
|
|
73
73
|
| Line chart | 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly, Seaborn | One or more continuous series over an ordered/temporal axis |
|
|
74
|
-
| Linear gauge | 1 continuous, a bounded scale | needs 180x50 px | D3 |
|
|
74
|
+
| Linear gauge | 1 continuous, a bounded scale | needs 180x50 px | D3 | A value against a ceiling the data states, overall or one track per category, in a short wide tile - percent complete, utilisation, progress to a target; a constant value is a valid reading |
|
|
75
75
|
| Lollipop chart | 1 categorical, 1 continuous | <=50 categories, needs 600x420 px | D3, PLOTLY | Best as a cleaner bar replacement for ranking many categories by a single measure |
|
|
76
76
|
| Marimekko / Mosaic plot | 2 categorical, 1 continuous | needs 320x280 px | D3 | Best for 3-10 outer × 3-10 inner categories with a positive measure |
|
|
77
77
|
| Mermaid diagram | 1 categorical | <=150 categories, needs 360x240 px | D3 | A source and a target column (a process, a hand-off chain, a dependency list), two to four nested categories (an org or product hierarchy), or a date beside an event name (milestones, releases, history) - up to a few dozen boxes |
|
|
@@ -87,12 +87,13 @@ on the same picture, not a looser one.
|
|
|
87
87
|
| Pareto chart | 1 categorical | <=30 categories, needs 320x280 px | D3, MATPLOTLIB, PLOTLY | Best for finding the vital few: defect/complaint/incident counts by cause, revenue concentration by product or customer (the 80/20 view) |
|
|
88
88
|
| Pie chart | 1 categorical | <=10 categories, needs 280x280 px | D3, Matplotlib, Plotly | Requires one categorical variable with proportions |
|
|
89
89
|
| Polar plot | 1 continuous | needs 299x283 px | D3, Matplotlib, Plotly | Requires angle and radius (two continuous variables) |
|
|
90
|
-
| Progress ring | 1 continuous, a bounded scale | needs 160x160 px | D3 |
|
|
90
|
+
| Progress ring | 1 continuous, a bounded scale | needs 160x160 px | D3 | A value against a ceiling the data states, overall or one ring per category, in a small or square tile - percent complete, attainment, utilisation; a constant value is a valid reading |
|
|
91
91
|
| Radar (spider) chart | 1 continuous | <=8 axes, needs 700x480 px | D3, Matplotlib, Plotly | Requires multiple continuous variables across categories |
|
|
92
92
|
| Range-compare time series | 1 continuous, 15+ time points | needs 480x320 px | D3 | Best for a single measure tracked over many dates when the story is a chosen window: drag the two handles to total a period and compare its endpoints, like revenue across a campaign or sensor readings around an incident |
|
|
93
93
|
| Regression plot (line with CI) | 2 continuous | needs 520x380 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires two continuous variables - a predictor and a response |
|
|
94
94
|
| Ridgeline plots | 1 categorical, 1 continuous | <=25 categories, needs 280x280 px | D3, Matplotlib, Plotly, Seaborn | Best with many observations per category |
|
|
95
95
|
| Rose (Coxcomb) chart | 1 categorical | <=24 categories, needs 300x300 px | D3 | Best for cyclical or directional categories: wind direction, hour of day, month of year, compass sectors - anywhere the categories wrap around rather than running left to right |
|
|
96
|
+
| Rotating carousel | 1 continuous | <=12 categories, needs 320x200 px | D3 | Best when each category value deserves its own small chart and the tile has room for only one at a time; the reader turns it, or it turns itself |
|
|
96
97
|
| Rug plot | 1 continuous | needs 360x280 px | D3, Matplotlib, Plotly, Seaborn | Requires one continuous variable |
|
|
97
98
|
| Sankey alluvial flow | 2 categorical, 1 continuous | <=10 categories, <=80 edges, needs 420x280 px | D3, Matplotlib, Plotly | Requires at least two categorical variables |
|
|
98
99
|
| Scatter plot | 2 continuous | needs 460x340 px | D3, Matplotlib, Plotly, Seaborn | Requires two continuous variables |
|
|
@@ -108,7 +109,7 @@ on the same picture, not a looser one.
|
|
|
108
109
|
| Sunburst chart | 2 categorical, 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly | Requires hierarchical categorical data |
|
|
109
110
|
| Tabular with embedded | 1 categorical, 1 continuous | <=40 categories, needs 420x200 px | D3 | Best for a small set of items compared across several attributes at once (a forecast/roster/scorecard table) |
|
|
110
111
|
| Ternary plot | 3 continuous | needs 320x320 px | D3 | Three measures that add up to the same total on every row - product mix, budget allocation, vote share, channel split |
|
|
111
|
-
| Thermometer | 1 continuous, a bounded scale | needs 90x220 px | D3 |
|
|
112
|
+
| Thermometer | 1 continuous, a bounded scale | needs 90x220 px | D3 | A value against a ceiling the data states, overall or one tube per category, in a tall narrow tile - a sidebar rail or a column of KPIs down a page edge; a constant value is a valid reading |
|
|
112
113
|
| Time series plot | 1 continuous | needs 460x340 px | D3, Plotly | Requires a datetime axis with at least one continuous variable |
|
|
113
114
|
| Trail | 2 continuous | needs 360x220 px | D3 | Best when a series has both a level and a weight: price with traded volume, sentiment with mention count, a metric with the sample size behind it |
|
|
114
115
|
| Treemap | 1 categorical, 1 continuous | needs 240x220 px | D3, Matplotlib, Plotly | Requires hierarchical categorical data with values |
|
|
@@ -21,6 +21,16 @@ Skip this if you already have a table. If you are holding a semantic model, a wa
|
|
|
21
21
|
a database, **you** pick the fields — the tools take a table, not a model, and they cannot see
|
|
22
22
|
what you can. Three decisions, and the third is the one that goes wrong.
|
|
23
23
|
|
|
24
|
+
**Asked which charts suit the model itself** ("good chart types for this dataset", "what could we
|
|
25
|
+
chart here")? Over a semantic model in a Fabric App, `suggest_charts` answers in one call: it reads
|
|
26
|
+
the model's tables, measures and relationships, finds its natural views (measures by place, flows
|
|
27
|
+
between places, measures over time, measures by category), queries each at its grain, keeps the
|
|
28
|
+
measures that vary there and asks the eligibility engine what fits each. Answer from its reply -
|
|
29
|
+
the counts in it are the model's own - and build the one the person picks from the rows it saved
|
|
30
|
+
(`.bic/data/<view>.csv`, the DAX beside it). Pass `views` (`columns` as `Table[Column]`, `measures`
|
|
31
|
+
by name, or your own `dax`) to ask about other groupings. Elsewhere, pull each candidate grouping
|
|
32
|
+
and ask `list_eligible_charts` about each.
|
|
33
|
+
|
|
24
34
|
**Fields and filters** follow from the question. *"All loans over the last 6 months, highlighting
|
|
25
35
|
those which defaulted, paying attention to credit score"* wants `LoanId`, `Defaulted`,
|
|
26
36
|
`CreditScore`, filtered to six months.
|
|
@@ -464,8 +474,14 @@ build.
|
|
|
464
474
|
`src/lib/bic-chart-options.ts` (`useBicChartOptions()`: the app's Fabric theme - palette, foreground,
|
|
465
475
|
background - as chart options, so charts follow light and dark) and `src/components/bic-chart-panel.tsx`
|
|
466
476
|
(`<BicChartPanel result={useSemanticModelQuery(...)}>`: a box that measures itself and shows the query's
|
|
467
|
-
loading, error and empty states), and ignores `.bic/`. It
|
|
468
|
-
|
|
477
|
+
loading, error and empty states), and ignores `.bic/`. It also puts every `@microsoft/rayfin-*` package on one version -
|
|
478
|
+
npm's latest, the release `rayfin init` installs, so run it before `rayfin init` and init has nothing left to split
|
|
479
|
+
(`rayfin init` installs the latest of only some packages, which leaves two copies of `rayfin-auth` on any other version).
|
|
480
|
+
Its reply lists every `package.json` range it changed and names the Rayfin version installed; an app already on npm's
|
|
481
|
+
latest is left alone. It fixes the template's own `scripts/validate-visual.spec.mjs`, which fails under ESM on a fresh
|
|
482
|
+
scaffold (its reply says so), and adds
|
|
483
|
+
`.agents/skills/regiabi-charts/SKILL.md` beside the template's skills, for agents that read that folder. It never
|
|
484
|
+
overwrites a file, and its reply ends with a worked example.
|
|
469
485
|
|
|
470
486
|
**1. Pull each chart's rows at the chart's GRAIN, with a query written for that chart.** One row per
|
|
471
487
|
country for a country map, one per route for a flow map, one per month for a trend. The template's
|
|
@@ -484,7 +500,7 @@ simply offered other charts. Save the result to a file and pass `csv_path`, or p
|
|
|
484
500
|
`RevenuePerCapita`, and apply the SAME rename to the live rows at run time - the chart reads columns by
|
|
485
501
|
name. Pass the model's metadata as you rename: `isMeasure`, `format` and `description` (from
|
|
486
502
|
`INFO.VIEW.MEASURES()` / `INFO.VIEW.COLUMNS()`) go on each column in `data`, which is the context a
|
|
487
|
-
Power BI visual gets for free.
|
|
503
|
+
Power BI visual gets for free. In `INFO.VIEW.COLUMNS()` a column's name is `[Name]`, not `[Column]`.
|
|
488
504
|
|
|
489
505
|
**Steps 1-3 are one call: `query_semantic_model`.** Give it the chart's DAX (`dax`, or a `.dax` file) and a
|
|
490
506
|
`name`; it runs the query through the project's own `fabric-app-data` CLI, renames the headers (dropping the
|
|
@@ -568,40 +584,200 @@ function CountryMap({ table, onPick }) { // table = the live query resu
|
|
|
568
584
|
- **`<BicChartGroup destination>` needs `@bicharts/chart-host` 0.6.50 or later.** Before it, a flow
|
|
569
585
|
map's far end is not built.
|
|
570
586
|
|
|
587
|
+
**Layout: a chart is never clipped or squashed.** Charts sit in cards, and a card is where they get cut off.
|
|
588
|
+
- Render every chart inside `BicChartPanel`. It sizes the chart from its cell, never from the chart.
|
|
589
|
+
- Give each chart its minimum: `minChartWidth` and `minChartHeight` (a world map about 560 x 300; a time series wants
|
|
590
|
+
height). Below that the panel scrolls; it never squashes or clips the chart.
|
|
591
|
+
- Take the actual size from the layout - percent, flex or grid `fr` - and never a fixed height smaller than the chart's
|
|
592
|
+
minimum.
|
|
593
|
+
- Put `min-h-0` on every flex child between a card and its chart (`min-w-0` in a row). A flex item's default
|
|
594
|
+
`min-height: auto` lets it grow past its card, and the card's `overflow-hidden` slices off the rest.
|
|
595
|
+
|
|
571
596
|
**6. Cross-filter across queries by KEY, not by row.** Charts fed by different queries don't share a
|
|
572
597
|
row space, so one group can't span them. Put each query's charts in their own group, turn a click
|
|
573
598
|
into the model's key (`CountryCode` above), keep it in app state, and filter the other queries with
|
|
574
599
|
it. Anything saved against a mark - a note, a flag - is keyed the same way, never by `__rowIdx__`
|
|
575
600
|
(see the row-index hazard above).
|
|
576
601
|
|
|
577
|
-
**Notes (or flags)
|
|
578
|
-
|
|
579
|
-
|
|
602
|
+
**Notes (or flags) on marks, end to end.** People leave a note on a country, a route, a product, and see
|
|
603
|
+
it on that mark: the notes live in Rayfin's data service, and the badge is the chart's `annotations`.
|
|
604
|
+
This recipe was checked against Rayfin's own docs for Rayfin 1.36.1 (a preview), the version
|
|
605
|
+
`setup_fabric_app` installs as this is written, and on that version it IS the version-matched source:
|
|
606
|
+
follow it as written, and read Rayfin's docs (`node_modules/@microsoft/rayfin-guide/assets/docs/data/`)
|
|
607
|
+
only for what it doesn't cover. `setup_fabric_app`'s reply names the version it installed; where that
|
|
608
|
+
differs from 1.36.1, Rayfin's docs win.
|
|
580
609
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
610
|
+
*a. Turn the data service on.* The data app template ships it off. Run `setup_fabric_app` first (it puts every `@microsoft/rayfin-*` package on one version:
|
|
611
|
+
npm's latest, the release `rayfin init` installs), then:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
npx rayfin init --services auth,data --auth-methods fabric --dialect mssql --overwrite --project-name <id from rayfin/rayfin.yml> .
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Each flag is needed: without `--overwrite` it prints "Initialization cancelled" and exits 0 with no
|
|
618
|
+
reason; without `--project-name` (pass the `id:` already in `rayfin/rayfin.yml`, so nothing is renamed)
|
|
619
|
+
it stops with "--project-name is required in non-interactive mode". It rewrites `rayfin/rayfin.yml`
|
|
620
|
+
and `.gitignore`, so `git diff` both and put back what's yours (on 1.36.1 it dropped the template's
|
|
621
|
+
`src/fabric.generated.ts` line from `.gitignore`). It writes `rayfin/tsconfig.json` - keep its
|
|
622
|
+
`outDir`, `.temp/compiled`, which is where the deploy reads the entities from - and no entities.
|
|
623
|
+
|
|
624
|
+
*b. Make it type-check.* Three edits, each needed on the data app template:
|
|
625
|
+
- `rayfin/tsconfig.json`, in `compilerOptions`: `"tsBuildInfoFile": ".temp/tsconfig.tsbuildinfo"` and
|
|
626
|
+
`"allowImportingTsExtensions": false` (it inherits both from the root: TS6377 and TS5096 otherwise).
|
|
627
|
+
- the root `tsconfig.json`: `"references": [{ "path": "./rayfin" }]`.
|
|
628
|
+
- type the client with your schema: `RayfinClient<AppSchema>` in `src/lib/rayfin-client.ts`
|
|
629
|
+
(`import type { AppSchema } from "../../rayfin/data/schema";`) and in the constructor of
|
|
630
|
+
`src/services/rayfin-auth.service.ts` (TS2345 otherwise).
|
|
631
|
+
|
|
632
|
+
Check with `npx tsc -b`; a bare `tsc --noEmit` reports TS6305 until the `rayfin/` project is built. If a
|
|
633
|
+
TS2345 names two copies of `rayfin-auth`, something installed only some Rayfin packages: run
|
|
634
|
+
`setup_fabric_app` again.
|
|
635
|
+
|
|
636
|
+
*c. The entity, keyed by the model's key - never by a row position.* `rayfin/data/MarkNote.ts`, then
|
|
637
|
+
register it in `rayfin/data/schema.ts` (`export type AppSchema = { MarkNote: MarkNote };`):
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import { entity, authenticated, uuid, text, date } from '@microsoft/rayfin-core';
|
|
641
|
+
|
|
642
|
+
@entity()
|
|
643
|
+
@authenticated(['read', 'create'])
|
|
644
|
+
export class MarkNote {
|
|
645
|
+
@uuid() id!: string;
|
|
646
|
+
@text({ max: 64 }) chart!: string; // which chart: "country-map", "lanes"
|
|
647
|
+
@text({ max: 128 }) markKey!: string; // the mark's model key (CountryCode)
|
|
648
|
+
@text({ max: 128, optional: true }) markKeyTo?: string; // a route's other end; empty for one mark
|
|
649
|
+
@text({ max: 2000 }) body!: string;
|
|
650
|
+
@text({ max: 320 }) author!: string;
|
|
651
|
+
@date() createdAt!: Date;
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Give every entity an explicit permission decorator (without one, any signed-in user gets every
|
|
656
|
+
action), and every `@text` a `max` (without it MSSQL gets `NVARCHAR(MAX)` columns, which can break
|
|
657
|
+
the deployed GraphQL schema). Use `.js` extensions on relative imports between entity files.
|
|
658
|
+
|
|
659
|
+
*d. Read every note, write one.* `.execute()` returns one page (100 rows) and doesn't say there are
|
|
660
|
+
more, so page through:
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
// src/lib/mark-notes.ts
|
|
664
|
+
import type { MarkAnnotation } from "@bicharts/chart-host";
|
|
665
|
+
import { getRayfinClient } from "@/lib/rayfin-client";
|
|
666
|
+
import type { MarkNote } from "../../rayfin/data/MarkNote";
|
|
667
|
+
|
|
668
|
+
const FIELDS = ["id", "chart", "markKey", "markKeyTo", "body", "author", "createdAt"] as const;
|
|
669
|
+
|
|
670
|
+
export async function loadMarkNotes(): Promise<MarkNote[]> { // newest first
|
|
671
|
+
const all: MarkNote[] = [];
|
|
672
|
+
let after: string | undefined;
|
|
673
|
+
do {
|
|
674
|
+
const q = getRayfinClient().data.MarkNote.select([...FIELDS]).orderBy({ createdAt: "desc" }).first(100);
|
|
675
|
+
const page = await (after ? q.after(after) : q).executePaginated();
|
|
676
|
+
all.push(...page.items);
|
|
677
|
+
after = page.hasNextPage ? page.endCursor ?? undefined : undefined;
|
|
678
|
+
} while (after);
|
|
679
|
+
return all;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function addMarkNote(n: Pick<MarkNote, "chart" | "markKey" | "markKeyTo" | "body">, author: string) {
|
|
683
|
+
return getRayfinClient().data.MarkNote.create({ ...n, author, createdAt: new Date() });
|
|
684
|
+
}
|
|
586
685
|
```
|
|
587
686
|
|
|
588
|
-
|
|
589
|
-
|
|
687
|
+
In a hook, load inside the effect - `loadMarkNotes().then(all => { if (live) setNotes(all); })` with a
|
|
688
|
+
`live` flag the cleanup clears; the template's lint rejects an effect that calls a state-setting
|
|
689
|
+
function directly. The author is the signed-in user: `useAuth().session?.user?.email`.
|
|
690
|
+
|
|
691
|
+
*e. Badges on the marks: pass `annotations`, never measure the chart.* The host draws each badge on
|
|
692
|
+
its key's mark after every render and keeps it there through filters, re-queries and zooms; it never
|
|
693
|
+
reads a row position. One key column is `column`/`value`. A route is keyed by BOTH of its ends with
|
|
694
|
+
`where` - no key column to add, no query to change and no chart to regenerate:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
/** One badge per mark: a single key column, or a route's two ends. */
|
|
698
|
+
export function noteBadges(notes: MarkNote[], chart: string, key: [string] | [string, string]): MarkAnnotation[] {
|
|
699
|
+
const byMark = new Map<string, MarkNote[]>();
|
|
700
|
+
for (const n of notes) {
|
|
701
|
+
if (n.chart !== chart) continue;
|
|
702
|
+
const id = n.markKeyTo ? `${n.markKey}|${n.markKeyTo}` : n.markKey;
|
|
703
|
+
byMark.set(id, [...(byMark.get(id) ?? []), n]);
|
|
704
|
+
}
|
|
705
|
+
return [...byMark].map(([id, list]) => ({
|
|
706
|
+
id, label: String(list.length), title: list[0].body,
|
|
707
|
+
...(key.length === 2
|
|
708
|
+
? { where: { [key[0]]: list[0].markKey, [key[1]]: list[0].markKeyTo ?? "" } }
|
|
709
|
+
: { column: key[0], value: list[0].markKey }),
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
```
|
|
590
713
|
|
|
591
714
|
```tsx
|
|
592
|
-
|
|
593
|
-
|
|
715
|
+
<BivariateWorldChoroplethChart table={table} {...size} options={options}
|
|
716
|
+
annotations={noteBadges(notes, "country-map", ["CountryCode"])}
|
|
717
|
+
onAnnotationClick={a => setOpenMark(a.id ?? null)} />
|
|
718
|
+
// a route by both ends: annotations={noteBadges(notes, "lanes", ["OriginCountryCode", "DestinationCountryCode"])},
|
|
719
|
+
// one badge per route, each { where: { OriginCountryCode: n.origin, DestinationCountryCode: n.dest }, label, title }
|
|
594
720
|
```
|
|
595
721
|
|
|
596
722
|
Don't read `data-row-idx` or bounding boxes to place anything: that breaks on the next redraw, and
|
|
597
723
|
this does it for you.
|
|
598
724
|
|
|
725
|
+
*f. Open the notes over the page, never inside the chart's card.* Open them from `onAnnotationClick`
|
|
726
|
+
for a mark that has notes, and for a mark's first note from your own "Add a note" action on the
|
|
727
|
+
selected mark (`onSelectRows` gives you its key). Show them in a dialog, drawer or popover that
|
|
728
|
+
overlays the page through a portal: a chart's card has a fixed height and `overflow-hidden`, so a
|
|
729
|
+
panel inside it is clipped.
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
import { useState } from "react";
|
|
733
|
+
import { createPortal } from "react-dom";
|
|
734
|
+
|
|
735
|
+
export function NotesDialog({ title, notes, onAdd, onClose }: {
|
|
736
|
+
title: string; notes: MarkNote[]; onAdd: (body: string) => void; onClose: () => void;
|
|
737
|
+
}) {
|
|
738
|
+
const [draft, setDraft] = useState("");
|
|
739
|
+
return createPortal(
|
|
740
|
+
<div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 1000, display: "grid",
|
|
741
|
+
placeItems: "center", background: "rgb(0 0 0 / 0.35)" }}>
|
|
742
|
+
<div role="dialog" aria-modal="true" aria-label={title} onClick={e => e.stopPropagation()}
|
|
743
|
+
style={{ width: "min(480px, 92vw)", maxHeight: "80vh", overflow: "auto", padding: 16,
|
|
744
|
+
borderRadius: 8, background: "Canvas", color: "CanvasText" }}>
|
|
745
|
+
<h2>{title}</h2>
|
|
746
|
+
<ul>{notes.map(n => <li key={n.id}>{n.body} - {n.author}, {new Date(n.createdAt).toLocaleString()}</li>)}</ul>
|
|
747
|
+
<textarea value={draft} onChange={e => setDraft(e.target.value)} maxLength={2000} />
|
|
748
|
+
<button disabled={!draft.trim()} onClick={() => { onAdd(draft.trim()); setDraft(""); }}>Add note</button>
|
|
749
|
+
<button onClick={onClose}>Close</button>
|
|
750
|
+
</div>
|
|
751
|
+
</div>,
|
|
752
|
+
document.body);
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Style it with the app's own tokens. The table and the permissions are created on the next
|
|
757
|
+
`npx rayfin up`, and every later `rayfin up` applies entity changes.
|
|
758
|
+
|
|
759
|
+
Don't stand up a backend to test notes; the deploy creates the tables. Finish with `npx tsc -b` and the tests, and say the notes go live on deploy.
|
|
760
|
+
|
|
599
761
|
**7. A reader's choice inside a chart is view state.** A what-if chart's rate and horizon are its
|
|
600
762
|
own controls, reported through `viewState` on `<BicChart>` (`uiState` / `setUiState` in the
|
|
601
763
|
integration contract). To save a named scenario, store that - don't rebuild the slider beside it.
|
|
602
764
|
|
|
603
|
-
**8. What stays the template's job:** sign-in, the query hooks, the app's database
|
|
604
|
-
|
|
765
|
+
**8. What stays the template's job:** sign-in, the query hooks, the app's database (Rayfin's data
|
|
766
|
+
service - the notes recipe above), `VisualContainer` headers, deployment. The chart is a function you
|
|
767
|
+
render inside them.
|
|
768
|
+
|
|
769
|
+
**9. Check every chart, then deploy.** Check every chart locally with this server's `check_chart` tool,
|
|
770
|
+
at the size the panel will give it - by default it reads the chart's `minChartWidth` and `minChartHeight`
|
|
771
|
+
off its `BicChartPanel`, so pass no size unless you mean another one. A `WARNING` line in its reply is a
|
|
772
|
+
label likely cut off at that size: deal with it before you call the chart done.
|
|
773
|
+
You don't need to deploy to check a chart. Then `npx tsc -b` and the tests. Deploying (`npx rayfin up`), on Rayfin 1.36.1:
|
|
774
|
+
- Signed in? `npx rayfin login status` (a subcommand, not a `--status` flag); `npx rayfin login` if not.
|
|
775
|
+
- Name the workspace: `npx rayfin up -w "<workspace>"`. Left out, it deploys to My Workspace.
|
|
776
|
+
- The Fabric item is named by `id:` in `rayfin/rayfin.yml`, not `name:`. Two projects with the same
|
|
777
|
+
`id:` in one workspace deploy into one item: give each its own `id:`, or deploy with
|
|
778
|
+
`--item-name <unique-name>`. Pass `--yes` to reuse an existing item only when you know it's yours.
|
|
779
|
+
- Deleted the app outside Rayfin? `rayfin/.deployments.json` still names the deleted item, so the next
|
|
780
|
+
`rayfin up` fails with 404 Not Found. Move that file aside and deploy again.
|
|
605
781
|
|
|
606
782
|
## 4. Things that will otherwise cost you time
|
|
607
783
|
|