@bicharts/chart-mcp 0.6.114 → 0.6.116
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/LICENSE.txt +2 -2
- package/README.md +10 -0
- package/dist/check-chart.mjs +104 -151
- package/dist/index.mjs +154 -141
- package/package.json +7 -3
- package/skills/bic-charts/CHART-SHAPES.md +56 -55
- package/skills/bic-charts/SKILL.md +144 -12
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.116",
|
|
4
4
|
"description": "AI Data Components for Power BI, Excel, Fabric Apps and the web, over MCP: working what-if apps, interactive tables and bivariate maps first, plus 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",
|
|
7
7
|
"modelcontextprotocol",
|
|
8
|
+
"ai-data-components",
|
|
9
|
+
"data-apps",
|
|
10
|
+
"widgets",
|
|
11
|
+
"maps",
|
|
8
12
|
"charts",
|
|
9
13
|
"d3",
|
|
10
14
|
"dataviz",
|
|
@@ -47,8 +51,8 @@
|
|
|
47
51
|
},
|
|
48
52
|
"// 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
53
|
"devDependencies": {
|
|
50
|
-
"@bicharts/chart-host": "^0.6.
|
|
51
|
-
"@bicharts/shape-core": "^0.6.
|
|
54
|
+
"@bicharts/chart-host": "^0.6.116",
|
|
55
|
+
"@bicharts/shape-core": "^0.6.116",
|
|
52
56
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
53
57
|
"@types/node": "^26.6.5",
|
|
54
58
|
"@types/papaparse": "^5.3.14",
|
|
@@ -15,58 +15,59 @@ on the same picture, not a looser one.
|
|
|
15
15
|
| chart | needs | limits | renderers | good for |
|
|
16
16
|
| --- | --- | --- | --- | --- |
|
|
17
17
|
| 3D scatter plot | 3 continuous | needs 700x480 px | D3, Plotly | Three continuous measures whose joint structure is the question, on a tile large enough to turn. To read exact values off three or more measures, Pair plot (scatter matrix) and Parallel coordinates are both more precise |
|
|
18
|
-
| Africa (Bubbles) | lat/lon | needs
|
|
19
|
-
| Africa Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
20
|
-
| Africa Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs
|
|
21
|
-
| Animated bar chart | 1 categorical, 1 continuous, 5+ time points | <=40 categories, needs
|
|
22
|
-
| Animated treemap | 2 categorical, 1 continuous, 3+ time points | needs
|
|
18
|
+
| Africa (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point, city, province or country data within Africa: stores, offices, customers or revenue by place, with a measure for bubble size and optionally a second measure or category for color |
|
|
19
|
+
| Africa Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in Africa: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
20
|
+
| Africa Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in Africa, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
21
|
+
| Animated bar chart | 1 categorical, 1 continuous, 5+ time points | <=40 categories, needs 294x300 px | D3 | Requires one categorical, one measure and a time column with 5 or more ordered periods; best for a leaderboard that changes over time (top products by year, teams by season, countries by decade) |
|
|
22
|
+
| Animated treemap | 2 categorical, 1 continuous, 3+ time points | needs 294x560 px | D3 | Requires a 2-level categorical hierarchy plus an additive measure observed at 3 or more ordered time points |
|
|
23
23
|
| Animated World (Bubbles) | 3+ time points, lat/lon | needs 400x320 px | D3 | Best for a measure per place observed at 3 or more ordered time points - revenue by country by year, openings by city by quarter, throughput by port by month - when the question is how each place changed rather than where it ranks |
|
|
24
|
-
| Animated World Choropleth | 1 categorical, 3+ time points, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
24
|
+
| Animated World Choropleth | 1 categorical, 3+ time points, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for a country-level measure observed at 3 or more ordered time points — watch how sales, users, or population shift across countries year by year |
|
|
25
25
|
| Annotated heatmap | 2 categorical, 1 continuous | <=20 categories, needs 320x280 px | D3, Matplotlib, Plotly, Seaborn | Requires two variables: categorical × categorical with values |
|
|
26
26
|
| Arc diagram | 2 categorical | <=50 nodes, needs 380x240 px | D3 | Best for 5-50 nodes and a sparse-to-medium-density set of pairwise relationships |
|
|
27
27
|
| Area chart | 1 continuous | needs 360x280 px | D3, Matplotlib, Plotly, Seaborn | Requires ordered categories or time with a continuous variable |
|
|
28
|
-
| Asia (Bubbles) | lat/lon | needs
|
|
29
|
-
| Asia Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
30
|
-
| Asia Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs
|
|
31
|
-
| Autocorrelation plot | 1 continuous, 20+ time points | needs
|
|
28
|
+
| Asia (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point, city, province or country data within Asia: stores, offices, customers or revenue by place, with a measure for bubble size and optionally a second measure or category for color |
|
|
29
|
+
| Asia Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in Asia: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
30
|
+
| Asia Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in Asia, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
31
|
+
| Autocorrelation plot | 1 continuous, 20+ time points | needs 294x300 px | D3, Matplotlib, Plotly, Statsmodels | Requires a datetime series with one continuous variable |
|
|
32
32
|
| Bar chart (horizontal) | - | needs 360x280 px | D3, Matplotlib, Plotly, Seaborn | Requires one categorical and one numeric variable |
|
|
33
33
|
| Bar chart (vertical) | - | needs 600x420 px | D3, Matplotlib, Plotly, Seaborn | Requires one categorical and one numeric variable |
|
|
34
34
|
| Basic Sankey | 2 categorical, 1 continuous | <=80 categories, <=80 edges, needs 320x240 px | D3, Plotly | Requires two categorical (nodes) and a continuous variable |
|
|
35
35
|
| Beeswarm chart | 1 categorical, 1 continuous | <=30 categories, needs 280x280 px | D3, Matplotlib, Plotly, Seaborn | Best for showing distribution density across small categorical sets |
|
|
36
36
|
| Binned 2D heatmap | 2 continuous | needs 320x280 px | D3 | Best for dense scatter data where points overplot: thousands of observations of two continuous variables, where the question is where the mass is rather than where each point is |
|
|
37
37
|
| Binned bubble matrix | 1 continuous | needs 320x300 px | D3, Matplotlib, Plotly, Seaborn | Two banded/categorical axes (or two continuous variables split into ranges) plus an aggregatable size measure per cell, with optional independent color. Choose this over a continuous Bubble chart when raw points would overplot into a blob, or when the axes are naturally grouped |
|
|
38
|
-
| Bivariate USA Choropleth (by state) | 1 categorical, 2 continuous, geo: us-state-code/us-state-name | <=56 categories, needs
|
|
38
|
+
| Bivariate USA Choropleth (by state) | 1 categorical, 2 continuous, geo: us-state-code/us-state-name | <=56 categories, needs 294x300 px | D3 | Best for two state-level measures that might disagree: revenue vs return rate, income vs cost of living, cases vs vaccinations - at least 24 states and two measures that do not simply track each other |
|
|
39
39
|
| Bivariate World Choropleth | 1 categorical, 2 continuous, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 400x300 px | D3 | Best for two country-level measures that might disagree: wealth vs inequality, revenue vs margin, cases vs deaths - at least 24 countries and two measures that do not simply track each other |
|
|
40
40
|
| Box plot | 1 continuous | needs 460x340 px | D3, Matplotlib, Plotly, Seaborn | Requires one continuous variable (plus optional category) |
|
|
41
41
|
| Bubble chart | 3 continuous | needs 520x380 px | D3, Matplotlib, Plotly, Seaborn | Three continuous variables (x, y, size); one mark per row on continuous axes. If you want both axes grouped into bands with one aggregated bubble per cell, pick Binned bubble matrix instead |
|
|
42
|
-
| Budget allocator | 1 categorical, 2 continuous | needs
|
|
42
|
+
| Budget allocator | 1 categorical, 2 continuous | needs 440x380 px | D3 | One spend measure and one outcome by channel and period - monthly spend and signups for five marketing channels, budget and sales by region - with at least eight periods per channel |
|
|
43
43
|
| Bullet chart | 1 continuous, a reference value | needs 220x120 px | D3, PLOTLY | Best for actual-vs-target KPIs with thresholds (revenue vs goal, SLA vs target) — a denser, clearer replacement for gauges |
|
|
44
44
|
| Bump chart | 1 categorical, 1 continuous, 3+ time points | <=15 categories, needs 320x280 px | D3, PLOTLY | Best for leaderboard/position dynamics: how a handful of regions, products, or teams change rank quarter to quarter |
|
|
45
45
|
| Calendar heatmap | 1 continuous, 60+ time points | needs 600x420 px | D3, Matplotlib, Plotly, Seaborn | Requires daily datetime data with associated numeric values; best for activity/contribution tracking over time |
|
|
46
|
-
| Candlestick chart | 4 continuous, 10+ time points | needs
|
|
46
|
+
| Candlestick chart | 4 continuous, 10+ time points | needs 294x420 px | D3, Matplotlib, Plotly | Best for financial or trading data |
|
|
47
47
|
| Card | 1 continuous | needs 100x60 px | D3, Plotly | One numeric value that summarizes the dataset; small viewports |
|
|
48
48
|
| Card with embedded | 1 continuous | needs 200x120 px | D3 | One headline value that deserves one more channel beside it: attainment with its target, a metric with its history, a profile of a few comparable scores, or a total with its split across up to eight categories; small tiles |
|
|
49
49
|
| Card with multiple KPI | 2 continuous | needs 180x100 px | D3, Plotly | Multiple independent numeric metrics that should be surfaced together; KPI dashboards; long-narrow / short-wide viewports |
|
|
50
50
|
| Card with sparkline | 1 continuous | needs 140x80 px | D3, Plotly | One numeric metric trending over an ordered dimension; small viewports |
|
|
51
|
-
| Chord diagram | 2 categorical, 1 continuous | <=25 categories, <=30 nodes, needs
|
|
52
|
-
| Circular heatmap | 1 categorical, 1 continuous | <=60 categories, needs
|
|
51
|
+
| Chord diagram | 2 categorical, 1 continuous | <=25 categories, <=30 nodes, needs 294x320 px | D3 | Requires matrix data showing bidirectional flows between entities; best for migration, trade, or relationship intensity data |
|
|
52
|
+
| Circular heatmap | 1 categorical, 1 continuous | <=60 categories, needs 294x380 px | D3, Matplotlib, Plotly | Requires two dimensions - a cyclical one such as hour, weekday or month around the angle and a second category or date for the rings - and one measure for the colour |
|
|
53
53
|
| Clustered heatmap | 2 categorical, 1 continuous | <=50 categories, needs 320x320 px | Plotly | Requires two variables for matrix values |
|
|
54
54
|
| Connected scatterplot | 2 continuous, 6+ time points | needs 300x300 px | D3, PLOTLY | Best for the joint trajectory of two related metrics over time (e.g. units vs revenue, unemployment vs inflation) where the path shape is the insight |
|
|
55
55
|
| Custom small multiples | 1 categorical, 1 continuous | <=16 categories, needs 320x240 px | D3, Matplotlib, Plotly, Seaborn | Best when comparing patterns across categories |
|
|
56
56
|
| Cutoff tuner | 1 continuous | needs 560x360 px | D3 | One row per case with a model score or rating and the actual yes/no outcome - churn, fraud, default, conversion - when the question is where to draw the line and what each choice costs |
|
|
57
57
|
| Cycle plot | 1 continuous, 24+ time points | needs 320x280 px | D3, PLOTLY | Best for multi-year monthly/quarterly/weekly data where the question is seasonality vs trend ('is Q4 always highest, and is each Q4 growing?') |
|
|
58
|
+
| Decomposition cards | 2 categorical, 1 continuous | needs 294x200 px | D3 | One measure that adds up and two or more category fields; a second measure shows each card's change |
|
|
58
59
|
| Delta KPI | 2 continuous | needs 200x120 px | D3 | Two comparable measures, or one measure at two points in time - this period against last, actual against plan, after against before |
|
|
59
60
|
| Dendrogram (tree diagram) | 2 categorical, 1 continuous | <=60 leaves, needs 320x240 px | D3, Plotly | Best for 2-4 hierarchy levels with ≤ ~80 leaves |
|
|
60
61
|
| Density contour plot | 2 continuous | needs 600x420 px | D3, Matplotlib, Plotly, Seaborn | Requires two continuous variables |
|
|
61
62
|
| Donut chart | 1 categorical | <=10 categories, needs 520x380 px | D3, Matplotlib, Plotly | Requires one categorical variable with proportions |
|
|
62
63
|
| Dumbbell chart | 1 categorical, 2 continuous | needs 600x420 px | D3, Plotly | Best for before/after or comparison scenarios |
|
|
63
|
-
| ECDF plot | 1 continuous | needs
|
|
64
|
+
| ECDF plot | 1 continuous | needs 294x283 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires one continuous variable |
|
|
64
65
|
| Error band | 1 continuous | needs 360x240 px | D3 | Best for repeated measurements over time or over a continuous axis: latency trials, sensor readings, A/B metrics - anywhere the SPREAD matters as much as the average |
|
|
65
|
-
| Error bar plot | 1 categorical, 1 continuous | needs
|
|
66
|
-
| Europe (Bubbles) | lat/lon | needs
|
|
67
|
-
| Europe Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
68
|
-
| Europe Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs
|
|
69
|
-
| Event plot | 1 continuous | needs
|
|
66
|
+
| Error bar plot | 1 categorical, 1 continuous | needs 294x340 px | D3, Matplotlib, Plotly, Seaborn | Requires a category and a measure with a spread to show - repeated rows per category or a second measure holding the error |
|
|
67
|
+
| Europe (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point, city, province or country data within Europe: stores, offices, customers or revenue by place, with a measure for bubble size and optionally a second measure or category for color |
|
|
68
|
+
| Europe Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in Europe: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
69
|
+
| Europe Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in Europe, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
70
|
+
| Event plot | 1 continuous | needs 294x300 px | D3, Matplotlib, Plotly, Seaborn | Requires a date or ordered value for each event, with an optional category that splits the events into lanes |
|
|
70
71
|
| 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 |
|
|
71
72
|
| 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 |
|
|
72
73
|
| 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 |
|
|
@@ -78,8 +79,8 @@ on the same picture, not a looser one.
|
|
|
78
79
|
| Horizon chart | 1 categorical, 1 continuous, 12+ time points | needs 800x560 px | D3, Matplotlib, Plotly | Best for dense time series dashboards |
|
|
79
80
|
| Horizontal/vertical span plot | 1 categorical, 2 continuous | needs 400x300 px | D3, Matplotlib, Plotly, Seaborn | Requires a category and two numeric bounds per category - a low and a high, or a start and an end - on one shared scale |
|
|
80
81
|
| Icicle plot | 2 categorical, 1 continuous | <=60 leaves, needs 320x240 px | D3, Plotly | Best for 2-4 levels of hierarchy with ≤ ~50 leaves; an alternative to a sunburst when horizontal/vertical scanning is preferred |
|
|
81
|
-
| Interactive table | - | needs
|
|
82
|
-
| Joint plot | 2 continuous | needs
|
|
82
|
+
| Interactive table | - | needs 294x200 px | D3 | Any set of rows and columns that a reader wants to scan, sort and look into - an account list, a roster, a task or ticket list, a ledger, results with a status and a score; long lists a chart would crowd |
|
|
83
|
+
| Joint plot | 2 continuous | needs 294x280 px | D3, Matplotlib, Plotly, Seaborn | Requires two continuous variables |
|
|
83
84
|
| KDE plot (1D & 2D) | 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires one continuous variable (or two for 2D KDE) |
|
|
84
85
|
| 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 |
|
|
85
86
|
| Line chart | 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly, Seaborn | One or more continuous series over an ordered/temporal axis |
|
|
@@ -87,20 +88,20 @@ on the same picture, not a looser one.
|
|
|
87
88
|
| Lollipop chart | 1 categorical, 1 continuous | <=50 categories, needs 700x480 px | D3, PLOTLY | Best as a cleaner bar replacement for ranking many categories by a single measure |
|
|
88
89
|
| Marimekko / Mosaic plot | 2 categorical, 1 continuous | needs 320x280 px | D3 | Best for 3-10 outer × 3-10 inner categories with a positive measure |
|
|
89
90
|
| 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 |
|
|
90
|
-
| Middle East Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
91
|
-
| Motion bubble chart | 1 categorical, 2 continuous, 3+ time points | <=40 categories, needs
|
|
91
|
+
| Middle East Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in the Middle East: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
92
|
+
| Motion bubble chart | 1 categorical, 2 continuous, 3+ time points | <=40 categories, needs 294x320 px | D3 | Requires one categorical whose members are the bubbles, two measures for position and a time column with 3 or more ordered periods; best for watching entities travel - plans by subscribers and churn, countries by wealth and lifespan, teams by cost and output |
|
|
92
93
|
| Multi-level Sankey | 3 categorical, 1 continuous | <=80 edges, needs 420x280 px | D3, Plotly | Requires three (or more) categorical and one continuous variable |
|
|
93
94
|
| Network diagram | 2 categorical | <=150 categories, <=80 edges, needs 320x280 px | D3, Matplotlib | Requires node and edge data (source-target pairs); best for relationship, dependency, or social network visualization |
|
|
94
|
-
| Normalized stacked bar chart | 2 categorical, 1 continuous | needs
|
|
95
|
-
| North America (Bubbles) | lat/lon | needs
|
|
96
|
-
| North America Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
97
|
-
| North America Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon/us-state-code/us-state-name | <=2000 categories, needs
|
|
98
|
-
| Oceania (Bubbles) | lat/lon | needs
|
|
99
|
-
| Oceania Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
100
|
-
| Oceania Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs
|
|
101
|
-
| Origin-Destination Flow Map | lat/lon | needs
|
|
95
|
+
| Normalized stacked bar chart | 2 categorical, 1 continuous | needs 340x420 px | D3 | Best with two categoricals and one additive measure when the question is what the composition is rather than how big the total is |
|
|
96
|
+
| North America (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point-located data with latitude and longitude columns: stores, cities, facilities, or events across North America, with a measure for bubble size and optionally a second measure or category for color |
|
|
97
|
+
| North America Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in North America: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
98
|
+
| North America Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon/us-state-code/us-state-name | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in North America, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
99
|
+
| Oceania (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point, city, province or country data within Oceania: stores, offices, customers or revenue by place, with a measure for bubble size and optionally a second measure or category for color |
|
|
100
|
+
| Oceania Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in Oceania: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
101
|
+
| Oceania Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in Oceania, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
102
|
+
| Origin-Destination Flow Map | lat/lon | needs 294x300 px | D3 | Best for movement between places: flight routes, shipping and trade lanes, migration, commutes or transfers, where each row names where something started and where it ended plus a measure of how much moved |
|
|
102
103
|
| Packed circles | 2 categorical, 1 continuous | needs 280x280 px | D3 | Best with one numeric measure plus 1-2 categorical groupings producing 20-150 leaves |
|
|
103
|
-
| Pair plot (scatter matrix) | 3 continuous | needs
|
|
104
|
+
| Pair plot (scatter matrix) | 3 continuous | needs 400x400 px | D3, Matplotlib, Plotly, Seaborn | Requires at least three continuous variables |
|
|
104
105
|
| Parallel coordinates | 3 continuous | needs 700x480 px | D3, Plotly | Best for 4-8 numeric dimensions and up to ~1000 rows (heavier overlap with more) |
|
|
105
106
|
| 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) |
|
|
106
107
|
| Pie chart | 1 categorical | <=10 categories, needs 280x280 px | D3, Matplotlib, Plotly | Requires one categorical variable with proportions |
|
|
@@ -108,46 +109,46 @@ on the same picture, not a looser one.
|
|
|
108
109
|
| 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 |
|
|
109
110
|
| Radar (spider) chart | 1 continuous | <=8 axes, needs 700x480 px | D3, Matplotlib, Plotly | Requires multiple continuous variables across categories |
|
|
110
111
|
| 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 |
|
|
111
|
-
| Regression plot (line with CI) | 2 continuous | needs
|
|
112
|
+
| Regression plot (line with CI) | 2 continuous | needs 294x380 px | D3, Matplotlib, Plotly, Seaborn, Statsmodels | Requires two continuous variables - a predictor and a response |
|
|
112
113
|
| Ridgeline plots | 1 categorical, 1 continuous | <=25 categories, needs 280x280 px | D3, Matplotlib, Plotly, Seaborn | Best with many observations per category |
|
|
113
|
-
| Rose (Coxcomb) chart | 1 categorical | <=24 categories, needs
|
|
114
|
-
| Rotating carousel | 1 continuous | <=12 categories, needs
|
|
115
|
-
| Rug plot | 1 continuous | needs
|
|
114
|
+
| Rose (Coxcomb) chart | 1 categorical | <=24 categories, needs 294x300 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 |
|
|
115
|
+
| Rotating carousel | 1 continuous | <=12 categories, needs 294x200 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 |
|
|
116
|
+
| Rug plot | 1 continuous | needs 294x280 px | D3, Matplotlib, Plotly, Seaborn | Requires one continuous variable |
|
|
116
117
|
| Sankey alluvial flow | 2 categorical, 1 continuous | <=10 categories, <=80 edges, needs 420x280 px | D3, Matplotlib, Plotly | Requires at least two categorical variables |
|
|
117
118
|
| Scatter plot | 2 continuous | needs 460x340 px | D3, Matplotlib, Plotly, Seaborn | Requires two continuous variables |
|
|
118
119
|
| Seasonal decomposition | 1 continuous, 24+ time points | needs 320x360 px | Matplotlib, Plotly, Statsmodels | Requires a datetime series with one continuous variable and at least two full seasonal cycles |
|
|
119
|
-
| Selection explainer | 2 continuous | needs
|
|
120
|
+
| Selection explainer | 2 continuous | needs 294x360 px | D3 | Two measures over thirty or more rows that also carry other fields - region, plan, tenure, size - when the question is 'what do these points have in common?' |
|
|
120
121
|
| Slope chart | 1 categorical, 1 continuous, 2+ time points | <=40 categories, needs 460x340 px | D3, PLOTLY | Best for two-period change across many categories: YoY by region/segment, before/after comparisons, ranking shifts between two snapshots |
|
|
121
|
-
| South America (Bubbles) | lat/lon | needs
|
|
122
|
-
| South America Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
123
|
-
| South America Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs
|
|
122
|
+
| South America (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point, city, province or country data within South America: stores, offices, customers or revenue by place, with a measure for bubble size and optionally a second measure or category for color |
|
|
123
|
+
| South America Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3 | Best for country-level metrics when the countries are in South America: sales, users, or population by country (keyed to country names or ISO codes) |
|
|
124
|
+
| South America Choropleth (by province) | 1 categorical, geo: admin1-code/admin1-name/city-name/city-name-world/latlon | <=2000 categories, needs 294x300 px, host-only: the Power BI visual and the Excel add-in, never this MCP | D3 | Best for metrics by province, state or region in South America, or by city when you want the cities summed into their provinces (keyed to province names, ISO codes or city names) |
|
|
124
125
|
| Spiral plot | 1 continuous, 12+ time points | needs 320x320 px | D3, PLOTLY | Best for long, finely-grained, strongly periodic series (years of daily data) where the cyclic pattern is the story |
|
|
125
126
|
| Stacked area chart | 1 categorical, 1 continuous | needs 460x340 px | D3, Matplotlib, Plotly | Requires ordered categories with multiple continuous series |
|
|
126
127
|
| Stacked bar chart | 2 categorical, 1 continuous | needs 700x480 px | D3, Matplotlib, Plotly, Seaborn | Requires one categorical, one subcategory, and one numeric variable |
|
|
127
|
-
| Stem plot | 1 continuous | needs
|
|
128
|
-
| Step plot | 1 continuous | needs
|
|
128
|
+
| Stem plot | 1 continuous | needs 294x280 px | D3, Matplotlib, Plotly | Requires one measure read along an ordered sequence - a time step, a row index or an ordered category |
|
|
129
|
+
| Step plot | 1 continuous | needs 294x340 px | D3, Matplotlib, Plotly, Seaborn | Requires ordered categories with one continuous variable |
|
|
129
130
|
| Streamgraph | 1 categorical, 1 continuous, 8+ time points | needs 520x380 px | D3, Plotly | Best with 3-12 categorical series and a continuous time axis with at least 15-30 evenly-spaced points |
|
|
130
131
|
| Strip plot | 1 continuous | needs 700x480 px | D3, Matplotlib, Plotly, Seaborn | Requires one categorical and one continuous variable; jitter on the category axis prevents overplot at the same value |
|
|
131
132
|
| Sunburst chart | 2 categorical, 1 continuous | needs 400x300 px | D3, Matplotlib, Plotly | Requires hierarchical categorical data |
|
|
132
|
-
| Table with embedded | 1 categorical, 1 continuous | <=40 categories, needs
|
|
133
|
-
| Ternary plot | 3 continuous | needs
|
|
133
|
+
| Table with embedded | 1 categorical, 1 continuous | <=40 categories, needs 294x200 px | D3 | Best for a small set of items compared across several attributes at once (a forecast/roster/scorecard table) |
|
|
134
|
+
| Ternary plot | 3 continuous | needs 294x320 px | D3 | Three measures that add up to the same total on every row - product mix, budget allocation, vote share, channel split |
|
|
134
135
|
| 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 |
|
|
135
136
|
| Time series plot | 1 continuous | needs 460x340 px | D3, Plotly | Requires a datetime axis with at least one continuous variable |
|
|
136
|
-
| Trail | 2 continuous | needs
|
|
137
|
+
| Trail | 2 continuous | needs 294x220 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 |
|
|
137
138
|
| Treemap | 1 categorical, 1 continuous | needs 240x220 px | D3, Matplotlib, Plotly | Requires hierarchical categorical data with values |
|
|
138
139
|
| US hex-tile cartogram | 1 categorical, geo: us-state-code/us-state-name | needs 400x300 px | D3 | Best with one row per US state and a single measure, when the question is which states are high or low rather than where they are |
|
|
139
|
-
| USA Choropleth (by state) | 1 categorical, geo: us-state-code/us-state-name | <=56 categories, needs
|
|
140
|
-
| USA Choropleth (by ZIP-3) | 1 categorical, geo: us-zip5 | <=42000 categories, needs
|
|
141
|
-
| Variance chart (budget vs actual) | 1 categorical, 2 continuous, a measure naming itself budget/plan/target | <=60 categories, needs
|
|
140
|
+
| USA Choropleth (by state) | 1 categorical, geo: us-state-code/us-state-name | <=56 categories, needs 294x300 px | D3 | Best for state-level US metrics: sales, users, or population by state, any measure that varies by US state (keyed to state names or USPS codes) |
|
|
141
|
+
| USA Choropleth (by ZIP-3) | 1 categorical, geo: us-zip5 | <=42000 categories, needs 294x300 px | D3 | Best for regional US ZIP analysis: metrics by 3-digit ZIP prefix area, any measure that varies across US postal regions (keyed to 5-digit ZIP codes, grouped to their 3-digit prefix) |
|
|
142
|
+
| Variance chart (budget vs actual) | 1 categorical, 2 continuous, a measure naming itself budget/plan/target | <=60 categories, needs 294x240 px | D3 | One category or period column plus two measures where one names itself the reference - Revenue and Revenue Budget, Cost and Cost Plan, Attained and Quota; finance reporting, budget reviews, plan attainment |
|
|
142
143
|
| Variance grid | 1 categorical, 2 continuous, a measure naming itself budget/plan/target | needs 320x200 px | D3 | One or more category columns plus two measures where one names itself the reference - Revenue and Revenue Budget, Cost and Cost Plan, Attained and Quota; a P&L, a budget review, plan attainment by unit; many rows or nested groups that a chart would crowd |
|
|
143
144
|
| Violin plot | 1 continuous | needs 280x280 px | D3, Matplotlib, Plotly, Seaborn | Requires one continuous variable (plus optional category) |
|
|
144
145
|
| Voronoi treemap | 2 categorical, 1 continuous | <=50 cells, needs 320x280 px | D3 | Best for 5-30 categories with one non-negative measure; an alternative to a plain treemap when the organic look is desired |
|
|
145
146
|
| Waffle chart | 1 categorical, 1 continuous | <=6 categories, needs 520x380 px | D3, Matplotlib, Plotly | Best for showing part-to-whole relationships with small number of categories; more accurate than pie charts |
|
|
146
147
|
| Waterfall chart | 1 continuous | <=20 categories, needs 360x280 px | D3, Matplotlib, Plotly | Best for financial data showing contribution to total (revenue bridges, P&L analysis, budget variance) |
|
|
147
|
-
| What-if predictor | 3 continuous | needs
|
|
148
|
-
| What-if projection | 1 continuous, 6+ time points | needs
|
|
149
|
-
| What-if scenarios | 1 continuous, 6+ time points | needs
|
|
148
|
+
| What-if predictor | 3 continuous | needs 400x300 px | D3 | Three to ten numeric measures over at least thirty rows that move together - stores, products, campaigns, regions by quarter - when the question is 'if these were X and Y, what would the others likely be?'; an ordered rating or size and one category of up to six values can be pinned too |
|
|
149
|
+
| What-if projection | 1 continuous, 6+ time points | needs 294x280 px | D3 | A dated measure with at least six periods when the question is what happens next under an assumption the reader chooses - revenue against a target, headcount, subscribers; bind a What-If parameter measure to drive the rate from a slicer |
|
|
150
|
+
| What-if scenarios | 1 continuous, 6+ time points | needs 294x320 px | D3 | A dated measure with at least six periods when the question is how far apart the outcomes of several assumptions land - revenue under a range of growth rates, headcount, subscribers; bind a What-If parameter column to draw exactly the range you defined |
|
|
150
151
|
| Word cloud | 1 categorical | <=100 categories, needs 280x200 px | D3, Matplotlib, Plotly | Requires text data or pre-computed word-frequency pairs; best for qualitative text exploration |
|
|
151
|
-
| World (Bubbles) | lat/lon | needs
|
|
152
|
-
| World Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs
|
|
152
|
+
| World (Bubbles) | lat/lon | needs 294x300 px | D3 | Best for point or country-level data spanning more than one continent: offices, shipments, users or revenue by country, with a measure for bubble size and optionally a second measure or category for color |
|
|
153
|
+
| World Choropleth | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 294x300 px | D3, PLOTLY | Best for country-level metrics: sales, users, or population by country, any measure that varies by nation (keyed to country names or ISO codes) |
|
|
153
154
|
| World tile-grid cartogram | 1 categorical, geo: country-iso3/country-iso2/country-name | <=250 categories, needs 510x420 px | D3 | Best with one row per country and a single measure, when the question is which countries are high or low rather than where they are |
|
|
@@ -26,6 +26,13 @@ the call. Tell them a browser tab is about to open. If the call comes back with
|
|
|
26
26
|
continue" and a link instead, give them that link, and call again once they've signed in. A key in
|
|
27
27
|
the server's environment is only for machines with no browser, such as CI.
|
|
28
28
|
|
|
29
|
+
**On the hosted server** (`https://bizintelligencechampions.com/mcp`, which the Claude Code plugin and claude.ai
|
|
30
|
+
connect to), the tools can't reach the person's files: there is no `setup_fabric_app`, `query_semantic_model` or
|
|
31
|
+
`suggest_charts`, and no `out_dir` or `csv_path`. Pass each table as `data` or `csv_text`; `generate_chart` returns the
|
|
32
|
+
code, the rows and the binding in its reply, and you write them into the project. For a Fabric App, or charts written
|
|
33
|
+
straight to disk, the person adds the local server (`claude mcp add bic-chart-local -- npx -y @bicharts/chart-mcp`) and
|
|
34
|
+
the steps below apply as written.
|
|
35
|
+
|
|
29
36
|
## 0. Decide what to pull (only if you are starting from a data MODEL)
|
|
30
37
|
|
|
31
38
|
Skip this if you already have a table. If you are holding a semantic model, a warehouse schema or
|
|
@@ -630,6 +637,19 @@ is:
|
|
|
630
637
|
blank), `check_app` flags them in the app's `.dax` files, and `check_chart` warns when one value sets a diverging color
|
|
631
638
|
scale's span.
|
|
632
639
|
|
|
640
|
+
**A share of a whole is 0, not blank, where the whole is there.** `DIVIDE ( CALCULATE ( [Revenue], DimChannel[Channel] =
|
|
641
|
+
"Partner" ), [Revenue] )` is blank for a country with revenue but no partner sales: the part is BLANK, and DIVIDE passes it
|
|
642
|
+
on. A blank drops out of every rank, median and comparison of that field, so a Selection explainer comparing a circled
|
|
643
|
+
group on its partner share leaves the 0% countries out and shows no gap. Add `+ 0` to the part, which keeps the share
|
|
644
|
+
blank where the whole is (so SUMMARIZECOLUMNS still drops a row with no revenue at all):
|
|
645
|
+
|
|
646
|
+
```dax
|
|
647
|
+
"Partner Share", DIVIDE ( CALCULATE ( [Revenue], DimChannel[Channel] = "Partner" ) + 0, [Revenue] ) -- 0 with no partner sales
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`COALESCE ( DIVIDE ( ... ), 0 )` reads 0 too, but keeps a row for every country, revenue or not. `query_semantic_model` and
|
|
651
|
+
`check_app` flag a share written without either.
|
|
652
|
+
|
|
633
653
|
**A chart's categories come from the data.** Group by the category column and let the chart take every value the
|
|
634
654
|
model holds; never name the categories in the DAX one filter at a time (`CALCULATE([Sales], 'Channel'[Channel] =
|
|
635
655
|
"Online")`, then "Retail", then "Partner"). A value the model adds or renames would drop out silently, and a chart that
|
|
@@ -666,6 +686,44 @@ card's words stay as written. Write the card's words and the query's bound toget
|
|
|
666
686
|
projection, the window is the history it starts from, and its horizon is the chart's own control. `check_app` warns on a
|
|
667
687
|
card that names a window over a query with no bound on it.
|
|
668
688
|
|
|
689
|
+
**A query's range comes from the data or the person's words - never a date copied from a profile.** A window is in a
|
|
690
|
+
query only because the person asked for it ("2025", "the last seven years"). Otherwise the range is the data's own: the
|
|
691
|
+
first and last day a profile showed are where the data ran when you looked, and a query bounded to them drops every
|
|
692
|
+
later week, silently, once the model holds one. Leave the dates out, or bound the range with the data's own first and
|
|
693
|
+
last day, and print the range on the card from the rows the query returns, never as typed dates:
|
|
694
|
+
|
|
695
|
+
```dax
|
|
696
|
+
DATESBETWEEN ( 'Calendar'[Date], -- the range the cases themselves cover
|
|
697
|
+
CALCULATE ( MIN ( 'Cases'[Opened Date] ), REMOVEFILTERS () ),
|
|
698
|
+
CALCULATE ( MAX ( 'Cases'[Opened Date] ), REMOVEFILTERS () ) )
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
REMOVEFILTERS keeps the range whole under a page pick, so a quiet week at either end still reads 0. `check_app` flags a
|
|
702
|
+
query bounded by a date written into it: an error when the date is a day no one names a window by (a 25th, neither the
|
|
703
|
+
1st nor the end of a month), and a warning when no card around a chart reading the query names that window.
|
|
704
|
+
|
|
705
|
+
**A series over time returns every period in its range.** SUMMARIZECOLUMNS drops a row whose measures are all blank, so
|
|
706
|
+
under a pick with sparse data (one owner, one product) a week with no cases has no row at all, and a line joins the
|
|
707
|
+
weeks either side of it: a quiet week looks as busy as its neighbors. A count or sum reads 0 for an empty period, and a
|
|
708
|
+
rate or ratio stays blank (there's nothing to divide), so the line drops to 0 for the count and breaks for the rate:
|
|
709
|
+
|
|
710
|
+
```dax
|
|
711
|
+
EVALUATE
|
|
712
|
+
SUMMARIZECOLUMNS (
|
|
713
|
+
'Calendar'[Week],
|
|
714
|
+
DATESBETWEEN ( 'Calendar'[Date], -- the range the cases cover
|
|
715
|
+
CALCULATE ( MIN ( 'Cases'[Opened Date] ), REMOVEFILTERS () ),
|
|
716
|
+
CALCULATE ( MAX ( 'Cases'[Opened Date] ), REMOVEFILTERS () ) ),
|
|
717
|
+
"Cases Opened", COALESCE ( [Case Count], 0 ), -- 0 in a quiet week
|
|
718
|
+
"SLA Compliance", DIVIDE ( [Cases In SLA], [Case Count] ) -- blank in a quiet week
|
|
719
|
+
)
|
|
720
|
+
-- or let the period table drive: ADDMISSINGITEMS ( 'Calendar'[Week], SUMMARIZECOLUMNS ( ... ), 'Calendar'[Week] )
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
A COALESCE keeps every period of the period table, so bound the range with a filter on its date column: the data's own
|
|
724
|
+
first and last day, as above, or the window the person asked for. `check_app` warns on a query a line over time reads
|
|
725
|
+
that can drop a quiet period.
|
|
726
|
+
|
|
669
727
|
**2. Profile and generate from THE WHOLE RESULT of that query - every row, never a few pasted in.**
|
|
670
728
|
Eligibility reads row, region and category counts. A bivariate country map needs at least 24
|
|
671
729
|
countries, so the same 40-country query shown as 8 sample rows hides it, and nothing fails - you are
|
|
@@ -705,6 +763,7 @@ without naming:
|
|
|
705
763
|
| every place drawn at the same size, because which are high or low matters more than where | World tile-grid cartogram (or US hex-tile cartogram for states) |
|
|
706
764
|
| actual against a budget, plan or target, with the gap shown for each period or unit | Variance chart (budget vs actual) |
|
|
707
765
|
| that same actual-against-target comparison across many accounts or nested groups, read as a table with subtotals | Variance grid (a preview type) - it needs a category to group by; a date series alone is the Variance chart |
|
|
766
|
+
| one total split into its parts level by level, with the reader choosing which field splits each level (a decomposition tree) | Decomposition cards (a preview type) - it needs a measure that adds up and two or more category fields; the opening, re-splitting and paging are drawn inside the chart |
|
|
708
767
|
| a table with one row per item and each value DRAWN (bars, dots or icons in the cells), compact, for scanning down a column and nothing more | Table with embedded |
|
|
709
768
|
| a table a reader WORKS with: sorts, drags column widths, copies cells out, hides columns, scrolls a wide table sideways with frozen columns or rows, opens a row's detail; plain values by default, and where the request asks, data bars, chips, icons, or one value compared with several others as bars (this week against its averages) | Interactive table (a preview type) - name the cells to draw in `prompt`; widths, sort and hidden columns are the reader's, kept through the host's view state |
|
|
710
769
|
| circling a group of points to see what sets them apart from the rest | Selection explainer (a preview type) - the lasso and the comparison are drawn inside the chart; put the other fields to compare in the same table (which ones: below); on a page, give it `selects` so a circled group filters the page (step 6) |
|
|
@@ -722,7 +781,8 @@ group statistics (what the circled points share, against the rest) into the app
|
|
|
722
781
|
Which other fields to put in its query: 4 to 8 a reader would ask about that can differ across the plot - segments and
|
|
723
782
|
categories (region, main channel), mix shares (online, retail, partner), size (population, a revenue band), and one or two
|
|
724
783
|
measures at most. Leave out row keys, the axes and fields that just track an axis (revenue beside marketing spend): the
|
|
725
|
-
panel tests every field, and each extra field makes the test for every field stricter.
|
|
784
|
+
panel tests every field, and each extra field makes the test for every field stricter. A share is 0 where its part has
|
|
785
|
+
no rows (`DIVIDE ( CALCULATE ( [Revenue], ... ) + 0, [Revenue] )`): a blank share is left out of the comparison.
|
|
726
786
|
|
|
727
787
|
A chart that draws its own rate control needs the history and the goal, not the model's what-if parameter:
|
|
728
788
|
leave the parameter's table (a `'Price Change'[Price Change]` column, say) out of its query. Grouping by it repeats
|
|
@@ -831,9 +891,24 @@ function CountryMap({ table, onPick }) { // table = the live query resu
|
|
|
831
891
|
294 when no width is named), and if it doesn't fit, generate it again with `generate_chart` at `width: 294` (the
|
|
832
892
|
engine's small-screen rules then apply). A minimum wider than the phone's box draws the chart wider than its card,
|
|
833
893
|
so the card scrolls sideways and anything the chart keeps in place (a table's name column) scrolls away with it.
|
|
834
|
-
`check_app` warns on a `minChartWidth` above 294.
|
|
894
|
+
`check_app` warns on a `minChartWidth` above 294, except at or below the chart type's own minimum (next).
|
|
895
|
+
- **The exception: a chart type's own minimum.** Some chart types can't be read narrower than a minimum of their own,
|
|
896
|
+
and it can be wider than a phone. `generate_chart` reports it (a "Minimum size" line, and `chartMinWidth` /
|
|
897
|
+
`chartMinHeight` on its result), and the component it writes beside the chart keeps it for you: given less, the
|
|
898
|
+
component draws the chart at its minimum inside a box that scrolls, so on a phone that one chart scrolls sideways
|
|
899
|
+
and never squashes. Set nothing for it. Never regenerate a chart type below its own minimum to fit a phone - it
|
|
900
|
+
comes back squashed - and `check_chart` draws it at its minimum, as the component does.
|
|
901
|
+
- **From a laptop up, no chart scrolls sideways.** A phone scrolling a chart at its type's minimum is designed; a laptop
|
|
902
|
+
never should: at every width from 1024px, give each chart a card at least its minimum. A two-column grid leaves a card
|
|
903
|
+
about 436px at a 1024px page, and a three-column one about 358px at 1280px (after the page's padding, a `gap-5` and a
|
|
904
|
+
`p-4` card with a border), so a chart whose minimum is wider gets a row of its own at those widths (move its card out of
|
|
905
|
+
the grid, or `lg:col-span-2`), or a grid track at least its minimum plus the card's padding and border
|
|
906
|
+
(`minmax(544px, ...)` for a 510px chart in that card). `check_app` warns on a card narrower than its chart's minimum at
|
|
907
|
+
any width from 1024 to 1920px.
|
|
835
908
|
- Take the actual size from the layout - percent, flex or grid `fr` - and never a fixed height smaller than the chart's
|
|
836
|
-
minimum.
|
|
909
|
+
minimum. A card of fixed height holds its title and subtitle too, at a phone's width: a subtitle that takes one
|
|
910
|
+
line on a desktop wraps to three on a phone, and the chart's cell loses those lines. `check_app` warns on a card whose
|
|
911
|
+
fixed height leaves its chart under its minimum on a phone.
|
|
837
912
|
- 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
|
|
838
913
|
`min-height: auto` lets it grow past its card, and the card's `overflow-hidden` slices off the rest.
|
|
839
914
|
- A chart with controls drawn inside it (sliders, steppers, a reset) gets its card's full width. Put a side panel that
|
|
@@ -879,12 +954,16 @@ what the pick matches: its query goes through `filterQuery` / `treatAs` (or `fil
|
|
|
879
954
|
KPIs and tables included - a table that follows a click shows only the matching rows. The chart that was clicked keeps
|
|
880
955
|
all its marks and highlights the pick: a chart never filters itself, so the reader can change the pick there. When the
|
|
881
956
|
person names more than one chart to click ("on the map or in the table"), give each its own filter on the same key,
|
|
882
|
-
and pass every panel's query through a `treatAs` for each of those filters but its own - one filter
|
|
883
|
-
clicked charts leaves each unfiltered by the other's click:
|
|
957
|
+
all in ONE `group`, and pass every panel's query through a `treatAs` for each of those filters but its own - one filter
|
|
958
|
+
shared by two clicked charts leaves each unfiltered by the other's click. The group makes them one pick: a pick in one
|
|
959
|
+
clears the others, so the most recent pick is the page's one pick; every member's chart shows it, so clicking it again in
|
|
960
|
+
whichever panel shows it clears it; and only the member holding it is active (one chip, one scope line). Without the
|
|
961
|
+
group they are separate picks, ANDed: a country picked on the map and then in the table is held twice, and clicking it
|
|
962
|
+
again on the map leaves the page filtered by the table's. `check_app` flags filters on one key without a group.
|
|
884
963
|
|
|
885
964
|
```tsx
|
|
886
|
-
const fromMap = useBicFilter("CountryIso3", { label: "Country (map)", display: "CountryName" });
|
|
887
|
-
const fromTable = useBicFilter("CountryIso3", { label: "Country (table)", display: "CountryName" });
|
|
965
|
+
const fromMap = useBicFilter("CountryIso3", { label: "Country (map)", display: "CountryName", group: "country" });
|
|
966
|
+
const fromTable = useBicFilter("CountryIso3", { label: "Country (table)", display: "CountryName", group: "country" });
|
|
888
967
|
const COUNTRY = "Geography[CountryIso3]";
|
|
889
968
|
const mapQuery = filterQuery(MAP_DAX, treatAs(fromTable, COUNTRY)); // the map follows the table's pick
|
|
890
969
|
const tableQuery = filterQuery(TABLE_DAX, treatAs(fromMap, COUNTRY)); // the table follows the map's
|
|
@@ -893,6 +972,12 @@ const kpiQuery = filterQuery(KPI_DAX, treatAs(fromMap, COUNTRY), treatAs(fromTab
|
|
|
893
972
|
// <KpiChart ... scopedBy={[fromMap, fromTable]} />; every panel gets its query={...}
|
|
894
973
|
```
|
|
895
974
|
|
|
975
|
+
**A list beside those panels follows the page's one pick, never the union of filters.** An "open items for the picked
|
|
976
|
+
countries" panel, a count or an "Add" button reads the group members' keys
|
|
977
|
+
(`[fromMap, fromTable].flatMap(f => f.keys)`), which with the group are that one pick. Separate filters' keys are a union
|
|
978
|
+
the page never shows: a scatter's three circled countries and a map pick of one of them list all three beside a page
|
|
979
|
+
narrowed to one.
|
|
980
|
+
|
|
896
981
|
**A panel whose query takes a page filter passes that filter as `scopedBy`, so the chart says it's filtered.** Its rows
|
|
897
982
|
arrive already narrowed, and the chart can't see a filter applied to its query: a KPI deck kept its region's title over
|
|
898
983
|
one country's totals and ranked what the filter left. Given `scopedBy={country}` (or `scopedBy={[fromMap, fromTable]}`),
|
|
@@ -902,7 +987,9 @@ with `filterBy` needs nothing more, its component tells the chart itself. A pane
|
|
|
902
987
|
figure it plots (each measure under `REMOVEFILTERS`, `ALL` or an `ALLEXCEPT` that drops the picked column, as the closed
|
|
903
988
|
deal figures are under a stage pick) isn't narrowed by it: leave that pick out of its `scopedBy`, or apply it, or its
|
|
904
989
|
scope line names a stage its marks never move for. `check_app` flags a `scopedBy` pick the panel's query removes from
|
|
905
|
-
everything it plots.
|
|
990
|
+
everything it plots. A query that takes only part of a pick's key (`treatAs(cell, "'Accounts'[Region]")` on a region x
|
|
991
|
+
stage cell) names that part: `scopedBy={{ filter: cell, takes: ["Region"] }}` says "Europe", never "Europe · 3-Propose",
|
|
992
|
+
and a stage-only pick adds nothing to that chart's line.
|
|
906
993
|
|
|
907
994
|
`check_app` lists each chart's wiring - filtered query, highlight only, or none - and warns on a panel no click
|
|
908
995
|
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.
|
|
@@ -923,7 +1010,8 @@ What the library does, so the page doesn't:
|
|
|
923
1010
|
filter, and `country.value` is its FIRST key only. Anything that names or acts on the pick (a panel heading, an "Add
|
|
924
1011
|
an item" button) reads `country.keys`: with one, name it (`country.text`); with more, say how many ("3 countries")
|
|
925
1012
|
and list them, and offer a one-mark action only while `country.keys.length === 1`. When several filters share a key
|
|
926
|
-
(one per clicked chart), read every one of them
|
|
1013
|
+
(one per clicked chart), read every one of them; they're in one `group`, so their keys are the page's one pick, never
|
|
1014
|
+
a union. `check_app` flags a `.value` read.
|
|
927
1015
|
- **A label picks its own field.** On a chart of region by stage with a filter keyed `["Region", "PipelineStep"]`, a
|
|
928
1016
|
click on a region's axis label holds that region in ANY stage, not the stage tuples drawn at that moment: the key is
|
|
929
1017
|
`["EMEA", ANY_VALUE]` (`isAnyValue` tells a free column; both from `@bicharts/chart-host/react` or `/page`), the chip
|
|
@@ -1082,6 +1170,12 @@ when the form offers every account, list them with `ADDCOLUMNS ( SUMMARIZE ( 'Ac
|
|
|
1082
1170
|
... ), "Open Pipeline", [Revenue Open] )`. `check_app` flags a marker-hosting chart whose query drops rows the form can
|
|
1083
1171
|
target.
|
|
1084
1172
|
|
|
1173
|
+
**Markers go on every page that shows what they mark.** When the request asks for markers "on the map and in the table"
|
|
1174
|
+
and more than one page has a map or a table of those things, every one of them carries the badges, unless the person named
|
|
1175
|
+
the page, and every one of those pages lets the reader add an item and open a marker's items. A reader who adds an item on
|
|
1176
|
+
one page and then opens another, the default page included, looks for the marker there too. Say which pages carry them
|
|
1177
|
+
in your reply.
|
|
1178
|
+
|
|
1085
1179
|
The files use the template's own tokens; restyle them as you like. The table and the permissions are created on
|
|
1086
1180
|
the next `npx rayfin up`, and every later `rayfin up` applies entity changes.
|
|
1087
1181
|
|
|
@@ -1094,7 +1188,10 @@ files do:
|
|
|
1094
1188
|
- **The name shown comes from the caller's token too**, never only from the browser: the name is the signed-in email,
|
|
1095
1189
|
and the policy checks it with the id - `claims.sub.eq(item.createdById).and(claims.email.eq(item.createdBy))` - so a
|
|
1096
1190
|
hand-made call with your own id and someone else's name is refused. (`claims.email` is the token's email claim; a
|
|
1097
|
-
Rayfin function sees the same token as `ctx.accessToken`.)
|
|
1191
|
+
Rayfin function sees the same token as `ctx.accessToken`.) A function that checks the name against that token reads
|
|
1192
|
+
the email the way Rayfin's sign-in reads the session's: the `email` claim, else, on a managed-hosting token, which has
|
|
1193
|
+
none, `xms_attr.<app>.rfn_email`. It never falls back to `preferred_username` or `upn`: the session's email never
|
|
1194
|
+
comes from them, so where they differ the person's own email is refused, or a name passes that the policy refuses.
|
|
1098
1195
|
- **When** is the server's clock, never the browser's: every write that records a time (an item created, an item
|
|
1099
1196
|
marked done) goes through a Rayfin function that stamps it on the server. Rayfin's data layer (checked on 1.36.2) has
|
|
1100
1197
|
no server-time default for a field (a `default` is a fixed value), so the browser never sends the time at all. The
|
|
@@ -1164,10 +1261,39 @@ const markDone = useCallback(async (taskId: string) => {
|
|
|
1164
1261
|
("Nothing open") before the rows are in.
|
|
1165
1262
|
- A form's default that comes from data loading elsewhere (the item's owner) is adopted when it arrives, unless the
|
|
1166
1263
|
reader has already chosen; a lookup behind a choice list that fails says so in the form.
|
|
1167
|
-
- A due date
|
|
1264
|
+
- A due date is a calendar day with no time zone, and both sides read "today" one way. The form gives the field
|
|
1265
|
+
`min={today}` in the reader's own date (YYYY-MM-DD from `getFullYear()`, `getMonth()` and `getDate()`, never
|
|
1266
|
+
`toISOString()`, which is UTC's). The server function refuses only a day already past everywhere: one before the date
|
|
1267
|
+
at UTC-12 (`new Date(Date.now() - 12 * 3600_000).toISOString().slice(0, 10)`), the earliest calendar day still
|
|
1268
|
+
current anywhere, so never UTC's today or its own. The server can't know the reader's time zone and mustn't trust one
|
|
1269
|
+
the browser sends, and a server that refuses by UTC's today refuses the reader's today every evening west of UTC: the
|
|
1270
|
+
form offers it, and the save fails with "can't be in the past".
|
|
1168
1271
|
- Read what the page shows, not every row ever saved: bound the query with `.where(...)` and show the first page as it
|
|
1169
|
-
lands
|
|
1272
|
+
lands on the FIRST load. **A reload keeps what's on screen until the whole reload has landed**, then swaps it in
|
|
1273
|
+
one step: a reload (after a save, on window focus) that hands each page to the list as it arrives cuts the list to
|
|
1274
|
+
its newest page, and the markers and open items beyond it vanish and come back. A reload that fails keeps the list it
|
|
1275
|
+
had and says the refresh failed; it never replaces the list with an error. Bound the DONE items, by when they were done; never bound the open ones, least of all by their due date: the
|
|
1170
1276
|
most overdue open item is the one the reader most needs, and a due-date window drops it first.
|
|
1277
|
+
- **The panel keeps the rows beside a refresh error, too.** A hook that keeps its rows and sets one `error` on any
|
|
1278
|
+
failed load still loses the list when the panel renders `error ? <p>...</p> : <List />`: one failed refresh (a
|
|
1279
|
+
network blip as the tab regains focus) hides every row and its Mark done button, under a line that may even say the
|
|
1280
|
+
last ones are shown. Have the hook return the two failures apart, and show the load error in place of the rows only
|
|
1281
|
+
while nothing has loaded:
|
|
1282
|
+
|
|
1283
|
+
```ts
|
|
1284
|
+
// in the hook's reload: the first load has nothing to keep; a later one keeps the list it had
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
if (mine !== seq.current) return;
|
|
1287
|
+
if (loaded.current) setRefreshError(message(e)); else setLoadError(message(e));
|
|
1288
|
+
}
|
|
1289
|
+
// in the panel
|
|
1290
|
+
{loadError ? <p role="alert">The items couldn't load: {loadError} <button onClick={reload}>Retry</button></p>
|
|
1291
|
+
: !loaded ? <p>Loading...</p>
|
|
1292
|
+
: <>{refreshError ? <p role="status">Couldn't refresh ({refreshError}); showing the last ones loaded.</p> : null}
|
|
1293
|
+
<ItemList items={open} /></>}
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
`check_app` flags a panel that shows a refreshing hook's error in place of its rows.
|
|
1171
1297
|
- A panel whose query failed offers Retry: setup's `BicChartPanel` does, from the query hook's `refetch` (pass the
|
|
1172
1298
|
hook's whole result).
|
|
1173
1299
|
- A disabled Save says why, beside it ("Pick a country from the list", "The due date can't be in the past"), tied to
|
|
@@ -1188,6 +1314,12 @@ const markDone = useCallback(async (taskId: string) => {
|
|
|
1188
1314
|
in the form: an owner-of-each-account lookup run for the flag form also places the table's badges and fills the
|
|
1189
1315
|
follow-ups list, so while it loads, or after it fails, those read as truly empty unless the page says why. A blank
|
|
1190
1316
|
model value is left out of text, never put in with `String()`: `String(null)` prints "null".
|
|
1317
|
+
- A lookup's rows (any query no chart draws: a form's choice list, every account with its owner) are read by column
|
|
1318
|
+
name with `queryRows(result.data)` from `@bicharts/chart-host/dax`: `row["Account Number"]` for
|
|
1319
|
+
`Accounts[Account Number]`, `row["Total"]` for `[Total]`. Never parse the `Table[Column]` headers yourself, and
|
|
1320
|
+
write source with the file tools, never through a shell heredoc into a script: a pattern that passes through a
|
|
1321
|
+
shell and a script loses its backslashes, and every option reads blank while the tests and the type check pass.
|
|
1322
|
+
`check_app` flags a hand-written header reader.
|
|
1191
1323
|
- Other people's writes show without a page reload: the hook reloads when the page comes back into view
|
|
1192
1324
|
(`visibilitychange` to visible, and window `focus`), never on a timer, and a write that happens once (a done)
|
|
1193
1325
|
reloads whether it succeeds or is refused (`try { await markDone() } finally { await reload() }`), so a refusal
|