semiotic 3.3.0 → 3.4.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.
Files changed (75) hide show
  1. package/CLAUDE.md +16 -7
  2. package/README.md +1 -1
  3. package/ai/dist/mcp-server.js +104 -9
  4. package/ai/schema.json +97 -1
  5. package/dist/components/LinkedCharts.d.ts +8 -0
  6. package/dist/components/charts/index.d.ts +1 -1
  7. package/dist/components/charts/ordinal/BarChart.d.ts +9 -1
  8. package/dist/components/charts/ordinal/DonutChart.d.ts +2 -0
  9. package/dist/components/charts/ordinal/DotPlot.d.ts +9 -1
  10. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +4 -0
  11. package/dist/components/charts/ordinal/LikertChart.d.ts +5 -2
  12. package/dist/components/charts/ordinal/PieChart.d.ts +2 -0
  13. package/dist/components/charts/ordinal/StackedBarChart.d.ts +4 -0
  14. package/dist/components/charts/shared/hooks.d.ts +15 -2
  15. package/dist/components/charts/shared/selectionUtils.d.ts +9 -7
  16. package/dist/components/charts/shared/tooltipUtils.d.ts +13 -1
  17. package/dist/components/charts/shared/types.d.ts +5 -6
  18. package/dist/components/charts/shared/useChartSetup.d.ts +8 -0
  19. package/dist/components/charts/shared/useResolvedSelection.d.ts +2 -0
  20. package/dist/components/realtime/types.d.ts +54 -3
  21. package/dist/components/semiotic-geo.d.ts +4 -0
  22. package/dist/components/semiotic-network.d.ts +7 -0
  23. package/dist/components/semiotic-ordinal.d.ts +8 -0
  24. package/dist/components/semiotic-xy.d.ts +9 -0
  25. package/dist/components/server/renderToStaticSVG.d.ts +2 -1
  26. package/dist/components/server/serverChartConfigs.d.ts +17 -0
  27. package/dist/components/server/themeResolver.d.ts +4 -0
  28. package/dist/components/store/ThemeStore.d.ts +16 -0
  29. package/dist/components/stream/CanvasHitTester.d.ts +8 -3
  30. package/dist/components/stream/DataSourceAdapter.d.ts +17 -0
  31. package/dist/components/stream/GeoCanvasHitTester.d.ts +1 -1
  32. package/dist/components/stream/GeoPipelineStore.d.ts +19 -1
  33. package/dist/components/stream/NetworkPipelineStore.d.ts +12 -1
  34. package/dist/components/stream/OrdinalCanvasHitTester.d.ts +3 -1
  35. package/dist/components/stream/OrdinalPipelineStore.d.ts +38 -1
  36. package/dist/components/stream/ParticlePool.d.ts +4 -0
  37. package/dist/components/stream/PipelineStore.d.ts +17 -2
  38. package/dist/components/stream/StreamXYFrame.d.ts +17 -0
  39. package/dist/components/stream/geoTypes.d.ts +11 -4
  40. package/dist/components/stream/hoverUtils.d.ts +34 -0
  41. package/dist/components/stream/networkTypes.d.ts +33 -23
  42. package/dist/components/stream/ordinalTypes.d.ts +63 -12
  43. package/dist/components/stream/pipelineDecay.d.ts +6 -5
  44. package/dist/components/stream/pipelineTransitionUtils.d.ts +21 -0
  45. package/dist/components/stream/quadtreeHitTest.d.ts +22 -0
  46. package/dist/components/stream/renderers/resolveCSSColor.d.ts +23 -6
  47. package/dist/components/stream/types.d.ts +22 -0
  48. package/dist/components/stream/useFrame.d.ts +122 -0
  49. package/dist/geo.min.js +1 -1
  50. package/dist/geo.module.min.js +1 -1
  51. package/dist/network.min.js +1 -1
  52. package/dist/network.module.min.js +1 -1
  53. package/dist/ordinal.min.js +1 -1
  54. package/dist/ordinal.module.min.js +1 -1
  55. package/dist/realtime.min.js +1 -1
  56. package/dist/realtime.module.min.js +1 -1
  57. package/dist/semiotic-ai.min.js +1 -1
  58. package/dist/semiotic-ai.module.min.js +1 -1
  59. package/dist/semiotic-geo.d.ts +4 -0
  60. package/dist/semiotic-network.d.ts +7 -0
  61. package/dist/semiotic-ordinal.d.ts +8 -0
  62. package/dist/semiotic-themes.min.js +1 -1
  63. package/dist/semiotic-themes.module.min.js +1 -1
  64. package/dist/semiotic-utils.min.js +1 -1
  65. package/dist/semiotic-utils.module.min.js +1 -1
  66. package/dist/semiotic-xy.d.ts +9 -0
  67. package/dist/semiotic.min.js +1 -1
  68. package/dist/semiotic.module.min.js +1 -1
  69. package/dist/server.min.js +1 -1
  70. package/dist/server.module.min.js +1 -1
  71. package/dist/test-utils/canvasMock.d.ts +26 -0
  72. package/dist/test-utils/ordinalFixtures.d.ts +48 -0
  73. package/dist/xy.min.js +1 -1
  74. package/dist/xy.module.min.js +1 -1
  75. package/package.json +15 -14
package/CLAUDE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Quick Start
4
4
  - Install: `npm install semiotic`
