@bicharts/chart-mcp 0.6.108 → 0.6.110
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/dist/check-chart.mjs +125 -113
- package/dist/index.mjs +131 -115
- package/package.json +3 -3
- package/skills/bic-charts/SKILL.md +154 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.110",
|
|
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.
|
|
51
|
-
"@bicharts/shape-core": "^0.6.
|
|
50
|
+
"@bicharts/chart-host": "^0.6.110",
|
|
51
|
+
"@bicharts/shape-core": "^0.6.110",
|
|
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,45 @@ 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
|
|
765
|
-
|
|
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} scopedBy={country} />}</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} scopedBy={fromTable} /> and <TableChart ... selects={fromTable} scopedBy={fromMap} />;
|
|
821
|
+
// <KpiChart ... scopedBy={[fromMap, fromTable]} />; every panel gets its query={...}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**A panel whose query takes a page filter passes that filter as `scopedBy`, so the chart says it's filtered.** Its rows
|
|
825
|
+
arrive already narrowed, and the chart can't see a filter applied to its query: a KPI deck kept its region's title over
|
|
826
|
+
one country's totals and ranked what the filter left. Given `scopedBy={country}` (or `scopedBy={[fromMap, fromTable]}`),
|
|
827
|
+
the chart names the pick while it is active - "North America · United States only", "rank 2 of 3 (United States
|
|
828
|
+
only)" - and goes back to its own titles when it clears. Pass every filter its query's `treatAs` takes; a panel narrowed
|
|
829
|
+
with `filterBy` needs nothing more, its component tells the chart itself.
|
|
830
|
+
|
|
831
|
+
`check_app` lists each chart's wiring - filtered query, highlight only, or none - and warns on a panel no click
|
|
832
|
+
reaches, and on a filtered-query panel with no `scopedBy`. Say that list in your reply, with any panel you left whole and why, so a gap is seen before deploy.
|
|
833
|
+
|
|
773
834
|
What the library does, so the page doesn't:
|
|
774
835
|
- A click sets the filter. The same click again, or a click on empty canvas, clears it. Ctrl-click adds a key.
|
|
775
836
|
- A lasso is a selection too. On a chart whose result has `capabilities.lasso`, a drag around marks arrives through the same
|
|
@@ -836,6 +897,10 @@ dropped the `src/fabric.generated.ts` line. It writes `rayfin/tsconfig.json` - k
|
|
|
836
897
|
writes no `rayfin/data/schema.ts` either: don't write one by hand - `setup_fabric_app` with `features` (step c) writes it
|
|
837
898
|
when `rayfin/rayfin.yml` shows the data service on.
|
|
838
899
|
|
|
900
|
+
Then turn Rayfin's functions on, because each save runs on the server, which stamps its time (step e):
|
|
901
|
+
`npx rayfin functions init`. It scaffolds `rayfin/functions` (its `function_app.ts` and generated `types.ts`) and sets
|
|
902
|
+
`services.functions` in `rayfin/rayfin.yml`, with `auth.type: application`.
|
|
903
|
+
|
|
839
904
|
*b. Make it type-check.* Three edits, each needed on the data app template:
|
|
840
905
|
- `rayfin/tsconfig.json`, in `compilerOptions`: `"tsBuildInfoFile": ".temp/tsconfig.tsbuildinfo"` and
|
|
841
906
|
`"allowImportingTsExtensions": false` (it inherits both from the root: TS6377 and TS5096 otherwise).
|
|
@@ -853,28 +918,37 @@ TS2345 names two copies of `rayfin-auth`, something installed only some Rayfin p
|
|
|
853
918
|
*c. The files: `setup_fabric_app` with `features: ["notes"]`.* With the data service on, it writes them finished and
|
|
854
919
|
never rewrites them:
|
|
855
920
|
- `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
|
|
857
|
-
writes has an explicit permission decorator and a `max` on every `@text` (MSSQL gets `NVARCHAR(MAX)` without
|
|
858
|
-
which can break the deployed GraphQL schema).
|
|
921
|
+
position: `chart`, `markKey`, `markKeyTo` (a route's other end), `body`, `author`, `authorId`, `createdAt`. Every
|
|
922
|
+
entity it writes has an explicit permission decorator and a `max` on every `@text` (MSSQL gets `NVARCHAR(MAX)` without
|
|
923
|
+
one, which can break the deployed GraphQL schema). Its create permission carries a policy,
|
|
924
|
+
`claims.sub.eq(item.authorId)`: the server takes a note only under the caller's own id.
|
|
859
925
|
- `src/lib/bic-notes.ts`: `useMarkNotes(author)` - every note (paged with `.executePaginated()`: one query returns at
|
|
860
926
|
most 100 and doesn't say there are more), an `error` when they can't load, and `add(chart, key, body)`, which saves
|
|
861
|
-
|
|
862
|
-
`useAuth().session?.user
|
|
927
|
+
through the `addMarkNote` Rayfin function and reloads, and REJECTS when the save fails. The author is the signed-in
|
|
928
|
+
user, `useAuth().session?.user` (its id and email); with no one signed in, `add` refuses and says why - never "unknown".
|
|
929
|
+
- `rayfin/functions/src/bic-notes-functions.ts`, registered in `function_app.ts`: `addMarkNote`, which runs on the
|
|
930
|
+
server, stamps `createdAt` from the server's clock and saves with `.create(` as the caller, so the policy still checks
|
|
931
|
+
who. Written once Rayfin's functions are on (step a); before that, `setup_fabric_app` says to run
|
|
932
|
+
`npx rayfin functions init` and then itself again.
|
|
863
933
|
- `src/components/bic-notes-dialog.tsx`: `NotesDialog`, over the page through `createPortal`, never inside the
|
|
864
934
|
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.
|
|
935
|
+
the save succeeds; a failure is shown with the text kept. Tab stays inside it, Escape closes it, and focus goes back
|
|
936
|
+
to what opened it (`useDialogFocus`, from `src/lib/bic-dialog.ts`, which `setup_fabric_app` always writes).
|
|
866
937
|
|
|
867
|
-
*d. Badges on the marks: pass `annotations`, never measure the chart.* The host draws each badge on its key's mark
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
938
|
+
*d. Badges on the marks: pass `annotations`, never measure the chart.* The host draws each badge on its key's mark after
|
|
939
|
+
every render and keeps it there through filters, re-queries and zooms; it never reads a row position. On a table row it
|
|
940
|
+
is a small badge just after the row's label, showing its `label` (`noteBadges` makes that the count of notes). On a
|
|
941
|
+
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
|
|
942
|
+
it: the `label` and `title` are in its tooltip. Describe it that way to the person - a dot whose tooltip gives the
|
|
943
|
+
count, not a numbered marker. A route is keyed by BOTH of its ends with `where` - no key column to add, no query to
|
|
944
|
+
change and no chart to regenerate:
|
|
871
945
|
|
|
872
946
|
```tsx
|
|
873
947
|
import { noteBadges, notesFor, badgeKey } from "@bicharts/chart-host/react";
|
|
874
948
|
|
|
875
949
|
import { useAuth } from "@/hooks/auth.context";
|
|
876
950
|
|
|
877
|
-
const { notes, add } = useMarkNotes(useAuth().session?.user
|
|
951
|
+
const { notes, add } = useMarkNotes(useAuth().session?.user);
|
|
878
952
|
<BivariateWorldChoroplethChart table={table} {...size} options={options} selects={country}
|
|
879
953
|
annotations={noteBadges(notes, { chart: "occupancy-map", columns: "CountryIso3" })}
|
|
880
954
|
onAnnotationClick={a => setOpen({ chart: "occupancy-map", columns: "CountryIso3", key: badgeKey(a) })} />
|
|
@@ -894,6 +968,60 @@ or bounding boxes to place anything: that breaks on the next redraw, and this do
|
|
|
894
968
|
The files use the template's own tokens; restyle them as you like. The table and the permissions are created on
|
|
895
969
|
the next `npx rayfin up`, and every later `rayfin up` applies entity changes.
|
|
896
970
|
|
|
971
|
+
*e. Anything else the app writes (action items, flags, approvals) keeps the same rules.* Write it the way the notes
|
|
972
|
+
files do:
|
|
973
|
+
- **Who** is the signed-in user, `useAuth().session?.user`: store its `id` beside the name you show, and give the
|
|
974
|
+
entity's create permission a policy that checks it - `@role<Item>('authenticated', 'create', { policy: (claims, item) =>
|
|
975
|
+
claims.sub.eq(item.createdById) })` - so the server refuses a row filed under anyone else. With no one signed in,
|
|
976
|
+
refuse the save and say why (the form keeps what was typed); never store "unknown".
|
|
977
|
+
- **When** is the server's clock, never the browser's: every write that records a time (an item created, an item
|
|
978
|
+
marked done) goes through a Rayfin function that stamps it on the server. Rayfin's data layer (checked on 1.36.2) has
|
|
979
|
+
no server-time default for a field (a `default` is a fixed value), so the browser never sends the time at all. The
|
|
980
|
+
function saves with `ctx.getDataClient()`, which carries the caller's identity and permissions, so the who-check
|
|
981
|
+
above still holds.
|
|
982
|
+
- **A change that happens once** (done, approved, closed) is its own row, never an update anyone can repeat: an update
|
|
983
|
+
permission with no policy lets a second person mark a done item done again and overwrite who closed it. Key the row
|
|
984
|
+
by the item with a unique field, under the same who-check, so a second one fails at the database and the first
|
|
985
|
+
stays; an item is open while it has no such row, and only an open item offers the action.
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
// rayfin/data/TaskDone.ts
|
|
989
|
+
@entity()
|
|
990
|
+
@authenticated('read')
|
|
991
|
+
@role<TaskDone>('authenticated', 'create', { policy: (claims, item) => claims.sub.eq(item.doneById) })
|
|
992
|
+
export class TaskDone {
|
|
993
|
+
@uuid() id!: string;
|
|
994
|
+
@uuid({ unique: true }) taskId!: string; // one per task: a second "done" fails, so the first completer stays
|
|
995
|
+
@text({ max: 320 }) doneBy!: string; // the name shown (the session's email)
|
|
996
|
+
@text({ max: 128 }) doneById!: string; // session.user.id, checked against the caller by the policy
|
|
997
|
+
@date() doneAt!: Date; // the server's clock: set only in markTaskDone below
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// rayfin/functions/src/function_app.ts (next to the setup's registrations), after `const udf = new UserDataFunctions();`
|
|
1001
|
+
udf.func("markTaskDone", async (taskId: string, doneBy: string, doneById: string,
|
|
1002
|
+
ctx: RayfinContext<AppSchema>): Promise<void> => {
|
|
1003
|
+
await ctx.getDataClient().TaskDone.create({ taskId, doneBy, doneById, doneAt: new Date() }); // the server's time
|
|
1004
|
+
}, []);
|
|
1005
|
+
|
|
1006
|
+
// in the app: no time from the browser
|
|
1007
|
+
await getRayfinClient().functions.markTaskDone.invoke({ taskId: task.id, doneBy: user.email, doneById: user.id });
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
`RayfinContext` comes from `@microsoft/fabric-user-data-functions` and `AppSchema` from `../../data/schema.js`
|
|
1011
|
+
(`import type` for both). A function's input is the named values `invoke` passes, so give each one a type.
|
|
1012
|
+
|
|
1013
|
+
**What this can't stop yet.** A Rayfin function writes with the same permissions as the browser, and Rayfin has no
|
|
1014
|
+
server-time default for a field and no permission that only a function holds. So the app's own saves always carry the
|
|
1015
|
+
server's time, but a request someone writes by hand against the data API can still set its own. Say so if the person
|
|
1016
|
+
asks for an audit trail; a database the app reaches only through functions would close it, and Rayfin doesn't offer
|
|
1017
|
+
that today.
|
|
1018
|
+
|
|
1019
|
+
- **A dialog of your own** (a form over the page) uses `useDialogFocus(ref, onClose)` from `src/lib/bic-dialog.ts`:
|
|
1020
|
+
Tab stays inside it, Escape closes it, and focus goes back to what opened it.
|
|
1021
|
+
|
|
1022
|
+
`check_app` flags a placeholder name in a write, a time written from the browser's clock, an entity that takes any
|
|
1023
|
+
name on create or lets any row be updated again, and a dialog that doesn't hold the keyboard.
|
|
1024
|
+
|
|
897
1025
|
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
1026
|
|
|
899
1027
|
**7. A chart's own controls are page state: `useBicControls`.** A What-if chart's sliders (a rate, a horizon) are drawn
|
|
@@ -905,7 +1033,7 @@ import { ScenariosPanel } from "@/components/bic-scenarios-panel"; // setup_fa
|
|
|
905
1033
|
|
|
906
1034
|
const scenario = useBicControls();
|
|
907
1035
|
<WhatIfProjectionChart table={table} {...size} options={options} controls={scenario} />
|
|
908
|
-
<ScenariosPanel chart="bookings-forecast" controls={scenario} author={useAuth().session?.user
|
|
1036
|
+
<ScenariosPanel chart="bookings-forecast" controls={scenario} author={useAuth().session?.user} />
|
|
909
1037
|
```
|
|
910
1038
|
|
|
911
1039
|
`scenario.values` holds every slider's value from the first draw - no slider has to move before a scenario can be
|
|
@@ -914,7 +1042,8 @@ horizon 12 months"), so a saved scenario shows the unit the chart shows. `scenar
|
|
|
914
1042
|
returns to the chart's defaults. Don't read `uiState.knobs` or the chart's source, and never save a default you made
|
|
915
1043
|
up. `features: ["scenarios"]` writes the `SavedScenario` entity, `useSavedScenarios` and `ScenariosPanel` (save under a
|
|
916
1044
|
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)
|
|
1045
|
+
save keeps what was typed), and, with Rayfin's functions on, the `addSavedScenario` function each save runs through, so
|
|
1046
|
+
the server stamps its time.
|
|
918
1047
|
|
|
919
1048
|
**8. What stays the template's job - this is additive, never a replacement:** sign-in, the query hooks
|
|
920
1049
|
(`useSemanticModelQuery`; a page filter feeds it through `filterQuery`), the theme (`useCssTheme`, which
|
|
@@ -926,12 +1055,18 @@ deployment. The chart is a function you render inside them.
|
|
|
926
1055
|
at the size the panel will give it - by default it reads the chart's `minChartWidth` and `minChartHeight`
|
|
927
1056
|
off its `BicChartPanel`, so pass no size unless you mean another one. A `WARNING` line in its reply is a
|
|
928
1057
|
label cut off at that size: deal with it before you call the chart done. The reply says whether more room fixes it.
|
|
1058
|
+
So is a `WARNING` that one value sets a color scale's span (the rest then look even): fix the query or regenerate.
|
|
1059
|
+
For a table, its `Table` lines say how each column draws and what stays in place as the rows scroll, read from the
|
|
1060
|
+
drawn table: quote them in your reply wherever the person asked how the table draws or what stays put, and when they
|
|
1061
|
+
differ from the ask (a column printed where they asked for marks, more held in place than they allowed), regenerate
|
|
1062
|
+
with a prompt that names the difference.
|
|
929
1063
|
Never edit the generated chart code by hand: if something's wrong with a generated chart, regenerate it
|
|
930
1064
|
(`generate_chart` with a prompt describing the fix) or report it.
|
|
931
1065
|
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
|
|
933
|
-
chart in a `BicChartPanel`), draws every chart together, and compares the stack with npm.
|
|
934
|
-
the line and the call to use instead; fix the ERRORs in the app's own files before
|
|
1066
|
+
through `useBicFilter` + `selects`, each chart's filter wiring, a chart's own controls through `useBicControls`, notes
|
|
1067
|
+
through `annotations`, every chart in a `BicChartPanel`), draws every chart together, and compares the stack with npm.
|
|
1068
|
+
Each finding names the file, the line and the call to use instead; fix the ERRORs in the app's own files before
|
|
1069
|
+
deploying, and say its filter-wiring list in your reply.
|
|
935
1070
|
You don't need to deploy to check a chart. The template's `validate:visual` checks `VegaVisual` factories; a
|
|
936
1071
|
RegiaBI chart's check is `check_chart` - don't list it there. `check_chart` and `check_app` check the charts and how
|
|
937
1072
|
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,
|