semiotic 3.0.1 → 3.1.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 (122) hide show
  1. package/CLAUDE.md +227 -27
  2. package/README.md +43 -11
  3. package/ai/examples.md +358 -18
  4. package/ai/schema.json +64 -2
  5. package/ai/system-prompt.md +50 -12
  6. package/dist/components/Legend.d.ts +7 -1
  7. package/dist/components/charts/geo/ChoroplethMap.d.ts +53 -0
  8. package/dist/components/charts/geo/DistanceCartogram.d.ts +90 -0
  9. package/dist/components/charts/geo/FlowMap.d.ts +83 -0
  10. package/dist/components/charts/geo/ProportionalSymbolMap.d.ts +67 -0
  11. package/dist/components/charts/geo/index.d.ts +8 -0
  12. package/dist/components/charts/index.d.ts +2 -0
  13. package/dist/components/charts/network/ChordDiagram.d.ts +6 -5
  14. package/dist/components/charts/network/CirclePack.d.ts +2 -2
  15. package/dist/components/charts/network/ForceDirectedGraph.d.ts +9 -7
  16. package/dist/components/charts/network/OrbitDiagram.d.ts +21 -20
  17. package/dist/components/charts/network/SankeyDiagram.d.ts +6 -5
  18. package/dist/components/charts/network/TreeDiagram.d.ts +2 -2
  19. package/dist/components/charts/network/Treemap.d.ts +2 -2
  20. package/dist/components/charts/ordinal/BarChart.d.ts +7 -5
  21. package/dist/components/charts/ordinal/BoxPlot.d.ts +8 -6
  22. package/dist/components/charts/ordinal/DonutChart.d.ts +8 -6
  23. package/dist/components/charts/ordinal/DotPlot.d.ts +8 -6
  24. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +7 -5
  25. package/dist/components/charts/ordinal/Histogram.d.ts +8 -5
  26. package/dist/components/charts/ordinal/PieChart.d.ts +8 -6
  27. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +2 -0
  28. package/dist/components/charts/ordinal/StackedBarChart.d.ts +7 -5
  29. package/dist/components/charts/ordinal/SwarmPlot.d.ts +8 -6
  30. package/dist/components/charts/ordinal/ViolinPlot.d.ts +8 -5
  31. package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +24 -6
  32. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +28 -7
  33. package/dist/components/charts/realtime/RealtimeLineChart.d.ts +23 -5
  34. package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +24 -6
  35. package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +23 -5
  36. package/dist/components/charts/shared/colorUtils.d.ts +5 -0
  37. package/dist/components/charts/shared/hooks.d.ts +13 -1
  38. package/dist/components/charts/shared/legendUtils.d.ts +2 -3
  39. package/dist/components/charts/shared/statisticalOverlays.d.ts +1 -2
  40. package/dist/components/charts/shared/statisticalOverlaysLazy.d.ts +10 -0
  41. package/dist/components/charts/shared/tooltipUtils.d.ts +1 -1
  42. package/dist/components/charts/shared/types.d.ts +10 -4
  43. package/dist/components/charts/shared/useChartSetup.d.ts +112 -0
  44. package/dist/components/charts/shared/useStreamingLegend.d.ts +65 -0
  45. package/dist/components/charts/xy/AreaChart.d.ts +11 -6
  46. package/dist/components/charts/xy/BubbleChart.d.ts +11 -6
  47. package/dist/components/charts/xy/ConnectedScatterplot.d.ts +7 -6
  48. package/dist/components/charts/xy/Heatmap.d.ts +16 -5
  49. package/dist/components/charts/xy/LineChart.d.ts +21 -5
  50. package/dist/components/charts/xy/MinimapChart.d.ts +3 -0
  51. package/dist/components/charts/xy/QuadrantChart.d.ts +120 -0
  52. package/dist/components/charts/xy/Scatterplot.d.ts +9 -6
  53. package/dist/components/charts/xy/StackedAreaChart.d.ts +11 -6
  54. package/dist/components/geo/mergeData.d.ts +18 -0
  55. package/dist/components/geo/referenceGeography.d.ts +10 -0
  56. package/dist/components/geo/useReferenceAreas.d.ts +13 -0
  57. package/dist/components/realtime/RingBuffer.d.ts +1 -0
  58. package/dist/components/realtime/types.d.ts +17 -0
  59. package/dist/components/semiotic-data.d.ts +1 -0
  60. package/dist/components/semiotic-geo.d.ts +16 -0
  61. package/dist/components/semiotic-server.d.ts +1 -1
  62. package/dist/components/semiotic-xy.d.ts +1 -0
  63. package/dist/components/semiotic.d.ts +4 -4
  64. package/dist/components/server/renderToStaticSVG.d.ts +4 -2
  65. package/dist/components/stream/AccessibleDataTable.d.ts +50 -0
  66. package/dist/components/stream/CanvasHitTester.d.ts +8 -2
  67. package/dist/components/stream/DataSourceAdapter.d.ts +33 -4
  68. package/dist/components/stream/GeoCanvasHitTester.d.ts +19 -0
  69. package/dist/components/stream/GeoParticlePool.d.ts +46 -0
  70. package/dist/components/stream/GeoPipelineStore.d.ts +81 -0
  71. package/dist/components/stream/GeoTileRenderer.d.ts +31 -0
  72. package/dist/components/stream/NetworkPipelineStore.d.ts +16 -4
  73. package/dist/components/stream/NetworkSVGOverlay.d.ts +4 -1
  74. package/dist/components/stream/OrdinalPipelineStore.d.ts +8 -4
  75. package/dist/components/stream/OrdinalSVGOverlay.d.ts +23 -1
  76. package/dist/components/stream/PipelineStore.d.ts +57 -5
  77. package/dist/components/stream/SVGOverlay.d.ts +28 -1
  78. package/dist/components/stream/SceneGraph.d.ts +7 -3
  79. package/dist/components/stream/SceneToSVG.d.ts +2 -0
  80. package/dist/components/stream/StreamGeoFrame.d.ts +4 -0
  81. package/dist/components/stream/accessorUtils.d.ts +1 -0
  82. package/dist/components/stream/canvasSetup.d.ts +26 -0
  83. package/dist/components/stream/geoTypes.d.ts +186 -0
  84. package/dist/components/stream/layouts/forceLayoutPlugin.d.ts +0 -7
  85. package/dist/components/stream/layouts/index.d.ts +2 -1
  86. package/dist/components/stream/layouts/orbitLayoutPlugin.d.ts +2 -0
  87. package/dist/components/stream/legendRenderer.d.ts +33 -0
  88. package/dist/components/stream/networkTypes.d.ts +49 -1
  89. package/dist/components/stream/ordinalTypes.d.ts +10 -0
  90. package/dist/components/stream/pipelineTransitionUtils.d.ts +42 -0
  91. package/dist/components/stream/renderers/geoCanvasRenderer.d.ts +9 -0
  92. package/dist/components/stream/renderers/heatmapCanvasRenderer.d.ts +2 -1
  93. package/dist/components/stream/renderers/lineCanvasRenderer.d.ts +1 -0
  94. package/dist/components/stream/renderers/renderPulse.d.ts +50 -0
  95. package/dist/components/stream/types.d.ts +77 -3
  96. package/dist/components/types/legendTypes.d.ts +27 -3
  97. package/dist/geo.min.js +1 -0
  98. package/dist/geo.module.min.js +1 -0
  99. package/dist/network.min.js +1 -1
  100. package/dist/network.module.min.js +1 -1
  101. package/dist/ordinal.min.js +1 -1
  102. package/dist/ordinal.module.min.js +1 -1
  103. package/dist/realtime.min.js +1 -1
  104. package/dist/realtime.module.min.js +1 -1
  105. package/dist/semiotic-ai.min.js +1 -1
  106. package/dist/semiotic-ai.module.min.js +1 -1
  107. package/dist/semiotic-data.d.ts +1 -0
  108. package/dist/semiotic-data.min.js +1 -1
  109. package/dist/semiotic-data.module.min.js +1 -1
  110. package/dist/semiotic-geo.d.ts +16 -0
  111. package/dist/semiotic-server.d.ts +1 -1
  112. package/dist/semiotic-xy.d.ts +1 -0
  113. package/dist/semiotic.d.ts +4 -4
  114. package/dist/semiotic.min.js +1 -1
  115. package/dist/semiotic.module.min.js +1 -1
  116. package/dist/server.min.js +1 -1
  117. package/dist/server.module.min.js +1 -1
  118. package/dist/test-utils/canvasMock.d.ts +3 -0
  119. package/dist/xy.min.js +1 -1
  120. package/dist/xy.module.min.js +1 -1
  121. package/package.json +29 -7
  122. package/dist/test/canvasMock.d.ts +0 -2
