@bicharts/chart-mcp 0.3.1 → 0.3.2

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/index.mjs CHANGED
@@ -120,7 +120,7 @@ try {
120
120
  `);let o=t.code||"",n=F=>F.test(o),i=!!t.geoKind||n(/options\.geo\b|__geoIso__/),a=n(/__geoIso__/),s=o.includes(v7)||n(/animPlaySpeedMs|animMaxIdealFrames|animTimelineStyle|animAutoPlay/),l=o.includes(z8),c=o.includes(j4),p=n(/\boptions\.onSelect\b|\bonSelect\b/),u=n(/colorScaleLow|colorScaleHigh/),m=n(/options\.aggregation\b/),y=n(/themeFg|themeAccent/),d=n(/__geoLat__|__geoLon__/),h=[{re:/d3\.sankey|sankeyLinkHorizontal|sankeyJustify|sankeyCenter/,spec:"d3-sankey@0.12.3",named:"sankey, sankeyLinkHorizontal, sankeyLeft, sankeyRight, sankeyCenter, sankeyJustify"},{re:/d3\.hexbin|hexbin\(/,spec:"d3-hexbin@0.2.2",named:"hexbin"},{re:/weightedVoronoi/,spec:"d3-weighted-voronoi@1.1.3",named:"weightedVoronoi"},{re:/voronoiMap\b/,spec:"d3-voronoi-map@2.1.1",named:"voronoiMap",deps:["d3-weighted-voronoi@1.1.3"]},{re:/voronoiTreemap/,spec:"d3-voronoi-treemap@1.1.2",named:"voronoiTreemap",deps:["d3-weighted-voronoi@1.1.3","d3-voronoi-map@2.1.1"]}],f=h.filter(F=>n(F.re)),C=Array.from(new Set(f.flatMap(F=>[...F.deps??[],F.spec]))),M=C.map(F=>{let $=F.split("@")[0],o3=h.find(c3=>c3.spec===F)?.named??$.replace(/^d3-/,"").replace(/-([a-z])/g,(c3,Z)=>Z.toUpperCase());return{pkg:$,named:o3}}),v=n(/maxMapPoints/),S=n(/geoLandColor|geoNoDataColor/),b=[`INTEGRATION CONTRACT (D3 \u2014 a portable function, merge it anywhere) [host contract v${T8}]`,""," BEFORE YOU HAND-ROLL: @bicharts/chart-host is the reference host implementation."," It does everything in this document \u2014 compiles the code, applies the option"," defaults, resolves mark clicks (including through tooltip overlays), owns the"," selection affordance, translates row indices between cross-filtered charts, and"," handles teardown. React bindings: <BicChart> / <BicChartGroup>.",nA," Everything below is the FULL HAND-ROLL SPEC \u2014 what to build if you are not using"," it, and what it is doing for you if you are.",""," Entry point: function render(container, data, options)"," \u2022 container \u2014 a DOM element to draw into (the function owns its contents)."," It MUST have a resolved width/height BEFORE you call render: the chart draws"," once at whatever size it is handed and does not re-measure. A container inside"," a display:none tab, or measured before layout, yields a 0px chart forever."," \u2022 Requires D3 v7. The code reads a bare `d3` identifier, so it needs d3 either as"," a global (`window.d3 = d3`) OR injected as a parameter by whatever compiles it"," \u2014 @bicharts/chart-host's compileRenderFn injects it, so in a bundled app there is"," NO global and you pass your imported d3 in.",' npm install d3@7 // then: import * as d3 from "d3";',...f.length?[` \u2022 D3 PLUGINS \u2014 this chart calls ${M.map(F=>F.pkg).join(", ")}.`," These are SEPARATE packages that attach onto the d3 object; the generated"," code just calls d3.<plugin>(). Install them, then augment the same d3 you"," hand to the chart:",` npm install ${C.join(" ")}`,...f.some(F=>F.deps?.length)?[" (install order above is also the ATTACH order \u2014 the dependency packages"," must augment d3 before the one that builds on them)"]:[],' import * as d3 from "d3";',...M.map(F=>` import { ${F.named} } from "${F.pkg}";`),` Object.assign(d3, { ${M.map(F=>F.named).join(", ")} });`]:[]," \u2022 It is a plain function \u2014 drop it into any app; call render() yourself.",""," data = { columns, rows } (build it in THIS exact shape from YOUR data):"," \u2022 columns: array of { name, dataType, isMeasure } in fixed order:",r," (trailing __rowIdx__ = HOST METADATA, the row's index"+(a?"; __geoIso__ = the ISO/USPS join key this tool computed, null = unmatched).":d?"; __geoLat__/__geoLon__ = coordinates this tool resolved, null = unplaceable).":").")," \u2022 rows: array of POSITIONAL arrays aligned to columns:",a?" [ v0, v1, \u2026, vN, rowIndex, isoCode ] // last two = __rowIdx__, __geoIso__":d?" [ v0, v1, \u2026, vN, rowIndex, lat, lon ] // last three = __rowIdx__, __geoLat__, __geoLon__":" [ v0, v1, \u2026, vN, rowIndex ] // last cell = that row's __rowIdx__"," DateTime cells are ISO-8601 strings. Missing values are null.",""," options = {"," width, height, // px"," palette: string[], // hex colors; [] = the code's own defaults"," backgroundColor, cultureCode, // '' acceptable"];return y&&b.push(" themeFg, themeAccent, // foreground / accent hex (label + play-position color)"),b.push(" allowTooltips: boolean,"),u&&b.push(" colorScaleLow, colorScaleHigh, // force the sequential scale endpoints (live restyle)"),m&&b.push(" aggregation, // '' | sum|average|median|min|max|count|distinct (live restyle)"),a&&b.push(" geo: FeatureCollection, // REQUIRED \u2014 the geometry (feature.id = join code);"," // supplied by this tool (data.geo.json / preview)"," geoUnmatched: { count, examples }, // drives the 'N regions unmatched' annotation"),d&&b.push(" geo: FeatureCollection, // REQUIRED \u2014 the BASEMAP (context land, no join);"," // supplied by this tool (data.geo.json / preview)"," geoPoint: { // what the geocoding actually achieved \u2014 pass it"," precision, // 'latlon'|'zip'|'city'|'state': COARSEST tier used"," unplaced, unplacedExamples, // rows with no resolvable coordinate"," ambiguousRows }, // placed by a largest-match tie-break"," // The chart annotates these; omitting it makes a"," // sparse/approximate map LOOK exact."),S&&b.push(" geoLandColor, geoNoDataColor, // basemap fills ('' = the code's own defaults)"),v&&b.push(" maxMapPoints, // cap on plotted points (default 1000). Over it, the"," // chart keeps the LARGEST N and says so on-chart."),s&&b.push(` animMaxIdealFrames, // ${Or}..${Er} (default ${A7}): target keyframe count \u2014 the`," // code re-grains dates to the finest grain <= this,"," // LIVE (see 'Animation' below)",` animPlaySpeedMs, // ${Rr}..${$r} (default ${P7}): ms per period`," animTimelineStyle, // '' auto | 'line' | 'boxes'"," animAutoPlay, animStopAtEnd, filtersDuringPlay, // booleans"),b.push(" uiState, setUiState, // optional persisted view-state; {} + no-op is fine"," }",""),(c||l||p)&&(b.push(" Interactivity / cross-filter (what the bridge must do \u2014 chart-host does it):"),c&&b.push(" \u2022 Every selectable mark carries a data-row-idx attribute = the comma-joined"," __rowIdx__ values it represents. On a mark click, read data-row-idx, map the"," indices back to your rows, and update your filter/state, then re-render other"," panels with the filtered rows."," \u2022 ROW INDEX HAZARD \u2014 the one thing hand-rolled integrations get wrong."," __rowIdx__ is a position WITHIN THE PAYLOAD THAT CHART RECEIVED, not an id in"," your source table. The moment you re-render a chart with filtered rows, its"," payload renumbers from zero \u2014 so the SAME integer now denotes a DIFFERENT"," record, and comparing indices across two charts silently selects the wrong"," rows. It does not throw; it just quietly filters to the wrong thing."," Keep ONE source table plus a payload-row -> source-row map per chart, and"," translate on every hop. (<BicChartGroup> exists precisely to own this.)"," \u2022 Marks are MANY-TO-MANY with rows: a legend swatch, an aggregated bar or an"," axis-group label carries every row it covers, so treat a mark as selected"," when ANY of its rows is selected \u2014 that is what makes a legend click light up"," its swatch and its bars together."," \u2022 Clicks can be SWALLOWED by the chart's own tooltip/crosshair overlay: an"," overlay painted over the marks with a transparent (not `none`) fill still"," hit-tests, so event.target is the overlay and a naive closest('[data-row-idx]')"," finds nothing and reads as 'empty canvas'. Resolve a miss through"," document.elementsFromPoint before concluding the user clicked nothing."),l&&b.push(` \u2022 This chart also DISPATCHES a bubbling \`${z8}\` CustomEvent on the`," container when its selection changes (e.g. a scrubber period). Listen for it:",` container.addEventListener('${z8}', e => {`," if (e.detail.clear) { /* selection cleared */ }",` else { const idx = e.detail.mark.getAttribute('${j4}'); /* filter */ }`," });"," \u2022 Lifecycle slots the code sets on the container (call/read them from your host):",` container.${Ir}() // call to clear the chart's own selection`,` container.${Lr} // a mark to apply as the initial filter (may be set)`,` container.${v7}() // CALL THIS before unmount / before re-rendering`),p?b.push(" \u2022 options.onSelect(rowIdxs:number[]) is also invoked when provided \u2014 wire it to"," your cross-filter if you prefer a callback over the DOM event."):b.push(" \u2022 NOTE: this code does NOT call options.onSelect \u2014 use data-row-idx"+(l?" + the event above.":".")),b.push("")),s&&b.push(" Animation (keyframe playback + LIVE re-grain):"," \u2022 The chart draws its own play/pause control + period scrubber. It reads the anim"," options above; sensible defaults apply if you omit them."," \u2022 DYNAMIC KEYFRAMES: the code chooses the time grain AT RENDER from"," options.animMaxIdealFrames \u2014 it truncates dates to the finest calendar grain"," (day/week/month/quarter/year/decade) whose bucket count <= that value and"," aggregates rows within each bucket. To change the granularity LIVE, just call"," render(container, data, {...options, animMaxIdealFrames: N}) again \u2014 NO new"," generation needed. (This is what the Power BI 'Max Ideal Frames' slider does.)",` \u2022 Call container.${v7}() before you re-render or unmount, to stop its timer.`,""),d&&b.push(" Geography (POINT MAP):"," \u2022 Coordinates are HOST-RESOLVED. This tool geocoded your place columns"," (city / state / zip, or lat+lon when present) and appended __geoLat__ and"," __geoLon__ to every row \u2014 the chart plots those columns directly and does NOT"," geocode anything itself. Build them the same way if you re-derive the data, or"," the map draws nothing: the code bails when it can't find the two columns."," \u2022 options.geo is the BASEMAP (grey context land), NOT a join target \u2014 there is no"," __geoIso__ and no feature.id lookup. Without it you get points on a blank field."," \u2022 Pass options.geoPoint through. It is how the chart tells the truth about"," precision (city-centroid vs exact) and about rows it could not place.",""),a&&b.push(" Geography:"," \u2022 This is a choropleth: it REQUIRES options.geo (a GeoJSON FeatureCollection whose"," feature.id is the join code) and the __geoIso__ column in data (both provided by"," this tool \u2014 see data.geo.json when out_dir is set). Without options.geo it renders"," an empty map. Do NOT fetch or invent geometry; pass what the tool supplied.",""),b.push(" HOST PAGE PREREQUISITES (@bicharts/chart-host does all of these for you):"," \u2022 SIZE THE CONTAINER FIRST. Measure synchronously (getBoundingClientRect in a"," layout effect) and pass width/height explicitly \u2014 they have NO defaults, and a"," chart handed undefined/0 draws once at nothing and never recovers. Use a"," ResizeObserver for LATER resizes only: gating the FIRST paint on it costs"," seconds in a headless/background tab, where frames are barely produced."," \u2022 PIN TYPOGRAPHY AT THE BOUNDARY. Charts set font-size but almost never"," line-height or letter-spacing, so they inherit YOURS. A page with"," `:root { font: 18px/145% }` \u2014 the stock Vite template does exactly this \u2014 hands"," an 11px chart label a 26px line box and slices it through the middle, on a chart"," that renders perfectly elsewhere. Set `line-height: normal; letter-spacing:"," normal` on the chart container (a chart that sets its own still wins)."," \u2022 SELECTION STYLING IS YOURS. Generated code emits marks and data-row-idx; it"," never styles 'selected', because only the host knows what is selected. Add"," `lch-has-selection` to the container and `lch-mark-selected` to selected marks,"," and dim the rest via the `--lch-dim-opacity` custom property (default 0.25)."," Re-apply after EVERY render or a restyle silently drops the highlight."," \u2022 TEAR DOWN ON UNMOUNT. Call container.__llmAnimStop__() if present before you"," re-render or unmount. Under React StrictMode effects double-mount, so without"," this every animated chart leaks a running timer per mount, on every hot reload.",""," To re-render with filtered/updated data or changed options: call"," render(container, newData, newOptions) again (it repaints the container)."),b.join(`
121
121
  `)}function Gc(t,e){return e?Hr(t):[]}var Zr={chartName:z.string().describe("The chart type the backend produced."),language:z.string().describe("Renderer/language of the code (D3, PYTHON, \u2026)."),version:z.number().describe("Server-side version number of this generation."),creditCost:z.number().nullable().describe("Credits billed, when the server reported them."),correlationId:z.string().describe("Ties this generation to the server-side log \u2014 quote it in any support request."),codeSummary:z.string().optional().describe("The backend's own one-line description of what it built."),code:z.string().optional().describe("The render() source. Omitted when include_code is false."),requiredD3Plugins:z.array(z.string()).describe('D3 plugin PACKAGES this code calls (e.g. ["d3-sankey"]) \u2014 install and Object.assign onto the same d3 you pass the host, or the chart throws mid-render. Empty for core-only d3 and for non-D3 languages.'),geo:z.object({column:z.string().nullable().describe("Column joined to the geometry; null for a basemap-only point map."),kind:z.string().describe("Geometry asset kind \u2014 pass the matching geometry as options.geo."),hasGeometry:z.boolean().describe("False means no bundled geometry matched this kind: the map will draw no land."),unmatched:z.number().describe("Regions in the data with no geometry match."),unmatchedExamples:z.array(z.string())}).nullable().describe("Region/basemap wiring; null for non-geo charts."),geoPoint:z.object({placed:z.number(),total:z.number(),precision:z.string().nullable(),unplaced:z.number(),unplacedExamples:z.array(z.string()),ambiguousRows:z.number().describe("Rows placed by a largest-match tie-break rather than an exact one.")}).nullable().describe("Point-geocoding outcome; null when the chart geocodes no points."),data:z.any().optional().describe("The render payload {columns, rows, \u2026} to hand render() \u2014 INCLUDING any __geoIso__/__geoLat__/__geoLon__ columns the caller cannot reconstruct. Present only when out_dir was NOT given; otherwise read files.data."),files:z.object({code:z.string().optional(),data:z.string().optional(),geo:z.string().optional(),preview:z.string().optional()}).optional().describe("Absolute paths written, when out_dir was given."),hostContract:z.string().describe("Host contract version the code targets; a host on a different MAJOR may not interoperate."),build:z.string().describe("MCP server build stamp \u2014 the bundle actually serving this call.")},kx=z.object(Zr);function qc(t){return`${t}
122
122
  export { render };
123
- `}function Kc(t){let e=t.renderData.geoPoint,r=t.renderData.geoUnmatched;return{chartName:t.res.finalChartName||"",language:t.res.language,version:t.res.version,creditCost:t.res.creditCost??null,correlationId:t.res.correlationId,...t.res.codeSummary?{codeSummary:t.res.codeSummary}:{},...t.withCode?{code:t.code}:{},requiredD3Plugins:t.plugins,geo:t.res.geoKind?{column:t.res.geoColumn||null,kind:t.res.geoKind,hasGeometry:t.hasGeometry,unmatched:r?.count??0,unmatchedExamples:r?.examples??[]}:null,geoPoint:e?{placed:t.renderData.rows.length-(e.unplaced??0),total:t.renderData.rows.length,precision:e.precision??null,unplaced:e.unplaced??0,unplacedExamples:e.unplacedExamples??[],ambiguousRows:e.ambiguousRows??0}:null,...t.files?{files:t.files}:{data:t.renderData},hostContract:t.hostContract,build:t.build}}Yc();Qc();tu();/*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */function ru(t){if(!t)return;let e=t.toLowerCase();if(/^country/.test(e))return jr;if(e==="us-state-code"||e==="us-state-name")return Jc;if(e==="us-zip5")return eu;if(e==="north-america")return iA()}var Wr;function iA(){return Wr||(Wr={type:"FeatureCollection",features:jr.features.filter(t=>Ic.has(t?.id)&&t?.geometry&&t.geometry.type!=="Point").map(t=>Rc(t,Lc)).filter(t=>t!==null)}),Wr}import{readdirSync as aA,existsSync as sA,statSync as ou}from"node:fs";import{join as Vr}from"node:path";var lA=["package.json","tsconfig.json","jsconfig.json","deno.json","deno.jsonc","angular.json","svelte.config.js","vite.config.ts","vite.config.js","pnpm-lock.yaml","yarn.lock","package-lock.json"],cA=["pyproject.toml","requirements.txt","setup.py","setup.cfg","Pipfile","poetry.lock","environment.yml","environment.yaml","conda.yaml","tox.ini"],uA=new Set([".js",".jsx",".mjs",".cjs",".ts",".tsx"]),pA=new Set([".py",".pyi"]);function nu(t,e){return e.some(r=>sA(Vr(t,r)))}function dA(t){let e=0,r=0,o=i=>{let a;try{a=aA(i)}catch{return}for(let s of a){if(s==="node_modules"||s===".git"||s.startsWith("."))continue;let l=Vr(i,s),c=!1;try{c=ou(l).isFile()}catch{continue}if(!c)continue;let p=s.lastIndexOf(".");if(p<0)continue;let u=s.slice(p).toLowerCase();uA.has(u)?e++:pA.has(u)&&r++}};o(t);let n=Vr(t,"src");try{ou(n).isDirectory()&&o(n)}catch{}return{js:e,py:r}}function Ur(t=process.cwd()){let e=nu(t,lA),r=nu(t,cA);if(e&&!r)return{language:"javascript",reason:`found a JS/TS project marker in ${t}`};if(r&&!e)return{language:"python",reason:`found a Python project marker in ${t}`};let{js:o,py:n}=dA(t);return o>0&&n===0?{language:"javascript",reason:`found ${o} JS/TS source file(s) and no Python in ${t}`}:n>0&&o===0?{language:"python",reason:`found ${n} Python source file(s) and no JS/TS in ${t}`}:o>0&&n>0?o>=n*3?{language:"javascript",reason:`JS/TS files dominate (${o} vs ${n} Python) in ${t}`}:n>=o*3?{language:"python",reason:`Python files dominate (${n} vs ${o} JS/TS) in ${t}`}:{language:null,reason:`mixed JS/TS (${o}) and Python (${n}) in ${t}`}:e&&r?{language:null,reason:`both JS and Python project markers present in ${t}`}:{language:null,reason:`no JS or Python markers or source files found in ${t}`}}var hA="0.3.1",C5=new u5({name:"bic-chart",version:hA}),qr="2026-08-01T18:18:28Z src:6d793c086981 (8 files)",fA=z.object({columns:z.array(z.object({name:z.string(),dataType:z.enum(["String","Integer","Decimal","DateTime"]).optional().describe("Known type (DAX/semantic-model callers have it); omit to infer from values."),isMeasure:z.boolean().optional().describe("Known measure/dimension role; omit to apply the numeric-non-identifier heuristic. A semantic-model caller should set this."),format:z.string().optional().describe('Format string from the model (e.g. "$#,##0") \u2192 modelFormat.'),description:z.string().optional().describe("Column description from the model \u2192 modelDesc.")})).min(1),rows:z.array(z.union([z.array(z.any()),z.record(z.string(),z.any())])).min(1).describe("Rows as positional arrays aligned to columns, OR objects keyed by column name.")}).describe("A pre-typed {columns, rows} table (DAX query results, dataframe, arrow). Use INSTEAD of csv_path/csv_text."),Kr={csv_path:z.string().optional().describe("Path to a CSV file (header row required)."),csv_text:z.string().optional().describe("Inline CSV text (alternative to csv_path)."),data:fA.optional().describe("A pre-typed {columns, rows} table \u2014 the shape a Fabric DAX query / dataframe already has. Alternative to csv_path/csv_text; when columns carry isMeasure/format/description the shape matches PBI fidelity with no guessing."),measures:z.array(z.string()).optional().describe("Columns to treat as MEASURES (the PBI 'Values' well \u2014 fields the chart aggregates). Numeric non-identifier columns are measures by default."),dimensions:z.array(z.string()).optional().describe("Columns to treat as DIMENSIONS (a PBI category/axis well \u2014 fields to group/break by). Use for numeric codes/years that should NOT be aggregated."),formats:z.record(z.string(),z.string()).optional().describe(`Per-column number/date FORMAT STRING (e.g. {"Revenue":"$#,##0","Margin":"0.0%"}). PBI supplies these from its model; a CSV can't, so provide them to steer number formatting.`),descriptions:z.record(z.string(),z.string()).optional().describe("Per-column human DESCRIPTION \u2014 gives the model semantic context the bare column name lacks."),group_by:z.array(z.string()).optional().describe("Columns that are the PRIMARY series/breakdown (the PBI 'Group by' well); the backend leads the visualization with these."),non_additive:z.array(z.string()).optional().describe("MEASURE columns that must NOT be summed (a ratio/average/rate \u2014 use mean, never stack). Usually auto-detected; this is an authoritative override."),privacy_level:z.enum(["0","10","20","30"]).optional().describe("How much detail to compute/share: 0 names only, 10 +counts, 20 +detailed stats (default), 30 +obfuscated sample rows.")};function Gr(t){return{measures:t.measures,dimensions:t.dimensions,formats:t.formats,descriptions:t.descriptions,groupBy:t.group_by,nonAdditive:t.non_additive,privacyLevel:t.privacy_level??"20"}}function Yr(t){if(t.data)return Pc(t.data.columns,t.data.rows,Gr(t));if(t.csv_text)return Mr(t.csv_text,Gr(t));if(t.csv_path)return Mr(mA(iu(t.csv_path),"utf-8"),Gr(t));throw new Error("Provide one of: data ({columns, rows}), csv_text, or csv_path.")}C5.registerTool("assess_data_shape",{title:"Assess data shape",description:"Profile a dataset locally (no data leaves the machine): per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association). This is the same measurement engine the BIC Power BI visual uses before generating a chart.",inputSchema:Kr},async t=>{let e=Yr(t),r={totalRows:e.totalRows,leafCardinality:e.index.getLeafCardinality(),columns:e.shape};return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}});C5.registerTool("list_eligible_charts",{title:"List eligible chart types",description:"The AUTHORITATIVE list of chart types that can render this dataset \u2014 the server's own eligibility engine (ChartFilter.GetCharts), NOT a local guess. Profiles the data locally, then calls the backend's zero-credit /qualify endpoint. NO LLM, NO credit. By default the tool DETECTS the project's language (JS/TS vs Python from the working dir) and returns the charts for THAT language, SCOPED + RANKED by the picker's chart-type\xD7renderer weights (best first), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/\u2026) to rank one specific renderer, or renderer '' for the full renderer-agnostic eligibility across ALL renderers (unranked). If the language can't be auto-detected and none is given, the tool asks you to specify one. Credentials optional (a license only unlocks draft chart types).",inputSchema:{...Kr,language:z.enum(["javascript","typescript","python",""]).optional().describe("Project source language \u2192 the server ranks across that language's renderers (javascript/typescript = D3/Vega, python = Plotly/matplotlib). Omit to AUTO-DETECT from the working dir; `renderer` (if given) overrides this."),project_dir:z.string().optional().describe("Directory to auto-detect the project language from (default: the MCP server's working directory)."),renderer:z.enum(["D3","PLOTLY","PYMATPLOT","VEGA",""]).optional().describe("Force a specific renderer (overrides `language`): a concrete renderer ranks that renderer's charts; '' = the full renderer-agnostic eligibility (all renderers, unranked). Omit to use `language`/auto-detection."),top:z.number().optional().describe("Show only the top-N ranked results (scoped mode only). Omit for all."),width:z.number().optional().describe("Viewport width px (default 800) \u2014 affects small-viewport / facet-budget gates."),height:z.number().optional().describe("Viewport height px (default 600).")}},async t=>{let e=Yr(t),r="",o="",n="";if(t.renderer!==void 0)r=t.renderer;else if(t.language)o=t.language;else{let u=Ur(t.project_dir);if(u.language)o=u.language,n=` (auto-detected language: ${u.language} \u2014 ${u.reason})`;else return{content:[{type:"text",text:`Couldn't determine the project language: ${u.reason}. Re-run with language: "javascript" or "python" (or renderer: "D3"/"PLOTLY"/\u2026 for a specific renderer, or renderer: "" for the full renderer-agnostic list).`}]}}let i=await Fc({shape:e.shape,totalRows:e.totalRows,leafCardinality:e.index.getLeafCardinality(),width:t.width,height:t.height,renderer:r,language:o});if(!i.ok)return{isError:!0,content:[{type:"text",text:`Could not list eligible charts: ${i.errorMessage}`}]};let a=i.charts.length>0&&i.charts[0].rank!==void 0,s=o?`${o}`:r,l=i.charts,c;a?(t.top&&t.top>0&&(l=l.slice(0,t.top)),c=`Top ${l.length} chart type(s) for this data in ${s}, ranked by the picker's weights (authoritative \u2014 server ChartFilter + ScoreChartTypes, no credits)${n}:`):c=`${l.length} chart type(s) can render this data \u2014 renderer-agnostic eligibility (authoritative \u2014 ChartFilter.GetCharts, no credits):`;let p=l.map(u=>{if(u.rank!==void 0){let m=u.language?` [${u.language}]`:"",y=u.score!==void 0?` (${u.score})`:"";return`${u.rank}. ${u.name}${m}${y}${u.description?` \u2014 ${u.description}`:""}`}return`\u2022 ${u.name}${u.description?` \u2014 ${u.description}`:""}`});return{content:[{type:"text",text:[c,"",...p].join(`
123
+ `}function Kc(t){let e=t.renderData.geoPoint,r=t.renderData.geoUnmatched;return{chartName:t.res.finalChartName||"",language:t.res.language,version:t.res.version,creditCost:t.res.creditCost??null,correlationId:t.res.correlationId,...t.res.codeSummary?{codeSummary:t.res.codeSummary}:{},...t.withCode?{code:t.code}:{},requiredD3Plugins:t.plugins,geo:t.res.geoKind?{column:t.res.geoColumn||null,kind:t.res.geoKind,hasGeometry:t.hasGeometry,unmatched:r?.count??0,unmatchedExamples:r?.examples??[]}:null,geoPoint:e?{placed:t.renderData.rows.length-(e.unplaced??0),total:t.renderData.rows.length,precision:e.precision??null,unplaced:e.unplaced??0,unplacedExamples:e.unplacedExamples??[],ambiguousRows:e.ambiguousRows??0}:null,...t.files?{files:t.files}:{data:t.renderData},hostContract:t.hostContract,build:t.build}}Yc();Qc();tu();/*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */function ru(t){if(!t)return;let e=t.toLowerCase();if(/^country/.test(e))return jr;if(e==="us-state-code"||e==="us-state-name")return Jc;if(e==="us-zip5")return eu;if(e==="north-america")return iA()}var Wr;function iA(){return Wr||(Wr={type:"FeatureCollection",features:jr.features.filter(t=>Ic.has(t?.id)&&t?.geometry&&t.geometry.type!=="Point").map(t=>Rc(t,Lc)).filter(t=>t!==null)}),Wr}import{readdirSync as aA,existsSync as sA,statSync as ou}from"node:fs";import{join as Vr}from"node:path";var lA=["package.json","tsconfig.json","jsconfig.json","deno.json","deno.jsonc","angular.json","svelte.config.js","vite.config.ts","vite.config.js","pnpm-lock.yaml","yarn.lock","package-lock.json"],cA=["pyproject.toml","requirements.txt","setup.py","setup.cfg","Pipfile","poetry.lock","environment.yml","environment.yaml","conda.yaml","tox.ini"],uA=new Set([".js",".jsx",".mjs",".cjs",".ts",".tsx"]),pA=new Set([".py",".pyi"]);function nu(t,e){return e.some(r=>sA(Vr(t,r)))}function dA(t){let e=0,r=0,o=i=>{let a;try{a=aA(i)}catch{return}for(let s of a){if(s==="node_modules"||s===".git"||s.startsWith("."))continue;let l=Vr(i,s),c=!1;try{c=ou(l).isFile()}catch{continue}if(!c)continue;let p=s.lastIndexOf(".");if(p<0)continue;let u=s.slice(p).toLowerCase();uA.has(u)?e++:pA.has(u)&&r++}};o(t);let n=Vr(t,"src");try{ou(n).isDirectory()&&o(n)}catch{}return{js:e,py:r}}function Ur(t=process.cwd()){let e=nu(t,lA),r=nu(t,cA);if(e&&!r)return{language:"javascript",reason:`found a JS/TS project marker in ${t}`};if(r&&!e)return{language:"python",reason:`found a Python project marker in ${t}`};let{js:o,py:n}=dA(t);return o>0&&n===0?{language:"javascript",reason:`found ${o} JS/TS source file(s) and no Python in ${t}`}:n>0&&o===0?{language:"python",reason:`found ${n} Python source file(s) and no JS/TS in ${t}`}:o>0&&n>0?o>=n*3?{language:"javascript",reason:`JS/TS files dominate (${o} vs ${n} Python) in ${t}`}:n>=o*3?{language:"python",reason:`Python files dominate (${n} vs ${o} JS/TS) in ${t}`}:{language:null,reason:`mixed JS/TS (${o}) and Python (${n}) in ${t}`}:e&&r?{language:null,reason:`both JS and Python project markers present in ${t}`}:{language:null,reason:`no JS or Python markers or source files found in ${t}`}}var hA="0.3.2",C5=new u5({name:"bic-chart",version:hA}),qr="2026-08-01T22:24:51Z src:e86698de379b (8 files)",fA=z.object({columns:z.array(z.object({name:z.string(),dataType:z.enum(["String","Integer","Decimal","DateTime"]).optional().describe("Known type (DAX/semantic-model callers have it); omit to infer from values."),isMeasure:z.boolean().optional().describe("Known measure/dimension role; omit to apply the numeric-non-identifier heuristic. A semantic-model caller should set this."),format:z.string().optional().describe('Format string from the model (e.g. "$#,##0") \u2192 modelFormat.'),description:z.string().optional().describe("Column description from the model \u2192 modelDesc.")})).min(1),rows:z.array(z.union([z.array(z.any()),z.record(z.string(),z.any())])).min(1).describe("Rows as positional arrays aligned to columns, OR objects keyed by column name.")}).describe("A pre-typed {columns, rows} table (DAX query results, dataframe, arrow). Use INSTEAD of csv_path/csv_text."),Kr={csv_path:z.string().optional().describe("Path to a CSV file (header row required)."),csv_text:z.string().optional().describe("Inline CSV text (alternative to csv_path)."),data:fA.optional().describe("A pre-typed {columns, rows} table \u2014 the shape a Fabric DAX query / dataframe already has. Alternative to csv_path/csv_text; when columns carry isMeasure/format/description the shape matches PBI fidelity with no guessing."),measures:z.array(z.string()).optional().describe("Columns to treat as MEASURES (the PBI 'Values' well \u2014 fields the chart aggregates). Numeric non-identifier columns are measures by default."),dimensions:z.array(z.string()).optional().describe("Columns to treat as DIMENSIONS (a PBI category/axis well \u2014 fields to group/break by). Use for numeric codes/years that should NOT be aggregated."),formats:z.record(z.string(),z.string()).optional().describe(`Per-column number/date FORMAT STRING (e.g. {"Revenue":"$#,##0","Margin":"0.0%"}). PBI supplies these from its model; a CSV can't, so provide them to steer number formatting.`),descriptions:z.record(z.string(),z.string()).optional().describe("Per-column human DESCRIPTION \u2014 gives the model semantic context the bare column name lacks."),group_by:z.array(z.string()).optional().describe("Columns that are the PRIMARY series/breakdown (the PBI 'Group by' well); the backend leads the visualization with these."),non_additive:z.array(z.string()).optional().describe("MEASURE columns that must NOT be summed (a ratio/average/rate \u2014 use mean, never stack). Usually auto-detected; this is an authoritative override."),privacy_level:z.enum(["0","10","20","30"]).optional().describe("How much detail to compute/share: 0 names only, 10 +counts, 20 +detailed stats (default), 30 +obfuscated sample rows.")};function Gr(t){return{measures:t.measures,dimensions:t.dimensions,formats:t.formats,descriptions:t.descriptions,groupBy:t.group_by,nonAdditive:t.non_additive,privacyLevel:t.privacy_level??"20"}}function Yr(t){if(t.data)return Pc(t.data.columns,t.data.rows,Gr(t));if(t.csv_text)return Mr(t.csv_text,Gr(t));if(t.csv_path)return Mr(mA(iu(t.csv_path),"utf-8"),Gr(t));throw new Error("Provide one of: data ({columns, rows}), csv_text, or csv_path.")}C5.registerTool("assess_data_shape",{title:"Assess data shape",description:"Profile a dataset locally (no data leaves the machine): per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association). This is the same measurement engine the BIC Power BI visual uses before generating a chart.",inputSchema:Kr},async t=>{let e=Yr(t),r={totalRows:e.totalRows,leafCardinality:e.index.getLeafCardinality(),columns:e.shape};return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}});C5.registerTool("list_eligible_charts",{title:"List eligible chart types",description:"The AUTHORITATIVE list of chart types that can render this dataset \u2014 the server's own eligibility engine (ChartFilter.GetCharts), NOT a local guess. Profiles the data locally, then calls the backend's zero-credit /qualify endpoint. NO LLM, NO credit. By default the tool DETECTS the project's language (JS/TS vs Python from the working dir) and returns the charts for THAT language, SCOPED + RANKED by the picker's chart-type\xD7renderer weights (best first), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/\u2026) to rank one specific renderer, or renderer '' for the full renderer-agnostic eligibility across ALL renderers (unranked). If the language can't be auto-detected and none is given, the tool asks you to specify one. Credentials optional (a license only unlocks draft chart types).",inputSchema:{...Kr,language:z.enum(["javascript","typescript","python",""]).optional().describe("Project source language \u2192 the server ranks across that language's renderers (javascript/typescript = D3/Vega, python = Plotly/matplotlib). Omit to AUTO-DETECT from the working dir; `renderer` (if given) overrides this."),project_dir:z.string().optional().describe("Directory to auto-detect the project language from (default: the MCP server's working directory)."),renderer:z.enum(["D3","PLOTLY","PYMATPLOT","VEGA",""]).optional().describe("Force a specific renderer (overrides `language`): a concrete renderer ranks that renderer's charts; '' = the full renderer-agnostic eligibility (all renderers, unranked). Omit to use `language`/auto-detection."),top:z.number().optional().describe("Show only the top-N ranked results (scoped mode only). Omit for all."),width:z.number().optional().describe("Viewport width px (default 800) \u2014 affects small-viewport / facet-budget gates."),height:z.number().optional().describe("Viewport height px (default 600).")}},async t=>{let e=Yr(t),r="",o="",n="";if(t.renderer!==void 0)r=t.renderer;else if(t.language)o=t.language;else{let u=Ur(t.project_dir);if(u.language)o=u.language,n=` (auto-detected language: ${u.language} \u2014 ${u.reason})`;else return{content:[{type:"text",text:`Couldn't determine the project language: ${u.reason}. Re-run with language: "javascript" or "python" (or renderer: "D3"/"PLOTLY"/\u2026 for a specific renderer, or renderer: "" for the full renderer-agnostic list).`}]}}let i=await Fc({shape:e.shape,totalRows:e.totalRows,leafCardinality:e.index.getLeafCardinality(),width:t.width,height:t.height,renderer:r,language:o});if(!i.ok)return{isError:!0,content:[{type:"text",text:`Could not list eligible charts: ${i.errorMessage}`}]};let a=i.charts.length>0&&i.charts[0].rank!==void 0,s=o?`${o}`:r,l=i.charts,c;a?(t.top&&t.top>0&&(l=l.slice(0,t.top)),c=`Top ${l.length} chart type(s) for this data in ${s}, ranked by the picker's weights (authoritative \u2014 server ChartFilter + ScoreChartTypes, no credits)${n}:`):c=`${l.length} chart type(s) can render this data \u2014 renderer-agnostic eligibility (authoritative \u2014 ChartFilter.GetCharts, no credits):`;let p=l.map(u=>{if(u.rank!==void 0){let m=u.language?` [${u.language}]`:"",y=u.score!==void 0?` (${u.score})`:"";return`${u.rank}. ${u.name}${m}${y}${u.description?` \u2014 ${u.description}`:""}`}return`\u2022 ${u.name}${u.description?` \u2014 ${u.description}`:""}`});return{content:[{type:"text",text:[c,"",...p].join(`
124
124
  `)}]}});C5.registerTool("generate_chart",{title:"Generate chart code",description:"Generate ready-to-render chart code for a dataset via the BIC backend: the data shape is profiled locally, the backend picks a suitable chart type (or honors chart_type), and returns code that has passed the backend's eligibility gates and quality checks. Requires a trial or paid BIC account (license env vars / credentials file). The returned D3 code is a portable render(container, data, options) function meant to be MERGED into your own app; an integration contract describes exactly how to call it. A standalone HTML preview and file output are optional.",inputSchema:{...Kr,prompt:z.string().optional().describe("Freeform guidance for the generation \u2014 the same 'user commentary' lane the Power BI visual uses. Steer encoding/axes/emphasis in plain language, e.g. 'put Year on the x-axis, color by Division', 'render citations as a chord diagram', 'log scale on revenue', 'annotate the peak'."),chart_type:z.string().optional().describe("Preferred chart type by name (honored unless the data shape disqualifies it)."),rows_policy:z.enum(["auto","all"]).optional().describe("'auto' (default) = the engine decides how to summarize. 'all' = render EXACTLY one mark/row per source row, with no Top-N, no 'Other' rollup and no sampling. Use 'all' when your app depends on every row being present (a row-per-entity table, a point per location)."),required_columns:z.array(z.string()).optional().describe("Fields that MUST be visible in the output, each as its own column/channel. AUTHORITATIVE and VERIFIED: if the generated code never references one, the server regenerates automatically rather than returning a chart that silently dropped it."),forbid_derived_columns:z.boolean().optional().describe("True = show only fields present in the data (plus required_columns) \u2014 no per-capita, share-of-total, index, rank or z-score columns the engine would otherwise add. Use when your app owns the analytics and the chart is only the display."),renderer:z.enum(["D3","PLOTLY","PYMATPLOT","VISUAL",""]).optional().describe("Force a specific renderer (overrides `language`). Omit to use `language`/auto-detection; 'VISUAL'/'' = let the backend choose."),language:z.enum(["javascript","typescript","python",""]).optional().describe("Project source language: with no concrete `renderer`, the backend auto-picks a renderer WITHIN this language (javascript/typescript \u2192 D3/Vega, python \u2192 Plotly/matplotlib). Omit to AUTO-DETECT from the working dir."),project_dir:z.string().optional().describe("Directory to auto-detect the project language from (default: the MCP server's working directory)."),model:z.string().optional().describe("Model code; empty = backend default."),reasoning_mode:z.enum(["","1P","2P","3P","CP"]).optional().describe("LLM pass structure; empty = backend heuristic."),width:z.number().optional(),height:z.number().optional(),wrap:z.enum(["none","esm"]).optional().describe("Code form: 'none' (default) = the raw render() function to merge; 'esm' = same code plus `export { render };` so the file can be `import`ed. Both forms are accepted everywhere - @bicharts/chart-host strips the export clause before compiling - so this is a convenience, never a compatibility choice."),include_code:z.boolean().optional().describe("Include the full code in the response (default true) \u2014 governs BOTH the text block and structuredContent.code. Set false when you only want it written to out_dir."),preview_html:z.boolean().optional().describe("Also emit a standalone D3 preview (needs out_dir). Default false \u2014 the code is meant to be merged, not run as an index.html."),out_dir:z.string().optional().describe("Optional directory to write chart.js/py, a data.sample.json, and (if preview_html) preview.html.")},outputSchema:Zr},async(t,e)=>{let r=t.privacy_level??"20",o=await Ac(Yr(t),r),n=t.renderer,i=t.language??"",a="";if(t.renderer===void 0&&!i){let $=Ur(t.project_dir);$.language&&(i=$.language,a=` (auto-detected language: ${$.language})`)}n===""||n==="VISUAL"?n="VISUAL":n||(n=i?"VISUAL":"D3");let s=e?._meta?.progressToken,l=0,c="",p=$=>{s!==void 0&&e.sendNotification({method:"notifications/progress",params:{progressToken:s,progress:++l,message:$}}).catch(()=>{})},u=s===void 0?null:setInterval(()=>{p(c?`${c}\u2026`:"generating chart (multi-pass \u2014 can take a few minutes)\u2026")},1e4),m;try{m=await xc({shape:o.shape,totalRows:o.totalRows,leafCardinality:o.index.getLeafCardinality(),sampleCsv:o.sampleCsv,prompt:t.prompt,chartHint:t.chart_type,rowsPolicy:t.rows_policy==="all"?"all":void 0,requiredColumns:t.required_columns,forbidDerivedColumns:t.forbid_derived_columns,renderer:n,language:i,model:t.model,reasoningMode:t.reasoning_mode,width:t.width,height:t.height,onProgress:$=>{c=$,p(`${$}\u2026`)}})}finally{u&&clearInterval(u)}if(!m.ok)return{isError:!0,content:[{type:"text",text:`Generation failed (correlationId ${m.correlationId}): ${m.errorMessage||"no code returned"}`}]};let y=m.language.toUpperCase()==="D3",d=m.geoColumn&&m.geoKind?{column:m.geoColumn,kind:m.geoKind}:void 0,h=m.geoKind?ru(m.geoKind):void 0,f=m.pointCityColumn||m.pointStateColumn||m.pointZipColumn||m.pointLatColumn&&m.pointLonColumn?{city:m.pointCityColumn||void 0,state:m.pointStateColumn||void 0,zip:m.pointZipColumn||void 0,lat:m.pointLatColumn||void 0,lon:m.pointLonColumn||void 0}:null,C=Vc(o.index,o.shape,d,f),M=t.wrap==="esm"&&y?qc(m.code):m.code,v=[`Chart: ${m.finalChartName||"(backend pick)"} | language: ${m.language} | version: ${m.version}`+(m.creditCost!=null?` | credits: ${m.creditCost}`:"")+a];if(m.codeSummary&&v.push(`Summary: ${m.codeSummary}`),d){let $=C.geoUnmatched;v.push(`Geo: joined '${d.column}' as ${d.kind}`+(h?"":" (WARNING: no bundled geometry for this kind)")+($&&$.count>0?` \u2014 ${$.count} region(s) unmatched: ${$.examples.join(", ")}`:""))}else m.geoKind&&v.push(`Geo: ${m.geoKind} basemap attached as options.geo`+(h?"":" (WARNING: no bundled geometry for this kind \u2014 the map will draw no land)"));if(f){let $=[f.city&&"city",f.state&&"state",f.zip&&"zip",f.lat&&f.lon&&"lat/lon"].filter(Boolean).join("+"),o3=C.geoPoint,c3=C.rows.length-(o3?.unplaced??0);v.push(`Points: geocoded ${c3}/${C.rows.length} from ${$}`+(o3?.precision?` at ${o3.precision} precision`:"")+(o3&&o3.unplaced>0?` \u2014 ${o3.unplaced} unplaced: ${o3.unplacedExamples.join(", ")}`:"")+(o3&&o3.ambiguousRows>0?` \u2014 ${o3.ambiguousRows} placed by largest-match tie-break`:""))}v.push("",Uc({language:m.language,data:C,finalChartName:m.finalChartName,code:m.code,geoKind:m.geoKind}));let S={};if(t.out_dir){let $=iu(t.out_dir);yA($,{recursive:!0});let o3=m.language.toUpperCase()==="PYTHON"?"py":"js",c3=M5($,`chart.${o3}`),Z=o3==="py"?"#":"//",Y=`${Z} BIC generated chart \u2014 ${m.finalChartName||"chart"} [${m.language}]