5
- - **Use sub-path imports** — `semiotic/xy` (143KB gz), `semiotic/ordinal` (109KB), `semiotic/network` (98KB), `semiotic/geo` (93KB), `semiotic/realtime` (145KB), `semiotic/server` (100KB), `semiotic/utils` (31KB), `semiotic/themes` (5KB), `semiotic/data` (5KB). Full `semiotic` is 278KB gz.
5
+ - **Use sub-path imports** — `semiotic/xy` (78KB gz), `semiotic/ordinal` (65KB), `semiotic/network` (54KB), `semiotic/geo` (53KB), `semiotic/realtime` (77KB), `semiotic/server` (58KB), `semiotic/utils` (19KB), `semiotic/themes` (3KB), `semiotic/data` (3KB). Full `semiotic` is 158KB gz.
6
6
  - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
7
7
  - MCP: `npx semiotic-mcp`
8
8
 
@@ -12,7 +12,7 @@
12
12
  - Every HOC accepts `frameProps` for pass-through. TypeScript `strict: true`. Every HOC has error boundary + dev-mode validation.
13
13
 
14
14
  ## Common Props (all HOCs)
15
- `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `enableHover` (true), `tooltip` (boolean | "multi" | function | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true), `hoverHighlight` (boolean — dims non-hovered series, requires `colorBy`), `hoverRadius` (30)
15
+ `title`, `description` (aria-label), `summary` (sr-only), `width` (600), `height` (400), `responsiveWidth`, `responsiveHeight`, `margin`, `className`, `color` (uniform fill), `enableHover` (true), `tooltip` (boolean | "multi" | function | config object), `showLegend`, `showGrid` (false), `frameProps`, `onObservation`, `onClick`, `chartId`, `loading` (false), `emptyContent`, `legendInteraction` ("none"|"highlight"|"isolate"), `legendPosition` ("right"|"left"|"top"|"bottom"), `emphasis` ("primary"|"secondary"), `annotations` (array), `accessibleTable` (true), `hoverHighlight` (boolean — dims non-hovered series, requires `colorBy`), `hoverRadius` (30), `animate` (boolean | { duration?, easing?, intro? } — animated intro on first render + smooth transitions on data change; intro defaults to true when animate is enabled)
16
16
 
17
17
  `onClick` receives `(datum, { x, y })`. `onObservation` receives `{ type, datum?, x?, y?, timestamp, chartType, chartId }`.
18
18
 
@@ -27,18 +27,20 @@
27
27
  **QuadrantChart** — Scatterplot + `quadrants` (required), `xCenter`, `yCenter`
28
28
  **MultiAxisLineChart** — Dual Y-axis. `series` (required: `[{ yAccessor, label?, color?, format?, extent? }]`). Falls back to multi-line if not 2 series.
29
29
  **Heatmap** — `data`, `xAccessor`, `yAccessor`, `valueAccessor`, `colorScheme`, `showValues`, `cellBorderColor`
30
+ **ScatterplotMatrix** — `data`, `fields` (array of numeric field names for grid)
31
+ **MinimapChart** — Overview + detail with linked zoom. Wraps an XY chart.
30
32
 
31
33
  ## Ordinal Charts (`semiotic/ordinal`)
32
34
 
33
35
  **BarChart** — `data`, `categoryAccessor`, `valueAccessor`, `orientation`, `colorBy`, `sort`, `barPadding` (40)
34
- **StackedBarChart** — + `stackBy` (required), `normalize`
35
- **GroupedBarChart** — + `groupBy` (required), `barPadding` (60)
36
+ **StackedBarChart** — + `stackBy` (required), `normalize`, `sort` (default false — insertion order)
37
+ **GroupedBarChart** — + `groupBy` (required), `barPadding` (60), `sort` (default false — insertion order)
36
38
  **SwarmPlot** — `colorBy`, `sizeBy`, `pointRadius`, `pointOpacity`
37
39
  **BoxPlot** — + `showOutliers`, `outlierRadius`
38
40
  **Histogram** — + `bins` (25), `relative`. Always horizontal.
39
41
  **ViolinPlot** — + `bins`, `curve`, `showIQR`
40
42
  **RidgelinePlot** — + `bins`, `amplitude` (1.5)
41
- **DotPlot** — + `sort` (true), `dotRadius`, `showGrid` default true
43
+ **DotPlot** — + `sort` ("auto" — insertion order when streaming, value-desc on static), `dotRadius`, `showGrid` default true
42
44
  **PieChart** — `categoryAccessor`, `valueAccessor`, `colorBy`, `startAngle`
43
45
  **DonutChart** — PieChart + `innerRadius` (60), `centerContent`
44
46
  **FunnelChart** — `stepAccessor`, `valueAccessor`, `categoryAccessor` (optional), `connectorOpacity`, `orientation`
@@ -84,20 +86,23 @@ Most HOCs support push via `forwardRef`. **Omit** `data` — do NOT pass `data={
84
86
  const ref = useRef()
85
87
  ref.current.push({ id: "p1", x: 1, y: 2 })
86
88
  ref.current.pushMany([...points])
89
+ ref.current.replace([...points]) // ordinal only — full dataset replacement, preserves category order + transitions (progressively chunks large datasets)
87
90
  ref.current.remove("p1") // by ID — requires pointIdAccessor
88
91
  ref.current.remove(["p1", "p2"]) // batch remove
89
92
  ref.current.update("p1", d => ({ ...d, y: 99 })) // in-place update — requires pointIdAccessor
90
93
  ref.current.clear()
91
94
  ref.current.getData()
95
+ ref.current.getScales() // returns {o, r, projection} (ordinal) / {x, y} (XY) — null if not yet computed
92
96
  <Scatterplot ref={ref} xAccessor="x" yAccessor="y" pointIdAccessor="id" />
93
97
  ```
94
- `remove()` and `update()` require an ID accessor: `pointIdAccessor` on XY/realtime charts, `dataIdAccessor` on ordinal charts. Network HOC refs also use `remove(id)`/`update(id, updater)` (operates on nodes). For edge-level operations, use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)`, `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
98
+ `remove()` and `update()` require an ID accessor: `pointIdAccessor` on XY/realtime charts, `dataIdAccessor` on ordinal charts. `replace()` is ordinal-only and routes through a bounded-ingest path that preserves category insertion-order memory and the transition position snapshot — what aggregator HOCs like LikertChart use under the hood to re-aggregate streaming input without shuffling categories or losing animations. Network HOC refs also use `remove(id)`/`update(id, updater)` (operates on nodes). For edge-level operations, use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)` (requires `edgeIdAccessor`), `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
95
99
  Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
96
100
 
97
101
  ## Coordinated Views
98
102
 
99
103
  **LinkedCharts** — `selections`, **CategoryColorProvider** — `colors`|`categories` + `colorScheme`
100
104
  Chart props: `selection`, `linkedHover`, `linkedBrush`. Hooks: `useSelection`, `useLinkedHover`, `useBrushSelection`
105
+ **Shared categories inside LinkedCharts → wrap in `CategoryColorProvider`.** When two or more charts encode the same categorical field (e.g. both `colorBy="region"`), wrapping in `CategoryColorProvider` gives every chart identical colors per category AND makes `LinkedCharts` render one unified legend (and suppress individual chart legends). Without it, each chart renders its own legend independently — often with mismatched colors.
101
106
  **Linked crosshair**: `linkedHover={{ name: "sync", mode: "x-position", xField: "time" }}`. Click-to-lock: click locks crosshair (dashed white), click/Escape unlocks.
102
107
  **ScatterplotMatrix**, **ChartContainer** (`title`, `subtitle`, `actions`), **ChartGrid** (`columns`, `gap`), **ContextLayout**
103
108
 
@@ -150,7 +155,7 @@ All HOCs accept `annotations` (array). Coordinates use data field names.
150
155
 
151
156
  ## Theming
152
157
 
153
- CSS custom properties: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-secondary`, `--semiotic-border`, `--semiotic-grid`, `--semiotic-primary`, `--semiotic-focus`, `--semiotic-font-family`, `--semiotic-tooltip-bg`/`text`/`radius`/`font-size`/`shadow`.
158
+ CSS custom properties: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-secondary`, `--semiotic-border`, `--semiotic-grid`, `--semiotic-primary`, `--semiotic-focus`, `--semiotic-font-family`, `--semiotic-annotation-color`, `--semiotic-legend-font-size`, `--semiotic-title-font-size`, `--semiotic-tick-font-family`, `--semiotic-tooltip-bg`/`text`/`radius`/`font-size`/`shadow`.
154
159
 
155
160
  ```jsx
156
161
  <ThemeProvider theme="tufte"> {/* Named preset */}
@@ -174,6 +179,9 @@ Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveTh
174
179
  - **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
175
180
  - **Log scale**: Domain min clamped to 1e-6.
176
181
  - **barPadding**: Pixel value (40/60 default). Reduce for small charts.
182
+ - **sort on StackedBarChart/GroupedBarChart**: Default `false` preserves insertion order. The underlying frame defaults to value-descending if `oSort` is undefined, so always pass `sort` explicitly if order matters.
183
+ - **sort `"auto"`** (BarChart/StackedBarChart/GroupedBarChart/DotPlot): insertion order while streaming, value-desc on static data. The right choice when using the push API — avoids categories shuffling as values fluctuate. DotPlot's default; opt-in on others.
184
+ - **Tooltip format cascade**: `valueFormat` (ordinal) / `xFormat` / `yFormat` (XY) / `valueFormat` on Heatmap flow to the default tooltip automatically, so axis and tooltip read identically. Only applies to the default tooltip — a custom `tooltip` prop fully overrides; re-pass the formatter inside `Tooltip({format})` / `MultiLineTooltip({fields:[{format}]})` if you want it there. Bespoke-tooltip charts (Histogram, FunnelChart, LikertChart, GaugeChart) don't participate; customize via `tooltip`.
177
185
  - **Horizontal bars**: Need wider left margin: `margin={{ left: 120 }}`.
178
186
  - **Push API**: Omit `data` entirely. `data={[]}` clears on every render.
179
187
  - **frameProps style functions**: Bypass HOC color resolution — use `colorBy` prop instead.
@@ -186,6 +194,7 @@ Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveTh
186
194
  - **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
187
195
  - **categoryFormat/xFormat/yFormat**: Can return ReactNode (renders in `<foreignObject>`).
188
196
  - **Tick deduplication**: Adjacent identical labels auto-removed.
197
+ - **Composing overlays**: XY/Ordinal charts paint `--semiotic-bg` across the full canvas by default, hiding anything beneath. When stacking charts (e.g. `position: absolute` overlay on top of a base), pass `frameProps={{ background: "transparent" }}` on the overlay to skip the fill. Network/Geo frames don't paint bg by default, so this only matters for XY/Ordinal.
189
198
 
190
199
  ## Performance
191
200
 
package/README.md CHANGED
@@ -34,7 +34,7 @@ generate correct code without examples.
34
34
  Semiotic ships with everything an AI coding assistant needs to generate
35
35
  correct visualizations without trial and error:
36
36
 
37
- - **`semiotic/ai`** — a single import with all 38 chart components, optimized for LLM code generation
37
+ - **`semiotic/ai`** — a single import with 35 HOC charts (XY, ordinal, network, realtime), optimized for LLM code generation. Geo charts are in `semiotic/geo` to keep d3-geo out of non-geo bundles.
38
38
  - **`ai/schema.json`** — machine-readable prop schemas for every component
39
39
  - **`npx semiotic-mcp`** — an MCP server for tool-based chart rendering in any MCP client
40
40
  - **`npx semiotic-ai --doctor`** — validate component + props JSON from the command line with typo suggestions and anti-pattern detection
@@ -24642,10 +24642,9 @@ var ProgressTokenSchema = union([string2(), number2().int()]);
24642
24642
  var CursorSchema = string2();
24643
24643
  var TaskCreationParamsSchema = looseObject({
24644
24644
  /**
24645
- * Time in milliseconds to keep task results available after completion.
24646
- * If null, the task has unlimited lifetime until manually cleaned up.
24645
+ * Requested duration in milliseconds to retain task from creation.
24647
24646
  */
24648
- ttl: union([number2(), _null3()]).optional(),
24647
+ ttl: number2().optional(),
24649
24648
  /**
24650
24649
  * Time in milliseconds to wait between task status requests.
24651
24650
  */
@@ -24945,7 +24944,11 @@ var ClientCapabilitiesSchema = object2({
24945
24944
  /**
24946
24945
  * Present if the client supports task creation.
24947
24946
  */
24948
- tasks: ClientTasksCapabilitySchema.optional()
24947
+ tasks: ClientTasksCapabilitySchema.optional(),
24948
+ /**
24949
+ * Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
24950
+ */
24951
+ extensions: record(string2(), AssertObjectSchema).optional()
24949
24952
  });
24950
24953
  var InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
24951
24954
  /**
@@ -25007,7 +25010,11 @@ var ServerCapabilitiesSchema = object2({
25007
25010
  /**
25008
25011
  * Present if the server supports task creation.
25009
25012
  */
25010
- tasks: ServerTasksCapabilitySchema.optional()
25013
+ tasks: ServerTasksCapabilitySchema.optional(),
25014
+ /**
25015
+ * Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name).
25016
+ */
25017
+ extensions: record(string2(), AssertObjectSchema).optional()
25011
25018
  });
25012
25019
  var InitializeResultSchema = ResultSchema.extend({
25013
25020
  /**
@@ -25199,6 +25206,12 @@ var ResourceSchema = object2({
25199
25206
  * The MIME type of this resource, if known.
25200
25207
  */
25201
25208
  mimeType: optional(string2()),
25209
+ /**
25210
+ * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
25211
+ *
25212
+ * This can be used by Hosts to display file sizes and estimate context window usage.
25213
+ */
25214
+ size: optional(number2()),
25202
25215
  /**
25203
25216
  * Optional annotations for the client.
25204
25217
  */
@@ -27694,6 +27707,10 @@ var Protocol = class {
27694
27707
  this._progressHandlers.clear();
27695
27708
  this._taskProgressTokens.clear();
27696
27709
  this._pendingDebouncedNotifications.clear();
27710
+ for (const info of this._timeoutInfo.values()) {
27711
+ clearTimeout(info.timeoutId);
27712
+ }
27713
+ this._timeoutInfo.clear();
27697
27714
  for (const controller of this._requestHandlerAbortControllers.values()) {
27698
27715
  controller.abort();
27699
27716
  }
@@ -27824,7 +27841,9 @@ var Protocol = class {
27824
27841
  await capturedTransport?.send(errorResponse);
27825
27842
  }
27826
27843
  }).catch((error48) => this._onerror(new Error(`Failed to send response: ${error48}`))).finally(() => {
27827
- this._requestHandlerAbortControllers.delete(request.id);
27844
+ if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
27845
+ this._requestHandlerAbortControllers.delete(request.id);
27846
+ }
27828
27847
  });
27829
27848
  }
