@bicharts/chart-mcp 0.6.108 → 0.6.109

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.108",
3
+ "version": "0.6.109",
4
4
  "description": "AI Data Components for Power BI, Excel, Fabric Apps and the web, over MCP: bivariate maps, what-if projections and about 90 everyday chart types, with selection and cross-filtering already wired. It profiles your table locally; the code it writes is yours and runs with no license.",
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.108",
51
- "@bicharts/shape-core": "^0.6.108",
50
+ "@bicharts/chart-host": "^0.6.109",
51
+ "@bicharts/shape-core": "^0.6.109",
52
52
  "@modelcontextprotocol/sdk": "^1.32.1",
53
53
  "@types/node": "^26.6.5",
54
54
  "@types/papaparse": "^5.3.14",
@@ -215,6 +215,13 @@ the next `generate_chart`, or, for a live setting, an option in the emitted comp
215
215
  unattended run), ask nothing, use the defaults, and tell the user which settings were assumed from
216
216
  `settings.applied`. `get_guide` with topic `settings` lists every row with its options.
217
217
 
218
+ **Your final message keeps what the engine chose apart from what falls short.** Under what the engine chose, list its
219
+ choices (the `settings.applied` rows it picked - `default`, `server-roll` - and `engineChoices`) and each chart's limits
220
+ (a width it won't draw below, so the layout stacks it; a row cap; a type a count ruled out). Those are choices and
221
+ limits, not gaps. Keep "falls short" (or however you head it) for an ask the app doesn't meet, each with why and what
222
+ would meet it. A column printed as text is the engine's choice unless the person asked for every column drawn; then
223
+ it falls short, and says so.
224
+
218
225
  **Generation takes minutes.** Fire the calls for independent charts **in parallel** rather
219
226
  than in sequence — it is the difference between ~4 minutes and ~12.
220
227
 
@@ -579,6 +586,21 @@ has passed the template's check: don't run it again with `run-dax-query`. Don't
579
586
  table and aggregate it in the browser: the Execute Queries API refuses a response over 15 MB, and a
580
587
  column repeated on every fact row (a country's population on each sale) sums to nonsense.
581
588
 
589
+ **A "vs" measure is blank where there's nothing to compare against.** `[Goal Attainment %] - 1` is never blank:
590
+ BLANK() - 1 is -1, so SUMMARIZECOLUMNS keeps every row, a place with no goal reads -100%, and on a symmetric diverging
591
+ scale that one mark sets the span and every other draws near-neutral. The same goes for `1 - [Share]` and for
592
+ `[Bookings] - [Bookings Goal]` (with no goal, the whole value reads as the gap). Write each so it is blank where its base
593
+ is:
594
+
595
+ ```dax
596
+ "Vs Goal %", DIVIDE([Bookings] - [Bookings Goal], [Bookings Goal]) -- blank where there is no goal
597
+ "Vs Goal %", IF(NOT ISBLANK([Goal Attainment %]), [Goal Attainment %] - 1)
598
+ ```
599
+
600
+ `query_semantic_model` flags the never-blank forms in the query it runs (and a column at exactly -1 where its base is
601
+ blank), `check_app` flags them in the app's `.dax` files, and `check_chart` warns when one value sets a diverging color
602
+ scale's span.
603
+
582
604
  **2. Profile and generate from THE WHOLE RESULT of that query - every row, never a few pasted in.**
583
605
  Eligibility reads row, region and category counts. A bivariate country map needs at least 24
584
606
  countries, so the same 40-country query shown as 8 sample rows hides it, and nothing fails - you are
@@ -661,13 +683,19 @@ import QUERY from "@/queries/overview/occupancy-map.dax?raw"; // the template'
661
683
  const result = useSemanticModelQuery({ connection: "<connector name>", query: QUERY }); // the DAX it was generated on
662
684
  const options = useBicChartOptions();
663
685
  const country = useBicFilter("CountryIso3", { label: "Country", display: "CountryName" }); // step 6
664
- <BicChartPanel result={result}>
686
+ <BicChartPanel result={result} query={QUERY}>
665
687
  {(table, size) => ( // table = the live { columns, rows } result
666
688
  <BivariateWorldChoroplethChart table={table} {...size} options={options} selects={country} />
667
689
  )}
668
690
  </BicChartPanel>
669
691
  ```
670
692
 
693
+ **Give the panel the query string its hook gets (`query`).** The template's hook keeps its last result while the next one
694
+ loads, so a result can't say which query it answers; the panel keeps the query it last drew, and while a filter's new
695
+ rows load it dims the last chart under "Updating...", and a query that fails says so over it. Without `query`, old
696
+ figures can sit at full strength under the new selection's label. Build a filtered query once, in a variable, and pass
697
+ that variable to both.
698
+
671
699
  **A reader's view outlives the page only if you keep it.** What a reader sets inside a chart - an Interactive table's sort,
672
700
  column widths and hidden columns, a 3D camera, an expanded row - lives while the chart is mounted and is gone on reload.
673
701
  Pass `rememberAs="<a key per chart>"` to keep it in that browser, or `viewState={{ load, save }}` to keep it per signed-in
@@ -745,6 +773,9 @@ function CountryMap({ table, onPick }) { // table = the live query resu
745
773
  app-validation skill asks); only the card around it and the panel below may grow.
746
774
  - One frame per chart: a RegiaBI chart sits in your card. The template's `VisualContainer` frames its `VegaVisual`
747
775
  charts; don't put a second frame around one of ours.
776
+ - A card's title and subtitle are static text, so they never describe what the reader can change: no sort order
777
+ ("highest first", "sorted by..."), no current pick, no flipped face. One click on a header and "highest occupancy
778
+ first" is false. Say what the chart shows; the chart shows its own sort and selection.
748
779
  Give the chart's cell at least the chart's `minChartHeight` (`h-72` is 288px, under a 300 minimum).
749
780
 
750
781
  **6. Filter the page with `useBicFilter` - never write a select handler, a toggle or a Clear.** A page filter is
@@ -761,15 +792,37 @@ import FORECAST_DAX from "@/queries/overview/bookings-forecast.dax?raw";
761
792
  const country = useBicFilter("CountryIso3", { label: "Country", display: "CountryName" });
762
793
  const route = useBicFilter(["OriginCode", "DestinationCode"],
763
794
  { label: "Route", display: ["OriginName", "DestinationName"] });
764
- const forecast = useSemanticModelQuery({ connection: "<connector name>",
765
- query: filterQuery(FORECAST_DAX, treatAs(country, "Geography[CountryIso3]")) });
795
+ const forecastQuery = filterQuery(FORECAST_DAX, treatAs(country, "Geography[CountryIso3]"));
796
+ const forecast = useSemanticModelQuery({ connection: "<connector name>", query: forecastQuery });
766
797
 
767
798
  <BicFilterChips /> {/* "Country: Japan ×" - what's filtered, and its clear */}
768
799
  <BivariateWorldChoroplethChart table={countries} {...size} options={options} selects={country} />
769
800
  <OriginDestinationFlowMapChart table={routes} {...size} options={options} selects={route}
770
801
  filterBy={{ filter: country, columns: ["OriginCode", "DestinationCode"] }} />
802
+ <BicChartPanel result={forecast} query={forecastQuery}>{(table, size) => <WhatIfProjectionChart table={table} {...size} />}</BicChartPanel>
771
803
  ```
772
804
 
805
+ **"A click filters everything else" means every OTHER panel follows it.** Each panel the person didn't click shows only
806
+ what the pick matches: its query goes through `filterQuery` / `treatAs` (or `filterBy`, when its rows carry the key),
807
+ KPIs and tables included - a table that follows a click shows only the matching rows. The chart that was clicked keeps
808
+ all its marks and highlights the pick: a chart never filters itself, so the reader can change the pick there. When the
809
+ person names more than one chart to click ("on the map or in the table"), give each its own filter on the same key,
810
+ and pass every panel's query through a `treatAs` for each of those filters but its own - one filter shared by two
811
+ clicked charts leaves each unfiltered by the other's click:
812
+
813
+ ```tsx
814
+ const fromMap = useBicFilter("CountryIso3", { label: "Country (map)", display: "CountryName" });
815
+ const fromTable = useBicFilter("CountryIso3", { label: "Country (table)", display: "CountryName" });
816
+ const COUNTRY = "Geography[CountryIso3]";
817
+ const mapQuery = filterQuery(MAP_DAX, treatAs(fromTable, COUNTRY)); // the map follows the table's pick
818
+ const tableQuery = filterQuery(TABLE_DAX, treatAs(fromMap, COUNTRY)); // the table follows the map's
819
+ const kpiQuery = filterQuery(KPI_DAX, treatAs(fromMap, COUNTRY), treatAs(fromTable, COUNTRY)); // the rest follow both
820
+ // <MapChart ... selects={fromMap} /> and <TableChart ... selects={fromTable} />; every panel gets its query={...}
821
+ ```
822
+
823
+ `check_app` lists each chart's wiring - filtered query, highlight only, or none - and warns on a panel no click
824
+ reaches. Say that list in your reply, with any panel you left whole and why, so a gap is seen before deploy.
825
+
773
826
  What the library does, so the page doesn't:
774
827
  - A click sets the filter. The same click again, or a click on empty canvas, clears it. Ctrl-click adds a key.
775
828
  - A lasso is a selection too. On a chart whose result has `capabilities.lasso`, a drag around marks arrives through the same
@@ -836,6 +889,10 @@ dropped the `src/fabric.generated.ts` line. It writes `rayfin/tsconfig.json` - k
836
889
  writes no `rayfin/data/schema.ts` either: don't write one by hand - `setup_fabric_app` with `features` (step c) writes it
837
890
  when `rayfin/rayfin.yml` shows the data service on.
838
891
 
892
+ Then turn Rayfin's functions on, because each save runs on the server, which stamps its time (step e):
893
+ `npx rayfin functions init`. It scaffolds `rayfin/functions` (its `function_app.ts` and generated `types.ts`) and sets
894
+ `services.functions` in `rayfin/rayfin.yml`, with `auth.type: application`.
895
+
839
896
  *b. Make it type-check.* Three edits, each needed on the data app template:
840
897
  - `rayfin/tsconfig.json`, in `compilerOptions`: `"tsBuildInfoFile": ".temp/tsconfig.tsbuildinfo"` and
841
898
  `"allowImportingTsExtensions": false` (it inherits both from the root: TS6377 and TS5096 otherwise).
@@ -853,28 +910,37 @@ TS2345 names two copies of `rayfin-auth`, something installed only some Rayfin p
853
910
  *c. The files: `setup_fabric_app` with `features: ["notes"]`.* With the data service on, it writes them finished and
854
911
  never rewrites them:
855
912
  - `rayfin/data/MarkNote.ts`, registered in `rayfin/data/schema.ts` - keyed by the mark's model key, never a row
856
- position: `chart`, `markKey`, `markKeyTo` (a route's other end), `body`, `author`, `createdAt`. Every entity it
857
- writes has an explicit permission decorator and a `max` on every `@text` (MSSQL gets `NVARCHAR(MAX)` without one,
858
- which can break the deployed GraphQL schema).
913
+ position: `chart`, `markKey`, `markKeyTo` (a route's other end), `body`, `author`, `authorId`, `createdAt`. Every
914
+ entity it writes has an explicit permission decorator and a `max` on every `@text` (MSSQL gets `NVARCHAR(MAX)` without
915
+ one, which can break the deployed GraphQL schema). Its create permission carries a policy,
916
+ `claims.sub.eq(item.authorId)`: the server takes a note only under the caller's own id.
859
917
  - `src/lib/bic-notes.ts`: `useMarkNotes(author)` - every note (paged with `.executePaginated()`: one query returns at
860
918
  most 100 and doesn't say there are more), an `error` when they can't load, and `add(chart, key, body)`, which saves
861
- with `.create(` and reloads, and REJECTS when the save fails. The author is the signed-in user:
862
- `useAuth().session?.user?.email`.
919
+ through the `addMarkNote` Rayfin function and reloads, and REJECTS when the save fails. The author is the signed-in
920
+ user, `useAuth().session?.user` (its id and email); with no one signed in, `add` refuses and says why - never "unknown".
921
+ - `rayfin/functions/src/bic-notes-functions.ts`, registered in `function_app.ts`: `addMarkNote`, which runs on the
922
+ server, stamps `createdAt` from the server's clock and saves with `.create(` as the caller, so the policy still checks
923
+ who. Written once Rayfin's functions are on (step a); before that, `setup_fabric_app` says to run
924
+ `npx rayfin functions init` and then itself again.
863
925
  - `src/components/bic-notes-dialog.tsx`: `NotesDialog`, over the page through `createPortal`, never inside the
864
926
  chart's card (a card has a fixed height and `overflow-hidden`, and clips it). The reader's text is cleared only once
865
- the save succeeds; a failure is shown with the text kept.
927
+ the save succeeds; a failure is shown with the text kept. Tab stays inside it, Escape closes it, and focus goes back
928
+ to what opened it (`useDialogFocus`, from `src/lib/bic-dialog.ts`, which `setup_fabric_app` always writes).
866
929
 
867
- *d. Badges on the marks: pass `annotations`, never measure the chart.* The host draws each badge on its key's mark (on a
868
- table row, just after the row's label; on a chart type that asks for it, such as a tile map, a small dot inside the
869
- tile's top-right corner with the `label` and `title` in its tooltip) after every render and keeps it there through filters, re-queries and zooms; it never reads a row position. A route is
870
- keyed by BOTH of its ends with `where` - no key column to add, no query to change and no chart to regenerate:
930
+ *d. Badges on the marks: pass `annotations`, never measure the chart.* The host draws each badge on its key's mark after
931
+ every render and keeps it there through filters, re-queries and zooms; it never reads a row position. On a table row it
932
+ is a small badge just after the row's label, showing its `label` (`noteBadges` makes that the count of notes). On a
933
+ chart type that asks for it, such as a tile map, it is a plain dot inside the tile's top-right corner with no number on
934
+ it: the `label` and `title` are in its tooltip. Describe it that way to the person - a dot whose tooltip gives the
935
+ count, not a numbered marker. A route is keyed by BOTH of its ends with `where` - no key column to add, no query to
936
+ change and no chart to regenerate:
871
937
 
872
938
  ```tsx
873
939
  import { noteBadges, notesFor, badgeKey } from "@bicharts/chart-host/react";
874
940
 
875
941
  import { useAuth } from "@/hooks/auth.context";
876
942
 
877
- const { notes, add } = useMarkNotes(useAuth().session?.user?.email);
943
+ const { notes, add } = useMarkNotes(useAuth().session?.user);
878
944
  <BivariateWorldChoroplethChart table={table} {...size} options={options} selects={country}
879
945
  annotations={noteBadges(notes, { chart: "occupancy-map", columns: "CountryIso3" })}
880
946
  onAnnotationClick={a => setOpen({ chart: "occupancy-map", columns: "CountryIso3", key: badgeKey(a) })} />
@@ -894,6 +960,60 @@ or bounding boxes to place anything: that breaks on the next redraw, and this do
894
960
  The files use the template's own tokens; restyle them as you like. The table and the permissions are created on
895
961
  the next `npx rayfin up`, and every later `rayfin up` applies entity changes.
896
962
 
963
+ *e. Anything else the app writes (action items, flags, approvals) keeps the same rules.* Write it the way the notes
964
+ files do:
965
+ - **Who** is the signed-in user, `useAuth().session?.user`: store its `id` beside the name you show, and give the
966
+ entity's create permission a policy that checks it - `@role<Item>('authenticated', 'create', { policy: (claims, item) =>
967
+ claims.sub.eq(item.createdById) })` - so the server refuses a row filed under anyone else. With no one signed in,
968
+ refuse the save and say why (the form keeps what was typed); never store "unknown".
969
+ - **When** is the server's clock, never the browser's: every write that records a time (an item created, an item
970
+ marked done) goes through a Rayfin function that stamps it on the server. Rayfin's data layer (checked on 1.36.2) has
971
+ no server-time default for a field (a `default` is a fixed value), so the browser never sends the time at all. The
972
+ function saves with `ctx.getDataClient()`, which carries the caller's identity and permissions, so the who-check
973
+ above still holds.
974
+ - **A change that happens once** (done, approved, closed) is its own row, never an update anyone can repeat: an update
975
+ permission with no policy lets a second person mark a done item done again and overwrite who closed it. Key the row
976
+ by the item with a unique field, under the same who-check, so a second one fails at the database and the first
977
+ stays; an item is open while it has no such row, and only an open item offers the action.
978
+
979
+ ```ts
980
+ // rayfin/data/TaskDone.ts
981
+ @entity()
982
+ @authenticated('read')
983
+ @role<TaskDone>('authenticated', 'create', { policy: (claims, item) => claims.sub.eq(item.doneById) })
984
+ export class TaskDone {
985
+ @uuid() id!: string;
986
+ @uuid({ unique: true }) taskId!: string; // one per task: a second "done" fails, so the first completer stays
987
+ @text({ max: 320 }) doneBy!: string; // the name shown (the session's email)
988
+ @text({ max: 128 }) doneById!: string; // session.user.id, checked against the caller by the policy
989
+ @date() doneAt!: Date; // the server's clock: set only in markTaskDone below
990
+ }
991
+
992
+ // rayfin/functions/src/function_app.ts (next to the setup's registrations), after `const udf = new UserDataFunctions();`
993
+ udf.func("markTaskDone", async (taskId: string, doneBy: string, doneById: string,
994
+ ctx: RayfinContext<AppSchema>): Promise<void> => {
995
+ await ctx.getDataClient().TaskDone.create({ taskId, doneBy, doneById, doneAt: new Date() }); // the server's time
996
+ }, []);
997
+
998
+ // in the app: no time from the browser
999
+ await getRayfinClient().functions.markTaskDone.invoke({ taskId: task.id, doneBy: user.email, doneById: user.id });
1000
+ ```
1001
+
1002
+ `RayfinContext` comes from `@microsoft/fabric-user-data-functions` and `AppSchema` from `../../data/schema.js`
1003
+ (`import type` for both). A function's input is the named values `invoke` passes, so give each one a type.
1004
+
1005
+ **What this can't stop yet.** A Rayfin function writes with the same permissions as the browser, and Rayfin has no
1006
+ server-time default for a field and no permission that only a function holds. So the app's own saves always carry the
1007
+ server's time, but a request someone writes by hand against the data API can still set its own. Say so if the person
1008
+ asks for an audit trail; a database the app reaches only through functions would close it, and Rayfin doesn't offer
1009
+ that today.
1010
+
1011
+ - **A dialog of your own** (a form over the page) uses `useDialogFocus(ref, onClose)` from `src/lib/bic-dialog.ts`:
1012
+ Tab stays inside it, Escape closes it, and focus goes back to what opened it.
1013
+
1014
+ `check_app` flags a placeholder name in a write, a time written from the browser's clock, an entity that takes any
1015
+ name on create or lets any row be updated again, and a dialog that doesn't hold the keyboard.
1016
+
897
1017
  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.
898
1018
 
899
1019
  **7. A chart's own controls are page state: `useBicControls`.** A What-if chart's sliders (a rate, a horizon) are drawn
@@ -905,7 +1025,7 @@ import { ScenariosPanel } from "@/components/bic-scenarios-panel"; // setup_fa
905
1025
 
906
1026
  const scenario = useBicControls();
907
1027
  <WhatIfProjectionChart table={table} {...size} options={options} controls={scenario} />
908
- <ScenariosPanel chart="bookings-forecast" controls={scenario} author={useAuth().session?.user?.email} />
1028
+ <ScenariosPanel chart="bookings-forecast" controls={scenario} author={useAuth().session?.user} />
909
1029
  ```
910
1030
 
911
1031
  `scenario.values` holds every slider's value from the first draw - no slider has to move before a scenario can be
@@ -914,7 +1034,8 @@ horizon 12 months"), so a saved scenario shows the unit the chart shows. `scenar
914
1034
  returns to the chart's defaults. Don't read `uiState.knobs` or the chart's source, and never save a default you made
915
1035
  up. `features: ["scenarios"]` writes the `SavedScenario` entity, `useSavedScenarios` and `ScenariosPanel` (save under a
916
1036
  name, list, apply, and which one is applied now - `applied`, the saved scenario whose values the chart shows; a failed
917
- save keeps what was typed).
1037
+ save keeps what was typed), and, with Rayfin's functions on, the `addSavedScenario` function each save runs through, so
1038
+ the server stamps its time.
918
1039
 
919
1040
  **8. What stays the template's job - this is additive, never a replacement:** sign-in, the query hooks
920
1041
  (`useSemanticModelQuery`; a page filter feeds it through `filterQuery`), the theme (`useCssTheme`, which
@@ -926,12 +1047,18 @@ deployment. The chart is a function you render inside them.
926
1047
  at the size the panel will give it - by default it reads the chart's `minChartWidth` and `minChartHeight`
927
1048
  off its `BicChartPanel`, so pass no size unless you mean another one. A `WARNING` line in its reply is a
928
1049
  label cut off at that size: deal with it before you call the chart done. The reply says whether more room fixes it.
1050
+ So is a `WARNING` that one value sets a color scale's span (the rest then look even): fix the query or regenerate.
1051
+ For a table, its `Table` lines say how each column draws and what stays in place as the rows scroll, read from the
1052
+ drawn table: quote them in your reply wherever the person asked how the table draws or what stays put, and when they
1053
+ differ from the ask (a column printed where they asked for marks, more held in place than they allowed), regenerate
1054
+ with a prompt that names the difference.
929
1055
  Never edit the generated chart code by hand: if something's wrong with a generated chart, regenerate it
930
1056
  (`generate_chart` with a prompt describing the fix) or report it.
931
1057
  Then run `check_app` once on the whole app, before every deploy: it reads how the page uses the library (selection
932
- through `useBicFilter` + `selects`, a chart's own controls through `useBicControls`, notes through `annotations`, every
933
- chart in a `BicChartPanel`), draws every chart together, and compares the stack with npm. Each finding names the file,
934
- the line and the call to use instead; fix the ERRORs in the app's own files before deploying.
1058
+ through `useBicFilter` + `selects`, each chart's filter wiring, a chart's own controls through `useBicControls`, notes
1059
+ through `annotations`, every chart in a `BicChartPanel`), draws every chart together, and compares the stack with npm.
1060
+ Each finding names the file, the line and the call to use instead; fix the ERRORs in the app's own files before
1061
+ deploying, and say its filter-wiring list in your reply.
935
1062
  You don't need to deploy to check a chart. The template's `validate:visual` checks `VegaVisual` factories; a
936
1063
  RegiaBI chart's check is `check_chart` - don't list it there. `check_chart` and `check_app` check the charts and how
937
1064
  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,