@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/dist/check-chart.mjs +125 -113
- package/dist/index.mjs +131 -115
- package/package.json +3 -3
- package/skills/bic-charts/SKILL.md +146 -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.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.
|
|
51
|
-
"@bicharts/shape-core": "^0.6.
|
|
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
|
|
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} />}</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
|
|
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).
|
|
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
|
-
|
|
862
|
-
`useAuth().session?.user
|
|
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
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|