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,1088 @@
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Popover, PopoverTrigger, PopoverContent, Label, Input } from './chunk-MJPDJ5AS.js';
2
+ import { cn, buildFieldProfile, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, useDataLayer, CalculationManager, getChartFields, getChartTitle, Button, calculationFunctions, ActionTooltip } from './chunk-JNO7OXCC.js';
3
+ import { parseExpression } from './chunk-H5AVMMCE.js';
4
+ import { createContext, useContext, useState, useRef, useCallback, useMemo, useEffect } from 'react';
5
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+ import { ToggleLeft, CalendarDays, Type, Hash, CircleAlert, CheckCircle2 } from 'lucide-react';
8
+
9
+ function Tabs({
10
+ className,
11
+ ...props
12
+ }) {
13
+ return /* @__PURE__ */ jsx(
14
+ TabsPrimitive.Root,
15
+ {
16
+ "data-slot": "tabs",
17
+ className: cn("flex flex-col gap-2", className),
18
+ ...props
19
+ }
20
+ );
21
+ }
22
+ function TabsList({
23
+ className,
24
+ ...props
25
+ }) {
26
+ return /* @__PURE__ */ jsx(
27
+ TabsPrimitive.List,
28
+ {
29
+ "data-slot": "tabs-list",
30
+ className: cn(
31
+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-1",
32
+ className
33
+ ),
34
+ ...props
35
+ }
36
+ );
37
+ }
38
+ function TabsTrigger({
39
+ className,
40
+ ...props
41
+ }) {
42
+ return /* @__PURE__ */ jsx(
43
+ TabsPrimitive.Trigger,
44
+ {
45
+ "data-slot": "tabs-trigger",
46
+ className: cn(
47
+ "data-[state=active]:bg-background data-[state=active]:text-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 inline-flex items-center justify-center gap-2 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-invalid:focus-visible:ring-0 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
48
+ className
49
+ ),
50
+ ...props
51
+ }
52
+ );
53
+ }
54
+ function TabsContent({
55
+ className,
56
+ ...props
57
+ }) {
58
+ return /* @__PURE__ */ jsx(
59
+ TabsPrimitive.Content,
60
+ {
61
+ "data-slot": "tabs-content",
62
+ className: cn(
63
+ "ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 flex-1 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
64
+ className
65
+ ),
66
+ ...props
67
+ }
68
+ );
69
+ }
70
+ var typeLabels = {
71
+ numeric: "Number",
72
+ categorical: "Text",
73
+ datetime: "Date",
74
+ boolean: "Boolean"
75
+ };
76
+ var typeIcons = {
77
+ numeric: Hash,
78
+ categorical: Type,
79
+ datetime: CalendarDays,
80
+ boolean: ToggleLeft
81
+ };
82
+ var valueLabel = (value) => {
83
+ if (value == null || value === "") return "\u2014";
84
+ if (typeof value === "number") {
85
+ return new Intl.NumberFormat("en-US", { maximumFractionDigits: 3 }).format(
86
+ value
87
+ );
88
+ }
89
+ return String(value);
90
+ };
91
+ function resolveFieldProfile(name, profiles, getColumnData) {
92
+ const profile = profiles.find((item) => item.name === name);
93
+ if (profile) return profile;
94
+ const columnData = getColumnData(name);
95
+ return Object.keys(columnData).length ? buildFieldProfile(name, columnData) : void 0;
96
+ }
97
+ function dateRange(profile) {
98
+ if (profile.dataType !== "datetime") return void 0;
99
+ let first;
100
+ let last;
101
+ let firstTime = Infinity;
102
+ let lastTime = -Infinity;
103
+ for (const { value } of profile.categories?.distribution ?? []) {
104
+ const time = Date.parse(String(value));
105
+ if (!Number.isFinite(time)) continue;
106
+ if (time < firstTime) {
107
+ first = value;
108
+ firstTime = time;
109
+ }
110
+ if (time > lastTime) {
111
+ last = value;
112
+ lastTime = time;
113
+ }
114
+ }
115
+ if (first == null || last == null) return void 0;
116
+ const start = String(first);
117
+ const end = String(last);
118
+ return start === end ? start : `${start}\u2013${end}`;
119
+ }
120
+ function fieldMetadata(profile) {
121
+ const range = profile.statistics ? `${valueLabel(profile.statistics.min)}\u2013${valueLabel(profile.statistics.max)}` : dateRange(profile);
122
+ const sample = profile.categories?.topValues[0] ? valueLabel(profile.categories.topValues[0].value) : void 0;
123
+ return {
124
+ type: typeLabels[profile.dataType],
125
+ detail: range ?? (sample ? `e.g. ${sample}` : "No sample"),
126
+ detailLabel: range ? "Range" : "Sample",
127
+ nulls: `${profile.nullCount.toLocaleString()} null${profile.nullCount === 1 ? "" : "s"}`,
128
+ excluded: profile.excludedCount ? `${profile.excludedCount.toLocaleString()} not finite` : void 0
129
+ };
130
+ }
131
+ function FieldMetadata({
132
+ profile,
133
+ label,
134
+ compact = false,
135
+ showDetail = true,
136
+ className,
137
+ tooltipSide = "bottom"
138
+ }) {
139
+ if (!profile) return /* @__PURE__ */ jsx("span", { className, children: label });
140
+ const metadata = fieldMetadata(profile);
141
+ const description = [
142
+ metadata.type,
143
+ metadata.detail,
144
+ metadata.nulls,
145
+ metadata.excluded
146
+ ].filter(Boolean).join("; ");
147
+ const TypeIcon = typeIcons[profile.dataType];
148
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
149
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
150
+ "span",
151
+ {
152
+ className: cn(
153
+ compact ? "inline-flex min-w-0 items-center gap-1" : "inline-flex min-w-0 items-baseline gap-1.5",
154
+ className
155
+ ),
156
+ "aria-label": `${label ?? profile.name}: ${description}`,
157
+ children: [
158
+ compact && /* @__PURE__ */ jsx(
159
+ TypeIcon,
160
+ {
161
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
162
+ "aria-hidden": "true"
163
+ }
164
+ ),
165
+ label && /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: label }),
166
+ !compact && /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 gap-1.5 text-[10px] text-muted-foreground", children: [
167
+ /* @__PURE__ */ jsx("span", { className: "shrink-0", children: metadata.type }),
168
+ showDetail && /* @__PURE__ */ jsx("span", { className: "truncate", children: metadata.detail }),
169
+ /* @__PURE__ */ jsx("span", { className: "shrink-0", children: metadata.nulls })
170
+ ] })
171
+ ]
172
+ }
173
+ ) }),
174
+ /* @__PURE__ */ jsxs(TooltipContent, { side: tooltipSide, align: "start", collisionPadding: 12, children: [
175
+ /* @__PURE__ */ jsx("p", { className: "mb-2 font-semibold", children: label ?? profile.name }),
176
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs", children: [
177
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Type" }),
178
+ /* @__PURE__ */ jsx("dd", { children: metadata.type }),
179
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: metadata.detailLabel }),
180
+ /* @__PURE__ */ jsx("dd", { children: metadata.detail }),
181
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Nulls" }),
182
+ /* @__PURE__ */ jsx("dd", { children: metadata.nulls }),
183
+ metadata.excluded && /* @__PURE__ */ jsxs(Fragment, { children: [
184
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Excluded" }),
185
+ /* @__PURE__ */ jsx("dd", { children: metadata.excluded })
186
+ ] }),
187
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Distinct" }),
188
+ /* @__PURE__ */ jsx("dd", { children: profile.uniqueCount.toLocaleString() })
189
+ ] })
190
+ ] })
191
+ ] }) });
192
+ }
193
+ function CalculationForm({
194
+ draft,
195
+ onChange,
196
+ fields,
197
+ calculatedFields,
198
+ editing,
199
+ nameLocked,
200
+ checking,
201
+ canApply,
202
+ dirty,
203
+ status,
204
+ preview,
205
+ trace,
206
+ impact,
207
+ onApply,
208
+ onDiscard,
209
+ onClose
210
+ }) {
211
+ const fieldProfiles = useDataLayer((state) => state.fieldProfiles);
212
+ const getColumnData = useDataLayer((state) => state.getColumnData);
213
+ const input = useRef(null);
214
+ const [search, setSearch] = useState("");
215
+ const [tab, setTab] = useState(editing ? "chain" : "fields");
216
+ const insert = (text, selectStart = text.length, selectEnd = selectStart) => {
217
+ const start = input.current?.selectionStart ?? draft.expression.length;
218
+ const end = input.current?.selectionEnd ?? start;
219
+ onChange({
220
+ ...draft,
221
+ expression: draft.expression.slice(0, start) + text + draft.expression.slice(end)
222
+ });
223
+ requestAnimationFrame(() => {
224
+ input.current?.focus();
225
+ input.current?.setSelectionRange(start + selectStart, start + selectEnd);
226
+ });
227
+ };
228
+ const matches = fields.filter(
229
+ (field) => field.toLowerCase().includes(search.toLowerCase())
230
+ );
231
+ return /* @__PURE__ */ jsxs(
232
+ "form",
233
+ {
234
+ className: "eda-calc-form",
235
+ onSubmit: (event) => {
236
+ event.preventDefault();
237
+ if (canApply) onApply();
238
+ },
239
+ onKeyDown: (event) => {
240
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
241
+ event.preventDefault();
242
+ if (canApply) onApply();
243
+ }
244
+ },
245
+ children: [
246
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-editor-grid", children: [
247
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-main", children: [
248
+ /* @__PURE__ */ jsxs(
249
+ "div",
250
+ {
251
+ className: nameLocked ? "eda-calc-name eda-calc-name-locked" : "eda-calc-name",
252
+ children: [
253
+ /* @__PURE__ */ jsx(Label, { htmlFor: "calculation-name", children: "Field name" }),
254
+ /* @__PURE__ */ jsx(
255
+ Input,
256
+ {
257
+ id: "calculation-name",
258
+ value: draft.name,
259
+ readOnly: nameLocked,
260
+ onChange: (event) => onChange({ ...draft, name: event.target.value }),
261
+ placeholder: "e.g. Net sales"
262
+ }
263
+ ),
264
+ nameLocked && /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: "This name is used by other calculations or views. Its formula can still change." })
265
+ ]
266
+ }
267
+ ),
268
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-section-heading", children: [
269
+ /* @__PURE__ */ jsx(Label, { htmlFor: "calculation-expression", children: "Formula" }),
270
+ /* @__PURE__ */ jsx("span", { children: "One result per source row \xB7 UTC dates" })
271
+ ] }),
272
+ /* @__PURE__ */ jsx(
273
+ "textarea",
274
+ {
275
+ ref: input,
276
+ id: "calculation-expression",
277
+ value: draft.expression,
278
+ spellCheck: false,
279
+ autoCapitalize: "off",
280
+ autoComplete: "off",
281
+ placeholder: 'e.g. ["Revenue"] - ["Cost"]',
282
+ onChange: (event) => onChange({ ...draft, expression: event.target.value }),
283
+ "aria-describedby": "calculation-syntax calculation-validation"
284
+ }
285
+ ),
286
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-formula-tools", children: [
287
+ /* @__PURE__ */ jsx("p", { id: "calculation-syntax", className: "eda-calc-help", children: 'Use ["Field name"] for a field and "text" for a literal.' }),
288
+ /* @__PURE__ */ jsx(
289
+ "button",
290
+ {
291
+ type: "button",
292
+ className: "eda-calc-text-button",
293
+ onClick: () => setTab("functions"),
294
+ children: "Function help"
295
+ }
296
+ )
297
+ ] }),
298
+ /* @__PURE__ */ jsx(
299
+ "div",
300
+ {
301
+ id: "calculation-validation",
302
+ className: "eda-calc-validation",
303
+ "aria-live": "polite",
304
+ "aria-busy": checking,
305
+ children: status
306
+ }
307
+ ),
308
+ preview
309
+ ] }),
310
+ /* @__PURE__ */ jsx("aside", { className: "eda-calc-aside", "aria-label": "Calculation tools", children: /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: setTab, children: [
311
+ /* @__PURE__ */ jsxs(TabsList, { className: "w-full", children: [
312
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "chain", children: "Chain" }),
313
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "fields", children: "Fields" }),
314
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "functions", children: "Functions" })
315
+ ] }),
316
+ /* @__PURE__ */ jsx(TabsContent, { value: "chain", children: trace }),
317
+ /* @__PURE__ */ jsxs(TabsContent, { value: "fields", children: [
318
+ /* @__PURE__ */ jsx("p", { className: "eda-calc-help mb-3", children: "Insert at the cursor, or replace selected text." }),
319
+ /* @__PURE__ */ jsx(
320
+ Input,
321
+ {
322
+ "aria-label": "Find a field",
323
+ placeholder: "Find a field\u2026",
324
+ value: search,
325
+ onChange: (event) => setSearch(event.target.value)
326
+ }
327
+ ),
328
+ [true, false].map((calculated) => {
329
+ const group = matches.filter(
330
+ (field) => calculatedFields.includes(field) === calculated
331
+ );
332
+ return group.length > 0 && /* @__PURE__ */ jsxs(
333
+ "section",
334
+ {
335
+ className: "eda-calc-field-group",
336
+ children: [
337
+ /* @__PURE__ */ jsxs("h3", { children: [
338
+ calculated ? "Calculated fields" : "Source fields",
339
+ /* @__PURE__ */ jsx("span", { children: group.length })
340
+ ] }),
341
+ group.map((field) => /* @__PURE__ */ jsxs(
342
+ "button",
343
+ {
344
+ type: "button",
345
+ onClick: () => insert("[" + JSON.stringify(field) + "]"),
346
+ children: [
347
+ /* @__PURE__ */ jsx(
348
+ "span",
349
+ {
350
+ className: calculated ? "eda-calc-symbol" : "text-muted-foreground",
351
+ children: calculated ? "\u0192x" : "\xB7"
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsx(
355
+ FieldMetadata,
356
+ {
357
+ profile: resolveFieldProfile(
358
+ field,
359
+ fieldProfiles,
360
+ getColumnData
361
+ ),
362
+ label: field,
363
+ compact: true,
364
+ className: "min-w-0 flex-1"
365
+ }
366
+ ),
367
+ /* @__PURE__ */ jsx(
368
+ "span",
369
+ {
370
+ className: "ml-auto text-muted-foreground",
371
+ "aria-hidden": "true",
372
+ children: "+"
373
+ }
374
+ )
375
+ ]
376
+ },
377
+ field
378
+ ))
379
+ ]
380
+ },
381
+ String(calculated)
382
+ );
383
+ }),
384
+ !matches.length && /* @__PURE__ */ jsxs("p", { className: "eda-calc-help mt-4", children: [
385
+ "No fields match \u201C",
386
+ search,
387
+ "\u201D."
388
+ ] })
389
+ ] }),
390
+ /* @__PURE__ */ jsxs(TabsContent, { value: "functions", children: [
391
+ /* @__PURE__ */ jsx("p", { className: "eda-calc-help mb-3", children: "Select a function to insert it. Replace the selected argument with a field or value." }),
392
+ /* @__PURE__ */ jsx("div", { className: "eda-calc-function-list", children: Object.values(calculationFunctions).map((fn) => /* @__PURE__ */ jsxs(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ onClick: () => {
397
+ const text = fn.syntax.replace(", \u2026", "");
398
+ const start = text.indexOf("(") + 1;
399
+ const end = text.indexOf(",", start);
400
+ insert(text, start, end === -1 ? text.length - 1 : end);
401
+ },
402
+ children: [
403
+ /* @__PURE__ */ jsx("code", { children: fn.syntax }),
404
+ /* @__PURE__ */ jsx("span", { children: fn.description })
405
+ ]
406
+ },
407
+ fn.syntax
408
+ )) }),
409
+ /* @__PURE__ */ jsxs("details", { className: "eda-calc-syntax-help", children: [
410
+ /* @__PURE__ */ jsx("summary", { children: "Operators and rules" }),
411
+ /* @__PURE__ */ jsx("p", { children: "Arithmetic: + \u2212 * / ^" }),
412
+ /* @__PURE__ */ jsx("p", { children: "Compare: == != > >= < <=" }),
413
+ /* @__PURE__ */ jsx("p", { children: "Logic: && (and), || (or), ! (not)" }),
414
+ /* @__PURE__ */ jsx("code", { children: "if x > 0 then x else 0" }),
415
+ /* @__PURE__ */ jsx("p", { children: "Guard missing values with x == null." }),
416
+ /* @__PURE__ */ jsx("p", { children: "Date components: year, month, day, quarter, week (ISO)." })
417
+ ] })
418
+ ] })
419
+ ] }) })
420
+ ] }),
421
+ /* @__PURE__ */ jsxs("footer", { className: "eda-calc-editor-footer", children: [
422
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
423
+ /* @__PURE__ */ jsx("p", { children: impact }),
424
+ /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: dirty ? "Draft only. Apply to update the analysis. Closing keeps your draft in this session." : "Active filters stay in place when you apply a change." })
425
+ ] }),
426
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
427
+ dirty && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", onClick: onDiscard, children: "Discard draft" }),
428
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onClose, children: "Close" }),
429
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: !canApply, children: [
430
+ editing ? "Apply changes" : "Create calculation",
431
+ /* @__PURE__ */ jsx("span", { className: "eda-calc-shortcut", "aria-hidden": "true", children: "\u2318/Ctrl \u21B5" })
432
+ ] })
433
+ ] })
434
+ ] })
435
+ ]
436
+ }
437
+ );
438
+ }
439
+
440
+ // src/components/calculations/calculationHelpers.ts
441
+ function dependentCalculations(calculations, name) {
442
+ const names = /* @__PURE__ */ new Set([name]);
443
+ let added = true;
444
+ while (added) {
445
+ added = false;
446
+ for (const calc of calculations) {
447
+ if (!names.has(calc.resultColumnName) && calc.expression.dependencies.some((field) => names.has(field))) {
448
+ names.add(calc.resultColumnName);
449
+ added = true;
450
+ }
451
+ }
452
+ }
453
+ return calculations.filter(
454
+ (calc) => calc.resultColumnName !== name && names.has(calc.resultColumnName)
455
+ );
456
+ }
457
+ var numbers = new Intl.NumberFormat("en-US", { maximumFractionDigits: 6 });
458
+ function calculationValue(value) {
459
+ if (value == null) return "Missing";
460
+ if (typeof value === "number") return numbers.format(value);
461
+ if (typeof value === "boolean") return value ? "true" : "false";
462
+ return value === "" ? '""' : value;
463
+ }
464
+ function CalculationPreview({
465
+ calculation,
466
+ manager,
467
+ savedManager,
468
+ data,
469
+ selectedRow,
470
+ onSelectRow,
471
+ changed
472
+ }) {
473
+ const [errorsOnly, setErrorsOnly] = useState(false);
474
+ const [page, setPage] = useState(
475
+ () => Math.max(
476
+ 0,
477
+ Math.floor(data.findIndex((row) => row.__ID === selectedRow) / 5)
478
+ )
479
+ );
480
+ const name = calculation.resultColumnName;
481
+ const values = manager.executeCalculation(calculation);
482
+ const errors = manager.getErrors(name);
483
+ const dependencies = calculation.expression.dependencies;
484
+ const definitions = manager.getCalculations();
485
+ const saved = savedManager.getCalculations().find((calc) => calc.resultColumnName === name);
486
+ const before = saved ? savedManager.executeCalculation(saved) : null;
487
+ const inputValues = dependencies.map((field) => {
488
+ const calc = definitions.find((item) => item.resultColumnName === field);
489
+ return calc ? manager.executeCalculation(calc) : null;
490
+ });
491
+ const rows = errorsOnly ? data.filter((row) => errors.has(row.__ID)) : data;
492
+ const currentPage = Math.min(
493
+ page,
494
+ Math.max(0, Math.ceil(rows.length / 5) - 1)
495
+ );
496
+ const start = currentPage * 5;
497
+ return /* @__PURE__ */ jsxs("section", { className: "eda-calc-preview", "aria-label": "Calculation preview", children: [
498
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-section-heading", children: [
499
+ /* @__PURE__ */ jsx("h3", { children: "Inputs \u2192 result" }),
500
+ /* @__PURE__ */ jsx("span", { children: changed ? "Draft preview" : "Saved values" })
501
+ ] }),
502
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-3", children: [
503
+ /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: "All loaded rows, before chart filters. Select a row to follow its values through the chain." }),
504
+ errors.size > 0 && /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-xs", children: [
505
+ /* @__PURE__ */ jsx(
506
+ "input",
507
+ {
508
+ type: "checkbox",
509
+ checked: errorsOnly,
510
+ onChange: (event) => {
511
+ setErrorsOnly(event.target.checked);
512
+ setPage(0);
513
+ }
514
+ }
515
+ ),
516
+ "Failed rows only"
517
+ ] })
518
+ ] }),
519
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-preview-scroll", children: [
520
+ /* @__PURE__ */ jsxs("table", { children: [
521
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
522
+ /* @__PURE__ */ jsx("th", { children: "Row" }),
523
+ dependencies.map((field) => /* @__PURE__ */ jsxs("th", { children: [
524
+ definitions.some(
525
+ (calc) => calc.resultColumnName === field
526
+ ) && /* @__PURE__ */ jsx("span", { className: "eda-calc-symbol mr-1", children: "\u0192x" }),
527
+ field
528
+ ] }, field)),
529
+ changed && before && /* @__PURE__ */ jsx("th", { children: "Saved result" }),
530
+ /* @__PURE__ */ jsx("th", { children: changed ? "Draft result" : "Result" }),
531
+ /* @__PURE__ */ jsx("th", { children: "Status" })
532
+ ] }) }),
533
+ /* @__PURE__ */ jsx("tbody", { children: rows.slice(start, start + 5).map((row) => /* @__PURE__ */ jsxs("tr", { "data-selected": row.__ID === selectedRow, children: [
534
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
535
+ "button",
536
+ {
537
+ type: "button",
538
+ "aria-label": "Trace row " + (row.__ID + 1),
539
+ "aria-pressed": row.__ID === selectedRow,
540
+ onClick: () => onSelectRow(row.__ID),
541
+ children: row.__ID + 1
542
+ }
543
+ ) }),
544
+ dependencies.map((field, index) => /* @__PURE__ */ jsx("td", { children: calculationValue(
545
+ inputValues[index] ? inputValues[index].get(row.__ID) : row[field]
546
+ ) }, field)),
547
+ changed && before && /* @__PURE__ */ jsx("td", { className: "text-muted-foreground", children: calculationValue(before.get(row.__ID)) }),
548
+ /* @__PURE__ */ jsx("td", { className: "font-semibold", children: errors.has(row.__ID) ? "\u2014" : calculationValue(values.get(row.__ID)) }),
549
+ /* @__PURE__ */ jsx(
550
+ "td",
551
+ {
552
+ className: errors.has(row.__ID) ? "text-destructive" : "text-muted-foreground",
553
+ children: errors.get(row.__ID) ?? (values.get(row.__ID) == null ? "Missing value" : "Valid")
554
+ }
555
+ )
556
+ ] }, row.__ID)) })
557
+ ] }),
558
+ !rows.length && /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: errorsOnly ? "No failed rows." : "No loaded rows." })
559
+ ] }),
560
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 mt-2 text-xs text-muted-foreground", children: [
561
+ /* @__PURE__ */ jsxs("span", { children: [
562
+ rows.length ? start + 1 : 0,
563
+ "\u2013",
564
+ Math.min(start + 5, rows.length),
565
+ " of",
566
+ " ",
567
+ rows.length.toLocaleString(),
568
+ " rows"
569
+ ] }),
570
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
571
+ /* @__PURE__ */ jsx(
572
+ Button,
573
+ {
574
+ type: "button",
575
+ variant: "ghost",
576
+ size: "sm",
577
+ disabled: currentPage === 0,
578
+ onClick: () => setPage(currentPage - 1),
579
+ children: "Previous rows"
580
+ }
581
+ ),
582
+ /* @__PURE__ */ jsx(
583
+ Button,
584
+ {
585
+ type: "button",
586
+ variant: "ghost",
587
+ size: "sm",
588
+ disabled: start + 5 >= rows.length,
589
+ onClick: () => setPage(currentPage + 1),
590
+ children: "Next rows"
591
+ }
592
+ )
593
+ ] })
594
+ ] })
595
+ ] });
596
+ }
597
+ function CalculationTrace({
598
+ calculations,
599
+ manager,
600
+ row,
601
+ root,
602
+ selected,
603
+ draftFields,
604
+ previewing,
605
+ onSelect
606
+ }) {
607
+ const fieldProfiles = useDataLayer((state) => state.fieldProfiles);
608
+ const getColumnData = useDataLayer((state) => state.getColumnData);
609
+ const byName = new Map(
610
+ calculations.map((calc) => [calc.resultColumnName, calc])
611
+ );
612
+ const dependents = dependentCalculations(calculations, selected);
613
+ const node = (name, path) => {
614
+ if (path.has(name)) return null;
615
+ const calc = byName.get(name);
616
+ const value = calc ? manager.executeCalculation(calc).get(row.__ID) : row[name];
617
+ const error = calc && manager.getErrors(name).get(row.__ID);
618
+ const nextPath = /* @__PURE__ */ new Set([...path, name]);
619
+ return /* @__PURE__ */ jsxs("li", { children: [
620
+ calc ? /* @__PURE__ */ jsx(ActionTooltip, { content: calc.expression.rawInput, children: /* @__PURE__ */ jsxs(
621
+ "button",
622
+ {
623
+ type: "button",
624
+ className: "eda-calc-node",
625
+ "aria-pressed": name === selected,
626
+ onClick: () => onSelect(name),
627
+ children: [
628
+ /* @__PURE__ */ jsx("span", { className: "eda-calc-symbol", "aria-hidden": "true", children: "\u0192x" }),
629
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
630
+ /* @__PURE__ */ jsx(
631
+ FieldMetadata,
632
+ {
633
+ profile: resolveFieldProfile(
634
+ name,
635
+ fieldProfiles,
636
+ getColumnData
637
+ ),
638
+ label: name,
639
+ compact: true,
640
+ className: "min-w-0"
641
+ }
642
+ ),
643
+ draftFields.includes(name) && /* @__PURE__ */ jsx("small", { className: "block text-xs font-normal text-muted-foreground", children: name === selected && previewing ? "Draft preview" : "Draft kept" })
644
+ ] }),
645
+ /* @__PURE__ */ jsx(ActionTooltip, { content: error || String(value ?? "Missing"), children: /* @__PURE__ */ jsx(
646
+ "span",
647
+ {
648
+ className: error ? "text-destructive" : "eda-calc-node-value",
649
+ children: error ? "Error" : calculationValue(value)
650
+ }
651
+ ) })
652
+ ]
653
+ }
654
+ ) }) : /* @__PURE__ */ jsxs("div", { className: "eda-calc-source", children: [
655
+ /* @__PURE__ */ jsx(
656
+ FieldMetadata,
657
+ {
658
+ profile: resolveFieldProfile(name, fieldProfiles, getColumnData),
659
+ label: name,
660
+ compact: true,
661
+ className: "min-w-0 flex-1"
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsx(ActionTooltip, { content: String(value ?? "Missing"), children: /* @__PURE__ */ jsx("span", { className: "eda-calc-node-value", children: calculationValue(value) }) })
665
+ ] }),
666
+ calc && calc.expression.dependencies.length > 0 && /* @__PURE__ */ jsx("ul", { children: calc.expression.dependencies.map((field) => node(field, nextPath)) })
667
+ ] }, name);
668
+ };
669
+ return /* @__PURE__ */ jsxs("div", { className: "eda-calc-trace", children: [
670
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-section-heading", children: [
671
+ /* @__PURE__ */ jsx("h3", { children: "How this value is made" }),
672
+ /* @__PURE__ */ jsxs("span", { children: [
673
+ "Row ",
674
+ row.__ID + 1
675
+ ] })
676
+ ] }),
677
+ /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: "Follow the inputs below. Select a calculation to inspect or edit that step." }),
678
+ /* @__PURE__ */ jsx("ul", { className: "eda-calc-tree", "aria-label": "Calculation dependency chain", children: node(root, /* @__PURE__ */ new Set()) }),
679
+ root !== selected && /* @__PURE__ */ jsxs(
680
+ "button",
681
+ {
682
+ className: "eda-calc-text-button",
683
+ type: "button",
684
+ onClick: () => onSelect(root),
685
+ children: [
686
+ "Return to ",
687
+ root
688
+ ]
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-section-heading mt-6", children: [
692
+ /* @__PURE__ */ jsx("h3", { children: "Used by" }),
693
+ /* @__PURE__ */ jsxs("span", { children: [
694
+ dependents.length,
695
+ " calculations"
696
+ ] })
697
+ ] }),
698
+ dependents.length ? /* @__PURE__ */ jsx("div", { className: "eda-calc-link-list", children: dependents.map((calc) => /* @__PURE__ */ jsxs(
699
+ "button",
700
+ {
701
+ type: "button",
702
+ onClick: () => onSelect(calc.resultColumnName),
703
+ children: [
704
+ /* @__PURE__ */ jsx("span", { className: "eda-calc-symbol", children: "\u0192x" }),
705
+ calc.resultColumnName
706
+ ]
707
+ },
708
+ calc.resultColumnName
709
+ )) }) : /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: "No calculations depend on this field." })
710
+ ] });
711
+ }
712
+ var EditorContext = createContext(null);
713
+ var useCalculationEditor = () => useContext(EditorContext);
714
+ function CalculationEditorProvider({
715
+ children
716
+ }) {
717
+ const data = useDataLayer((state) => state.data);
718
+ const calculations = useDataLayer((state) => state.calculations);
719
+ const savedManager = useDataLayer((state) => state.calculationManager);
720
+ const charts = useDataLayer((state) => state.charts);
721
+ const getColumnNames = useDataLayer((state) => state.getColumnNames);
722
+ const getFieldLabel = useDataLayer((state) => state.getFieldLabel);
723
+ const addCalculation = useDataLayer((state) => state.addCalculation);
724
+ const updateCalculation = useDataLayer((state) => state.updateCalculation);
725
+ const [visible, setVisible] = useState(false);
726
+ const [selected, setSelected] = useState();
727
+ const [root, setRoot] = useState();
728
+ const [rowId, setRowId] = useState(0);
729
+ const [drafts, setDrafts] = useState({});
730
+ const [preview, setPreview] = useState();
731
+ const [saveError, setSaveError] = useState("");
732
+ const [savedMessage, setSavedMessage] = useState("");
733
+ const returnFocus = useRef(null);
734
+ const open = useCallback(
735
+ (name, row, target) => {
736
+ returnFocus.current = target ?? document.activeElement;
737
+ setSelected(name);
738
+ setRoot(name);
739
+ setRowId(row ?? 0);
740
+ setSaveError("");
741
+ setSavedMessage("");
742
+ setVisible(true);
743
+ },
744
+ []
745
+ );
746
+ const editor = useMemo(() => ({ open }), [open]);
747
+ useEffect(() => {
748
+ setDrafts({});
749
+ setVisible(false);
750
+ }, [data]);
751
+ const key = selected ?? "";
752
+ const calculation = calculations.find(
753
+ (calc) => calc.resultColumnName === selected
754
+ );
755
+ const draft = Object.hasOwn(drafts, key) ? drafts[key] : {
756
+ name: calculation?.resultColumnName ?? "",
757
+ expression: calculation?.expression.rawInput ?? ""
758
+ };
759
+ const dirty = draft.name !== (calculation?.resultColumnName ?? "") || draft.expression !== (calculation?.expression.rawInput ?? "");
760
+ const previewKey = JSON.stringify([selected, draft.name, draft.expression]);
761
+ useEffect(() => {
762
+ if (!visible) return;
763
+ setPreview(void 0);
764
+ const timer = setTimeout(() => {
765
+ if (!draft.name.trim() || !draft.expression.trim()) {
766
+ setPreview({ key: previewKey });
767
+ return;
768
+ }
769
+ try {
770
+ const candidate = {
771
+ resultColumnName: draft.name.trim(),
772
+ expression: parseExpression(draft.expression)
773
+ };
774
+ const manager = new CalculationManager(data, [
775
+ ...calculations.filter((calc) => calc.resultColumnName !== selected),
776
+ candidate
777
+ ]);
778
+ manager.executeCalculation(candidate);
779
+ setPreview({ key: previewKey, manager, calculation: candidate });
780
+ } catch (error) {
781
+ setPreview({
782
+ key: previewKey,
783
+ error: error instanceof Error ? error.message : String(error)
784
+ });
785
+ }
786
+ }, 220);
787
+ return () => clearTimeout(timer);
788
+ }, [visible, previewKey, data, calculations]);
789
+ const current = preview?.key === previewKey ? preview : void 0;
790
+ const checking = !current && Boolean(draft.name.trim() && draft.expression.trim());
791
+ const dependents = dependentCalculations(
792
+ calculations,
793
+ selected ?? draft.name
794
+ );
795
+ const affected = /* @__PURE__ */ new Set([
796
+ selected ?? draft.name,
797
+ ...dependents.map((calc) => calc.resultColumnName)
798
+ ]);
799
+ const usedViews = charts.filter(
800
+ (chart) => [
801
+ ...getChartFields(chart),
802
+ ...chart.filters.map((filter) => filter.field),
803
+ chart.facet?.rowVariable,
804
+ chart.facet?.type === "grid" ? chart.facet.columnVariable : void 0
805
+ ].some((field) => field && affected.has(field))
806
+ );
807
+ const nameLocked = Boolean(
808
+ calculation && (dependents.length || usedViews.length)
809
+ );
810
+ const canApply = Boolean(dirty && current?.manager && current.calculation);
811
+ const errorCount = current?.manager?.getErrors(draft.name.trim()).size ?? 0;
812
+ const removeDraft = () => setDrafts((previous) => {
813
+ const next = { ...previous };
814
+ delete next[key];
815
+ return next;
816
+ });
817
+ const apply = async () => {
818
+ if (!canApply || !current?.calculation) return;
819
+ try {
820
+ if (selected) updateCalculation(selected, current.calculation);
821
+ else await addCalculation(current.calculation);
822
+ removeDraft();
823
+ const name = current.calculation.resultColumnName;
824
+ setSelected(name);
825
+ if (!root || root === selected) setRoot(name);
826
+ setSaveError("");
827
+ setSavedMessage(
828
+ "Applied. All dependent calculations and views are up to date."
829
+ );
830
+ } catch (error) {
831
+ setSaveError(error instanceof Error ? error.message : String(error));
832
+ }
833
+ };
834
+ const traceManager = current?.manager ?? savedManager;
835
+ const traceCalculations = traceManager.getCalculations();
836
+ const traceRoot = root === selected ? current?.calculation?.resultColumnName ?? root : root ?? current?.calculation?.resultColumnName;
837
+ const selectedRow = data.find((row) => row.__ID === rowId) ?? data[0];
838
+ const status = saveError || current?.error ? /* @__PURE__ */ jsxs("p", { role: "alert", className: "eda-calc-error", children: [
839
+ /* @__PURE__ */ jsx(CircleAlert, { "aria-hidden": "true", size: 16 }),
840
+ /* @__PURE__ */ jsx("span", { children: saveError || current?.error })
841
+ ] }) : checking ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
842
+ "Checking formula against ",
843
+ data.length.toLocaleString(),
844
+ " loaded rows\u2026"
845
+ ] }) : current?.manager ? /* @__PURE__ */ jsxs(
846
+ "p",
847
+ {
848
+ role: "status",
849
+ className: errorCount ? "eda-calc-error" : "eda-calc-valid",
850
+ children: [
851
+ errorCount ? /* @__PURE__ */ jsx(CircleAlert, { size: 16, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(CheckCircle2, { size: 16, "aria-hidden": "true" }),
852
+ /* @__PURE__ */ jsxs("span", { children: [
853
+ errorCount ? errorCount.toLocaleString() + " of " + data.length.toLocaleString() + " rows failed. Valid rows remain available." : "Valid for all " + data.length.toLocaleString() + " rows.",
854
+ savedMessage && " " + savedMessage
855
+ ] })
856
+ ]
857
+ }
858
+ ) : /* @__PURE__ */ jsx("p", { children: "Enter a field name and formula to preview results." });
859
+ return /* @__PURE__ */ jsxs(EditorContext.Provider, { value: editor, children: [
860
+ children,
861
+ /* @__PURE__ */ jsx(Dialog, { open: visible, onOpenChange: setVisible, children: /* @__PURE__ */ jsxs(
862
+ DialogContent,
863
+ {
864
+ className: "eda-calc-editor",
865
+ onCloseAutoFocus: (event) => {
866
+ event.preventDefault();
867
+ if (returnFocus.current?.isConnected) returnFocus.current.focus();
868
+ },
869
+ children: [
870
+ /* @__PURE__ */ jsxs(DialogHeader, { className: "eda-calc-editor-header", children: [
871
+ /* @__PURE__ */ jsxs(DialogTitle, { children: [
872
+ /* @__PURE__ */ jsx("span", { className: "eda-calc-symbol mr-2", children: "\u0192x" }),
873
+ selected ? "Inspect calculation" : "New calculation"
874
+ ] }),
875
+ /* @__PURE__ */ jsx(DialogDescription, { children: root ? "Following " + root + ". Preview a change before applying it to the analysis." : "Turn source fields into values you can reuse in any view." })
876
+ ] }),
877
+ /* @__PURE__ */ jsx(
878
+ CalculationForm,
879
+ {
880
+ draft,
881
+ onChange: (next) => {
882
+ setDrafts((previous) => ({ ...previous, [key]: next }));
883
+ setSaveError("");
884
+ setSavedMessage("");
885
+ },
886
+ fields: getColumnNames().filter((field) => field !== selected),
887
+ calculatedFields: calculations.map((calc) => calc.resultColumnName),
888
+ editing: Boolean(calculation),
889
+ nameLocked,
890
+ checking,
891
+ canApply,
892
+ dirty,
893
+ status,
894
+ impact: dependents.length + " dependent calculations \xB7 " + usedViews.length + " affected views",
895
+ onApply: apply,
896
+ onDiscard: () => {
897
+ removeDraft();
898
+ setSaveError("");
899
+ setSavedMessage("");
900
+ },
901
+ onClose: () => setVisible(false),
902
+ preview: current?.manager && current.calculation ? /* @__PURE__ */ jsx(
903
+ CalculationPreview,
904
+ {
905
+ calculation: current.calculation,
906
+ manager: current.manager,
907
+ savedManager,
908
+ data,
909
+ selectedRow: selectedRow?.__ID ?? 0,
910
+ onSelectRow: setRowId,
911
+ changed: dirty
912
+ },
913
+ key
914
+ ) : /* @__PURE__ */ jsxs("div", { className: "eda-calc-preview-empty", children: [
915
+ /* @__PURE__ */ jsx("h3", { children: "See the result before you apply" }),
916
+ /* @__PURE__ */ jsx("p", { children: "Inputs, saved values, and draft results appear here once the formula is valid." })
917
+ ] }),
918
+ trace: /* @__PURE__ */ jsxs(Fragment, { children: [
919
+ dirty && !current?.manager && /* @__PURE__ */ jsx("p", { className: "eda-calc-help mb-3", children: "The chain shows saved values until this draft can be previewed." }),
920
+ traceRoot && selectedRow ? /* @__PURE__ */ jsx(
921
+ CalculationTrace,
922
+ {
923
+ calculations: traceCalculations,
924
+ manager: traceManager,
925
+ row: selectedRow,
926
+ root: traceRoot,
927
+ selected: selected ?? draft.name,
928
+ draftFields: Object.keys(drafts),
929
+ previewing: Boolean(current?.manager),
930
+ onSelect: (field) => {
931
+ if (root && dependentCalculations(calculations, root).some(
932
+ (calc) => calc.resultColumnName === field
933
+ ))
934
+ setRoot(field);
935
+ setSelected(field);
936
+ setSaveError("");
937
+ setSavedMessage("");
938
+ }
939
+ }
940
+ ) : /* @__PURE__ */ jsx("p", { className: "eda-calc-help", children: "Add a formula to see how its fields connect." }),
941
+ usedViews.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mt-6", children: [
942
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Used in views" }),
943
+ /* @__PURE__ */ jsx("ul", { className: "eda-calc-view-list", children: usedViews.map((chart) => /* @__PURE__ */ jsx("li", { children: getChartTitle(chart, getFieldLabel) }, chart.id)) })
944
+ ] })
945
+ ] })
946
+ },
947
+ key
948
+ )
949
+ ]
950
+ }
951
+ ) })
952
+ ] });
953
+ }
954
+ function CalculatedFieldBadge({
955
+ field,
956
+ showName = false,
957
+ rowId = 0,
958
+ children,
959
+ hoverOpen = true,
960
+ side = "bottom"
961
+ }) {
962
+ const calculations = useDataLayer((state) => state.calculations) ?? [];
963
+ const manager = useDataLayer((state) => state.calculationManager);
964
+ const data = useDataLayer((state) => state.data);
965
+ const fieldProfiles = useDataLayer((state) => state.fieldProfiles);
966
+ const getColumnData = useDataLayer((state) => state.getColumnData);
967
+ const editor = useCalculationEditor();
968
+ const calc = calculations.find((item) => item.resultColumnName === field);
969
+ const [open, setOpen] = useState(false);
970
+ const pinned = useRef(false);
971
+ const restoreFocus = useRef(false);
972
+ const timer = useRef(void 0);
973
+ const trigger = useRef(null);
974
+ const cancelTimer = () => clearTimeout(timer.current);
975
+ useEffect(() => () => clearTimeout(timer.current), []);
976
+ if (!calc || !editor) return /* @__PURE__ */ jsx(Fragment, { children });
977
+ const leave = () => {
978
+ cancelTimer();
979
+ if (!pinned.current) timer.current = setTimeout(() => setOpen(false), 180);
980
+ };
981
+ const errors = open ? manager.getErrors(field) : /* @__PURE__ */ new Map();
982
+ const value = open ? manager.executeCalculation(calc).get(rowId) : void 0;
983
+ const profile = open ? resolveFieldProfile(field, fieldProfiles, getColumnData) : void 0;
984
+ return /* @__PURE__ */ jsxs(
985
+ Popover,
986
+ {
987
+ open,
988
+ onOpenChange: (next) => {
989
+ setOpen(next);
990
+ if (!next) pinned.current = false;
991
+ },
992
+ children: [
993
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
994
+ "button",
995
+ {
996
+ ref: trigger,
997
+ type: "button",
998
+ className: children !== void 0 ? "eda-calc-cell" : "eda-calc-badge",
999
+ "aria-label": "Inspect " + field + " calculation" + (children !== void 0 ? ", row " + (rowId + 1) : ""),
1000
+ onPointerEnter: (event) => {
1001
+ if (!hoverOpen || event.pointerType === "touch" || event.buttons)
1002
+ return;
1003
+ cancelTimer();
1004
+ timer.current = setTimeout(() => setOpen(true), 250);
1005
+ },
1006
+ onPointerLeave: leave,
1007
+ onClick: (event) => {
1008
+ event.preventDefault();
1009
+ event.stopPropagation();
1010
+ cancelTimer();
1011
+ pinned.current = true;
1012
+ restoreFocus.current = true;
1013
+ setOpen(true);
1014
+ },
1015
+ children: children !== void 0 ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
1016
+ /* @__PURE__ */ jsx("span", { className: "eda-calc-symbol", "aria-hidden": "true", children: "\u0192x" }),
1017
+ showName && /* @__PURE__ */ jsx("span", { children: field })
1018
+ ] })
1019
+ }
1020
+ ) }),
1021
+ /* @__PURE__ */ jsxs(
1022
+ PopoverContent,
1023
+ {
1024
+ className: "eda-calc-peek",
1025
+ align: "start",
1026
+ side,
1027
+ collisionPadding: 12,
1028
+ onPointerEnter: cancelTimer,
1029
+ onPointerLeave: leave,
1030
+ onOpenAutoFocus: (event) => {
1031
+ if (!pinned.current) event.preventDefault();
1032
+ },
1033
+ onCloseAutoFocus: (event) => {
1034
+ event.preventDefault();
1035
+ if (restoreFocus.current) trigger.current?.focus();
1036
+ restoreFocus.current = false;
1037
+ },
1038
+ children: [
1039
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-section-heading", children: [
1040
+ /* @__PURE__ */ jsx(FieldMetadata, { profile, label: field, compact: true }),
1041
+ /* @__PURE__ */ jsx("span", { children: "Calculated field" })
1042
+ ] }),
1043
+ /* @__PURE__ */ jsx("pre", { children: calc.expression.rawInput }),
1044
+ /* @__PURE__ */ jsxs("p", { className: "eda-calc-help", children: [
1045
+ "Uses",
1046
+ " ",
1047
+ calc.expression.dependencies.length ? calc.expression.dependencies.join(", ") : "constant values",
1048
+ "."
1049
+ ] }),
1050
+ /* @__PURE__ */ jsxs("div", { className: "eda-calc-peek-result", children: [
1051
+ /* @__PURE__ */ jsxs("span", { children: [
1052
+ "Row ",
1053
+ rowId + 1
1054
+ ] }),
1055
+ /* @__PURE__ */ jsx("strong", { children: errors.get(rowId) ?? calculationValue(value) })
1056
+ ] }),
1057
+ /* @__PURE__ */ jsxs("p", { className: "eda-calc-help", children: [
1058
+ errors.size ? errors.size.toLocaleString() + " of " + data.length.toLocaleString() + " rows failed" : "All " + data.length.toLocaleString() + " rows valid",
1059
+ " ",
1060
+ "\xB7 ",
1061
+ dependentCalculations(calculations, field).length,
1062
+ " dependent calculations"
1063
+ ] }),
1064
+ /* @__PURE__ */ jsx(
1065
+ Button,
1066
+ {
1067
+ type: "button",
1068
+ className: "mt-4 w-full",
1069
+ onClick: () => {
1070
+ setOpen(false);
1071
+ pinned.current = false;
1072
+ restoreFocus.current = false;
1073
+ editor.open(field, rowId, trigger.current);
1074
+ },
1075
+ children: "Inspect chain & edit"
1076
+ }
1077
+ )
1078
+ ]
1079
+ }
1080
+ )
1081
+ ]
1082
+ }
1083
+ );
1084
+ }
1085
+
1086
+ export { CalculatedFieldBadge, CalculationEditorProvider, FieldMetadata, Tabs, TabsContent, TabsList, TabsTrigger, dependentCalculations, resolveFieldProfile, useCalculationEditor };
1087
+ //# sourceMappingURL=chunk-NDF5LCST.js.map
1088
+ //# sourceMappingURL=chunk-NDF5LCST.js.map