package/ai/examples.md CHANGED
@@ -111,6 +111,125 @@ const data = [
111
111
 
112
112
  Key props: `valueAccessor`, `colorScheme` ("blues"|"reds"|"greens"|"viridis"), `showValues`
113
113
 
114
+ ### Heatmap with Gradient Legend
115
+
116
+ ```jsx
117
+ import { Heatmap } from "semiotic/ai"
118
+
119
+ <Heatmap
120
+ data={data}
121
+ xAccessor="hour"
122
+ yAccessor="day"
123
+ valueAccessor="count"
124
+ colorScheme="viridis"
125
+ showLegend
126
+ legendPosition="right"
127
+ />
128
+ ```
129
+
130
+ Key props: `showLegend` enables gradient legend, `legendPosition` ("right"|"left"|"top"|"bottom")
131
+
132
+ ### AreaChart
133
+
134
+ ```jsx
135
+ import { AreaChart } from "semiotic/ai"
136
+
137
+ const data = [
138
+ { month: 1, revenue: 4200 },
139
+ { month: 2, revenue: 5800 },
140
+ { month: 3, revenue: 5200 },
141
+ { month: 4, revenue: 7100 },
142
+ { month: 5, revenue: 6800 },
143
+ { month: 6, revenue: 7500 }
144
+ ]
145
+
146
+ <AreaChart
147
+ data={data}
148
+ xAccessor="month"
149
+ yAccessor="revenue"
150
+ gradientFill
151
+ xLabel="Month"
152
+ yLabel="Revenue ($)"
153
+ />
154
+ ```
155
+
156
+ Key props: `areaBy` (group into multiple areas), `y0Accessor` (band/ribbon), `gradientFill`, `areaOpacity` (0.7)
157
+
158
+ ### AreaChart — Percentile Band with Main Line
159
+
160
+ **IMPORTANT**: `showLine` on AreaChart only draws the TOP edge of the area (the `yAccessor` line). To render a separate main line (e.g., p50 median), you must layer two charts.
161
+
162
+ ```jsx
163
+ import { AreaChart, LineChart } from "semiotic/ai"
164
+
165
+ const data = [
166
+ { x: 0, p5: 10, p50: 25, p95: 45 },
167
+ { x: 1, p5: 12, p50: 28, p95: 50 },
168
+ { x: 2, p5: 11, p50: 30, p95: 52 },
169
+ { x: 3, p5: 14, p50: 32, p95: 55 },
170
+ { x: 4, p5: 13, p50: 35, p95: 58 },
171
+ { x: 5, p5: 15, p50: 37, p95: 62 },
172
+ ]
173
+
174
+ // Band (p5–p95) + main line (p50): two separate charts layered
175
+ <div style={{ position: "relative" }}>
176
+ <AreaChart
177
+ data={data}
178
+ xAccessor="x"
179
+ yAccessor="p95"
180
+ y0Accessor="p5"
181
+ showLine={false}
182
+ areaOpacity={0.3}
183
+ gradientFill
184
+ width={600}
185
+ height={400}
186
+ />
187
+ <div style={{ position: "absolute", top: 0, left: 0 }}>
188
+ <LineChart
189
+ data={data}
190
+ xAccessor="x"
191
+ yAccessor="p50"
192
+ lineWidth={2}
193
+ width={600}
194
+ height={400}
195
+ />
196
+ </div>
197
+ </div>
198
+ ```
199
+
200
+ Key props: `y0Accessor` defines band bottom, `yAccessor` defines band top, `showLine={false}` hides the top edge stroke. Layer a `LineChart` on top for the main metric.
201
+
202
+ ### StackedAreaChart
203
+
204
+ ```jsx
205
+ import { StackedAreaChart } from "semiotic/ai"
206
+
207
+ // IMPORTANT: Use a flat array with an areaBy field for grouping.
208
+ // Do NOT use lineBy or lineDataAccessor — those are LineChart props.
209
+ const data = [
210
+ { month: 1, value: 20, category: "Product" },
211
+ { month: 2, value: 25, category: "Product" },
212
+ { month: 3, value: 30, category: "Product" },
213
+ { month: 1, value: 15, category: "Service" },
214
+ { month: 2, value: 12, category: "Service" },
215
+ { month: 3, value: 18, category: "Service" },
216
+ { month: 1, value: 8, category: "Consulting" },
217
+ { month: 2, value: 10, category: "Consulting" },
218
+ { month: 3, value: 7, category: "Consulting" }
219
+ ]
220
+
221
+ <StackedAreaChart
222
+ data={data}
223
+ xAccessor="month"
224
+ yAccessor="value"
225
+ areaBy="category"
226
+ colorBy="category"
227
+ showLegend
228
+ />
229
+ ```
230
+
231
+ Key props: **`areaBy`** (required — groups flat data into stacked areas), `colorBy`, `normalize` (100% stacked). Data must be a flat array, not pre-grouped objects.
232
+
114
233
  ### ConnectedScatterplot
115
234
 
116
235
  ```jsx
@@ -452,11 +571,11 @@ Key props: `orbitMode` ("flat"|"solar"|"atomic"|number[]), `speed`, `animated`,
452
571
  import { ForceDirectedGraph } from "semiotic/ai"
453
572
 
454
573
  const nodes = [
455
- { id: "Alice", team: "Engineering" },
456
- { id: "Bob", team: "Engineering" },
457
- { id: "Carol", team: "Design" },
458
- { id: "Dave", team: "Design" },
459
- { id: "Eve", team: "Product" }
574
+ { id: "Alice", team: "Engineering", influence: 10 },
575
+ { id: "Bob", team: "Engineering", influence: 6 },
576
+ { id: "Carol", team: "Design", influence: 8 },
577
+ { id: "Dave", team: "Design", influence: 4 },
578
+ { id: "Eve", team: "Product", influence: 12 }
460
579
  ]
461
580
 
462
581
  const edges = [
@@ -472,12 +591,51 @@ const edges = [
472
591
  nodes={nodes}
473
592
  edges={edges}
474
593
  colorBy="team"
475
- nodeSize={10}
594
+ nodeSize="influence"
595
+ nodeSizeRange={[5, 25]}
476
596
  showLabels
597
+ showLegend
598
+ edgeOpacity={0.4}
599
+ tooltip={(d) => <div><strong>{d.data.id}</strong><br/>Team: {d.data.team}</div>}
477
600
  />
478
601
  ```
479
602
 
480
- Key props: **`nodes`** and **`edges`** (both required), `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`
603
+ Key props: **`nodes`** and **`edges`** (both required), `nodeIDAccessor` (default "id"), `sourceAccessor` (default "source"), `targetAccessor` (default "target"), `colorBy`, `nodeSize` (number, string field name, or function), `nodeSizeRange`, `showLabels`, `showLegend`, `tooltip`
604
+
605
+ **Note**: Always use `ForceDirectedGraph` (not `StreamNetworkFrame`) unless you need sophisticated control it doesn't expose. `StreamNetworkFrame` is a low-level escape hatch whose callbacks receive internal `RealtimeNode` wrappers — access your data via `.data` (e.g., `nodeStyle={(d) => ({ fill: d.data?.color })}`). HOC components like `ForceDirectedGraph` handle this automatically.
606
+
607
+ ### ForceDirectedGraph with custom click/hover
608
+
609
+ ```jsx
610
+ import { useState } from "react"
611
+ import { ForceDirectedGraph } from "semiotic/ai"
612
+
613
+ const [selected, setSelected] = useState(null)
614
+
615
+ <ForceDirectedGraph
616
+ nodes={nodes}
617
+ edges={edges}
618
+ colorBy="team"
619
+ nodeSize="influence"
620
+ nodeSizeRange={[5, 25]}
621
+ showLabels
622
+ width={800}
623
+ height={600}
624
+ frameProps={{
625
+ customClickBehavior: (d) => {
626
+ // d is { type: "node"|"edge", data: <your raw node/edge>, x, y } or null
627
+ if (d?.type === "node") setSelected(d.data)
628
+ },
629
+ customHoverBehavior: (d) => {
630
+ // same shape as click — d.data is your original object
631
+ if (d?.type === "node") console.log("Hovering:", d.data.id)
632
+ },
633
+ background: "#1a1a2e",
634
+ }}
635
+ />
636
+ ```
637
+
638
+ Key props: `frameProps` passes through to the underlying `StreamNetworkFrame` for advanced control (custom click/hover, background, annotations). Callback `d.data` is always your original node/edge object.
481
639
 
482
640
  ### SankeyDiagram
483
641
 
@@ -531,7 +689,98 @@ Key props: **`edges`** (required), shows bidirectional relationships in a circle
531
689
 
532
690
  ## Realtime — Push API via Ref
533
691
 
534
- ### Streaming Sankey (StreamNetworkFrame)
692
+ ### RealtimeLineChart
693
+
694
+ ```jsx
695
+ import { useRef, useEffect } from "react"
696
+ import { RealtimeLineChart } from "semiotic/ai"
697
+
698
+ const chartRef = useRef()
699
+
700
+ // Push data — MUST include a time field
701
+ useEffect(() => {
702
+ const interval = setInterval(() => {
703
+ chartRef.current?.push({ time: Date.now(), value: Math.random() * 100 })
704
+ }, 500)
705
+ return () => clearInterval(interval)
706
+ }, [])
707
+
708
+ <RealtimeLineChart
709
+ ref={chartRef}
710
+ size={[600, 300]}
711
+ timeAccessor="time"
712
+ valueAccessor="value"
713
+ windowSize={120}
714
+ stroke="#76b7b2"
715
+ />
716
+ ```
717
+
718
+ Key props: `timeAccessor`, `valueAccessor`, `windowSize`, `stroke`, `strokeWidth`
719
+
720
+ ### RealtimeHistogram
721
+
722
+ ```jsx
723
+ import { useRef, useEffect } from "react"
724
+ import { RealtimeHistogram } from "semiotic/ai"
725
+
726
+ const chartRef = useRef()
727
+
728
+ // IMPORTANT: Include time field even though this shows a distribution — it's used for windowing
729
+ useEffect(() => {
730
+ const interval = setInterval(() => {
731
+ chartRef.current?.push({
732
+ time: Date.now(),
733
+ value: Math.random() * 1000
734
+ })
735
+ }, 200)
736
+ return () => clearInterval(interval)
737
+ }, [])
738
+
739
+ <RealtimeHistogram
740
+ ref={chartRef}
741
+ size={[400, 250]}
742
+ timeAccessor="time"
743
+ valueAccessor="value"
744
+ binSize={100}
745
+ />
746
+ ```
747
+
748
+ Key props: **`binSize`** (required), `timeAccessor` ("time"), `valueAccessor` ("value"). Without a time field in your data, the chart renders blank.
749
+
750
+ ### RealtimeHeatmap
751
+
752
+ ```jsx
753
+ import { useRef, useEffect } from "react"
754
+ import { RealtimeHeatmap } from "semiotic/ai"
755
+
756
+ const chartRef = useRef()
757
+
758
+ // IMPORTANT: Data must have fields matching timeAccessor and valueAccessor (defaults: "time" and "value")
759
+ useEffect(() => {
760
+ const interval = setInterval(() => {
761
+ chartRef.current?.push({
762
+ time: Date.now(),
763
+ value: Math.random() * 500
764
+ })
765
+ }, 100)
766
+ return () => clearInterval(interval)
767
+ }, [])
768
+
769
+ <RealtimeHeatmap
770
+ ref={chartRef}
771
+ size={[400, 250]}
772
+ timeAccessor="time"
773
+ valueAccessor="value"
774
+ heatmapXBins={30}
775
+ heatmapYBins={10}
776
+ />
777
+ ```
778
+
779
+ Key props: `timeAccessor` ("time"), `valueAccessor` ("value"), `heatmapXBins`, `heatmapYBins`. Both accessors must match your data fields or the chart renders blank.
780
+
781
+ ### Streaming Sankey with Particles (StreamNetworkFrame)
782
+
783
+ Use `StreamNetworkFrame` only when you need low-level control that HOC charts don't expose. For most cases, use `ForceDirectedGraph`, `SankeyDiagram`, or `ChordDiagram` instead.
535
784
 
536
785
  ```jsx
537
786
  import { useRef, useEffect } from "react"
@@ -539,18 +788,18 @@ import { StreamNetworkFrame } from "semiotic"
539
788
 
540
789
  const chartRef = useRef()
541
790
 
542
- // Push edges at any frequency
543
791
  useEffect(() => {
544
- const edges = [
545
- { source: "Salary", target: "Budget", value: 5000 },
546
- { source: "Freelance", target: "Budget", value: 1500 },
792
+ // Push individual edges NOT a full snapshot.
793
+ // Each push adds one edge event to the streaming sankey.
794
+ chartRef.current.push({ source: "Salary", target: "Budget", value: 5000 })
795
+ chartRef.current.push({ source: "Freelance", target: "Budget", value: 1500 })
796
+
797
+ // Or batch multiple edges at once:
798
+ chartRef.current.pushMany([
547
799
  { source: "Budget", target: "Rent", value: 2000 },
548
800
  { source: "Budget", target: "Food", value: 800 },
549
- { source: "Budget", target: "Savings", value: 1500 }
550
- ]
551
- for (const edge of edges) {
552
- chartRef.current.push(edge)
553
- }
801
+ { source: "Budget", target: "Savings", value: 1500 },
802
+ ])
554
803
  }, [])
555
804
 
556
805
  <StreamNetworkFrame
@@ -558,7 +807,98 @@ useEffect(() => {
558
807
  chartType="sankey"
559
808
  size={[800, 400]}
560
809
  edgeOpacity={0.4}
810
+ showParticles={true}
811
+ particleStyle={{
812
+ radius: 2,
813
+ opacity: 0.8,
814
+ speedMultiplier: 1.5,
815
+ maxPerEdge: 4,
816
+ colorBy: "source"
817
+ }}
561
818
  />
562
819
  ```
563
820
 
564
- Key props: push via ref (`push`, `pushMany`, `clear`), `chartType="sankey"`, uses `StreamNetworkFrame` from `semiotic`
821
+ Key props: `chartType="sankey"`, `showParticles` (boolean — enables animated particles flowing along edges), `particleStyle` (`{ radius, opacity, speedMultiplier, maxPerEdge, colorBy }`), push via ref (`push` for single edge, `pushMany` for batch, `clear` to reset)
822
+
823
+ ### Streaming any chart type via Stream Frames
824
+
825
+ The Realtime* HOCs are convenience wrappers. For streaming versions of scatter, stacked area, bar, or any other chart, use the corresponding Stream Frame with `runtimeMode="streaming"`:
826
+
827
+ ```jsx
828
+ import { useRef } from "react"
829
+ import { StreamXYFrame } from "semiotic/xy"
830
+
831
+ const chartRef = useRef()
832
+
833
+ // Push data via ref — same push API as Realtime* HOCs
834
+ chartRef.current?.push({ time: Date.now(), size: 42, category: "A" })
835
+
836
+ <StreamXYFrame
837
+ ref={chartRef}
838
+ chartType="scatter"
839
+ runtimeMode="streaming"
840
+ xAccessor="time"
841
+ yAccessor="size"
842
+ colorAccessor="category"
843
+ size={[600, 300]}
844
+ margin={{ top: 10, bottom: 30, left: 40, right: 10 }}
845
+ />
846
+ ```
847
+
848
+ Available Stream Frames: `StreamXYFrame` (line, area, stackedArea, scatter), `StreamOrdinalFrame` (bar, grouped bar), `StreamNetworkFrame` (sankey, force, chord)
849
+
850
+ ---
851
+
852
+ ## Server-Side Rendering
853
+
854
+ ### renderToStaticSVG (Node.js)
855
+
856
+ ```ts
857
+ // IMPORTANT: frameType is "xy" | "ordinal" | "network" — NOT a component name like "BarChart"
858
+ import { renderOrdinalToStaticSVG } from "semiotic/server"
859
+
860
+ const data = [
861
+ { category: "Q1", value: 120 },
862
+ { category: "Q2", value: 148 },
863
+ { category: "Q3", value: 135 },
864
+ { category: "Q4", value: 162 },
865
+ ]
866
+
867
+ const svg: string = renderOrdinalToStaticSVG({
868
+ data,
869
+ categoryAccessor: "category",
870
+ valueAccessor: "value",
871
+ width: 600,
872
+ height: 400,
873
+ })
874
+
875
+ // Or use the generic function:
876
+ import { renderToStaticSVG } from "semiotic/server"
877
+ const svg2 = renderToStaticSVG("ordinal", { data, categoryAccessor: "category", valueAccessor: "value" })
878
+ ```
879
+
880
+ Key: `renderToStaticSVG(frameType, props)` where frameType is `"xy"` | `"ordinal"` | `"network"`. Type-specific shortcuts: `renderXYToStaticSVG`, `renderOrdinalToStaticSVG`, `renderNetworkToStaticSVG`.
881
+
882
+ ---
883
+
884
+ ## Export
885
+
886
+ ### exportChart (browser)
887
+
888
+ ```jsx
889
+ import { useRef } from "react"
890
+ import { Scatterplot } from "semiotic/ai"
891
+ import { exportChart } from "semiotic"
892
+
893
+ const containerRef = useRef<HTMLDivElement>(null)
894
+
895
+ // Pass the WRAPPER DIV, not the SVG element — exportChart finds canvas+SVG internally
896
+ <div ref={containerRef}>
897
+ <Scatterplot data={data} xAccessor="x" yAccessor="y" width={600} height={400} />
898
+ </div>
899
+ <button onClick={() => exportChart(containerRef.current!, { format: "png" })}>
900
+ Download PNG
901
+ </button>
902
+ ```
903
+
904
+ Key: `exportChart(wrapperDiv, { format, filename, scale, background })`. It queries the wrapper for canvas and SVG elements internally. Default format: PNG with 2x scale.
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.0.0",
4
+ "version": "3.1.0",
5
5
  "description": "React data visualization library for charts, networks, and beyond",
6
6
  "tools": [
7
7
  {
@@ -294,6 +294,57 @@
294
294
  }
295
295
  }
296
296
  },
