exploreda 0.0.6 → 0.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 (208) hide show
  1. package/README.md +144 -0
  2. package/dist/ExplorEda.cjs +4608 -11288
  3. package/dist/ExplorEda.cjs.map +1 -1
  4. package/dist/ExplorEda.css +2282 -1689
  5. package/dist/ExplorEda.css.map +1 -1
  6. package/dist/ExplorEda.d.cts +124 -278
  7. package/dist/ExplorEda.d.ts +124 -278
  8. package/dist/ExplorEda.js +4479 -11166
  9. package/dist/ExplorEda.js.map +1 -1
  10. package/dist/calculations.cjs +18 -0
  11. package/dist/calculations.cjs.map +1 -0
  12. package/dist/calculations.d.cts +63 -0
  13. package/dist/calculations.d.ts +63 -0
  14. package/dist/calculations.js +3 -0
  15. package/dist/calculations.js.map +1 -0
  16. package/dist/charts/bar.cjs +27 -0
  17. package/dist/charts/bar.cjs.map +1 -0
  18. package/dist/charts/bar.d.cts +2 -0
  19. package/dist/charts/bar.d.ts +2 -0
  20. package/dist/charts/bar.js +18 -0
  21. package/dist/charts/bar.js.map +1 -0
  22. package/dist/charts/box-plot.cjs +26 -0
  23. package/dist/charts/box-plot.cjs.map +1 -0
  24. package/dist/charts/box-plot.d.cts +2 -0
  25. package/dist/charts/box-plot.d.ts +2 -0
  26. package/dist/charts/box-plot.js +17 -0
  27. package/dist/charts/box-plot.js.map +1 -0
  28. package/dist/charts/color-legend.cjs +22 -0
  29. package/dist/charts/color-legend.cjs.map +1 -0
  30. package/dist/charts/color-legend.d.cts +2 -0
  31. package/dist/charts/color-legend.d.ts +2 -0
  32. package/dist/charts/color-legend.js +13 -0
  33. package/dist/charts/color-legend.js.map +1 -0
  34. package/dist/charts/data-table.cjs +22 -0
  35. package/dist/charts/data-table.cjs.map +1 -0
  36. package/dist/charts/data-table.d.cts +2 -0
  37. package/dist/charts/data-table.d.ts +2 -0
  38. package/dist/charts/data-table.js +13 -0
  39. package/dist/charts/data-table.js.map +1 -0
  40. package/dist/charts/line.cjs +30 -0
  41. package/dist/charts/line.cjs.map +1 -0
  42. package/dist/charts/line.d.cts +2 -0
  43. package/dist/charts/line.d.ts +2 -0
  44. package/dist/charts/line.js +17 -0
  45. package/dist/charts/line.js.map +1 -0
  46. package/dist/charts/markdown.cjs +16 -0
  47. package/dist/charts/markdown.cjs.map +1 -0
  48. package/dist/charts/markdown.css +77 -0
  49. package/dist/charts/markdown.css.map +1 -0
  50. package/dist/charts/markdown.d.cts +2 -0
  51. package/dist/charts/markdown.d.ts +2 -0
  52. package/dist/charts/markdown.js +7 -0
  53. package/dist/charts/markdown.js.map +1 -0
  54. package/dist/charts/pivot-table.cjs +23 -0
  55. package/dist/charts/pivot-table.cjs.map +1 -0
  56. package/dist/charts/pivot-table.d.cts +2 -0
  57. package/dist/charts/pivot-table.d.ts +2 -0
  58. package/dist/charts/pivot-table.js +14 -0
  59. package/dist/charts/pivot-table.js.map +1 -0
  60. package/dist/charts/row.cjs +27 -0
  61. package/dist/charts/row.cjs.map +1 -0
  62. package/dist/charts/row.d.cts +6 -0
  63. package/dist/charts/row.d.ts +6 -0
  64. package/dist/charts/row.js +18 -0
  65. package/dist/charts/row.js.map +1 -0
  66. package/dist/charts/scatter.cjs +24 -0
  67. package/dist/charts/scatter.cjs.map +1 -0
  68. package/dist/charts/scatter.d.cts +2 -0
  69. package/dist/charts/scatter.d.ts +2 -0
  70. package/dist/charts/scatter.js +15 -0
  71. package/dist/charts/scatter.js.map +1 -0
  72. package/dist/charts/summary-table.cjs +19 -0
  73. package/dist/charts/summary-table.cjs.map +1 -0
  74. package/dist/charts/summary-table.d.cts +2 -0
  75. package/dist/charts/summary-table.d.ts +2 -0
  76. package/dist/charts/summary-table.js +10 -0
  77. package/dist/charts/summary-table.js.map +1 -0
  78. package/dist/charts/three-d-scatter.cjs +24 -0
  79. package/dist/charts/three-d-scatter.cjs.map +1 -0
  80. package/dist/charts/three-d-scatter.d.cts +6 -0
  81. package/dist/charts/three-d-scatter.d.ts +6 -0
  82. package/dist/charts/three-d-scatter.js +15 -0
  83. package/dist/charts/three-d-scatter.js.map +1 -0
  84. package/dist/chunk-3PGYFLE3.cjs +499 -0
  85. package/dist/chunk-3PGYFLE3.cjs.map +1 -0
  86. package/dist/chunk-4HNDKMWF.js +167 -0
  87. package/dist/chunk-4HNDKMWF.js.map +1 -0
  88. package/dist/chunk-4QNENKTV.cjs +1214 -0
  89. package/dist/chunk-4QNENKTV.cjs.map +1 -0
  90. package/dist/chunk-54M32JJB.js +132 -0
  91. package/dist/chunk-54M32JJB.js.map +1 -0
  92. package/dist/chunk-5B23JVH3.cjs +2238 -0
  93. package/dist/chunk-5B23JVH3.cjs.map +1 -0
  94. package/dist/chunk-5LH6NSNL.js +456 -0
  95. package/dist/chunk-5LH6NSNL.js.map +1 -0
  96. package/dist/chunk-6DOKKCJ5.cjs +313 -0
  97. package/dist/chunk-6DOKKCJ5.cjs.map +1 -0
  98. package/dist/chunk-6HZX5CBT.cjs +1074 -0
  99. package/dist/chunk-6HZX5CBT.cjs.map +1 -0
  100. package/dist/chunk-6U5DOIQM.js +1071 -0
  101. package/dist/chunk-6U5DOIQM.js.map +1 -0
  102. package/dist/chunk-7GBZIK2R.cjs +80 -0
  103. package/dist/chunk-7GBZIK2R.cjs.map +1 -0
  104. package/dist/chunk-7PTJNIOD.js +520 -0
  105. package/dist/chunk-7PTJNIOD.js.map +1 -0
  106. package/dist/chunk-AHZYQ7FN.js +319 -0
  107. package/dist/chunk-AHZYQ7FN.js.map +1 -0
  108. package/dist/chunk-AOODAOL4.cjs +55 -0
  109. package/dist/chunk-AOODAOL4.cjs.map +1 -0
  110. package/dist/chunk-ASKFIRMC.cjs +165 -0
  111. package/dist/chunk-ASKFIRMC.cjs.map +1 -0
  112. package/dist/chunk-B7OQUBH4.cjs +171 -0
  113. package/dist/chunk-B7OQUBH4.cjs.map +1 -0
  114. package/dist/chunk-BIMEHDLB.js +72 -0
  115. package/dist/chunk-BIMEHDLB.js.map +1 -0
  116. package/dist/chunk-C62KHV7S.js +1225 -0
  117. package/dist/chunk-C62KHV7S.js.map +1 -0
  118. package/dist/chunk-C7GQT65O.cjs +1119 -0
  119. package/dist/chunk-C7GQT65O.cjs.map +1 -0
  120. package/dist/chunk-CGDWDV4Z.js +1211 -0
  121. package/dist/chunk-CGDWDV4Z.js.map +1 -0
  122. package/dist/chunk-CJQQFNEY.js +69 -0
  123. package/dist/chunk-CJQQFNEY.js.map +1 -0
  124. package/dist/chunk-DHYLSXNR.js +964 -0
  125. package/dist/chunk-DHYLSXNR.js.map +1 -0
  126. package/dist/chunk-DKFA4KOJ.cjs +87 -0
  127. package/dist/chunk-DKFA4KOJ.cjs.map +1 -0
  128. package/dist/chunk-DS5BS7PP.cjs +357 -0
  129. package/dist/chunk-DS5BS7PP.cjs.map +1 -0
  130. package/dist/chunk-EGMFQSGK.js +909 -0
  131. package/dist/chunk-EGMFQSGK.js.map +1 -0
  132. package/dist/chunk-EU5EFHO5.cjs +53 -0
  133. package/dist/chunk-EU5EFHO5.cjs.map +1 -0
  134. package/dist/chunk-FM46QSDM.js +837 -0
  135. package/dist/chunk-FM46QSDM.js.map +1 -0
  136. package/dist/chunk-GV2LRR32.cjs +843 -0
  137. package/dist/chunk-GV2LRR32.cjs.map +1 -0
  138. package/dist/chunk-H3XKFSA5.cjs +322 -0
  139. package/dist/chunk-H3XKFSA5.cjs.map +1 -0
  140. package/dist/chunk-H5AVMMCE.js +347 -0
  141. package/dist/chunk-H5AVMMCE.js.map +1 -0
  142. package/dist/chunk-JDMUN4VK.cjs +986 -0
  143. package/dist/chunk-JDMUN4VK.cjs.map +1 -0
  144. package/dist/chunk-JNO7OXCC.js +2171 -0
  145. package/dist/chunk-JNO7OXCC.js.map +1 -0
  146. package/dist/chunk-JQMG4ZR7.js +111 -0
  147. package/dist/chunk-JQMG4ZR7.js.map +1 -0
  148. package/dist/chunk-L2Q2R43U.cjs +118 -0
  149. package/dist/chunk-L2Q2R43U.cjs.map +1 -0
  150. package/dist/chunk-LBTZVERO.js +466 -0
  151. package/dist/chunk-LBTZVERO.js.map +1 -0
  152. package/dist/chunk-LUUGGTTK.cjs +1227 -0
  153. package/dist/chunk-LUUGGTTK.cjs.map +1 -0
  154. package/dist/chunk-MEEPH7ZC.cjs +911 -0
  155. package/dist/chunk-MEEPH7ZC.cjs.map +1 -0
  156. package/dist/chunk-MJPDJ5AS.js +184 -0
  157. package/dist/chunk-MJPDJ5AS.js.map +1 -0
  158. package/dist/chunk-MJROUIDZ.cjs +219 -0
  159. package/dist/chunk-MJROUIDZ.cjs.map +1 -0
  160. package/dist/chunk-MUWSBYJS.js +528 -0
  161. package/dist/chunk-MUWSBYJS.js.map +1 -0
  162. package/dist/chunk-NDF5LCST.js +1088 -0
  163. package/dist/chunk-NDF5LCST.js.map +1 -0
  164. package/dist/chunk-NGWTPED4.js +311 -0
  165. package/dist/chunk-NGWTPED4.js.map +1 -0
  166. package/dist/chunk-PP4VCONW.js +739 -0
  167. package/dist/chunk-PP4VCONW.js.map +1 -0
  168. package/dist/chunk-PRMC2A6P.cjs +488 -0
  169. package/dist/chunk-PRMC2A6P.cjs.map +1 -0
  170. package/dist/chunk-PST6UU6S.cjs +742 -0
  171. package/dist/chunk-PST6UU6S.cjs.map +1 -0
  172. package/dist/chunk-Q7LK2UWH.js +33 -0
  173. package/dist/chunk-Q7LK2UWH.js.map +1 -0
  174. package/dist/chunk-SFV5LZPD.js +162 -0
  175. package/dist/chunk-SFV5LZPD.js.map +1 -0
  176. package/dist/chunk-SKXGYJJG.cjs +370 -0
  177. package/dist/chunk-SKXGYJJG.cjs.map +1 -0
  178. package/dist/chunk-SWLNKY6R.js +477 -0
  179. package/dist/chunk-SWLNKY6R.js.map +1 -0
  180. package/dist/chunk-TGVLNHUB.cjs +483 -0
  181. package/dist/chunk-TGVLNHUB.cjs.map +1 -0
  182. package/dist/chunk-TLLRW6S3.js +74 -0
  183. package/dist/chunk-TLLRW6S3.js.map +1 -0
  184. package/dist/chunk-TRAIXZ6M.cjs +140 -0
  185. package/dist/chunk-TRAIXZ6M.cjs.map +1 -0
  186. package/dist/chunk-U74A5VK5.cjs +74 -0
  187. package/dist/chunk-U74A5VK5.cjs.map +1 -0
  188. package/dist/chunk-UAVDLBTA.cjs +74 -0
  189. package/dist/chunk-UAVDLBTA.cjs.map +1 -0
  190. package/dist/chunk-UJWS6TRV.js +354 -0
  191. package/dist/chunk-UJWS6TRV.js.map +1 -0
  192. package/dist/chunk-UUJMUPB3.cjs +531 -0
  193. package/dist/chunk-UUJMUPB3.cjs.map +1 -0
  194. package/dist/chunk-VTKHZIJB.cjs +549 -0
  195. package/dist/chunk-VTKHZIJB.cjs.map +1 -0
  196. package/dist/chunk-YXPAWUXO.js +51 -0
  197. package/dist/chunk-YXPAWUXO.js.map +1 -0
  198. package/dist/chunk-ZD64SCZB.js +80 -0
  199. package/dist/chunk-ZD64SCZB.js.map +1 -0
  200. package/dist/core.cjs +28 -0
  201. package/dist/core.cjs.map +1 -0
  202. package/dist/core.d.cts +22 -0
  203. package/dist/core.d.ts +22 -0
  204. package/dist/core.js +3 -0
  205. package/dist/core.js.map +1 -0
  206. package/dist/dataLayerState-B4XmQjQN.d.cts +258 -0
  207. package/dist/dataLayerState-B4XmQjQN.d.ts +258 -0
  208. package/package.json +30 -52