27830
27849
  _onprogress(notification) {
@@ -29837,6 +29856,9 @@ var McpServer = class {
29837
29856
  annotations = rest.shift();
29838
29857
  }
29839
29858
  } else if (typeof firstArg === "object" && firstArg !== null) {
29859
+ if (Object.values(firstArg).some((v) => typeof v === "object" && v !== null)) {
29860
+ throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`);
29861
+ }
29840
29862
  annotations = rest.shift();
29841
29863
  }
29842
29864
  }
@@ -29955,6 +29977,9 @@ function getZodSchemaObject(schema2) {
29955
29977
  if (isZodRawShapeCompat(schema2)) {
29956
29978
  return objectFromShape(schema2);
29957
29979
  }
29980
+ if (!isZodSchemaInstance(schema2)) {
29981
+ throw new Error("inputSchema must be a Zod schema or raw shape, received an unrecognized object");
29982
+ }
29958
29983
  return schema2;
29959
29984
  }
29960
29985
  function promptArgumentsFromSchema(schema2) {
@@ -30240,6 +30265,17 @@ var requestPrototype = {
30240
30265
  }
30241
30266
  });
30242
30267
  });
30268
+ Object.defineProperty(requestPrototype, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), {
30269
+ value: function(depth, options, inspectFn) {
30270
+ const props = {
30271
+ method: this.method,
30272
+ url: this.url,
30273
+ headers: this.headers,
30274
+ nativeRequest: this[requestCache]
30275
+ };
30276
+ return `Request (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`;
30277
+ }
30278
+ });
30243
30279
  Object.setPrototypeOf(requestPrototype, Request.prototype);
30244
30280
  var newRequest = (incoming, defaultHostname) => {
30245
30281
  const req = Object.create(requestPrototype);
@@ -30344,6 +30380,17 @@ var Response2 = class _Response {
30344
30380
  }
30345
30381
  });
30346
30382
  });
30383
+ Object.defineProperty(Response2.prototype, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), {
30384
+ value: function(depth, options, inspectFn) {
30385
+ const props = {
30386
+ status: this.status,
30387
+ headers: this.headers,
30388
+ ok: this.ok,
30389
+ nativeResponse: this[responseCache]
30390
+ };
30391
+ return `Response (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`;
30392
+ }
30393
+ });
30347
30394
  Object.setPrototypeOf(Response2, GlobalResponse);
30348
30395
  Object.setPrototypeOf(Response2.prototype, GlobalResponse.prototype);
30349
30396
  async function readWithoutBlocking(readPromise) {
@@ -30415,6 +30462,50 @@ if (typeof global.crypto === "undefined") {
30415
30462
  global.crypto = import_crypto.default;
30416
30463
  }
30417
30464
  var outgoingEnded = /* @__PURE__ */ Symbol("outgoingEnded");
30465
+ var incomingDraining = /* @__PURE__ */ Symbol("incomingDraining");
30466
+ var DRAIN_TIMEOUT_MS = 500;
30467
+ var MAX_DRAIN_BYTES = 64 * 1024 * 1024;
30468
+ var drainIncoming = (incoming) => {
30469
+ const incomingWithDrainState = incoming;
30470
+ if (incoming.destroyed || incomingWithDrainState[incomingDraining]) {
30471
+ return;
30472
+ }
30473
+ incomingWithDrainState[incomingDraining] = true;
30474
+ if (incoming instanceof import_http2.Http2ServerRequest) {
30475
+ try {
30476
+ ;
30477
+ incoming.stream?.close?.(import_http2.constants.NGHTTP2_NO_ERROR);
30478
+ } catch {
30479
+ }
30480
+ return;
30481
+ }
30482
+ let bytesRead = 0;
30483
+ const cleanup = () => {
30484
+ clearTimeout(timer);
30485
+ incoming.off("data", onData);
30486
+ incoming.off("end", cleanup);
30487
+ incoming.off("error", cleanup);
30488
+ };
30489
+ const forceClose = () => {
30490
+ cleanup();
30491
+ const socket = incoming.socket;
30492
+ if (socket && !socket.destroyed) {
30493
+ socket.destroySoon();
30494
+ }
30495
+ };
30496
+ const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);
30497
+ timer.unref?.();
30498
+ const onData = (chunk) => {
30499
+ bytesRead += chunk.length;
30500
+ if (bytesRead > MAX_DRAIN_BYTES) {
30501
+ forceClose();
30502
+ }
30503
+ };
30504
+ incoming.on("data", onData);
30505
+ incoming.on("end", cleanup);
30506
+ incoming.on("error", cleanup);
30507
+ incoming.resume();
30508
+ };
30418
30509
  var handleRequestError = () => new Response(null, {
30419
30510
  status: 400
30420
30511
  });
@@ -30586,14 +30677,18 @@ var getRequestListener = (fetchCallback, options = {}) => {
30586
30677
  setTimeout(() => {
30587
30678
  if (!incomingEnded) {
30588
30679
  setTimeout(() => {
30589
- incoming.destroy();
30590
- outgoing.destroy();
30680
+ drainIncoming(incoming);
30591
30681
  });
30592
30682
  }
30593
30683
  });
30594
30684
  }
30595
30685
  };
30596
30686
  }
30687
+ outgoing.on("finish", () => {
30688
+ if (!incomingEnded) {
30689
+ drainIncoming(incoming);
30690
+ }
30691
+ });
30597
30692
  }
30598
30693
  outgoing.on("close", () => {
30599
30694
  const abortController = req[abortControllerKey];
@@ -30608,7 +30703,7 @@ var getRequestListener = (fetchCallback, options = {}) => {
30608
30703
  setTimeout(() => {
30609
30704
  if (!incomingEnded) {
30610
30705
  setTimeout(() => {
30611
- incoming.destroy();
30706
+ drainIncoming(incoming);
30612
30707
  });
30613
30708
  }
30614
30709
  });
package/ai/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "name": "semiotic",
4
- "version": "3.3.0",
4
+ "version": "3.4.0",
5
5
  "description": "React data visualization library for charts, networks, and beyond",
6
6
  "tools": [
7
7
  {
@@ -4793,6 +4793,102 @@
4793
4793
  ]
4794
4794
  }
4795
4795
  }
4796
+ },
4797
+ {
4798
+ "type": "function",
4799
+ "function": {
4800
+ "name": "ScatterplotMatrix",
4801
+ "description": "Multi-panel scatterplot grid with crossfilter brushing. Requires data array with numeric fields.",
4802
+ "parameters": {
4803
+ "type": "object",
4804
+ "properties": {
4805
+ "data": { "type": "array" },
4806
+ "fields": { "type": "array", "items": { "type": "string" } }
4807
+ },
4808
+ "required": ["data", "fields"]
4809
+ }
4810
+ }
4811
+ },
4812
+ {
4813
+ "type": "function",
4814
+ "function": {
4815
+ "name": "MinimapChart",
4816
+ "description": "Overview + detail chart with linked zoom. Wraps an XY chart with a minimap navigation pane.",
4817
+ "parameters": {
4818
+ "type": "object",
4819
+ "properties": {
4820
+ "data": { "type": "array" }
4821
+ },
4822
+ "required": ["data"]
4823
+ }
4824
+ }
4825
+ },
4826
+ {
4827
+ "type": "function",
4828
+ "function": {
4829
+ "name": "ChoroplethMap",
4830
+ "description": "Geographic choropleth map with colored regions based on data values.",
4831
+ "parameters": {
4832
+ "type": "object",
4833
+ "properties": {
4834
+ "areas": { "type": ["array", "string"], "description": "GeoJSON features or reference geography name" },
4835
+ "valueAccessor": { "type": "string" },
4836
+ "colorScheme": { "type": "string" },
4837
+ "projection": { "type": "string", "default": "equalEarth" }
4838
+ },
4839
+ "required": ["areas"]
4840
+ }
4841
+ }
4842
+ },
4843
+ {
4844
+ "type": "function",
4845
+ "function": {
4846
+ "name": "ProportionalSymbolMap",
4847
+ "description": "Geographic map with sized symbols at point locations.",
4848
+ "parameters": {
4849
+ "type": "object",
4850
+ "properties": {
4851
+ "points": { "type": "array" },
4852
+ "xAccessor": { "type": "string", "default": "lon" },
4853
+ "yAccessor": { "type": "string", "default": "lat" },
4854
+ "sizeBy": { "type": "string" },
4855
+ "areas": { "type": ["array", "string"] }
4856
+ },
4857
+ "required": ["points"]
4858
+ }
4859
+ }
4860
+ },
4861
+ {
4862
+ "type": "function",
4863
+ "function": {
4864
+ "name": "FlowMap",
4865
+ "description": "Geographic flow map showing movement between locations with animated particles.",
4866
+ "parameters": {
4867
+ "type": "object",
4868
+ "properties": {
4869
+ "flows": { "type": "array" },
4870
+ "nodes": { "type": "array" },
4871
+ "valueAccessor": { "type": "string" }
4872
+ },
4873
+ "required": ["flows"]
4874
+ }
4875
+ }
4876
+ },
4877
+ {
4878
+ "type": "function",
4879
+ "function": {
4880
+ "name": "DistanceCartogram",
4881
+ "description": "Cartogram distorting geographic positions based on travel time or cost from a center point.",
4882
+ "parameters": {
4883
+ "type": "object",
4884
+ "properties": {
4885
+ "points": { "type": "array" },
4886
+ "center": { "type": "array" },
4887
+ "costAccessor": { "type": "string" }
4888
+ },
4889
+ "required": ["points"]
4890
+ }
4891
+ }
4796
4892
  }
4797
4893
  ]
4798
4894
  }
@@ -44,6 +44,14 @@ export interface LinkedChartsProps {
44
44
  */
45
45
  legendField?: string;
46
46
  }
47
+ /**
48
+ * Mirror of the wrap logic in Legend's renderLegendGroupHorizontal:
49
+ * itemWidth = SWATCH(16) + 10 + label.length * 7, wrap when the cursor
50
+ * would exceed maxWidth. Used only to size the container SVG — the
51
+ * authoritative layout still happens inside <Legend>. When width is
52
+ * unknown (e.g. first paint, SSR), return 1 so we don't pre-grow.
53
+ */
54
+ export declare function estimateLegendRowCount(labels: string[], width: number): number;
47
55
  /**
48
56
  * LinkedCharts — context provider for coordinated chart views.
49
57
  *
@@ -33,7 +33,7 @@ export type { BarChartProps } from "./ordinal/BarChart";
33
33
  export { StackedBarChart } from "./ordinal/StackedBarChart";
34
34
  export type { StackedBarChartProps } from "./ordinal/StackedBarChart";
35
35
  export { LikertChart } from "./ordinal/LikertChart";
36
- export type { LikertChartProps } from "./ordinal/LikertChart";
36
+ export type { LikertChartProps, LikertChartHandle } from "./ordinal/LikertChart";
37
37
  export { SwarmPlot } from "./ordinal/SwarmPlot";
38
38
  export type { SwarmPlotProps } from "./ordinal/SwarmPlot";
39
39
  export { BoxPlot } from "./ordinal/BoxPlot";
@@ -17,8 +17,16 @@ export interface BarChartProps<TDatum extends Record<string, any> = Record<strin
17
17
  valueFormat?: (d: number | string) => string;
18
18
  colorBy?: ChartAccessor<TDatum, string>;
19
19
  colorScheme?: string | string[];
20
- sort?: boolean | "asc" | "desc" | ((a: Record<string, any>, b: Record<string, any>) => number);
20
+ /** Category ordering. `false` (default) = insertion order. `"asc"` /
21
+ * `"desc"` sorts by total value. `"auto"` preserves insertion order
22
+ * while streaming and falls through to value-desc on static data.
23
+ * `true` = value-desc regardless of source. Function comparators
24
+ * receive category name strings (not row objects) and run against
25
+ * the category list on the axis. */
26
+ sort?: boolean | "asc" | "desc" | "auto" | ((a: string, b: string) => number);
21
27
  barPadding?: number;
28
+ /** Rounded top corner radius in pixels. Only the end away from the baseline is rounded. */
29
+ roundedTop?: number;
22
30
  /** When true, adds padding below the 0 baseline. Default false (bars flush with axis). */
23
31
  baselinePadding?: boolean;
24
32
  enableHover?: boolean;
@@ -13,6 +13,8 @@ export interface DonutChartProps<TDatum extends Record<string, any> = Record<str
13
13
  colorBy?: ChartAccessor<TDatum, string>;
14
14
  colorScheme?: string | string[];
15
15
  startAngle?: number;
16
+ /** Rounded corner radius on wedge arcs */
17
+ cornerRadius?: number;
16
18
  enableHover?: boolean;
17
19
  showCategoryTicks?: boolean;
18
20
  showLegend?: boolean;
@@ -14,7 +14,15 @@ export interface DotPlotProps<TDatum extends Record<string, any> = Record<string
14
14
  valueFormat?: (d: number | string) => string;
15
15
  colorBy?: ChartAccessor<TDatum, string>;
16
16
  colorScheme?: string | string[];
17
- sort?: boolean | "asc" | "desc" | ((a: Record<string, any>, b: Record<string, any>) => number);
17
+ /** Category ordering. Default (`undefined`) resolves to `"auto"`, which
18
+ * preserves insertion order while streaming and falls through to
19
+ * value-desc on static data — the recommended choice when using the
20
+ * push API so categories don't jump around as values fluctuate.
21
+ * `true` forces value-desc regardless of source. `"asc"` / `"desc"`
22
+ * sorts by total value. `false` for insertion order regardless of
23
+ * source. Function comparators receive category name strings (not
24
+ * row objects) and run against the category list on the axis. */
25
+ sort?: boolean | "asc" | "desc" | "auto" | ((a: string, b: string) => number);
18
26
  dotRadius?: number;
19
27
  categoryPadding?: number;
20
28
  enableHover?: boolean;
@@ -15,7 +15,11 @@ export interface GroupedBarChartProps<TDatum extends Record<string, any> = Recor
15
15
  valueFormat?: (d: number | string) => string;
16
16
  colorBy?: ChartAccessor<TDatum, string>;
17
17
  colorScheme?: string | string[];
18
+ /** Category sort order. Default: `false` (data insertion order). `"asc"`/`"desc"` sorts by total grouped value. `"auto"` preserves insertion order while streaming and falls through to value-desc on static data. Custom comparators receive category keys. */
19
+ sort?: boolean | "asc" | "desc" | "auto" | ((a: string, b: string) => number);
18
20
  barPadding?: number;
21
+ /** Rounded corner radius on bar ends (away from baseline). */
22
+ roundedTop?: number;
19
23
  baselinePadding?: boolean;
20
24
  enableHover?: boolean;
21
25
  showGrid?: boolean;
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { StreamOrdinalFrameProps } from "../../stream/ordinalTypes";
2
+ import type { StreamOrdinalFrameProps, StreamOrdinalFrameHandle } from "../../stream/ordinalTypes";
3
3
  import type { LegendInteractionMode } from "../shared/hooks";
4
4
  import type { BaseChartProps, ChartAccessor, CategoryFormatFn } from "../shared/types";
5
5
  import { type TooltipProp } from "../../Tooltip/Tooltip";
@@ -88,7 +88,10 @@ export interface LikertChartProps<TDatum extends Record<string, any> = Record<st
88
88
  categoryFormat?: CategoryFormatFn;
89
89
  frameProps?: Partial<Omit<StreamOrdinalFrameProps, "data" | "size">>;
90
90
  }
91
+ export interface LikertChartHandle extends RealtimeFrameHandle {
92
+ getScales(): ReturnType<NonNullable<StreamOrdinalFrameHandle["getScales"]>>;
93
+ }
91
94
  export declare const LikertChart: {
92
- <TDatum extends Record<string, any> = Record<string, any>>(props: LikertChartProps<TDatum> & React.RefAttributes<RealtimeFrameHandle>): React.ReactElement | null;
95
+ <TDatum extends Record<string, any> = Record<string, any>>(props: LikertChartProps<TDatum> & React.RefAttributes<LikertChartHandle>): React.ReactElement | null;
93
96
  displayName?: string;
94
97
  };
@@ -11,6 +11,8 @@ export interface PieChartProps<TDatum extends Record<string, any> = Record<strin
11
11
  colorBy?: ChartAccessor<TDatum, string>;
12
12
  colorScheme?: string | string[];
13
13
  startAngle?: number;
14
+ /** Rounded corner radius on wedge arcs */
15
+ cornerRadius?: number;
14
16
  enableHover?: boolean;
15
17
  showCategoryTicks?: boolean;
16
18
  showLegend?: boolean;
@@ -16,7 +16,11 @@ export interface StackedBarChartProps<TDatum extends Record<string, any> = Recor
16
16
  colorBy?: ChartAccessor<TDatum, string>;
17
17
  colorScheme?: string | string[];
18
18
  normalize?: boolean;
19
+ /** Category sort order. Default: `false` (data insertion order). `"asc"`/`"desc"` sorts by total stacked value. `"auto"` preserves insertion order while streaming and falls through to value-desc on static data. Custom comparators receive category keys. */
20
+ sort?: boolean | "asc" | "desc" | "auto" | ((a: string, b: string) => number);
19
21
  barPadding?: number;
22
+ /** Rounded top corner radius. Only the topmost stacked segment gets rounded. */
23
+ roundedTop?: number;
20
24
  baselinePadding?: boolean;
21
25
  enableHover?: boolean;
22
26
  showGrid?: boolean;
@@ -33,8 +33,17 @@ export declare function useColorScale(data: Array<Record<string, any>>, colorBy:
33
33
  /**
34
34
  * Hook to sort data by a value accessor.
35
35
  * Used by BarChart and DotPlot.
36
+ *
37
+ * `"auto"` and function comparators are pass-through here. The frame-
38
+ * level `resolveCategories` decides the visual category order:
39
+ * • `"auto"` → insertion order when streaming, value-desc when static.
40
+ * • function → runs as a category-key comparator on the axis list.
41
+ * The HOC's row-level `data` order only seeds insertion order via the
42
+ * store's category Set; rearranging rows with a category comparator
43
+ * makes no sense (it would call the comparator with row objects instead
44
+ * of strings), so we decline to sort in both cases.
36
45
  */
37
- export declare function useSortedData(data: Array<Record<string, any>>, sort: boolean | "asc" | "desc" | ((a: Record<string, any>, b: Record<string, any>) => number), valueAccessor: Accessor<number>): Array<Record<string, any>>;
46
+ export declare function useSortedData(data: Array<Record<string, any>>, sort: boolean | "asc" | "desc" | "auto" | ((a: string, b: string) => number), valueAccessor: Accessor<number>): Array<Record<string, any>>;
38
47
  /**
39
48
  * Hook to set up selection and linked hover for a chart component.
40
49
  * Consolidates normalizeLinkedHover, useSelection, useLinkedHover,
@@ -121,7 +130,11 @@ export interface LegendInteractionState {
121
130
  }
122
131
  /**
123
132
  * Hook managing legend highlight/isolate interaction.
124
- * - "highlight": hover over legend item dims everything else to 30% opacity
133
+ * - "highlight": hover over a legend item produces a selection hook that
134
+ * `wrapStyleWithSelection` uses to dim non-matching data. The actual
135
+ * dim opacity resolves in this order: per-chart
136
+ * `selection.unselectedOpacity` → `theme.colors.selectionOpacity` →
137
+ * `DEFAULT_SELECTION_OPACITY` fallback.
125
138
  * - "isolate": click toggles category visibility; click all to reset
126
139
  */
127
140
  export declare function useLegendInteraction(mode: LegendInteractionMode | undefined, colorBy: string | ((d: any) => string) | undefined, allCategories: string[]): LegendInteractionState;
@@ -48,19 +48,21 @@ export interface SelectionStyleConfig {
48
48
  unselectedStyle?: Record<string, any>;
49
49
  selectedStyle?: Record<string, any>;
50
50
  }
51
- /** Default opacity for unselected (dimmed) elements */
52
- export declare const DEFAULT_SELECTION_OPACITY = 0.2;
53
51
  /**
54
- * Read the --semiotic-selection-opacity CSS variable from a container element.
55
- * Returns the numeric value or the default if not set or not parseable.
52
+ * Library fallback opacity for unselected (dimmed) elements.
53
+ *
54
+ * This is the last-resort value when nothing else supplies one. Clients
55
+ * control the effective default declaratively through `ThemeProvider`'s
56
+ * `colors.selectionOpacity` (built-in presets set this). Per-chart
57
+ * `selection.unselectedOpacity` still overrides the theme value.
56
58
  */
57
- export declare function readSelectionOpacityFromCSS(container: Element | null): number;
59
+ export declare const DEFAULT_SELECTION_OPACITY = 0.5;
58
60
  /**
59
61
  * Wrap a base style function with selection awareness.
60
62
  * When a selection is active, non-matching datums get dimmed.
61
63
  *
62
64
  * Dimming opacity is resolved in this order:
63
- * 1. `config.unselectedOpacity` (explicit prop)
64
- * 2. `DEFAULT_SELECTION_OPACITY` (0.2)
65
+ * 1. `config.unselectedOpacity` (explicit, usually per-chart or theme-merged)
66
+ * 2. `DEFAULT_SELECTION_OPACITY`
65
67
  */
66
68
  export declare function wrapStyleWithSelection(baseStyleFn: (d: Record<string, any>) => Record<string, any>, selectionHook: SelectionHookResult | null, config?: SelectionStyleConfig): (d: Record<string, any>) => Record<string, any>;
@@ -4,6 +4,14 @@ export interface TooltipFieldConfig {
4
4
  label: string;
5
5
  accessor: string | ((d: any) => any);
6
6
  role?: "title" | "x" | "y" | "color" | "size" | "group" | "value";
7
+ /** Per-field formatter. HOCs pass `xFormat`/`yFormat`/`valueFormat` here so
8
+ * the default tooltip renders values consistently with the axis. Typed
9
+ * permissively (`any` → `ReactNode`) to match the mixed formatter
10
+ * signatures across ordinal (`(d: string | number) => string`) and XY
11
+ * (`(d, index?, allTicks?) => ReactNode`). A ReactNode return renders
12
+ * as-is in the tooltip span. If the formatter throws, the tooltip
13
+ * falls back to the built-in `formatVal`. */
14
+ format?: (v: any, ...rest: any[]) => React.ReactNode;
7
15
  }
8
16
  /**
9
17
  * Extract a display name from an accessor.
@@ -25,10 +33,14 @@ export declare function buildDefaultTooltip(fields: TooltipFieldConfig[]): (hove
25
33
  * @param pieData - If true, extracts datum via `d.data?.[0] || d.data || d`
26
34
  * (PieChart/DonutChart wrap data in arrays). Default: `d.data || d`.
27
35
  */
28
- export declare function buildOrdinalTooltip({ categoryAccessor, valueAccessor, groupAccessor, groupLabel, pieData, }: {
36
+ export declare function buildOrdinalTooltip({ categoryAccessor, valueAccessor, groupAccessor, groupLabel, pieData, valueFormat, }: {
29
37
  categoryAccessor: string | ((d: any) => any);
30
38
  valueAccessor: string | ((d: any) => any);
31
39
  groupAccessor?: string | ((d: any) => any);
32
40
  groupLabel?: string;
33
41
  pieData?: boolean;
42
+ /** Same formatter the HOC passes to the value axis. Threaded here so the
43
+ * default tooltip shows values consistently with the axis ("$450k", not
44
+ * "450000"). Override by passing a custom `tooltip` prop. */
45
+ valueFormat?: (v: any, ...rest: any[]) => React.ReactNode;
34
46
  }): (d: Record<string, any>) => React.ReactNode;