297
+ {
298
+ "type": "function",
299
+ "function": {
300
+ "name": "QuadrantChart",
301
+ "description": "Scatterplot divided into four labeled, colored quadrants by center lines. Use for BCG matrices, priority matrices, and any 2x2 strategic framework.",
302
+ "parameters": {
303
+ "type": "object",
304
+ "properties": {
305
+ "data": {
306
+ "type": "array",
307
+ "items": { "type": "object" },
308
+ "description": "Array of data objects"
309
+ },
310
+ "xAccessor": { "type": "string", "default": "x" },
311
+ "yAccessor": { "type": "string", "default": "y" },
312
+ "xCenter": { "type": "number", "description": "X-coordinate of the vertical center line. Defaults to midpoint of x domain." },
313
+ "yCenter": { "type": "number", "description": "Y-coordinate of the horizontal center line. Defaults to midpoint of y domain." },
314
+ "quadrants": {
315
+ "type": "object",
316
+ "description": "Configuration for the four quadrants: { topRight, topLeft, bottomRight, bottomLeft }, each with { label, color, opacity? }",
317
+ "properties": {
318
+ "topRight": { "type": "object", "properties": { "label": { "type": "string" }, "color": { "type": "string" }, "opacity": { "type": "number" } }, "required": ["label", "color"] },
319
+ "topLeft": { "type": "object", "properties": { "label": { "type": "string" }, "color": { "type": "string" }, "opacity": { "type": "number" } }, "required": ["label", "color"] },
320
+ "bottomRight": { "type": "object", "properties": { "label": { "type": "string" }, "color": { "type": "string" }, "opacity": { "type": "number" } }, "required": ["label", "color"] },
321
+ "bottomLeft": { "type": "object", "properties": { "label": { "type": "string" }, "color": { "type": "string" }, "opacity": { "type": "number" } }, "required": ["label", "color"] }
322
+ },
323
+ "required": ["topRight", "topLeft", "bottomRight", "bottomLeft"]
324
+ },
325
+ "showQuadrantLabels": { "type": "boolean", "default": true },
326
+ "quadrantLabelSize": { "type": "number", "default": 12 },
327
+ "colorBy": { "type": "string", "description": "Key to determine point color" },
328
+ "colorScheme": { "type": ["string", "array"], "default": "category10" },
329
+ "sizeBy": { "type": "string", "description": "Key for variable point sizing" },
330
+ "sizeRange": { "type": "array", "items": { "type": "number" }, "default": [3, 15] },
331
+ "pointRadius": { "type": "number", "default": 5 },
332
+ "pointOpacity": { "type": "number", "default": 0.8 },
333
+ "xLabel": { "type": "string" },
334
+ "yLabel": { "type": "string" },
335
+ "title": { "type": "string" },
336
+ "width": { "type": "number", "default": 600 },
337
+ "height": { "type": "number", "default": 400 },
338
+ "enableHover": { "type": "boolean", "default": true },
339
+ "showLegend": { "type": "boolean" },
340
+ "showGrid": { "type": "boolean", "default": false },
341
+ "margin": { "type": "object" },
342
+ "className": { "type": "string" }
343
+ },
344
+ "required": ["quadrants"]
345
+ }
346
+ }
347
+ },
297
348
  {
298
349
  "type": "function",
299
350
  "function": {
@@ -377,7 +428,18 @@
377
428
  "height": { "type": "number", "default": 400 },
378
429
  "enableHover": { "type": "boolean", "default": true },
379
430
  "margin": { "type": "object" },
380
- "className": { "type": "string" }
431
+ "className": { "type": "string" },
432
+ "showLegend": {
433
+ "type": "boolean",
434
+ "description": "Show a gradient color legend",
435
+ "default": false
436
+ },
437
+ "legendPosition": {
438
+ "type": "string",
439
+ "enum": ["right", "left", "top", "bottom"],
440
+ "description": "Position of the gradient legend",
441
+ "default": "right"
442
+ }
381
443
  },
382
444
  "required": ["data"]
383
445
  }