@@ -0,0 +1,1225 @@
1
+ import { useChartTrace, useChartTraceApi, useTraceSource, guideTargets, resolveGuideTrace } from './chunk-54M32JJB.js';
2
+ import { useColorScales, makeColorScale } from './chunk-4HNDKMWF.js';
3
+ import { DEFAULT_CHART_SETTINGS } from './chunk-CJQQFNEY.js';
4
+ import { planChartMargin, numericScale, planAxes, getRangeFilterForField, useBrush, PlannedGrid, PlannedAxes, findAxisGuide } from './chunk-7PTJNIOD.js';
5
+ import { FieldSelector } from './chunk-SFV5LZPD.js';
6
+ import { applyFilter } from './chunk-YXPAWUXO.js';
7
+ import { CalculatedFieldBadge } from './chunk-NDF5LCST.js';
8
+ import { Label, Input } from './chunk-MJPDJ5AS.js';
9
+ import { useDataLayer, formatFieldValue, getChartTitle, categoryValue, categoryKey, categoryIncludes, categoryLabel, hasFieldDisplayFormat, getFieldLabel, finiteNumber, convertFieldValue } from './chunk-JNO7OXCC.js';
10
+ import { ScatterChart } from 'lucide-react';
11
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
+ import { useState, useId, useRef, useMemo, useEffect, useCallback } from 'react';
13
+
14
+ function ScatterPlotSettingsPanel({
15
+ settings,
16
+ onSettingsChange
17
+ }) {
18
+ const { getOrCreateScaleForField } = useColorScales();
19
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
20
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[120px_1fr] items-center gap-4", children: [
21
+ /* @__PURE__ */ jsx(Label, { children: "X Field" }),
22
+ /* @__PURE__ */ jsx(
23
+ FieldSelector,
24
+ {
25
+ label: "",
26
+ value: settings.xField,
27
+ onChange: (value) => onSettingsChange({ ...settings, xField: value })
28
+ }
29
+ ),
30
+ /* @__PURE__ */ jsx(Label, { children: "Y Field" }),
31
+ /* @__PURE__ */ jsx(
32
+ FieldSelector,
33
+ {
34
+ label: "",
35
+ value: settings.yField,
36
+ onChange: (value) => onSettingsChange({ ...settings, yField: value })
37
+ }
38
+ ),
39
+ /* @__PURE__ */ jsx(Label, { children: "Color Field" }),
40
+ /* @__PURE__ */ jsx(
41
+ FieldSelector,
42
+ {
43
+ label: "",
44
+ value: settings.colorField ?? "",
45
+ allowClear: true,
46
+ onChange: (value) => onSettingsChange({
47
+ ...settings,
48
+ colorField: value || void 0,
49
+ colorScaleId: value ? getOrCreateScaleForField(value) : void 0
50
+ })
51
+ }
52
+ ),
53
+ /* @__PURE__ */ jsx(Label, { htmlFor: "scatter-point-size", children: "Point size" }),
54
+ /* @__PURE__ */ jsx(
55
+ Input,
56
+ {
57
+ id: "scatter-point-size",
58
+ type: "number",
59
+ min: 1,
60
+ max: 12,
61
+ step: 0.5,
62
+ value: settings.pointSize ?? 3,
63
+ onChange: (event) => {
64
+ const value = Number(event.target.value);
65
+ if (value >= 1 && value <= 12)
66
+ onSettingsChange({ ...settings, pointSize: value });
67
+ }
68
+ }
69
+ ),
70
+ /* @__PURE__ */ jsx(Label, { htmlFor: "scatter-opacity", children: "Opacity" }),
71
+ /* @__PURE__ */ jsx(
72
+ Input,
73
+ {
74
+ id: "scatter-opacity",
75
+ type: "number",
76
+ min: 0.1,
77
+ max: 1,
78
+ step: 0.1,
79
+ value: settings.pointOpacity ?? 0.7,
80
+ onChange: (event) => {
81
+ const value = Number(event.target.value);
82
+ if (value >= 0.1 && value <= 1)
83
+ onSettingsChange({ ...settings, pointOpacity: value });
84
+ }
85
+ }
86
+ )
87
+ ] }),
88
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Smaller, transparent points reveal overlap. Drag a rectangle to filter the other views." })
89
+ ] });
90
+ }
91
+
92
+ // src/components/charts/ScatterPlot/planScatterPoints.ts
93
+ function planScatterPoints({
94
+ settings,
95
+ style,
96
+ ids,
97
+ xData,
98
+ yData,
99
+ colorData,
100
+ xScale,
101
+ yScale,
102
+ getColor
103
+ }) {
104
+ const xFilter = getRangeFilterForField(settings.filters, settings.xField);
105
+ const yFilter = getRangeFilterForField(settings.filters, settings.yField);
106
+ const colorFilters = settings.filters.filter(
107
+ (filter) => filter.field === settings.colorField
108
+ );
109
+ const points = [];
110
+ for (const sourceId of ids) {
111
+ const rawX = xData[sourceId];
112
+ const rawY = yData[sourceId];
113
+ const xValue = finiteNumber(rawX) ?? NaN;
114
+ const yValue = finiteNumber(rawY) ?? NaN;
115
+ if (!Number.isFinite(xValue) || !Number.isFinite(yValue)) {
116
+ continue;
117
+ }
118
+ const x = xScale(xValue);
119
+ const y = yScale(yValue);
120
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
121
+ continue;
122
+ }
123
+ const passesOwnFilter = (!xFilter || applyFilter(xValue, xFilter)) && (!yFilter || applyFilter(yValue, yFilter)) && colorFilters.every((filter) => applyFilter(colorData[sourceId], filter));
124
+ const mappedColor = getColor(colorData[sourceId]);
125
+ points.push({
126
+ id: `${settings.id}:point:${sourceId}`,
127
+ sourceId,
128
+ xValue,
129
+ yValue,
130
+ colorValue: colorData[sourceId],
131
+ mappedColor,
132
+ x,
133
+ y,
134
+ color: passesOwnFilter ? mappedColor : "rgb(156 163 175)",
135
+ opacity: passesOwnFilter ? style.opacity.value : style.dimmedOpacity.value,
136
+ radius: style.radius.value,
137
+ passesOwnFilter
138
+ });
139
+ }
140
+ return points;
141
+ }
142
+
143
+ // src/components/charts/ScatterPlot/scatterPlan.ts
144
+ function numeric(value) {
145
+ return finiteNumber(value) ?? NaN;
146
+ }
147
+ function bounds(ids, data) {
148
+ let min = Infinity;
149
+ let max = -Infinity;
150
+ for (const id of ids) {
151
+ const value = numeric(data[id]);
152
+ if (!Number.isFinite(value)) {
153
+ continue;
154
+ }
155
+ min = Math.min(min, value);
156
+ max = Math.max(max, value);
157
+ }
158
+ return min === Infinity ? [0, 1] : [min, max];
159
+ }
160
+ function fieldLabel(field, settings) {
161
+ return field === "__ID" ? "Row sequence" : getFieldLabel(field, settings[field]);
162
+ }
163
+ function display(field, value, settings) {
164
+ return hasFieldDisplayFormat(settings[field]) ? formatFieldValue(field, value, settings[field]) : String(value);
165
+ }
166
+ function planScatter(settings, snapshot, width, height) {
167
+ const xLabel = settings.xAxisLabel || fieldLabel(settings.xField, snapshot.fieldSettings);
168
+ const yLabel = settings.yAxisLabel || fieldLabel(settings.yField, snapshot.fieldSettings);
169
+ const [xMin, xMax] = bounds(snapshot.allIds, snapshot.xData);
170
+ const [yMin, yMax] = bounds(snapshot.allIds, snapshot.yData);
171
+ const xBuffer = (xMax - xMin) * 0.1;
172
+ const yBuffer = (yMax - yMin) * 0.1;
173
+ const yDomain = [yMin - yBuffer, yMax + yBuffer];
174
+ const { margin, policy: marginPolicy } = planChartMargin({
175
+ margin: settings.margin,
176
+ width,
177
+ yDomain,
178
+ hasXLabel: Boolean(xLabel),
179
+ hasYLabel: Boolean(yLabel)
180
+ });
181
+ const plotWidth = width - margin.left - margin.right;
182
+ const plotHeight = height - margin.top - margin.bottom;
183
+ const xDomain = [
184
+ settings.xAxis.scaleType === "symlog" ? xMin : xMin - xBuffer,
185
+ xMax + xBuffer
186
+ ];
187
+ const actualYDomain = settings.yAxis.scaleType === "symlog" ? [yMin, yDomain[1]] : yDomain;
188
+ const xScale = numericScale(settings.xAxis).domain(xDomain).range([0, plotWidth]);
189
+ const yScale = numericScale(settings.yAxis).domain(actualYDomain).range([plotHeight, 0]);
190
+ const xType = settings.xAxis.scaleType === "symlog" ? "symlog" : "linear";
191
+ const yType = settings.yAxis.scaleType === "symlog" ? "symlog" : "linear";
192
+ const facetSet = new Set(snapshot.facetIds);
193
+ const ids = snapshot.facetIds?.length ? snapshot.chartIds.filter((id) => facetSet.has(id)) : snapshot.chartIds;
194
+ const resolveColor = snapshot.colorScale ? makeColorScale(snapshot.colorScale) : null;
195
+ const getColor = (value) => {
196
+ if (!settings.colorScaleId) {
197
+ return "#3479a8";
198
+ }
199
+ if (!resolveColor) {
200
+ return "#000000";
201
+ }
202
+ try {
203
+ return resolveColor(value);
204
+ } catch {
205
+ return "#000000";
206
+ }
207
+ };
208
+ const filteredSet = new Set(snapshot.filteredIds);
209
+ const pointStyle = {
210
+ radius: {
211
+ value: settings.pointSize ?? 3,
212
+ source: settings.pointSize == null ? "scatter-default" : "chart-setting"
213
+ },
214
+ opacity: {
215
+ value: settings.pointOpacity ?? 0.7,
216
+ source: settings.pointOpacity == null ? "scatter-default" : "chart-setting"
217
+ },
218
+ dimmedOpacity: { value: 0.15, source: "own-filter-rule" }
219
+ };
220
+ const points = planScatterPoints({
221
+ settings,
222
+ style: pointStyle,
223
+ ids,
224
+ xData: snapshot.xData,
225
+ yData: snapshot.yData,
226
+ colorData: snapshot.colorData,
227
+ xScale,
228
+ yScale,
229
+ getColor
230
+ }).map((point) => ({
231
+ ...point,
232
+ passesAllFilters: filteredSet.has(point.sourceId)
233
+ }));
234
+ const included = new Set(points.map((point) => point.sourceId));
235
+ const format = (field) => (value) => formatFieldValue(field, value, snapshot.fieldSettings[field]);
236
+ const labelSource = (local) => local ? "chart-setting" : "field-label";
237
+ const axes = planAxes({
238
+ plotWidth,
239
+ plotHeight,
240
+ margin,
241
+ marginPolicy,
242
+ x: {
243
+ scale: xScale,
244
+ scaleType: xType,
245
+ field: settings.xField,
246
+ fieldLabel: xLabel,
247
+ density: settings.xGridLines,
248
+ grid: settings.xAxis.grid,
249
+ format: format(settings.xField),
250
+ label: [xLabel, xType === "symlog" && "symlog"].filter(Boolean).join(" \xB7 "),
251
+ labelSource: labelSource(settings.xAxisLabel),
252
+ domainSource: {
253
+ population: "all source rows",
254
+ rows: snapshot.allIds.length,
255
+ bounds: [xMin, xMax],
256
+ padding: xType === "symlog" ? "10% above" : "10% on each side"
257
+ }
258
+ },
259
+ y: {
260
+ scale: yScale,
261
+ scaleType: yType,
262
+ field: settings.yField,
263
+ fieldLabel: yLabel,
264
+ density: settings.yGridLines,
265
+ grid: settings.yAxis.grid,
266
+ format: format(settings.yField),
267
+ label: [yLabel, yType === "symlog" && "symlog"].filter(Boolean).join(" \xB7 "),
268
+ labelSource: labelSource(settings.yAxisLabel),
269
+ rule: false,
270
+ domainSource: {
271
+ population: "all source rows",
272
+ rows: snapshot.allIds.length,
273
+ bounds: [yMin, yMax],
274
+ padding: yType === "symlog" ? "10% above" : "10% on each side"
275
+ }
276
+ }
277
+ });
278
+ const xFilter = getRangeFilterForField(settings.filters, settings.xField);
279
+ const yFilter = getRangeFilterForField(settings.filters, settings.yField);
280
+ const round = (value) => Math.round(value * 1e4) / 1e4;
281
+ const brushExtent = xFilter?.min !== void 0 && xFilter.max !== void 0 && yFilter?.min !== void 0 && yFilter.max !== void 0 ? [
282
+ [round(xScale(xFilter.min)), round(yScale(yFilter.max))],
283
+ [round(xScale(xFilter.max)), round(yScale(yFilter.min))]
284
+ ] : null;
285
+ const title = getChartTitle(
286
+ settings,
287
+ (field) => fieldLabel(field, snapshot.fieldSettings)
288
+ );
289
+ const calculatedBadges = [];
290
+ if (snapshot.calculatedFields?.includes(settings.xField)) {
291
+ calculatedBadges.push({
292
+ id: "calculation:x",
293
+ field: settings.xField,
294
+ x: margin.left + plotWidth / 2 + Math.min(90, xLabel.length * 3.2) + 8,
295
+ y: margin.top + plotHeight + Math.max(30, margin.bottom - 10) - 12,
296
+ rotation: 0
297
+ });
298
+ }
299
+ if (snapshot.calculatedFields?.includes(settings.yField)) {
300
+ calculatedBadges.push({
301
+ id: "calculation:y",
302
+ field: settings.yField,
303
+ x: 0,
304
+ y: margin.top + plotHeight / 2 - Math.min(90, yLabel.length * 3.2) - 30,
305
+ rotation: -90
306
+ });
307
+ }
308
+ let legend;
309
+ if (settings.colorField && settings.colorScaleId && snapshot.colorScale) {
310
+ const scale = snapshot.colorScale;
311
+ if (scale.type === "categorical") {
312
+ const values = /* @__PURE__ */ new Map();
313
+ for (const id of snapshot.allIds) {
314
+ const value = categoryValue(snapshot.colorData[id]);
315
+ values.set(categoryKey(value), value);
316
+ }
317
+ const counts = /* @__PURE__ */ new Map();
318
+ for (const id of snapshot.chartIds) {
319
+ const key = categoryKey(snapshot.colorData[id]);
320
+ counts.set(key, (counts.get(key) ?? 0) + 1);
321
+ }
322
+ const selected = settings.filters.filter(
323
+ (filter) => filter.type === "value" && filter.field === settings.colorField
324
+ ).flatMap((filter) => filter.values);
325
+ legend = {
326
+ field: settings.colorField,
327
+ scaleId: scale.id,
328
+ type: "categorical",
329
+ palette: [...scale.palette],
330
+ items: [...values].map(([id, value]) => ({
331
+ id,
332
+ value,
333
+ label: hasFieldDisplayFormat(
334
+ snapshot.fieldSettings[settings.colorField]
335
+ ) ? formatFieldValue(
336
+ settings.colorField,
337
+ value,
338
+ snapshot.fieldSettings[settings.colorField]
339
+ ) : categoryLabel(value),
340
+ count: counts.get(id) ?? 0,
341
+ color: getColor(value),
342
+ selected: categoryIncludes(selected, value)
343
+ }))
344
+ };
345
+ } else {
346
+ legend = {
347
+ field: settings.colorField,
348
+ scaleId: scale.id,
349
+ type: "numerical",
350
+ palette: scale.palette,
351
+ domain: [scale.min, scale.max],
352
+ items: []
353
+ };
354
+ }
355
+ }
356
+ return {
357
+ revision: snapshot.revision,
358
+ width,
359
+ height,
360
+ margin,
361
+ plotWidth,
362
+ plotHeight,
363
+ clipWidth: Math.max(0, plotWidth),
364
+ clipHeight: Math.max(0, plotHeight),
365
+ pixelRatio: snapshot.pixelRatio ?? 1,
366
+ calculatedBadges,
367
+ xScale: { type: xType, domain: xDomain, range: [0, plotWidth] },
368
+ yScale: { type: yType, domain: actualYDomain, range: [plotHeight, 0] },
369
+ title,
370
+ description: [
371
+ `Interactive scatter chart.`,
372
+ settings.xAxisLabel && `Horizontal axis: ${settings.xAxisLabel}.`,
373
+ settings.yAxisLabel && `Vertical axis: ${settings.yAxisLabel}.`
374
+ ].filter(Boolean).join(" "),
375
+ axes,
376
+ domainInputs: {
377
+ x: [xMin, xMax],
378
+ y: [yMin, yMax],
379
+ buffer: 0.1,
380
+ population: "all"
381
+ },
382
+ legend,
383
+ brushExtent,
384
+ pointStyle,
385
+ points,
386
+ exclusions: ids.filter((id) => !included.has(id)).map((sourceId) => ({
387
+ sourceId,
388
+ reason: !Number.isFinite(numeric(snapshot.xData[sourceId])) ? "invalid-x" : !Number.isFinite(numeric(snapshot.yData[sourceId])) ? "invalid-y" : "nonfinite-position"
389
+ })),
390
+ populations: {
391
+ all: snapshot.allIds.length,
392
+ chart: snapshot.chartIds.length,
393
+ filtered: snapshot.filteredIds.length,
394
+ facet: ids.length
395
+ },
396
+ rowSets: {
397
+ all: snapshot.allIds,
398
+ chart: snapshot.chartIds,
399
+ filtered: snapshot.filteredIds,
400
+ facet: ids
401
+ },
402
+ xField: settings.xField,
403
+ yField: settings.yField,
404
+ xDisplay: xLabel,
405
+ yDisplay: yLabel,
406
+ fieldSettings: snapshot.fieldSettings
407
+ };
408
+ }
409
+ function scatterPointReadout(plan, point) {
410
+ return {
411
+ xText: display(plan.xField, point.xValue, plan.fieldSettings),
412
+ yText: display(plan.yField, point.yValue, plan.fieldSettings)
413
+ };
414
+ }
415
+ function scatterHoverReadout(plan, snapshot, settings, point) {
416
+ const value = (field, item) => hasFieldDisplayFormat(snapshot.fieldSettings[field]) ? formatFieldValue(field, item, snapshot.fieldSettings[field]) : categoryLabel(item);
417
+ return {
418
+ ...scatterPointReadout(plan, point),
419
+ colorText: settings.colorField ? value(settings.colorField, point.colorValue) : void 0,
420
+ facetRowText: settings.facet.enabled ? value(
421
+ settings.facet.rowVariable,
422
+ snapshot.facetRowData?.[point.sourceId]
423
+ ) : void 0,
424
+ facetColumnText: settings.facet.enabled && settings.facet.type === "grid" ? value(
425
+ settings.facet.columnVariable,
426
+ snapshot.facetColumnData?.[point.sourceId]
427
+ ) : void 0
428
+ };
429
+ }
430
+ function brushFilters(plan, extent) {
431
+ const xScale = numericScale({ scaleType: plan.xScale.type }).domain(plan.xScale.domain).range(plan.xScale.range);
432
+ const yScale = numericScale({ scaleType: plan.yScale.type }).domain(plan.yScale.domain).range(plan.yScale.range);
433
+ return {
434
+ x: [
435
+ Math.min(xScale.invert(extent[0][0]), xScale.invert(extent[1][0])),
436
+ Math.max(xScale.invert(extent[0][0]), xScale.invert(extent[1][0]))
437
+ ],
438
+ y: [
439
+ Math.min(yScale.invert(extent[0][1]), yScale.invert(extent[1][1])),
440
+ Math.max(yScale.invert(extent[0][1]), yScale.invert(extent[1][1]))
441
+ ]
442
+ };
443
+ }
444
+ function planScatterOverlay(plan, extent, hoveredId) {
445
+ const brush = [];
446
+ if (extent) {
447
+ const [[x0, y0], [x1, y1]] = extent;
448
+ brush.push({
449
+ kind: "rect",
450
+ id: "brush",
451
+ x: x0,
452
+ y: y0,
453
+ width: Math.max(0, x1 - x0),
454
+ height: Math.max(0, y1 - y0),
455
+ fill: "var(--primary)",
456
+ fillOpacity: 0.09,
457
+ stroke: "var(--primary)",
458
+ strokeWidth: 1.25,
459
+ cursor: "move"
460
+ });
461
+ for (const [i, x] of [x0, x1].entries()) {
462
+ brush.push({
463
+ kind: "rect",
464
+ id: `brush:x:${i}`,
465
+ x: x - 2,
466
+ y: (y0 + y1) / 2 - 8,
467
+ width: 4,
468
+ height: 16,
469
+ rx: 2,
470
+ fill: "var(--primary)",
471
+ cursor: "ew-resize"
472
+ });
473
+ }
474
+ for (const [i, y] of [y0, y1].entries()) {
475
+ brush.push({
476
+ kind: "rect",
477
+ id: `brush:y:${i}`,
478
+ x: (x0 + x1) / 2 - 8,
479
+ y: y - 2,
480
+ width: 16,
481
+ height: 4,
482
+ rx: 2,
483
+ fill: "var(--primary)",
484
+ cursor: "ns-resize"
485
+ });
486
+ }
487
+ }
488
+ const point = plan.points.find((item) => item.id === hoveredId);
489
+ const readout = [];
490
+ let readoutLabel;
491
+ if (point) {
492
+ const { x, y, color } = point;
493
+ const { xText, yText } = scatterPointReadout(plan, point);
494
+ readoutLabel = `Source row ${point.sourceId}; ${plan.xDisplay}: ${xText}; ${plan.yDisplay}: ${yText}`;
495
+ const xWidth = xText.length * 6 + 12;
496
+ const yWidth = yText.length * 6 + 12;
497
+ const labelX = Math.max(
498
+ xWidth / 2,
499
+ Math.min(plan.plotWidth - xWidth / 2, x)
500
+ );
501
+ readout.push(
502
+ {
503
+ kind: "line",
504
+ id: "readout:x-rule",
505
+ x1: x,
506
+ x2: x,
507
+ y1: y,
508
+ y2: plan.plotHeight + 3,
509
+ stroke: color,
510
+ strokeOpacity: 0.4,
511
+ strokeWidth: 1,
512
+ strokeDasharray: "2 3"
513
+ },
514
+ {
515
+ kind: "line",
516
+ id: "readout:y-rule",
517
+ x1: x,
518
+ x2: 0,
519
+ y1: y,
520
+ y2: y,
521
+ stroke: color,
522
+ strokeOpacity: 0.4,
523
+ strokeWidth: 1,
524
+ strokeDasharray: "2 3"
525
+ },
526
+ {
527
+ kind: "circle",
528
+ id: "readout:ring",
529
+ cx: x,
530
+ cy: y,
531
+ r: point.radius + 5,
532
+ fill: "none",
533
+ stroke: "var(--primary)",
534
+ strokeWidth: 2.5
535
+ },
536
+ { kind: "circle", id: "readout:center", cx: x, cy: y, r: 2, fill: color },
537
+ {
538
+ kind: "rect",
539
+ id: "readout:x-bg",
540
+ x: labelX - xWidth / 2,
541
+ y: plan.plotHeight + 5,
542
+ width: xWidth,
543
+ height: 20,
544
+ fill: "var(--card)"
545
+ },
546
+ {
547
+ kind: "rect",
548
+ id: "readout:y-bg",
549
+ x: -yWidth - 3,
550
+ y: y - 10,
551
+ width: yWidth,
552
+ height: 20,
553
+ fill: "var(--card)"
554
+ },
555
+ {
556
+ kind: "text",
557
+ id: "readout:x-text",
558
+ x: labelX,
559
+ y: plan.plotHeight + 18,
560
+ text: xText,
561
+ fill: color,
562
+ fontSize: 10,
563
+ fontWeight: 600,
564
+ textAnchor: "middle"
565
+ },
566
+ {
567
+ kind: "text",
568
+ id: "readout:y-text",
569
+ x: -8,
570
+ y,
571
+ dy: ".32em",
572
+ text: yText,
573
+ fill: color,
574
+ fontSize: 10,
575
+ fontWeight: 600,
576
+ textAnchor: "end"
577
+ }
578
+ );
579
+ }
580
+ return {
581
+ brush,
582
+ readout,
583
+ readoutLabel
584
+ };
585
+ }
586
+ function Primitive({
587
+ item,
588
+ selected
589
+ }) {
590
+ if (item.kind === "line") {
591
+ const { kind: kind2, id: id2, hitStrokeWidth, ...props2 } = item;
592
+ const hasHitTarget = hitStrokeWidth !== void 0;
593
+ const line = /* @__PURE__ */ jsx(
594
+ "line",
595
+ {
596
+ "data-plan-id": id2,
597
+ ...props2,
598
+ style: selected ? { stroke: "var(--primary)" } : void 0,
599
+ strokeOpacity: selected ? 1 : item.strokeOpacity,
600
+ strokeWidth: selected ? 2.5 : item.strokeWidth,
601
+ pointerEvents: hasHitTarget ? "none" : void 0
602
+ }
603
+ );
604
+ return hasHitTarget ? /* @__PURE__ */ jsxs("g", { children: [
605
+ line,
606
+ /* @__PURE__ */ jsx(
607
+ "line",
608
+ {
609
+ "data-plan-id": id2,
610
+ x1: item.x1,
611
+ y1: item.y1,
612
+ x2: item.x2,
613
+ y2: item.y2,
614
+ stroke: "transparent",
615
+ strokeWidth: hitStrokeWidth,
616
+ pointerEvents: "stroke"
617
+ }
618
+ )
619
+ ] }) : line;
620
+ }
621
+ if (item.kind === "rect") {
622
+ const { kind: kind2, id: id2, cursor, ...props2 } = item;
623
+ return /* @__PURE__ */ jsx("rect", { "data-plan-id": id2, ...props2, style: { cursor } });
624
+ }
625
+ if (item.kind === "circle") {
626
+ const { kind: kind2, id: id2, ...props2 } = item;
627
+ return /* @__PURE__ */ jsx("circle", { "data-plan-id": id2, ...props2 });
628
+ }
629
+ const { kind, id, title, text, ...props } = item;
630
+ return /* @__PURE__ */ jsx(
631
+ "text",
632
+ {
633
+ "data-plan-id": id,
634
+ ...props,
635
+ "aria-label": title,
636
+ fill: selected ? "var(--primary)" : item.fill,
637
+ textDecoration: selected ? "underline" : void 0,
638
+ children: text
639
+ }
640
+ );
641
+ }
642
+ function Primitives({
643
+ items,
644
+ selectedId
645
+ }) {
646
+ return items.map((item) => /* @__PURE__ */ jsx(Primitive, { item, selected: item.id === selectedId }, item.id));
647
+ }
648
+ function ScatterSvg({
649
+ plan,
650
+ hoveredId,
651
+ onBrushChange,
652
+ onInspectPoint,
653
+ onInspectGuide,
654
+ onInspectOverlay,
655
+ selectedId
656
+ }) {
657
+ const svgRef = useRef(null);
658
+ const altAtDown = useRef(false);
659
+ const [hoveredGuideId, setHoveredGuideId] = useState(null);
660
+ const [altHover, setAltHover] = useState(false);
661
+ const chartId = useId();
662
+ const brush = useBrush({
663
+ svgRef,
664
+ marginLeft: plan.margin.left,
665
+ marginTop: plan.margin.top,
666
+ innerWidth: plan.plotWidth,
667
+ innerHeight: plan.plotHeight,
668
+ mode: "2d",
669
+ onBrushChange,
670
+ onPlotClick: (_, event) => event.altKey || altAtDown.current,
671
+ defaultExtent: plan.brushExtent
672
+ });
673
+ const overlay = planScatterOverlay(plan, brush.extent ?? null, hoveredId);
674
+ if (plan.width < 1 || plan.height < 1) {
675
+ return null;
676
+ }
677
+ return /* @__PURE__ */ jsxs(
678
+ "svg",
679
+ {
680
+ ref: svgRef,
681
+ width: plan.width,
682
+ height: plan.height,
683
+ role: "group",
684
+ "aria-label": plan.title,
685
+ "aria-describedby": `${chartId}-description`,
686
+ className: "absolute select-none",
687
+ style: {
688
+ cursor: altHover && (hoveredId || hoveredGuideId) ? "pointer" : brush.getCursor(),
689
+ touchAction: "none"
690
+ },
691
+ tabIndex: 0,
692
+ onKeyDown: (event) => {
693
+ const id = event.target.closest("[data-plan-id]")?.getAttribute("data-plan-id");
694
+ if (event.altKey && event.key === "Enter" && id && findAxisGuide(plan.axes, id)) {
695
+ event.preventDefault();
696
+ event.stopPropagation();
697
+ onInspectGuide(id);
698
+ return;
699
+ }
700
+ if (event.key === "Escape") {
701
+ event.stopPropagation();
702
+ brush.clear();
703
+ }
704
+ },
705
+ onPointerDownCapture: (event) => {
706
+ altAtDown.current = event.altKey;
707
+ brush.handlePointerDown(event);
708
+ },
709
+ onPointerMoveCapture: (event) => {
710
+ brush.handlePointerMove(event);
711
+ setAltHover(event.altKey);
712
+ const id = event.target.closest("[data-plan-id]")?.getAttribute("data-plan-id");
713
+ setHoveredGuideId(
714
+ event.altKey && id && findAxisGuide(plan.axes, id) ? id : null
715
+ );
716
+ },
717
+ onPointerLeave: () => {
718
+ setHoveredGuideId(null);
719
+ setAltHover(false);
720
+ },
721
+ onPointerUpCapture: brush.handlePointerUp,
722
+ onPointerCancel: brush.cancel,
723
+ onLostPointerCapture: brush.cancel,
724
+ onClick: (event) => {
725
+ if (!event.altKey || brush.wasDrag.current) return;
726
+ const rect = event.currentTarget.getBoundingClientRect();
727
+ const x = event.clientX - rect.left - plan.margin.left;
728
+ const y = event.clientY - rect.top - plan.margin.top;
729
+ if (x >= 0 && x <= plan.plotWidth && y >= 0 && y <= plan.plotHeight && onInspectPoint(x, y))
730
+ return;
731
+ const id = event.target.closest("[data-plan-id]")?.getAttribute("data-plan-id");
732
+ if (id && findAxisGuide(plan.axes, id)) {
733
+ onInspectGuide(id);
734
+ return;
735
+ }
736
+ if (id && overlay.brush.some((item) => item.id === id)) {
737
+ onInspectOverlay(id);
738
+ }
739
+ },
740
+ children: [
741
+ /* @__PURE__ */ jsx("desc", { id: `${chartId}-description`, children: plan.description }),
742
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: `${chartId}-plot`, children: /* @__PURE__ */ jsx("rect", { width: plan.clipWidth, height: plan.clipHeight }) }) }),
743
+ /* @__PURE__ */ jsxs("g", { transform: `translate(${plan.margin.left},${plan.margin.top})`, children: [
744
+ /* @__PURE__ */ jsx("g", { clipPath: `url(#${chartId}-plot)`, children: /* @__PURE__ */ jsx(
745
+ PlannedGrid,
746
+ {
747
+ plan: plan.axes,
748
+ interactive: true,
749
+ activeId: hoveredGuideId ?? selectedId
750
+ }
751
+ ) }),
752
+ /* @__PURE__ */ jsx("g", { className: "eda-brush", clipPath: `url(#${chartId}-plot)`, children: /* @__PURE__ */ jsx(Primitives, { items: overlay.brush }) }),
753
+ /* @__PURE__ */ jsx(
754
+ PlannedAxes,
755
+ {
756
+ plan: plan.axes,
757
+ interactive: true,
758
+ activeId: hoveredGuideId ?? selectedId
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsx(
762
+ "g",
763
+ {
764
+ className: "eda-axis-readout",
765
+ role: overlay.readoutLabel ? "img" : void 0,
766
+ "aria-label": overlay.readoutLabel,
767
+ pointerEvents: "none",
768
+ children: /* @__PURE__ */ jsx(Primitives, { items: overlay.readout })
769
+ }
770
+ )
771
+ ] })
772
+ ]
773
+ }
774
+ );
775
+ }
776
+
777
+ // src/components/charts/ScatterPlot/scatterTrace.ts
778
+ function sourceFields(trace) {
779
+ const calculated = new Set(trace.dependencies.map((item) => item.field));
780
+ return [
781
+ .../* @__PURE__ */ new Set([
782
+ ...trace.inputs.filter((item) => !calculated.has(item.field)).map((item) => item.field),
783
+ ...trace.dependencies.flatMap(sourceFields)
784
+ ])
785
+ ];
786
+ }
787
+ function resolveScatterTrace(selection, plan, snapshot, settings, rawRows, preparedRows, profiles, manager) {
788
+ if (selection.kind === "guide")
789
+ return resolveGuideTrace(plan.axes, selection.id, plan.revision);
790
+ if (selection.kind === "badge") {
791
+ const badge = plan.calculatedBadges.find(
792
+ (item) => item.id === selection.id
793
+ );
794
+ if (!badge) return void 0;
795
+ return { kind: "badge", id: badge.id, revision: plan.revision, badge };
796
+ }
797
+ if (selection.kind === "overlay") {
798
+ const primitive = planScatterOverlay(
799
+ plan,
800
+ plan.brushExtent,
801
+ null
802
+ ).brush.find((item) => item.id === selection.id);
803
+ if (!primitive) return void 0;
804
+ return {
805
+ kind: "overlay",
806
+ id: selection.id,
807
+ revision: plan.revision,
808
+ primitive,
809
+ extent: plan.brushExtent,
810
+ filters: settings.filters.filter(
811
+ (filter) => filter.type === "range" && (filter.field === settings.xField || filter.field === settings.yField)
812
+ )
813
+ };
814
+ }
815
+ const conversionFor = (field, raw) => {
816
+ const fieldSetting = snapshot.fieldSettings[field] ?? {};
817
+ const applied = Boolean(
818
+ fieldSetting.type || fieldSetting.datePreset || fieldSetting.nullTokens?.length
819
+ );
820
+ const type = fieldSetting.type ?? profiles.find((item) => item.name === field)?.dataType;
821
+ return applied && type ? { type, error: convertFieldValue(raw, type, fieldSetting).error } : void 0;
822
+ };
823
+ const fieldTrace = (field, value, sourceId) => {
824
+ const raw = rawRows[sourceId]?.[field];
825
+ const calculation = manager.traceRow(field, sourceId);
826
+ const conversion = calculation ? void 0 : conversionFor(field, raw);
827
+ const sources = calculation ? sourceFields(calculation).map((name) => ({
828
+ field: name,
829
+ raw: rawRows[sourceId]?.[name],
830
+ prepared: preparedRows[sourceId]?.[name],
831
+ conversion: conversionFor(name, rawRows[sourceId]?.[name])
832
+ })) : [];
833
+ return { field, raw, prepared: value, conversion, calculation, sources };
834
+ };
835
+ if (selection.kind === "point") {
836
+ const point = plan.points.find((item) => item.id === selection.id);
837
+ if (!point) return void 0;
838
+ return {
839
+ kind: "point",
840
+ id: point.id,
841
+ revision: plan.revision,
842
+ plan,
843
+ sourceId: point.sourceId,
844
+ x: {
845
+ ...fieldTrace(settings.xField, point.xValue, point.sourceId),
846
+ pixel: point.x
847
+ },
848
+ y: {
849
+ ...fieldTrace(settings.yField, point.yValue, point.sourceId),
850
+ pixel: point.y
851
+ },
852
+ color: settings.colorField ? {
853
+ ...fieldTrace(
854
+ settings.colorField,
855
+ point.colorValue,
856
+ point.sourceId
857
+ ),
858
+ mapped: point.mappedColor,
859
+ rendered: point.color
860
+ } : void 0,
861
+ facet: settings.facet.enabled && snapshot.facetIds?.length ? {
862
+ type: settings.facet.type,
863
+ row: fieldTrace(
864
+ settings.facet.rowVariable,
865
+ snapshot.facetRowData?.[point.sourceId],
866
+ point.sourceId
867
+ ),
868
+ column: settings.facet.type === "grid" ? fieldTrace(
869
+ settings.facet.columnVariable,
870
+ snapshot.facetColumnData?.[point.sourceId],
871
+ point.sourceId
872
+ ) : void 0,
873
+ sourceRows: snapshot.facetIds.length,
874
+ chartRows: plan.populations.facet
875
+ } : void 0,
876
+ radius: point.radius,
877
+ opacity: point.opacity,
878
+ passesOwnFilter: point.passesOwnFilter,
879
+ passesAllFilters: point.passesAllFilters,
880
+ hover: scatterHoverReadout(plan, snapshot, settings, point)
881
+ };
882
+ }
883
+ if (selection.kind === "excluded") {
884
+ const sourceId = Number(selection.id);
885
+ const exclusion = plan.exclusions.find(
886
+ (item) => item.sourceId === sourceId
887
+ );
888
+ if (!exclusion) return void 0;
889
+ return {
890
+ kind: "excluded",
891
+ id: selection.id,
892
+ revision: plan.revision,
893
+ sourceId,
894
+ reason: exclusion.reason,
895
+ x: fieldTrace(settings.xField, snapshot.xData[sourceId], sourceId),
896
+ y: fieldTrace(settings.yField, snapshot.yData[sourceId], sourceId)
897
+ };
898
+ }
899
+ return void 0;
900
+ }
901
+ function scatterTraceTargets(plan) {
902
+ return [
903
+ ...guideTargets(plan.axes),
904
+ ...planScatterOverlay(plan, plan.brushExtent, null).brush.map(({ id }) => ({
905
+ kind: "overlay",
906
+ id,
907
+ label: id
908
+ })),
909
+ ...plan.calculatedBadges.map((badge) => ({
910
+ kind: "badge",
911
+ id: badge.id,
912
+ label: `Calculated: ${badge.field}`
913
+ }))
914
+ ];
915
+ }
916
+ function findScatterTraceRow(plan, id) {
917
+ const point = plan.points.find((item) => item.sourceId === id);
918
+ if (point) return { kind: "point", id: point.id };
919
+ return plan.exclusions.some((item) => item.sourceId === id) ? { kind: "excluded", id: String(id) } : void 0;
920
+ }
921
+ function ScatterPlot({
922
+ settings,
923
+ width,
924
+ height,
925
+ facetIds
926
+ }) {
927
+ const [hoveredId, setHoveredId] = useState(null);
928
+ const trace = useChartTrace();
929
+ const traceApi = useChartTraceApi();
930
+ const owner = useId();
931
+ const canvasRef = useRef(null);
932
+ const data = useDataLayer((state) => state.data);
933
+ const rawData = useDataLayer((state) => state.rawData);
934
+ const profiles = useDataLayer((state) => state.fieldProfiles);
935
+ const manager = useDataLayer((state) => state.calculationManager);
936
+ const calculations = useDataLayer((state) => state.calculations);
937
+ const nonce = useDataLayer((state) => state.nonce);
938
+ const chartItems = useDataLayer((state) => state.liveItems[settings.id]);
939
+ const crossfilter = useDataLayer((state) => state.crossfilterWrapper);
940
+ const getColumnData = useDataLayer((state) => state.getColumnData);
941
+ const fieldSettings = useDataLayer((state) => state.fieldSettings);
942
+ const colorScale = useDataLayer(
943
+ (state) => state.colorScales.find((item) => item.id === settings.colorScaleId)
944
+ );
945
+ const updateChart = useDataLayer((state) => state.updateChart);
946
+ const allIds = useMemo(() => data.map((row) => row.__ID), [data]);
947
+ const snapshot = useMemo(() => {
948
+ const chartIds = chartItems?.items.filter((item) => item.value > 0).map((item) => item.key) ?? [];
949
+ const column = (field) => field ? getColumnData(field) : {};
950
+ return {
951
+ revision: `${nonce}:${chartItems?.nonce ?? 0}`,
952
+ allIds,
953
+ chartIds,
954
+ filteredIds: crossfilter.getFilteredRowIds(),
955
+ facetIds: facetIds?.slice(),
956
+ xData: column(settings.xField),
957
+ yData: column(settings.yField),
958
+ colorData: column(settings.colorField),
959
+ facetRowData: settings.facet.enabled ? column(settings.facet.rowVariable) : void 0,
960
+ facetColumnData: settings.facet.enabled && settings.facet.type === "grid" ? column(settings.facet.columnVariable) : void 0,
961
+ fieldSettings: Object.fromEntries(
962
+ Object.entries(fieldSettings).map(([field, value]) => [
963
+ field,
964
+ { ...value }
965
+ ])
966
+ ),
967
+ colorScale: colorScale?.type === "categorical" ? {
968
+ ...colorScale,
969
+ mapping: new Map(colorScale.mapping),
970
+ palette: [...colorScale.palette]
971
+ } : colorScale && { ...colorScale },
972
+ calculatedFields: calculations.map((calc) => calc.resultColumnName),
973
+ pixelRatio: typeof window === "undefined" ? 1 : window.devicePixelRatio || 1
974
+ };
975
+ }, [
976
+ allIds,
977
+ chartItems,
978
+ crossfilter,
979
+ getColumnData,
980
+ fieldSettings,
981
+ colorScale,
982
+ calculations,
983
+ nonce,
984
+ settings.xField,
985
+ settings.yField,
986
+ settings.colorField,
987
+ settings.facet,
988
+ facetIds
989
+ ]);
990
+ const plan = useMemo(
991
+ () => planScatter(settings, snapshot, width, height),
992
+ [settings, snapshot, width, height]
993
+ );
994
+ const source = useMemo(
995
+ () => ({
996
+ role: "chart",
997
+ revision: plan.revision,
998
+ resolve: (kind, id) => resolveScatterTrace(
999
+ { kind, id },
1000
+ plan,
1001
+ snapshot,
1002
+ settings,
1003
+ rawData,
1004
+ data,
1005
+ profiles,
1006
+ manager
1007
+ ),
1008
+ findRow: (id) => findScatterTraceRow(plan, id),
1009
+ targets: () => scatterTraceTargets(plan),
1010
+ legendItems: plan.legend?.type === "categorical" ? plan.legend.items : void 0
1011
+ }),
1012
+ [plan, snapshot, settings, rawData, data, profiles, manager]
1013
+ );
1014
+ useTraceSource(owner, source);
1015
+ const choose = (kind, id) => traceApi?.inspect(owner, kind, id);
1016
+ const activeSelection = trace?.selection?.owner === owner ? trace.selection : null;
1017
+ const pointAt = (x, y) => {
1018
+ let nearest;
1019
+ let distance = 100;
1020
+ for (const point of plan.points) {
1021
+ const dx = point.x - x;
1022
+ const dy = point.y - y;
1023
+ const squared = dx * dx + dy * dy;
1024
+ if (squared < distance) {
1025
+ nearest = point;
1026
+ distance = squared;
1027
+ }
1028
+ }
1029
+ return nearest;
1030
+ };
1031
+ const hoveredPoint = plan.points.find((point) => point.id === hoveredId);
1032
+ const hoveredText = hoveredPoint && scatterHoverReadout(plan, snapshot, settings, hoveredPoint);
1033
+ useEffect(() => {
1034
+ const canvas = canvasRef.current;
1035
+ if (!canvas) {
1036
+ return;
1037
+ }
1038
+ const ctx = canvas.getContext("2d");
1039
+ if (!ctx) {
1040
+ return;
1041
+ }
1042
+ const dpr = plan.pixelRatio;
1043
+ canvas.width = width * dpr;
1044
+ canvas.height = height * dpr;
1045
+ ctx.scale(dpr, dpr);
1046
+ ctx.clearRect(0, 0, width, height);
1047
+ ctx.translate(plan.margin.left, plan.margin.top);
1048
+ ctx.beginPath();
1049
+ ctx.rect(0, 0, plan.plotWidth, plan.plotHeight);
1050
+ ctx.clip();
1051
+ for (const point of plan.points) {
1052
+ ctx.fillStyle = point.color;
1053
+ ctx.globalAlpha = point.opacity;
1054
+ ctx.beginPath();
1055
+ ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
1056
+ ctx.fill();
1057
+ }
1058
+ }, [plan, width, height]);
1059
+ const handleBrushChange = useCallback(
1060
+ (extent) => {
1061
+ const filters = settings.filters.filter(
1062
+ (filter) => filter.field !== settings.xField && filter.field !== settings.yField
1063
+ );
1064
+ if (extent) {
1065
+ const next = brushFilters(plan, extent);
1066
+ filters.push({
1067
+ type: "range",
1068
+ field: settings.xField,
1069
+ min: next.x[0],
1070
+ max: next.x[1]
1071
+ });
1072
+ filters.push({
1073
+ type: "range",
1074
+ field: settings.yField,
1075
+ min: next.y[0],
1076
+ max: next.y[1]
1077
+ });
1078
+ }
1079
+ updateChart(settings.id, { filters });
1080
+ },
1081
+ [plan, settings, updateChart]
1082
+ );
1083
+ return /* @__PURE__ */ jsx(
1084
+ "div",
1085
+ {
1086
+ style: { width, height },
1087
+ className: "relative",
1088
+ onPointerLeave: () => setHoveredId(null),
1089
+ onPointerDownCapture: () => setHoveredId(null),
1090
+ onKeyDownCapture: (event) => {
1091
+ if (event.key === "Escape") {
1092
+ setHoveredId(null);
1093
+ }
1094
+ },
1095
+ onPointerMove: (event) => {
1096
+ if (event.buttons) {
1097
+ return;
1098
+ }
1099
+ const bounds2 = event.currentTarget.getBoundingClientRect();
1100
+ const px = event.clientX - bounds2.left - plan.margin.left;
1101
+ const py = event.clientY - bounds2.top - plan.margin.top;
1102
+ if (px < 0 || px > plan.plotWidth || py < 0 || py > plan.plotHeight) {
1103
+ setHoveredId(null);
1104
+ return;
1105
+ }
1106
+ setHoveredId(pointAt(px, py)?.id ?? null);
1107
+ },
1108
+ children: plan.populations.facet > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1109
+ /* @__PURE__ */ jsx(
1110
+ "canvas",
1111
+ {
1112
+ ref: canvasRef,
1113
+ className: "absolute inset-0 pointer-events-none",
1114
+ style: { width, height }
1115
+ }
1116
+ ),
1117
+ /* @__PURE__ */ jsx(
1118
+ ScatterSvg,
1119
+ {
1120
+ plan,
1121
+ hoveredId,
1122
+ onBrushChange: handleBrushChange,
1123
+ onInspectPoint: (x, y) => {
1124
+ const point = pointAt(x, y);
1125
+ if (!point) return false;
1126
+ return Boolean(choose("point", point.id));
1127
+ },
1128
+ onInspectGuide: (id) => choose("guide", id),
1129
+ onInspectOverlay: (id) => choose("overlay", id),
1130
+ selectedId: activeSelection?.kind === "guide" ? activeSelection.id : void 0
1131
+ }
1132
+ ),
1133
+ plan.calculatedBadges.map((badge) => /* @__PURE__ */ jsx(
1134
+ "span",
1135
+ {
1136
+ className: `eda-scatter-axis-calc ${badge.rotation ? "eda-scatter-axis-calc-y" : ""}`,
1137
+ style: { left: badge.x, top: badge.y },
1138
+ onClickCapture: (event) => {
1139
+ if (!event.altKey) return;
1140
+ event.stopPropagation();
1141
+ choose("badge", badge.id);
1142
+ },
1143
+ children: /* @__PURE__ */ jsx(
1144
+ CalculatedFieldBadge,
1145
+ {
1146
+ field: badge.field,
1147
+ hoverOpen: false,
1148
+ side: badge.rotation ? "right" : "top"
1149
+ }
1150
+ )
1151
+ },
1152
+ badge.id
1153
+ )),
1154
+ hoveredPoint && /* @__PURE__ */ jsxs(
1155
+ "div",
1156
+ {
1157
+ className: "pointer-events-none absolute left-2 top-2 max-w-[min(16rem,70%)] rounded border border-border bg-card/95 px-2 py-1 text-xs text-card-foreground shadow-sm",
1158
+ role: "status",
1159
+ children: [
1160
+ /* @__PURE__ */ jsxs("div", { children: [
1161
+ plan.xDisplay,
1162
+ ": ",
1163
+ hoveredText?.xText
1164
+ ] }),
1165
+ /* @__PURE__ */ jsxs("div", { children: [
1166
+ plan.yDisplay,
1167
+ ": ",
1168
+ hoveredText?.yText
1169
+ ] }),
1170
+ settings.colorField && /* @__PURE__ */ jsxs("div", { children: [
1171
+ "Color \xB7 ",
1172
+ settings.colorField,
1173
+ ": ",
1174
+ hoveredText?.colorText
1175
+ ] }),
1176
+ settings.facet.enabled && /* @__PURE__ */ jsxs("div", { children: [
1177
+ "Facet ",
1178
+ settings.facet.rowVariable,
1179
+ ":",
1180
+ " ",
1181
+ hoveredText?.facetRowText,
1182
+ settings.facet.type === "grid" && ` \xB7 ${settings.facet.columnVariable}: ${hoveredText?.facetColumnText}`
1183
+ ] })
1184
+ ]
1185
+ }
1186
+ )
1187
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground", children: "No data available" })
1188
+ }
1189
+ );
1190
+ }
1191
+
1192
+ // src/components/charts/ScatterPlot/definition.ts
1193
+ var scatterPlotDefinition = {
1194
+ type: "scatter",
1195
+ name: "Scatter Plot",
1196
+ description: "Display data as points in a 2D space",
1197
+ icon: ScatterChart,
1198
+ component: ScatterPlot,
1199
+ settingsPanel: ScatterPlotSettingsPanel,
1200
+ createDefaultSettings: (layout, field) => ({
1201
+ ...DEFAULT_CHART_SETTINGS,
1202
+ id: crypto.randomUUID(),
1203
+ type: "scatter",
1204
+ title: "Scatter Plot",
1205
+ layout,
1206
+ margin: { top: 20, right: 20, bottom: 20, left: 20 },
1207
+ xField: "__ID",
1208
+ yField: field ?? "",
1209
+ filters: []
1210
+ }),
1211
+ validateSettings: (settings) => {
1212
+ return !!settings.xField && !!settings.yField;
1213
+ },
1214
+ getFilterFunction: (settings, fieldGetter) => {
1215
+ const filters = settings.filters.map((filter) => ({
1216
+ filter,
1217
+ values: fieldGetter(filter.field)
1218
+ }));
1219
+ return (id) => filters.every(({ filter, values }) => applyFilter(values[id], filter));
1220
+ }
1221
+ };
1222
+
1223
+ export { scatterPlotDefinition };
1224
+ //# sourceMappingURL=chunk-C62KHV7S.js.map
1225
+ //# sourceMappingURL=chunk-C62KHV7S.js.map