@amcharts/amcharts5-mcp 1.1.8 → 1.2.0

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/content/SKILL.md CHANGED
@@ -164,6 +164,34 @@ series.set("tooltip", am5.Tooltip.new(root, {
164
164
 
165
165
  **Inline formatting:** `"[bold]{name}[/]: [fontSize: 20px]{value}[/]"`
166
166
 
167
+ **A tooltip with no text renders as an empty bubble.** Enabling a `am5.Tooltip` without `labelText` (or `series.tooltipText`) shows a blank tooltip. Series text lives on `tooltip.labelText` or `series.set("tooltipText", ...)`; for column/bar charts the text usually goes on `series.columns.template.set("tooltipText", "{categoryX}: {valueY}")`.
168
+
169
+ ### Styling a tooltip
170
+
171
+ The tooltip background is a **`PointedRectangle`** (NOT a `RoundedRectangle`), so it has a single `cornerRadius` — there are no `cornerRadiusTL/TR/BL/BR`:
172
+
173
+ ```js
174
+ const tooltip = am5.Tooltip.new(root, { labelText: "{valueY}" });
175
+
176
+ // Background color + shape — must turn OFF getFillFromSprite, else bg copies the series color
177
+ tooltip.set("getFillFromSprite", false);
178
+ tooltip.get("background").setAll({
179
+ fill: am5.color(0x000000),
180
+ fillOpacity: 0.8,
181
+ stroke: am5.color(0xffffff),
182
+ strokeOpacity: 0.3,
183
+ cornerRadius: 6 // single radius — PointedRectangle, not four corners
184
+ });
185
+
186
+ // Text color — must turn OFF autoTextColor (it auto-picks for contrast), then set on the label
187
+ tooltip.set("autoTextColor", false);
188
+ tooltip.label.setAll({ fill: am5.color(0xffffff) }); // tooltip.label is read-only accessor
189
+
190
+ series.set("tooltip", tooltip);
191
+ ```
192
+
193
+ `getStrokeFromSprite` (default `false`) copies the sprite's stroke color when `true`.
194
+
167
195
  ## Chart title
168
196
 
169
197
  Do NOT add titles as HTML elements — they are outside the canvas and won't appear in exports. Add an `am5.Label` to the container BEFORE the chart, and set `verticalLayout`:
@@ -358,8 +386,15 @@ sprite.setAll({ fill: am5.color(0xff0000), strokeWidth: 2 });
358
386
  // Read current value
359
387
  const fill = sprite.get("fill");
360
388
  const width = sprite.getPrivate("width"); // read-only internal values
389
+
390
+ // set() RETURNS the value — handy for capturing the created object inline:
391
+ const cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
361
392
  ```
362
393
 
394
+ **Reading back a `Percent`:** a `Percent` exposes two numbers — `.percent` is the 0–100 value, `.value` is the normalized 0–1 fraction. `am5.percent(50).percent === 50` but `am5.percent(50).value === 0.5`. When you read a percent setting back (e.g. `sprite.get("x")` after setting `am5.percent(50)`), use `.percent` for a 0–100 number; `.value` gives `0.5`.
395
+
396
+ **Reading animated settings is unreliable mid-animation.** Right after `series.appear()` / `chart.appear()`, animated settings like `opacity` are still transitioning — `get("opacity")` can return `0` (the start value). Read such settings after the animation completes, or don't persist values read during appear (a common way to accidentally bake `opacity:0` into generated code).
397
+
363
398
  ## Dynamic data
364
399
 
365
400
  ```js
@@ -639,7 +674,7 @@ onUnmounted(() => { root.dispose(); });
639
674
  26. **`VoronoiTreemap` has NO `.rectangles` property** — Unlike `Treemap` (which has `series.rectangles.template`), `VoronoiTreemap` renders organic polygon cells. Style via `series.nodes.template` and its children, not `.rectangles`.
640
675
  27. **Labels with data placeholders need `populateText: true`** — When a Label uses data field placeholders like `text: "{name}"`, you MUST also set `populateText: true`. Without it, the placeholder is not resolved and the label appears blank. This applies everywhere Labels display dynamic data — bullet labels, map point labels, etc. Example: `am5.Label.new(root, { text: "{name}", populateText: true, ... })`.
641
676
  28. **Easing: `am5.ease.in()` does NOT exist** — amCharts 5 provides base easing functions (`am5.ease.cubic`, `am5.ease.bounce`, `am5.ease.elastic`, `am5.ease.linear`, `am5.ease.quad`, `am5.ease.sine`, `am5.ease.circle`, `am5.ease.exp`, `am5.ease.pow`) and three modifiers: `am5.ease.out()`, `am5.ease.inOut()`, `am5.ease.yoyo()`. Usage: `am5.ease.cubic` (ease-in by default), `am5.ease.out(am5.ease.cubic)` (ease-out), `am5.ease.inOut(am5.ease.cubic)` (ease in+out). There is NO `am5.ease.in()` — using it throws a runtime error. The base functions already ease-in by default.
642
- 29. **Use `forceHidden` instead of `visible: false` when you need to guarantee something stays hidden** — amCharts internally manages `visible` on many elements (cursor lines, labels, ticks, tooltips, grid, etc.), toggling them on/off in response to user interaction or data changes. If you set `visible: false`, the library may set it back to `true`. `forceHidden: true` overrides all system visibility changes and keeps the element hidden regardless. Examples: `cursor.lineX.set("forceHidden", true)`, `yRenderer.labels.template.set("forceHidden", true)`, `xRenderer.grid.template.set("forceHidden", true)`. For cursor lines specifically, always access `cursor.lineX`/`cursor.lineY` after creation — do NOT pass them as constructor options.
677
+ 29. **`forceHidden` vs `visible`** — `forceHidden: true` ALWAYS hides an element, immune to states, themes/library and even `show()`. `visible` is weaker: amCharts re-manages it on many elements (notably cursor lines, but also labels, ticks, tooltips, grid) in response to interaction or data changes, so a `visible: false` you set may be flipped back to `true`. Rules of thumb: for a **persistent hide**, use `forceHidden: true` (`cursor.lineX.set("forceHidden", true)`, `yRenderer.labels.template.set("forceHidden", true)`, `xRenderer.grid.template.set("forceHidden", true)`). To **reveal** an element that defaults hidden — notably **axis ticks** (`ticks.template` defaults to `visible:false`) — you must raise `visible: true`; flipping `forceHidden` alone won't show it. For cursor lines specifically, always access `cursor.lineX`/`cursor.lineY` after creation — do NOT pass them as constructor options.
643
678
  30. **`snapTooltip: true` on series for cursor tooltip snapping** — When using a cursor and you want tooltips to snap to data points, set `snapTooltip: true` on the series in addition to (or instead of) `snapToSeries` on the cursor. This is especially useful for timeline/curve charts.
644
679
  31. **Globe rotation uses negative coordinates** — To center the globe (`geoOrthographic`) on a geographic point, set `rotationX` to **-longitude** and `rotationY` to **-latitude**. E.g., to center on Paris (48.86°N, 2.35°E): `chart.animate({ key: "rotationX", to: -2.35 }); chart.animate({ key: "rotationY", to: -48.86 });`. Using positive values rotates the globe the wrong way.
645
680
  32. **`positionOnLine` with multi-segment lines limits per-segment control** — `MapPointSeries` data items can animate along a line via `positionOnLine` (0→1). If the line has 3+ points (multi-segment), position 0.5 is the midpoint of the *entire* path, making per-segment effects (scaling at each segment midpoint, pausing between segments, etc.) difficult. For advanced per-segment animations, use **single-segment lines** (2 points each) and animate the bullet across them sequentially. E.g., instead of one line [A,B,C,D], create [A,B], [B,C], [C,D].
@@ -679,6 +714,32 @@ If you have the ability to run shell commands, validate your generated chart cod
679
714
 
680
715
  Skip this step entirely if you cannot execute code (e.g., chat-only context with no tool access).
681
716
 
717
+ ## Recent API changes (newer than the bundled class reference)
718
+
719
+ The bundled per-class API reference was snapshotted on **2026-03-15**, so it predates the changes below. Prefer these names/settings; for anything newer, verify against the live docs (see next section).
720
+
721
+ **Renamed settings (old name still works but is deprecated — use the new one):**
722
+
723
+ | Class(es) | Old | New | Since |
724
+ |-----------|-----|-----|-------|
725
+ | `MovingAverage`, `MovingAverageDeviation`, `MovingAverageEnvelope`, `BollingerBands` | `type` | `maType` | 5.18.0 |
726
+ | `VoronoiTreemap` | `type` | `shapeType` | 5.18.0 |
727
+
728
+ ```js
729
+ // Moving-average indicators: use maType, not type
730
+ am5stock.MovingAverage.new(root, { maType: "exponential", period: 20 });
731
+ am5hierarchy.VoronoiTreemap.new(root, { shapeType: "rectangle" }); // was: type
732
+ ```
733
+
734
+ **New settings / methods worth knowing:**
735
+ - `Tree`: `fitNodes` (5.18.0, exclude hidden nodes from layout), `nodeSeparation` (5.16.2, custom node-spacing fn), `clustered` (5.16.2, dendrogram layout — leaves at same depth).
736
+ - `ValueAxis`: `syncZeros` (5.16.2, align zero across synced axes — needs `syncWithAxis`).
737
+ - `Hierarchy`: `parentIdField` setting + `setFlatData(data)` method (5.16.2) — feed flat `{id, parentId}` data instead of nested `children`.
738
+ - All entities: `onDebounced(key, cb, delay)` / `offDebounced(key, cb?)` and `onPrivateDebounced` / `offDebouncedPrivate` (5.17.3) — fire once after rapid changes settle.
739
+ - `Label`: `fontFamily: "inherit"` (5.17.3) uses the chart container's computed font.
740
+ - `MapSankeySeries` (5.17.0) — Sankey overlaid on a map; auto-resolves `sourceId`/`targetId` once `polygonSeries` geoJSON loads (5.17.1), so no `datavalidated` wrapper needed. See `references/map.md`.
741
+ - `MapPointSeries` defaults changed to `longitudeField: "longitude"`, `latitudeField: "latitude"` (5.16.1).
742
+
682
743
  ## Verify unfamiliar API before using it
683
744
 
684
745
  If you are unsure whether a method, property, or setting exists on an amCharts 5 class, **verify it before using it**. Check the class reference (class name in lowercase, e.g. `LineSeries` → `lineseries`):
package/content/map.md CHANGED
@@ -243,22 +243,20 @@ Two data patterns — polygon IDs or explicit coordinates.
243
243
 
244
244
  **Pattern 1: `sourceId` / `targetId` (country codes)**
245
245
 
246
- IDs use ISO 3166-1 alpha-2 codes matching the geodata. **CRITICAL:** Data MUST be set inside `polygonSeries.events.once("datavalidated", ...)` — the polygon series needs to finish parsing geoJSON before MapSankeySeries can resolve country IDs to centroids. Without this wrapper, `getDataItemById()` returns null and flows silently don't appear (no errors, no bands).
246
+ IDs use ISO 3166-1 alpha-2 codes matching the geodata. Set data directly — `MapSankeySeries` auto-resolves `sourceId`/`targetId` once the referenced `polygonSeries` finishes loading, so data can be set at any time:
247
247
 
248
248
  ```js
249
- polygonSeries.events.once("datavalidated", function() {
250
- sankeySeries.data.setAll([
251
- { sourceId: "BR", targetId: "DE", value: 350 },
252
- { sourceId: "BR", targetId: "US", value: 450 },
253
- { sourceId: "DE", targetId: "FR", value: 150 },
254
- { sourceId: "DE", targetId: "PL", value: 100 }
255
- ]);
256
- });
249
+ sankeySeries.data.setAll([
250
+ { sourceId: "BR", targetId: "DE", value: 350 },
251
+ { sourceId: "BR", targetId: "US", value: 450 },
252
+ { sourceId: "DE", targetId: "FR", value: 150 },
253
+ { sourceId: "DE", targetId: "PL", value: 100 }
254
+ ]);
257
255
  ```
258
256
 
259
- **Pattern 2: Explicit coordinates (no timing needed)**
257
+ **Pattern 2: Explicit coordinates**
260
258
 
261
- When using `sourceLongitude`/`sourceLatitude`/`targetLongitude`/`targetLatitude`, data can be set immediately — no `datavalidated` wrapper required:
259
+ When using `sourceLongitude`/`sourceLatitude`/`targetLongitude`/`targetLatitude`, centroid lookup is skipped entirely:
262
260
  ```js
263
261
  sankeySeries.data.setAll([
264
262
  {
@@ -271,17 +269,15 @@ sankeySeries.data.setAll([
271
269
 
272
270
  **Waypoints** — route flows through intermediate points:
273
271
  ```js
274
- polygonSeries.events.once("datavalidated", function() {
275
- sankeySeries.data.setAll([
276
- {
277
- sourceId: "GB", targetId: "JP", value: 40,
278
- waypoints: [
279
- { longitude: 30, latitude: 65 },
280
- { longitude: 90, latitude: 55 }
281
- ]
282
- }
283
- ]);
284
- });
272
+ sankeySeries.data.setAll([
273
+ {
274
+ sourceId: "GB", targetId: "JP", value: 40,
275
+ waypoints: [
276
+ { longitude: 30, latitude: 65 },
277
+ { longitude: 90, latitude: 55 }
278
+ ]
279
+ }
280
+ ]);
285
281
  ```
286
282
 
287
283
  **WARNING:** Never call `polygonSeries.data.setAll()` with custom objects — this replaces all geoJSON-derived data items, breaking MapSankeySeries centroid resolution. To set per-country properties (like colors), iterate existing data items after `datavalidated` instead:
@@ -405,16 +401,14 @@ Bullets automatically hide on the back side of the globe (orthographic projectio
405
401
  The same country can be both target and source — incoming and outgoing bands share unified stacking at intermediate nodes:
406
402
 
407
403
  ```js
408
- polygonSeries.events.once("datavalidated", function() {
409
- sankeySeries.data.setAll([
410
- // Level 1: Producers > Hubs
411
- { sourceId: "BR", targetId: "DE", value: 350 },
412
- { sourceId: "VN", targetId: "DE", value: 200 },
413
- // Level 2: Hubs > Consumers
414
- { sourceId: "DE", targetId: "FR", value: 150 },
415
- { sourceId: "DE", targetId: "PL", value: 100 }
416
- ]);
417
- });
404
+ sankeySeries.data.setAll([
405
+ // Level 1: Producers > Hubs
406
+ { sourceId: "BR", targetId: "DE", value: 350 },
407
+ { sourceId: "VN", targetId: "DE", value: 200 },
408
+ // Level 2: Hubs > Consumers
409
+ { sourceId: "DE", targetId: "FR", value: 150 },
410
+ { sourceId: "DE", targetId: "PL", value: 100 }
411
+ ]);
418
412
  ```
419
413
 
420
414
  ### Animating bullets along lines (positionOnLine)
@@ -1222,36 +1216,34 @@ Demonstrates: `MapSankeySeries` with `sourceId`/`targetId` data (country codes),
1222
1216
  });
1223
1217
  });
1224
1218
 
1225
- // Set data after polygonSeries loads (sourceId/targetId need centroid lookup)
1226
- // CRITICAL: data MUST be set inside datavalidated handler
1227
- polygonSeries.events.once("datavalidated", function() {
1228
- sankeySeries.data.setAll([
1229
- // Producers > Hubs
1230
- { sourceId: "BR", targetId: "DE", value: 350 },
1231
- { sourceId: "BR", targetId: "US", value: 450 },
1232
- { sourceId: "BR", targetId: "IT", value: 200 },
1233
- { sourceId: "VN", targetId: "DE", value: 200 },
1234
- { sourceId: "VN", targetId: "BE", value: 150 },
1235
- { sourceId: "CO", targetId: "US", value: 250 },
1236
- { sourceId: "CO", targetId: "DE", value: 80 },
1237
- { sourceId: "ET", targetId: "DE", value: 60 },
1238
- { sourceId: "ET", targetId: "BE", value: 40 },
1239
- { sourceId: "ID", targetId: "US", value: 80 },
1240
- { sourceId: "HN", targetId: "DE", value: 60 },
1241
- { sourceId: "HN", targetId: "BE", value: 40 },
1242
- // Hubs > Consumer Markets
1243
- { sourceId: "DE", targetId: "FR", value: 150 },
1244
- { sourceId: "DE", targetId: "PL", value: 100 },
1245
- { sourceId: "DE", targetId: "SE", value: 80 },
1246
- { sourceId: "DE", targetId: "RU", value: 120 },
1247
- { sourceId: "BE", targetId: "GB", value: 100 },
1248
- { sourceId: "BE", targetId: "NL", value: 80 },
1249
- { sourceId: "IT", targetId: "GR", value: 50 },
1250
- { sourceId: "IT", targetId: "AT", value: 40 },
1251
- { sourceId: "US", targetId: "CA", value: 120 },
1252
- { sourceId: "US", targetId: "JP", value: 80 }
1253
- ]);
1254
- });
1219
+ // Set data directly — MapSankeySeries auto-resolves sourceId/targetId
1220
+ // once polygonSeries finishes loading.
1221
+ sankeySeries.data.setAll([
1222
+ // Producers > Hubs
1223
+ { sourceId: "BR", targetId: "DE", value: 350 },
1224
+ { sourceId: "BR", targetId: "US", value: 450 },
1225
+ { sourceId: "BR", targetId: "IT", value: 200 },
1226
+ { sourceId: "VN", targetId: "DE", value: 200 },
1227
+ { sourceId: "VN", targetId: "BE", value: 150 },
1228
+ { sourceId: "CO", targetId: "US", value: 250 },
1229
+ { sourceId: "CO", targetId: "DE", value: 80 },
1230
+ { sourceId: "ET", targetId: "DE", value: 60 },
1231
+ { sourceId: "ET", targetId: "BE", value: 40 },
1232
+ { sourceId: "ID", targetId: "US", value: 80 },
1233
+ { sourceId: "HN", targetId: "DE", value: 60 },
1234
+ { sourceId: "HN", targetId: "BE", value: 40 },
1235
+ // Hubs > Consumer Markets
1236
+ { sourceId: "DE", targetId: "FR", value: 150 },
1237
+ { sourceId: "DE", targetId: "PL", value: 100 },
1238
+ { sourceId: "DE", targetId: "SE", value: 80 },
1239
+ { sourceId: "DE", targetId: "RU", value: 120 },
1240
+ { sourceId: "BE", targetId: "GB", value: 100 },
1241
+ { sourceId: "BE", targetId: "NL", value: 80 },
1242
+ { sourceId: "IT", targetId: "GR", value: 50 },
1243
+ { sourceId: "IT", targetId: "AT", value: 40 },
1244
+ { sourceId: "US", targetId: "CA", value: 120 },
1245
+ { sourceId: "US", targetId: "JP", value: 80 }
1246
+ ]);
1255
1247
 
1256
1248
  // Set country names on auto-created nodes and animate bullets
1257
1249
  var countryNames = {
@@ -1270,31 +1262,27 @@ Demonstrates: `MapSankeySeries` with `sourceId`/`targetId` data (country codes),
1270
1262
  }
1271
1263
  });
1272
1264
 
1273
- // Defer bullet animation — bullets are not yet instantiated
1274
- // when datavalidated fires inside polygonSeries handler
1275
- setTimeout(function() {
1276
- am5.array.each(sankeySeries.dataItems, function(dataItem) {
1277
- var bullets = dataItem.bullets;
1278
- if (bullets) {
1279
- am5.array.each(bullets, function(bullet) {
1280
- var randomDur = 3000 + Math.random() * 3000;
1281
- var delay = Math.random() * randomDur;
1282
- setTimeout(function() {
1283
- var sprite = bullet.get("sprite");
1284
- if (sprite) sprite.set("visible", true);
1285
- bullet.animate({
1286
- key: "locationX",
1287
- from: 0,
1288
- to: 1,
1289
- duration: randomDur,
1290
- easing: am5.ease.linear,
1291
- loops: Infinity
1292
- });
1293
- }, delay);
1294
- });
1295
- }
1296
- });
1297
- }, 100);
1265
+ am5.array.each(sankeySeries.dataItems, function(dataItem) {
1266
+ var bullets = dataItem.bullets;
1267
+ if (bullets) {
1268
+ am5.array.each(bullets, function(bullet) {
1269
+ var randomDur = 3000 + Math.random() * 3000;
1270
+ var delay = Math.random() * randomDur;
1271
+ setTimeout(function() {
1272
+ var sprite = bullet.get("sprite");
1273
+ if (sprite) sprite.set("visible", true);
1274
+ bullet.animate({
1275
+ key: "locationX",
1276
+ from: 0,
1277
+ to: 1,
1278
+ duration: randomDur,
1279
+ easing: am5.ease.linear,
1280
+ loops: Infinity
1281
+ });
1282
+ }, delay);
1283
+ });
1284
+ }
1285
+ });
1298
1286
  });
1299
1287
 
1300
1288
  // Title
@@ -1606,7 +1594,6 @@ Demonstrates: `MapSankeySeries` with explicit `sourceLongitude`/`sourceLatitude`
1606
1594
  var wpPortugal = { longitude: -10, latitude: 39 };
1607
1595
  var viaAtlantic = [wpHormuz, wpAden, wpSuez, wpSicily, wpPortugal];
1608
1596
 
1609
- // No datavalidated wrapper needed — explicit coordinates resolve immediately
1610
1597
  sankeySeries.data.setAll([
1611
1598
  // Ras Tanura (Saudi Arabia)
1612
1599
  flow(rasTanura, "Ras Tanura", ningbo, "Ningbo", 1100, viaChina),
package/content/xy.md CHANGED
@@ -81,6 +81,8 @@ The field name combines the **value type** + **axis direction**:
81
81
  - Date axis on X → `valueXField: "date"` (dates are numeric timestamps)
82
82
  - Open/high/low/close → `openValueYField`, `highValueYField`, `lowValueYField`, `valueYField`
83
83
 
84
+ **There is NO `dateXField` in v5** (a common wrong guess carried over from v4). A `DateAxis` series reads its date from `valueXField`, pointing at a numeric timestamp (`new Date(...).getTime()`).
85
+
84
86
  ## Common additions
85
87
 
86
88
  ### Cursor
@@ -109,7 +111,11 @@ legend.data.setAll(chart.series.values);
109
111
  series.set("tooltip", am5.Tooltip.new(root, {
110
112
  labelText: "{name}: {valueY}"
111
113
  }));
114
+
115
+ // For column/bar series the tooltip text often lives on the columns template instead:
116
+ series.columns.template.set("tooltipText", "{categoryX}: {valueY}");
112
117
  ```
118
+ A tooltip enabled with no `labelText`/`tooltipText` shows an empty bubble — always give it text. See SKILL.md → Tooltip for background/text styling.
113
119
 
114
120
  ### Stacking
115
121
  ```js
@@ -253,6 +259,11 @@ renderer.labels.template.setAll({
253
259
  });
254
260
  ```
255
261
 
262
+ **Axis sub-element defaults:** grid and labels are visible by default; **ticks default to `visible:false`**. To show ticks you must raise `visible` (and give them a non-zero `strokeOpacity`) — toggling `forceHidden` won't reveal them:
263
+ ```js
264
+ renderer.ticks.template.setAll({ visible: true, strokeOpacity: 0.5, length: 5 });
265
+ ```
266
+
256
267
  ## LineSeries
257
268
 
258
269
  For line and area charts.
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: "Map Sankey Series"
3
+ source: "https://www.amcharts.com/docs/v5/charts/map-chart/map-sankey-series/"
4
+ category: "maps"
5
+ added: "2026-06-04"
6
+ ---
7
+
8
+ `MapSankeySeries` (added in amCharts 5.17.0) draws variable-width curved bands between geographic points on a `MapChart` — a Sankey diagram projected onto a map, where band thickness encodes a numeric `value`. It extends `MapPolygonSeries` and generates real GeoJSON geometry, so bands follow the projection during pan, zoom, and globe rotation.
9
+
10
+ Key implementation details
11
+ A `MapSankeySeries` needs a reference `polygonSeries` (a `MapPolygonSeries` with geodata) to resolve country IDs to geographic centroids. Provide flow data as `{ sourceId, targetId, value }` using ISO 3166-1 alpha-2 country codes, or as explicit `{ sourceLongitude, sourceLatitude, targetLongitude, targetLatitude, value }` coordinates. When using `sourceId`/`targetId`, the series automatically waits for `polygonSeries` to finish parsing its geoJSON before resolving centroids — no `datavalidated` wrapper around `data.setAll()` is needed.
12
+
13
+ Key settings (on `MapSankeySeries`):
14
+ - `polygonSeries` — the `MapPolygonSeries` used to look up centroids (required for `sourceId`/`targetId` data).
15
+ - `maxWidth` (default `5`) — maximum band width, in geographic degrees.
16
+ - `controlPointDistance` (default `0.5`) — bezier curvature of the bands.
17
+ - `nodeType` (default `"circle"`) — node shape: `"circle"` or `"bar"`.
18
+ - `nodePadding` — extra padding around node shapes, in degrees.
19
+
20
+ **WARNING:** never call `polygonSeries.data.setAll()` with your own objects — that replaces the geoJSON-derived data items and breaks `MapSankeySeries` centroid resolution. To set per-country properties (e.g. colors), iterate the existing data items inside the polygon series' `datavalidated` event instead.
21
+
22
+ ## JavaScript
23
+
24
+ ```javascript
25
+ // Create root element
26
+ // https://www.amcharts.com/docs/v5/getting-started/#Root_element
27
+ var root = am5.Root.new("chartdiv");
28
+
29
+ // Set themes
30
+ // https://www.amcharts.com/docs/v5/concepts/themes/
31
+ root.setThemes([am5themes_Animated.new(root)]);
32
+
33
+ // Create the map chart
34
+ // https://www.amcharts.com/docs/v5/charts/map-chart/
35
+ var chart = root.container.children.push(am5map.MapChart.new(root, {
36
+ panX: "rotateX",
37
+ panY: "translateY",
38
+ projection: am5map.geoNaturalEarth1()
39
+ }));
40
+
41
+ // Country polygons — also used by the Sankey series to resolve centroids
42
+ // https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/
43
+ var polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
44
+ geoJSON: am5geodata_worldLow
45
+ }));
46
+ polygonSeries.mapPolygons.template.setAll({
47
+ fill: am5.color(0xdddddd),
48
+ stroke: am5.color(0xffffff),
49
+ strokeWidth: 0.5
50
+ });
51
+
52
+ // Highlight the countries involved (set fills on existing geoJSON data items —
53
+ // do NOT call polygonSeries.data.setAll() with custom objects)
54
+ var endpoints = ["BR", "VN", "CO", "DE", "US", "FR", "JP", "CA"];
55
+ polygonSeries.events.on("datavalidated", function () {
56
+ am5.array.each(polygonSeries.dataItems, function (di) {
57
+ if (endpoints.indexOf(di.get("id")) !== -1) {
58
+ di.get("mapPolygon").set("fill", am5.color(0xb0c4a0));
59
+ }
60
+ });
61
+ });
62
+
63
+ // Map Sankey series
64
+ // https://www.amcharts.com/docs/v5/charts/map-chart/map-sankey-series/
65
+ var sankeySeries = chart.series.push(am5map.MapSankeySeries.new(root, {
66
+ polygonSeries: polygonSeries,
67
+ maxWidth: 3,
68
+ controlPointDistance: 0.4,
69
+ nodePadding: 0.3
70
+ }));
71
+
72
+ // Band (link) appearance
73
+ sankeySeries.mapPolygons.template.setAll({
74
+ fill: am5.color(0x6b4f3a),
75
+ fillOpacity: 0.6,
76
+ strokeOpacity: 0,
77
+ tooltipText: "{sourceNode.name} → {targetNode.name}: {value}"
78
+ });
79
+
80
+ // Node appearance
81
+ sankeySeries.nodes.mapPolygons.template.setAll({
82
+ fill: am5.color(0x3c2415),
83
+ stroke: am5.color(0xffffff),
84
+ strokeWidth: 1,
85
+ tooltipText: "{name}: {sum}"
86
+ });
87
+
88
+ // Flow data — { sourceId, targetId, value } using ISO 3166-1 alpha-2 codes.
89
+ // Set directly: the series auto-resolves IDs once polygonSeries loads its geoJSON.
90
+ sankeySeries.data.setAll([
91
+ // Producers → hubs
92
+ { sourceId: "BR", targetId: "US", value: 450 },
93
+ { sourceId: "BR", targetId: "DE", value: 350 },
94
+ { sourceId: "VN", targetId: "DE", value: 200 },
95
+ { sourceId: "CO", targetId: "US", value: 250 },
96
+ // Hubs → consumer markets
97
+ { sourceId: "DE", targetId: "FR", value: 150 },
98
+ { sourceId: "US", targetId: "CA", value: 120 },
99
+ { sourceId: "US", targetId: "JP", value: 80 }
100
+ ]);
101
+
102
+ // Auto-created nodes only know their id — give them readable names
103
+ var countryNames = {
104
+ BR: "Brazil", VN: "Vietnam", CO: "Colombia", DE: "Germany",
105
+ US: "United States", FR: "France", JP: "Japan", CA: "Canada"
106
+ };
107
+ sankeySeries.events.on("datavalidated", function () {
108
+ am5.array.each(sankeySeries.nodes.dataItems, function (di) {
109
+ var id = di.get("id");
110
+ if (id && countryNames[id]) {
111
+ di.set("name", countryNames[id]);
112
+ }
113
+ });
114
+ });
115
+
116
+ // ALTERNATIVE — explicit coordinates instead of country IDs (no polygonSeries
117
+ // lookup needed for these endpoints). Optional `waypoints` route the band
118
+ // through intermediate points (keys MUST be "longitude"/"latitude"):
119
+ // sankeySeries.data.setAll([
120
+ // {
121
+ // sourceLongitude: -47.9, sourceLatitude: -15.8, // Brasília
122
+ // targetLongitude: 13.4, targetLatitude: 52.5, // Berlin
123
+ // value: 350,
124
+ // waypoints: [{ longitude: -25.0, latitude: 20.0 }]
125
+ // }
126
+ // ]);
127
+
128
+ chart.appear(1000, 100);
129
+ ```
@@ -32,6 +32,7 @@ Extended by: LinkedHierarchy, Pack, Partition, Treemap, VoronoiTreemap
32
32
  - **downDepth** (`undefined | number`) — Number of child levels to open when clicking on a node. Click here for more info
33
33
  - **fillField** (`undefined | string`) — A field in data that holds color used for fills for various elements, such as nodes.
34
34
  - **initialDepth** (`undefined | number`) — Number of levels to show on chart's first load. Click here for more info
35
+ - **parentIdField** (`undefined | string`) — A field in data that holds the parent node's ID. Used with the `setFlatData(data)` method to build the hierarchy from a flat array (with parent-ID references) instead of nested `children`. @since 5.16.2
35
36
  - **patterns** (`PatternSet`) — A PatternSet to use when asigning patterns for nodes. Click here for more info @since 5.10.0
36
37
  - **selectedDataItem** (`DataItem`) — A data item for currently selected node. Click here for more info
37
38
  - **singleBranchOnly** (`undefined | false | true`) — If set to true will make all other branches collapse when some branch is expanded.
@@ -99,6 +99,7 @@ Extended by: ILinkedHierarchySettings, IPackSettings, IPartitionSettings, ITreem
99
99
  - **downDepth** (`undefined | number`) — Number of child levels to open when clicking on a node. Click here for more info
100
100
  - **fillField** (`undefined | string`) — A field in data that holds color used for fills for various elements, such as nodes.
101
101
  - **initialDepth** (`undefined | number`) — Number of levels to show on chart's first load. Click here for more info
102
+ - **parentIdField** (`undefined | string`) — A field in data that holds the parent node's ID. Used with the `setFlatData(data)` method to build the hierarchy from a flat array (with parent-ID references) instead of nested `children`. @since 5.16.2
102
103
  - **patterns** (`PatternSet`) — A PatternSet to use when asigning patterns for nodes. Click here for more info @since 5.10.0
103
104
  - **selectedDataItem** (`DataItem`) — A data item for currently selected node. Click here for more info
104
105
  - **singleBranchOnly** (`undefined | false | true`) — If set to true will make all other branches collapse when some branch is expanded.
@@ -73,5 +73,6 @@ Extends: IChartIndicatorSettings
73
73
 
74
74
  - **decreasingColor** (`Color`) — Decreasing color.
75
75
  - **increasingColor** (`Color`) — Increasing color.
76
- - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average.
76
+ - **maType** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average. (Renamed from `type` in 5.18.0.)
77
+ - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — *Deprecated since 5.18.0 — use `maType` (still works).* Default "simple" Type of the moving average.
77
78
  - **unit** (`"points" | "percent"`) — Default "points" How units are calculated.
@@ -73,4 +73,5 @@ Extended by: IBollingerBandsSettings, IMovingAverageEnvelopeSettings
73
73
  ## Properties
74
74
 
75
75
  - **offset** (`undefined | number`) — Default 0 Offset.
76
- - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average.
76
+ - **maType** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average. (Renamed from `type` in 5.18.0.)
77
+ - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — *Deprecated since 5.18.0 — use `maType` (still works).* Default "simple" Type of the moving average.
@@ -91,5 +91,8 @@ Extends: ILinkedHierarchySettings
91
91
 
92
92
  ## Properties
93
93
 
94
+ - **clustered** (`undefined | false | true`) — Default false If set to true, uses a cluster (dendrogram) layout where all leaf nodes are placed at the same depth. @since 5.16.2
95
+ - **fitNodes** (`undefined | false | true`) — Default false If set to true, hidden nodes are excluded from the layout, giving more space to visible ones. @since 5.18.0
94
96
  - **inversed** (`undefined | false | true`) — Default false If set to true, will flip the tree direction. @since 5.2.4
97
+ - **nodeSeparation** (`undefined | (a: DataItem, b: DataItem) => number`) — Custom node-separation function; receives two data items and returns a numeric spacing value. @since 5.16.2
95
98
  - **orientation** (`"horizontal" | "vertical"`) — Default "vertical" Orientation of the diagram.
@@ -87,5 +87,6 @@ Extended by: IDateAxisSettings, IDurationAxisSettings
87
87
  - **strictMinMax** (`undefined | false | true`) — Force axis scale to be precisely at values as set in min and/or max. In case min and/or max is not set, the axis will fix its scale to precise lowest and highest values available through all of the series attached to it. This effectively locks the axis from auto-zooming itself when chart is zoomed in. If you need to zoom to actual low/high values within currently visible scope, use strictMinMaxSelection instead. Use extraMin and extraMax to add extra "padding". Click here for more info
88
88
  - **strictMinMaxSelection** (`undefined | false | true`) — Force axis to auto-zoom to exact lowest and highest values from attached series' data items within currently visible range. This is a good feature when your series is plotted from derivative values, like valueYChangeSelection as it helps to avoid frequent jumping of series to adjusted min and max of the axis. This will not work if strictMinMax is set to true (the axis will not zoom at all in this case). Use extraMin and extraMax to add extra "padding". @since 5.1.11
89
89
  - **syncWithAxis** (`ValueAxis`) — This setting can be set to an instance of another ValueAxis. If set the grid of this axis will be synced with grid of the target axis. NOTE: this is not 100% guaranteed to work. In some rare cases perfect sync might not be possible. Click here for more info
90
+ - **syncZeros** (`undefined | false | true`) — Aligns the zero baseline across synced axes. Works only if `syncWithAxis` is set and zero is within the min/max of the axis. @since 5.16.2
90
91
  - **tooltipNumberFormat** (`string | NumberFormatOptions`) — A numeric format used for numbers displayed in axis tooltip. Click here for more info
91
92
  - **treatZeroAs** (`undefined | number`) — Treat zero values as some other value. Useful in situations where zero would result in error, i.e. logarithmic scale. Click here for more info
@@ -95,4 +95,5 @@ Extends: IHierarchySettings
95
95
  - **cornerCount** (`undefined | number`) — Default 120 Number of corners when type is "polygon". 120 means the polygoon will look like a circle. NOTE: this setting is ignored if type="rectangle".
96
96
  - **maxIterationCount** (`undefined | number`) — Default 100 Maximum allowed number of iterations when computing the layout. Computation is stopped when it number of iterations is reached, even if the convergenceRatio is not yet reached. Bigger number means finer results, but slower performance.
97
97
  - **minWeightRatio** (`undefined | number`) — Default 0.005 Minimum weight ratio which allows computing the minimum allowed weight (= [maximum weight] * minWeightRatio). Setting very small minWeigtRatio might result flickering. NOTE: the nodes that have smaller weight will be scaled up and will not represent their true value correctly.
98
- - **type** (`"rectangle" | "polygon"`) — Default "polygon" Type of the diagram's shape. Click here for more info
98
+ - **shapeType** (`"rectangle" | "polygon"`) — Default "polygon" Type of the diagram's shape. (Renamed from `type` in 5.18.0.) Click here for more info
99
+ - **type** (`"rectangle" | "polygon"`) — *Deprecated since 5.18.0 — use `shapeType` (still works).* Default "polygon" Type of the diagram's shape.
@@ -24,7 +24,8 @@ Extended by: BollingerBands, MovingAverageEnvelope
24
24
  ## Settings
25
25
 
26
26
  - **offset** (`undefined | number`) — Default 0 Offset.
27
- - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average.
27
+ - **maType** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average. (Renamed from `type` in 5.18.0.)
28
+ - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — *Deprecated since 5.18.0 — use `maType` (still works).* Default "simple" Type of the moving average.
28
29
 
29
30
  ## Properties
30
31
 
@@ -24,7 +24,8 @@ Extends: ChartIndicator
24
24
 
25
25
  - **decreasingColor** (`Color`) — Decreasing color.
26
26
  - **increasingColor** (`Color`) — Increasing color.
27
- - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average.
27
+ - **maType** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — Default "simple" Type of the moving average. (Renamed from `type` in 5.18.0.)
28
+ - **type** (`"simple" | "weighted" | "exponential" | "dema" | "tema"`) — *Deprecated since 5.18.0 — use `maType` (still works).* Default "simple" Type of the moving average.
28
29
  - **unit** (`"points" | "percent"`) — Default "points" How units are calculated.
29
30
 
30
31
  ## Properties
@@ -22,5 +22,8 @@ Extends: LinkedHierarchy
22
22
 
23
23
  ## Settings
24
24
 
25
+ - **clustered** (`undefined | false | true`) — Default false If set to true, uses a cluster (dendrogram) layout where all leaf nodes are placed at the same depth. @since 5.16.2
26
+ - **fitNodes** (`undefined | false | true`) — Default false If set to true, hidden nodes are excluded from the layout, giving more space to visible ones. @since 5.18.0
25
27
  - **inversed** (`undefined | false | true`) — Default false If set to true, will flip the tree direction. @since 5.2.4
28
+ - **nodeSeparation** (`undefined | (a: DataItem, b: DataItem) => number`) — Custom node-separation function; receives two data items and returns a numeric spacing value. @since 5.16.2
26
29
  - **orientation** (`"horizontal" | "vertical"`) — Default "vertical" Orientation of the diagram.
@@ -38,5 +38,6 @@ Extended by: DateAxis, DurationAxis
38
38
  - **strictMinMax** (`undefined | false | true`) — Force axis scale to be precisely at values as set in min and/or max. In case min and/or max is not set, the axis will fix its scale to precise lowest and highest values available through all of the series attached to it. This effectively locks the axis from auto-zooming itself when chart is zoomed in. If you need to zoom to actual low/high values within currently visible scope, use strictMinMaxSelection instead. Use extraMin and extraMax to add extra "padding". Click here for more info
39
39
  - **strictMinMaxSelection** (`undefined | false | true`) — Force axis to auto-zoom to exact lowest and highest values from attached series' data items within currently visible range. This is a good feature when your series is plotted from derivative values, like valueYChangeSelection as it helps to avoid frequent jumping of series to adjusted min and max of the axis. This will not work if strictMinMax is set to true (the axis will not zoom at all in this case). Use extraMin and extraMax to add extra "padding". @since 5.1.11
40
40
  - **syncWithAxis** (`ValueAxis`) — This setting can be set to an instance of another ValueAxis. If set the grid of this axis will be synced with grid of the target axis. NOTE: this is not 100% guaranteed to work. In some rare cases perfect sync might not be possible. Click here for more info
41
+ - **syncZeros** (`undefined | false | true`) — Aligns the zero baseline across synced axes. Works only if `syncWithAxis` is set and zero is within the min/max of the axis. @since 5.16.2
41
42
  - **tooltipNumberFormat** (`string | NumberFormatOptions`) — A numeric format used for numbers displayed in axis tooltip. Click here for more info
42
43
  - **treatZeroAs** (`undefined | number`) — Treat zero values as some other value. Useful in situations where zero would result in error, i.e. logarithmic scale. Click here for more info
@@ -27,7 +27,8 @@ Extends: Hierarchy
27
27
  - **cornerCount** (`undefined | number`) — Default 120 Number of corners when type is "polygon". 120 means the polygoon will look like a circle. NOTE: this setting is ignored if type="rectangle".
28
28
  - **maxIterationCount** (`undefined | number`) — Default 100 Maximum allowed number of iterations when computing the layout. Computation is stopped when it number of iterations is reached, even if the convergenceRatio is not yet reached. Bigger number means finer results, but slower performance.
29
29
  - **minWeightRatio** (`undefined | number`) — Default 0.005 Minimum weight ratio which allows computing the minimum allowed weight (= [maximum weight] * minWeightRatio). Setting very small minWeigtRatio might result flickering. NOTE: the nodes that have smaller weight will be scaled up and will not represent their true value correctly.
30
- - **type** (`"rectangle" | "polygon"`) — Default "polygon" Type of the diagram's shape. Click here for more info
30
+ - **shapeType** (`"rectangle" | "polygon"`) — Default "polygon" Type of the diagram's shape. (Renamed from `type` in 5.18.0.) Click here for more info
31
+ - **type** (`"rectangle" | "polygon"`) — *Deprecated since 5.18.0 — use `shapeType` (still works).* Default "polygon" Type of the diagram's shape.
31
32
 
32
33
  ## Properties
33
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amcharts/amcharts5-mcp",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.amcharts/amcharts5",
5
5
  "description": "On-demand access to amCharts 5 docs, code examples, and API reference",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -10,6 +10,10 @@ import { fileURLToPath } from "url";
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  const ROOT = join(__dirname, "..");
12
12
 
13
+ // Single source of truth for the server version: package.json
14
+ const PKG = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
15
+ const VERSION = PKG.version;
16
+
13
17
  // Submodule path (local dev) or content/ fallback (npm install)
14
18
  const SUBMODULE_DIR = join(ROOT, "amcharts5-skill", "amcharts5-skill");
15
19
  const CONTENT_DIR = join(ROOT, "content");
@@ -177,10 +181,38 @@ loadExtendedDir(EXTENDED_DIR);
177
181
  // Search helpers
178
182
  // ---------------------------------------------------------------------------
179
183
 
180
- function searchDocs(query, maxResults = 10) {
184
+ /**
185
+ * Import sections carry two ### sub-blocks: one for ES modules, one for CDN /
186
+ * script tags. Return only the requested one (esm | html), or "" if not found.
187
+ */
188
+ function extractImportBlock(body, format) {
189
+ const lines = body.split("\n");
190
+ const wantEsm = format === "esm";
191
+ let capturing = false;
192
+ let captured = [];
193
+ let sawSub = false;
194
+ for (const line of lines) {
195
+ const sub = line.match(/^###\s+(.+)/);
196
+ if (sub) {
197
+ sawSub = true;
198
+ const h = sub[1].toLowerCase();
199
+ const isEsm = /es module|esm|typescript|import/.test(h);
200
+ // Note: "typescript" contains "script", so match "script tag"/"cdn", not bare "script".
201
+ const isCdn = /cdn|script tag|<script/.test(h);
202
+ capturing = wantEsm ? isEsm : isCdn;
203
+ continue;
204
+ }
205
+ if (capturing) captured.push(line);
206
+ }
207
+ if (!sawSub) return ""; // no sub-blocks → caller falls back to full body
208
+ return captured.join("\n").trim();
209
+ }
210
+
211
+ function searchDocs(query, maxResults = 10, scope = "skill") {
181
212
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
182
213
  const results = [];
183
214
 
215
+ // Skill docs (SKILL.md + references/*.md)
184
216
  for (const [name, doc] of docs) {
185
217
  for (const section of doc.sections) {
186
218
  const text = (section.heading + " " + section.body).toLowerCase();
@@ -188,15 +220,39 @@ function searchDocs(query, maxResults = 10) {
188
220
  if (score > 0) {
189
221
  results.push({
190
222
  file: name,
223
+ source: "skill",
191
224
  docTitle: doc.title,
192
225
  heading: section.heading,
193
226
  body: section.body.trim(),
194
- score,
227
+ // Nudge skill results slightly ahead of extended on ties — the skill
228
+ // is the curated layer; extended is the exhaustive API reference.
229
+ score: score + 0.1,
195
230
  });
196
231
  }
197
232
  }
198
233
  }
199
234
 
235
+ // Extended docs (charts/, concepts/, getting-started/, reference/<class>) —
236
+ // this is where the per-class API + defaults live. Only scanned when scope=all.
237
+ if (scope === "all") {
238
+ for (const [key, doc] of extendedDocs) {
239
+ for (const section of doc.sections) {
240
+ const text = (section.heading + " " + section.body).toLowerCase();
241
+ const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
242
+ if (score > 0) {
243
+ results.push({
244
+ file: key,
245
+ source: "extended",
246
+ docTitle: doc.title,
247
+ heading: section.heading,
248
+ body: section.body.trim(),
249
+ score,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+
200
256
  results.sort((a, b) => b.score - a.score);
201
257
  return results.slice(0, maxResults);
202
258
  }
@@ -207,7 +263,7 @@ function searchDocs(query, maxResults = 10) {
207
263
 
208
264
  const server = new McpServer({
209
265
  name: "amcharts5",
210
- version: "1.0.0",
266
+ version: VERSION,
211
267
  });
212
268
 
213
269
  // --- Tool: list_chart_types ---
@@ -269,21 +325,25 @@ server.tool(
269
325
  // --- Tool: search_docs ---
270
326
  server.tool(
271
327
  "search_docs",
272
- "Search across all amCharts 5 documentation for a keyword or topic. Returns matching sections ranked by relevance.",
328
+ "Search the curated amCharts 5 skill docs (SKILL.md + chart references) for a keyword or topic, returning matching sections ranked by relevance. NOTE: by default this scans ONLY the skill layer. The per-class API reference and per-setting DEFAULTS (e.g. tooltip background cornerRadius, axis tick `visible` default, cursor line strokeDasharray) live in the extended docs — pass scope:'all' here, or use search_all / get_api_reference / get_doc, to reach them.",
273
329
  {
274
330
  query: z.string().describe("Search query, e.g. 'legend', 'axis label rotation', 'tooltip format', 'data processor'"),
275
331
  maxResults: z.number().optional().default(5).describe("Maximum number of results to return (default 5)"),
332
+ scope: z.enum(["skill", "all"]).optional().default("skill").describe("'skill' (default) searches only the curated skill docs; 'all' also searches the extended docs / per-class API reference where settings and defaults live."),
276
333
  },
277
- async ({ query, maxResults }) => {
278
- const results = searchDocs(query, maxResults);
334
+ async ({ query, maxResults, scope }) => {
335
+ const results = searchDocs(query, maxResults, scope);
279
336
  if (results.length === 0) {
280
- return { content: [{ type: "text", text: `No results found for "${query}".` }] };
337
+ const hint = scope === "skill"
338
+ ? ` Nothing in the skill docs — retry with scope:'all' to include the API reference, or use search_all.`
339
+ : "";
340
+ return { content: [{ type: "text", text: `No results found for "${query}".${hint}` }] };
281
341
  }
282
342
 
283
- let text = `# Search results for "${query}"\n\n`;
343
+ let text = `# Search results for "${query}" (scope: ${scope})\n\n`;
284
344
  for (const r of results) {
285
345
  text += `## ${r.docTitle} → ${r.heading}\n`;
286
- text += `*(source: ${r.file}.md, relevance: ${r.score})*\n\n`;
346
+ text += `*(source: ${r.source}/${r.file}.md, relevance: ${r.score})*\n\n`;
287
347
  // Truncate very long sections
288
348
  const body = r.body.length > 2000 ? r.body.slice(0, 2000) + "\n\n...(truncated)" : r.body;
289
349
  text += body + "\n\n---\n\n";
@@ -338,9 +398,16 @@ server.tool(
338
398
  };
339
399
  }
340
400
 
341
- // Find the setup/core pattern section
401
+ // Grab the imports section first so we can exclude it from the setup match.
402
+ const importSection = doc.sections.find(s =>
403
+ /import/i.test(s.heading)
404
+ );
405
+
406
+ // Find the setup/core pattern section — must NOT be the imports section
407
+ // (headings like "Required imports" otherwise win and the actual core
408
+ // setup pattern is never shown, making esm/html identical).
342
409
  const setupSection = doc.sections.find(s =>
343
- /setup|core.*pattern|basic|quick.*start|required.*import/i.test(s.heading)
410
+ s !== importSection && /setup|core.*pattern|basic|quick.*start/i.test(s.heading)
344
411
  );
345
412
 
346
413
  if (!setupSection) {
@@ -349,17 +416,17 @@ server.tool(
349
416
  return { content: [{ type: "text", text: intro }] };
350
417
  }
351
418
 
352
- // Also grab the imports section
353
- const importSection = doc.sections.find(s =>
354
- /import/i.test(s.heading)
355
- );
356
-
357
419
  let text = `# Quick Start: ${doc.title}\n\n`;
358
420
  if (importSection && importSection !== setupSection) {
421
+ // Import sections contain both an "ES modules" and a "CDN / script tags"
422
+ // sub-block. Emit only the one matching the requested format so esm and
423
+ // html actually differ (instead of relabeling the same body).
424
+ const esm = extractImportBlock(importSection.body, "esm");
425
+ const cdn = extractImportBlock(importSection.body, "html");
359
426
  if (format === "esm") {
360
- text += `## Imports (ES modules)\n${importSection.body}\n\n`;
427
+ text += `## Imports (ES modules)\n${esm || importSection.body}\n\n`;
361
428
  } else {
362
- text += `## Imports\n${importSection.body}\n\n`;
429
+ text += `## Imports (CDN / script tags)\n${cdn || importSection.body}\n\n`;
363
430
  }
364
431
  }
365
432
  text += `## ${setupSection.heading}\n${setupSection.body}`;
@@ -433,7 +500,7 @@ server.tool(
433
500
  "get_doc",
434
501
  "Get a full documentation page from the extended amCharts 5 docs. Use search_all first to find the right path.",
435
502
  {
436
- path: z.string().describe("Doc path, e.g. 'charts/xy-chart/cursor', 'concepts/events', 'getting-started/integrations/react'"),
503
+ path: z.string().describe("Doc path, e.g. 'charts/xy-chart/cursor', 'concepts/events', 'getting-started/integrations/react', 'reference/xycursor' (per-class API). For a class's full settings + defaults prefer get_api_reference."),
437
504
  },
438
505
  async ({ path: docPath }) => {
439
506
  const key = docPath.replace(/^\/|\/$/g, "").replace(/\.md$/, "");
@@ -450,6 +517,56 @@ server.tool(
450
517
  }
451
518
  );
452
519
 
520
+ // --- Tool: get_api_reference ---
521
+ server.tool(
522
+ "get_api_reference",
523
+ "Get the per-class API reference for an amCharts 5 class — the class page PLUS its settings table (with DEFAULTS) and, optionally, its private/read-only settings. This is the authoritative source for exact setting names and default values (e.g. tooltip background cornerRadius, axis tick `visible` default). Pass a class name in any casing, e.g. 'XYCursor', 'Tooltip', 'PieSeries', 'AxisRendererX'.",
524
+ {
525
+ className: z.string().describe("amCharts 5 class name, e.g. 'XYCursor', 'Tooltip', 'PieSeries', 'ColumnSeries', 'AxisRendererX'. Casing/punctuation are ignored."),
526
+ includePrivate: z.boolean().optional().default(false).describe("Also include the private (read-only / internal) settings table. Default false."),
527
+ },
528
+ async ({ className, includePrivate }) => {
529
+ // Normalize: "XYCursor" / "xy-cursor" / "XY Cursor" → "xycursor"
530
+ const norm = className.toLowerCase().replace(/[^a-z0-9]/g, "");
531
+ const classDoc = extendedDocs.get(`reference/${norm}`);
532
+ const settingsDoc = extendedDocs.get(`reference/i${norm}settings`);
533
+ const privateDoc = extendedDocs.get(`reference/i${norm}private`);
534
+
535
+ if (!classDoc && !settingsDoc) {
536
+ // Friendly not-found: suggest close matches among the class pages
537
+ // (reference keys that are NOT i…settings / i…private interface pages).
538
+ const classKeys = [...extendedDocs.keys()]
539
+ .filter(k => k.startsWith("reference/"))
540
+ .map(k => k.slice("reference/".length))
541
+ .filter(k => !k.startsWith("i"));
542
+ const suggestions = classKeys
543
+ .filter(k => k.includes(norm) || norm.includes(k))
544
+ .slice(0, 10);
545
+ let msg = `No API reference found for "${className}" (normalized "${norm}").`;
546
+ if (suggestions.length) {
547
+ msg += `\n\nDid you mean: ${suggestions.join(", ")}?`;
548
+ }
549
+ msg += `\n\nTip: use search_all to find the right class, or list_chart_types for chart families.`;
550
+ return { content: [{ type: "text", text: msg }] };
551
+ }
552
+
553
+ let text = "";
554
+ if (classDoc) {
555
+ text += `# ${classDoc.title}\n\n${classDoc.content}\n\n`;
556
+ }
557
+ if (settingsDoc) {
558
+ text += `\n---\n\n# ${settingsDoc.title} (settings + defaults)\n\n${settingsDoc.content}\n\n`;
559
+ }
560
+ if (includePrivate && privateDoc) {
561
+ text += `\n---\n\n# ${privateDoc.title} (private / read-only)\n\n${privateDoc.content}\n\n`;
562
+ }
563
+ if (!includePrivate && privateDoc) {
564
+ text += `\n---\n\n*Private/read-only settings also exist — call get_api_reference("${className}", includePrivate: true) to include them.*\n`;
565
+ }
566
+ return { content: [{ type: "text", text: text.trim() }] };
567
+ }
568
+ );
569
+
453
570
  // --- Tool: list_examples ---
454
571
  server.tool(
455
572
  "list_examples",