@@ -5,7 +5,7 @@ Use `import { ComponentName } from "semiotic/ai"` for all components below.
5
5
  ## Flat Array Data (`data: object[]`)
6
6
  - **LineChart** — `xAccessor`, `yAccessor`, `lineBy` (multi-line), `curve`
7
7
  - **AreaChart** — `xAccessor`, `yAccessor`, `areaBy`, `areaOpacity`
8
- - **StackedAreaChart** — `xAccessor`, `yAccessor`, `areaBy` (required), `normalize`
8
+ - **StackedAreaChart** — `xAccessor`, `yAccessor`, **`areaBy`** (required, groups flat data into stacked areas), `colorBy`, `normalize`. Data must be a flat array. Do NOT use `lineBy` or `lineDataAccessor`.
9
9
  - **Scatterplot** — `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`
10
10
  - **BubbleChart** — `xAccessor`, `yAccessor`, **`sizeBy`** (required), `sizeRange`
11
11
  - **ConnectedScatterplot** — `xAccessor`, `yAccessor`, `orderAccessor` (sequencing field), `pointRadius`
@@ -15,30 +15,68 @@ Use `import { ComponentName } from "semiotic/ai"` for all components below.
15
15
  - **GroupedBarChart** — `categoryAccessor`, `valueAccessor`, **`groupBy`** (required)