125
125
  ${Z} host contract v${T8}. A host implementing a DIFFERENT major
126
126
  ${Z} version may not interoperate with this file's mark/slot grammar.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "MCP (Model Context Protocol) stdio server for BIC AI charts: profile a dataset locally, then generate chart code that has passed the BIC backend's render gates. Works with any MCP-capable client (Claude Code, Claude Desktop, Cursor, Copilot Studio). Requires a BIC trial or paid account.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -45,8 +45,8 @@
45
45
  "zod": "^3.23.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@bicharts/chart-host": "^0.4.0",
49
- "@bicharts/shape-core": "^0.4.0",
48
+ "@bicharts/chart-host": "^0.4.1",
49
+ "@bicharts/shape-core": "^0.4.1",
50
50
  "@types/node": "^20.0.0",
51
51
  "@types/papaparse": "^5.3.14",
52
52
  "esbuild": "^0.28.1",
@@ -74,7 +74,7 @@ const rows = payload.rows.map(r =>
74
74
 
75
75
  ```tsx
76
76
  <BicChartGroup columns={columns} rows={rows} point={{ city: "City", state: "StateOrProvince" }}>
77
- <BicChart id="map" code={mapCode} d3={d3} geoKind="na" respondsWith="highlight" />
77
+ <BicChart id="map" code={mapCode} d3={d3} geoKind="north-america" respondsWith="highlight" />
78
78
  <BicChart id="table" code={tableCode} d3={d3} />
79
79
  </BicChartGroup>
80
80
  ```
@@ -112,6 +112,11 @@ Sizing is yours. Give each `<BicChart>` a sized container and pass `width`/`heig
112
112
  `structuredContent.plugins` on the MCP result says the same thing.
113
113
  - **Read `integration_contract`** in the MCP result if you need anything beyond this file —
114
114
  it describes the exact payload and option shape for the chart you just generated.
115
+ - **Basemap geometry.** The React `<BicChart>` fetches the geometry for its `geoKind`
116
+ itself (chart-host ≥ 0.4.1) and re-renders when it lands. To skip the one-frame basemap
117
+ pop-in, `await loadGeo("north-america")` (from `@bicharts/chart-host`) before the first
118
+ mount. Outside React, `render()` is synchronous by contract, so that preload is
119
+ **required** — a cold cache draws marks over no land, with only a console warning.
115
120
 
116
121
  ## 5. Check it
117
122