16
16
  - **SwarmPlot** — `categoryAccessor`, `valueAccessor`, `pointRadius`
17
17
  - **BoxPlot** — `categoryAccessor`, `valueAccessor`, `showOutliers`
18
- - **Histogram** — `categoryAccessor`, `valueAccessor`, `bins` (default 25), `relative`
18
+ - **Histogram** — `categoryAccessor` (optional, defaults to `"category"` — omit for single-group), `valueAccessor`, `bins` (default 25), `relative`
19
19
  - **ViolinPlot** — `categoryAccessor`, `valueAccessor`, `bins`, `curve`, `showIQR`
20
20
  - **DotPlot** — `categoryAccessor`, `valueAccessor`, `sort`, `dotRadius`
21
21
  - **PieChart** — `categoryAccessor`, `valueAccessor`
22
- - **DonutChart** — `categoryAccessor`, `valueAccessor`, `innerRadius`, `centerContent`
22
+ - **DonutChart** — `categoryAccessor`, `valueAccessor`, `innerRadius`, `centerContent` (ReactNode, e.g. `<div>50%</div>`)
23
23
 
24
24
  ## Hierarchical Data (`data: { children: [...] }`)
25
25
  - **TreeDiagram** — `childrenAccessor`, `nodeIdAccessor`, `layout` ("tree"|"cluster"|"partition"), `orientation`
26
26
  - **Treemap** — `childrenAccessor`, `valueAccessor`, `nodeIdAccessor`, `colorByDepth`
27
27
  - **CirclePack** — `childrenAccessor`, `valueAccessor`, `nodeIdAccessor`, `colorByDepth`
28
- - **OrbitDiagram** — `childrenAccessor`, `nodeIdAccessor`, `orbitMode` ("flat"|"solar"|"atomic"|number[]), `speed`, `animated`
28
+ - **OrbitDiagram** — animated radial/orbital hierarchy (use this, not TreeDiagram, for animated orbiting nodes). `childrenAccessor`, `nodeIdAccessor`, `orbitMode` ("flat"|"solar"|"atomic"|number[]), `speed`, `animated`. For static radial trees use `TreeDiagram layout="radial"`.
29
29
 
30
30
  ## Network Data (`nodes: object[]`, `edges: object[]`)
31
- - **ForceDirectedGraph** — **`nodes`**, **`edges`** (both required), `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`
31
+ - **ForceDirectedGraph** — **`nodes`**, **`edges`** (both required), `nodeIDAccessor`, `sourceAccessor`, `targetAccessor`, `colorBy`, `nodeSize` (number|string|fn), `nodeSizeRange`, `edgeWidth`, `edgeOpacity`, `iterations`, `forceStrength`, `showLabels`, `nodeLabel`, `tooltip`, `showLegend`
32
32
  - **SankeyDiagram** — **`edges`** (required), `sourceAccessor`, `targetAccessor`, `valueAccessor`
33
33
  - **ChordDiagram** — **`edges`** (required), `sourceAccessor`, `targetAccessor`, `valueAccessor`
34
34
 
35
+ **Important**: Always use these HOC components for network charts unless you need sophisticated control they don't expose. `StreamNetworkFrame` is a low-level escape hatch — its callbacks receive internal wrapper objects (`RealtimeNode`), not your raw data.
36
+
35
37
  ## Realtime (ref-based push API, canvas)
36
- - **RealtimeLineChart** — `ref.current.push(datum)`, `timeAccessor`, `valueAccessor`, `windowSize`
37
- - **RealtimeHistogram** **`binSize`** (required), `timeAccessor`, `valueAccessor`
38
- - **RealtimeSwarmChart** — `timeAccessor`, `valueAccessor`, `categoryAccessor`
39
- - **RealtimeWaterfallChart** `timeAccessor`, `valueAccessor`, `positiveColor`, `negativeColor`
40
- - **RealtimeHeatmap** — `timeAccessor`, `valueAccessor`, `heatmapXBins`, `heatmapYBins`, `aggregation`
41
- - **StreamNetworkFrame** (`chartType="sankey"`) — `ref.current.push({ source, target, value })`, `sourceAccessor`, `targetAccessor`, `valueAccessor` (import from `semiotic`)
38
+
39
+ **IMPORTANT**: All pushed data must include a time field (default: `"time"`). Set `timeAccessor` if your field is named differently. Without a valid time field, charts silently render blank.
40
+
41
+ Sizing: all Realtime HOCs accept both `size={[600, 400]}` (tuple) and `width={600} height={400}`.
42
+
43
+ - **RealtimeLineChart** — `ref.current.push(datum)`, **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `windowSize`
44
+ - **RealtimeHistogram** — **`binSize`** (required), **`timeAccessor`** ("time"), **`valueAccessor`** ("value"). Time field required even though this is a distribution — it's used for windowing.
45
+ - **RealtimeSwarmChart** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `categoryAccessor`
46
+ - **RealtimeWaterfallChart** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `positiveColor`, `negativeColor`
47
+ - **RealtimeHeatmap** — **`timeAccessor`** ("time"), **`valueAccessor`** ("value"), `heatmapXBins`, `heatmapYBins`, `aggregation`. Both must match your data fields.
48
+ - **StreamNetworkFrame** (`chartType="sankey"`) — push **individual edges**: `ref.current.push({ source, target, value })`. Use `ref.current.pushMany([...edges])` for batches. Do NOT push full edge snapshots. Props: `sourceAccessor`, `targetAccessor`, `valueAccessor`, `showParticles` (boolean), `particleStyle` (`{ radius, opacity, speedMultiplier, maxPerEdge, colorBy }`) (import from `semiotic`)
49
+
50
+ Pushed data shape: `{ time: Date.now(), value: 42 }` for line/waterfall/heatmap, add `category` for histogram/swarm.
51
+
52
+ ### Push API on HOC charts
53
+ Many HOC charts support the push API via `forwardRef`. **Omit the `data` prop** (do NOT pass `data={[]}`) and push imperatively:
54
+ ```jsx
55
+ const chartRef = useRef()
56
+ chartRef.current.push({ x: 1, y: 2 })
57
+ <Scatterplot ref={chartRef} xAccessor="x" yAccessor="y" />
58
+ ```
59
+ Methods: `push(datum)`, `pushMany(data)`, `clear()`, `getData()`. Streaming-specific props (`windowSize`, `decay`, `pulse`) go in `frameProps`. Supported: XY charts, ordinal charts, network charts (ForceDirectedGraph, SankeyDiagram, ChordDiagram), ProportionalSymbolMap, DistanceCartogram. Not supported: hierarchy charts (TreeDiagram, Treemap, CirclePack, OrbitDiagram), ChoroplethMap, FlowMap, ScatterplotMatrix.
60
+
61
+ For advanced streaming control, use Stream Frames (`StreamXYFrame`, `StreamOrdinalFrame`, `StreamNetworkFrame`) with `runtimeMode="streaming"` and ref-based push.
62
+
63
+ ## ChartContainer
64
+ - **ChartContainer** — wrapper with title, subtitle, status indicator, toolbar. `title`, `subtitle`, `height` (default **400** — always set this to match your chart height), `width` ("100%"), `status` ("live"|"stale"|"error"), `loading`, `error`, `actions`, `style`
65
+ - When using with `size={[w, h]}`, set `height={h}` on the container or you'll get extra whitespace.
42
66
 
43
67
  ## Common Props (all components)
44
- `width`, `height`, `margin`, `title`, `colorBy`, `colorScheme`, `enableHover`, `tooltip`, `showLegend`, `className`, `frameProps`
68
+ `width`, `height`, `margin`, `title`, `colorBy`, `colorScheme`, `enableHover`, `tooltip`, `showLegend`, `className`, `frameProps`, `onObservation`, `emphasis` ("primary"|"secondary")
69
+
70
+ ### tooltip
71
+ `true` (default) | `false` | `(datum) => ReactNode` (function receives your raw data) | config `{ fields?, title?, format?, style? }`
72
+
73
+ ### onObservation
74
+ Callback receiving `ChartObservation`: `{ type: "hover"|"click"|"brush"|"selection", datum: <your data>, x, y, timestamp, chartType, chartId }`. The `datum` field is your original data object. Hover-end/click-end events omit `datum`.
75
+
76
+ ### emphasis
77
+ `emphasis="primary"` makes a chart span two columns inside a `ChartGrid`.
78
+
79
+ ## Key Patterns
80
+ - **Percentile band + main line**: Layer `<AreaChart yAccessor="p95" y0Accessor="p5" showLine={false} />` + `<LineChart yAccessor="p50" />`. AreaChart's `showLine` only draws the top edge, NOT a separate main line.
81
+ - **SSR**: `renderToStaticSVG("ordinal", props)` or `renderOrdinalToStaticSVG(props)` from `semiotic/server`. Frame type is `"xy"` | `"ordinal"` | `"network"` (NOT component name).
82
+ - **exportChart**: Pass the wrapper div, not the SVG element: `exportChart(wrapperDiv, { format: "png" })`. It finds canvas+SVG internally.
@@ -1,3 +1,9 @@
1
1
  import * as React from "react";
2
- import { LegendProps } from "./types/legendTypes";
2
+ import { LegendProps, GradientLegendConfig } from "./types/legendTypes";
3
+ /** Gradient legend for continuous/sequential color scales */
4
+ export declare function GradientLegend({ config, orientation, width, }: {
5
+ config: GradientLegendConfig;
6
+ orientation?: "vertical" | "horizontal";
7
+ width?: number;
8
+ }): React.JSX.Element;
3
9
  export default function Legend(props: LegendProps): React.JSX.Element;
@@ -0,0 +1,53 @@
1
+ import * as React from "react";
2
+ import type { StreamGeoFrameProps, ProjectionProp } from "../../stream/geoTypes";
3
+ import type { BaseChartProps, ChartAccessor } from "../shared/types";
4
+ import { type TooltipProp } from "../../Tooltip/Tooltip";
5
+ import type { LegendInteractionMode } from "../shared/hooks";
6
+ import { type AreasProp } from "../../geo/useReferenceAreas";
7
+ export interface ChoroplethMapProps<TDatum extends Record<string, any> = Record<string, any>> extends BaseChartProps {
8
+ /** GeoJSON features or a reference string ("world-110m", "world-50m", "land-110m", "land-50m") */
9
+ areas: AreasProp;
10
+ /** Accessor for the numeric value to encode as color */
11
+ valueAccessor: ChartAccessor<TDatum, number>;
12
+ /** Sequential color scheme @default "blues" */
13
+ colorScheme?: string;
14
+ /** Geographic projection @default "equalEarth" */
15
+ projection?: ProjectionProp;
16
+ /** Show graticule grid lines */
17
+ graticule?: boolean | import("../../stream/geoTypes").GraticuleConfig;
18
+ /** Tooltip config */
19
+ tooltip?: TooltipProp;
20
+ /** Show legend @default true */
21
+ showLegend?: boolean;
22
+ /** Legend interaction mode */
23
+ legendInteraction?: LegendInteractionMode;
24
+ /** Padding fraction for auto-fit projection. 0.1 = 10% inset from edges. @default 0 */
25
+ fitPadding?: number;
26
+ /** Enable zoom/pan. Defaults to true when tileURL is set, false otherwise. */
27
+ zoomable?: boolean;
28
+ /** [minZoom, maxZoom] @default [1, 8] */
29
+ zoomExtent?: [number, number];
30
+ /** Zoom change callback */
31
+ onZoom?: StreamGeoFrameProps["onZoom"];
32
+ /**
33
+ * When true, drag gestures rotate the projection (globe spinning)
34
+ * instead of panning. Defaults to true for orthographic projection.
35
+ */
36
+ dragRotate?: boolean;
37
+ /** Raster tile URL template or function. Enables tile basemap (Mercator only). */
38
+ tileURL?: string | ((z: number, x: number, y: number, dpr: number) => string);
39
+ /** Attribution text for tile provider */
40
+ tileAttribution?: string;
41
+ /** Max cached tiles @default 256 */
42
+ tileCacheSize?: number;
43
+ /** Fill opacity for area polygons. Useful for layering over tile basemaps. @default 1 */
44
+ areaOpacity?: number;
45
+ /** Annotations */
46
+ annotations?: Record<string, any>[];
47
+ /** Passthrough to StreamGeoFrame */
48
+ frameProps?: Partial<Omit<StreamGeoFrameProps, "areas" | "projection">>;
49
+ }
50
+ export declare function ChoroplethMap<TDatum extends Record<string, any> = Record<string, any>>(props: ChoroplethMapProps<TDatum>): React.JSX.Element | null;
51
+ export declare namespace ChoroplethMap {
52
+ var displayName: string;
53
+ }