@marimo-team/frontend 0.22.1-dev34 → 0.22.1-dev38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{JsonOutput-D1i8P1dG.js → JsonOutput-DboWEw2n.js} +1 -1
- package/dist/assets/{add-connection-dialog-ogwy8tvS.js → add-connection-dialog-DiHC8_uD.js} +1 -1
- package/dist/assets/{agent-panel-BHLkHj7k.js → agent-panel-CWWFNdAZ.js} +1 -1
- package/dist/assets/{cell-editor-_GDTh-4a.js → cell-editor-CKnV9MwH.js} +1 -1
- package/dist/assets/{column-preview-NDhbeu0E.js → column-preview-BvDPfcdF.js} +1 -1
- package/dist/assets/{command-palette-uJxkhle4.js → command-palette-CPRmmNjA.js} +1 -1
- package/dist/assets/{edit-page-Cr_DnrkM.js → edit-page-B-kCXJQD.js} +3 -3
- package/dist/assets/{file-explorer-panel-g9KppC7Y.js → file-explorer-panel-DyDct4o3.js} +1 -1
- package/dist/assets/{form-BVnQnVQ2.js → form-Byef3KYr.js} +1 -1
- package/dist/assets/{hooks-zQJ9iU_R.js → hooks-BoxLBXGI.js} +1 -1
- package/dist/assets/index-B1HqiNNr.js +42 -0
- package/dist/assets/{index-BaQAJwyb.css → index-BkdonYlq.css} +1 -1
- package/dist/assets/{layout-Cudicm29.js → layout-BjQtqcnj.js} +1 -1
- package/dist/assets/{panels-BCRqI88j.js → panels-aBXT7_tR.js} +1 -1
- package/dist/assets/{run-page-C-ySBXLo.js → run-page-yA3m6QEA.js} +1 -1
- package/dist/assets/{scratchpad-panel-C5Pogi1P.js → scratchpad-panel-Cul3iUG0.js} +1 -1
- package/dist/assets/{session-panel-DEsnZWks.js → session-panel-CzPIEKo8.js} +1 -1
- package/dist/assets/{state-CV8Wy3e4.js → state-C79RsVoe.js} +1 -1
- package/dist/assets/useNotebookActions-A-2lB6Y1.js +1 -0
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/src/components/data-table/data-table.tsx +12 -12
- package/src/components/editor/actions/pair-with-agent-modal.tsx +142 -0
- package/src/components/editor/actions/useNotebookActions.tsx +10 -0
- package/src/plugins/impl/plotly/PlotlyPlugin.tsx +62 -44
- package/src/plugins/impl/plotly/__tests__/PlotlyPlugin.test.tsx +114 -0
- package/src/plugins/impl/plotly/__tests__/selection.test.ts +158 -196
- package/src/plugins/impl/plotly/selection.ts +274 -56
- package/dist/assets/index-KI45dku7.js +0 -35
- package/dist/assets/useNotebookActions-Bb4xxjuJ.js +0 -1
|
@@ -2,20 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import { pick } from "lodash-es";
|
|
4
4
|
import type * as Plotly from "plotly.js";
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { createParser, type PlotlyTemplateParser } from "./parse-from-template";
|
|
7
7
|
|
|
8
8
|
type AxisName = string;
|
|
9
9
|
type AxisDatum = unknown;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const SUNBURST_DATA_KEYS: (keyof Plotly.SunburstPlotDatum)[] = [
|
|
12
|
+
"color",
|
|
13
|
+
"curveNumber",
|
|
14
|
+
"entry",
|
|
15
|
+
"hovertext",
|
|
16
|
+
"id",
|
|
17
|
+
"label",
|
|
18
|
+
"parent",
|
|
19
|
+
"percentEntry",
|
|
20
|
+
"percentParent",
|
|
21
|
+
"percentRoot",
|
|
22
|
+
"pointNumber",
|
|
23
|
+
"root",
|
|
24
|
+
"value",
|
|
25
|
+
] as const;
|
|
17
26
|
|
|
18
|
-
const
|
|
27
|
+
const LINE_CLICK_TRACE_TYPES = new Set(["scatter", "scattergl"]);
|
|
19
28
|
|
|
20
29
|
const STANDARD_POINT_KEYS: string[] = [
|
|
21
30
|
"x",
|
|
@@ -29,61 +38,282 @@ const STANDARD_POINT_KEYS: string[] = [
|
|
|
29
38
|
"pointIndex",
|
|
30
39
|
] as const;
|
|
31
40
|
|
|
41
|
+
type PointWithFullData = Plotly.PlotDatum & {
|
|
42
|
+
pointNumbers?: number[];
|
|
43
|
+
fullData?: {
|
|
44
|
+
type?: string;
|
|
45
|
+
mode?: string;
|
|
46
|
+
x?: unknown[];
|
|
47
|
+
y?: unknown[];
|
|
48
|
+
hovertemplate?: string | string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface TraceSource {
|
|
53
|
+
type?: string;
|
|
54
|
+
mode?: string;
|
|
55
|
+
x?: unknown[];
|
|
56
|
+
y?: unknown[];
|
|
57
|
+
hovertemplate?: string | string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ModeBarButton = NonNullable<
|
|
61
|
+
Plotly.Config["modeBarButtonsToAdd"]
|
|
62
|
+
>[number];
|
|
63
|
+
|
|
64
|
+
function coalesceField<T>(
|
|
65
|
+
primary: T | undefined,
|
|
66
|
+
fallback: T | undefined,
|
|
67
|
+
): T | undefined {
|
|
68
|
+
return primary ?? fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getTraceSource(point: Plotly.PlotDatum): TraceSource {
|
|
72
|
+
const withFullData = point as PointWithFullData;
|
|
73
|
+
const data = (point.data ?? {}) as TraceSource;
|
|
74
|
+
const fullData = (withFullData.fullData ?? {}) as TraceSource;
|
|
75
|
+
|
|
76
|
+
// Plotly click payloads sometimes include partial `data` plus richer `fullData`.
|
|
77
|
+
// Merge field-by-field so we don't lose type/mode/x/y metadata for pure lines.
|
|
78
|
+
return {
|
|
79
|
+
type: coalesceField(data.type, fullData.type),
|
|
80
|
+
mode: coalesceField(data.mode, fullData.mode),
|
|
81
|
+
x: coalesceField(data.x, fullData.x),
|
|
82
|
+
y: coalesceField(data.y, fullData.y),
|
|
83
|
+
hovertemplate: coalesceField(data.hovertemplate, fullData.hovertemplate),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
88
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
89
|
+
? value
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
32
93
|
function getPointIndex(point: Plotly.PlotDatum): number | undefined {
|
|
33
|
-
|
|
34
|
-
|
|
94
|
+
const pointIndex = asFiniteNumber(point.pointIndex);
|
|
95
|
+
if (pointIndex !== undefined) {
|
|
96
|
+
return pointIndex;
|
|
35
97
|
}
|
|
36
98
|
|
|
37
|
-
|
|
38
|
-
|
|
99
|
+
const pointNumber = asFiniteNumber(point.pointNumber);
|
|
100
|
+
if (pointNumber !== undefined) {
|
|
101
|
+
return pointNumber;
|
|
39
102
|
}
|
|
40
103
|
|
|
41
|
-
|
|
104
|
+
const pointNumbers = (point as PointWithFullData).pointNumbers;
|
|
105
|
+
if (!Array.isArray(pointNumbers)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return pointNumbers.map(asFiniteNumber).find((n) => n !== undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isLinePoint(point: Plotly.PlotDatum): boolean {
|
|
113
|
+
const trace = getTraceSource(point);
|
|
114
|
+
if (!LINE_CLICK_TRACE_TYPES.has(String(trace.type))) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mode = trace.mode;
|
|
119
|
+
if (typeof mode !== "string") {
|
|
120
|
+
// Some Plotly click payloads omit mode on point.data, especially with
|
|
121
|
+
// line traces; treat scatter/scattergl as line-like in this case.
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return mode.split("+").includes("lines");
|
|
42
126
|
}
|
|
43
127
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
128
|
+
function isPureLineMode(mode: unknown): boolean {
|
|
129
|
+
if (typeof mode !== "string") {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const parts = mode.split("+");
|
|
133
|
+
return parts.includes("lines") && !parts.includes("markers");
|
|
49
134
|
}
|
|
50
135
|
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
136
|
+
export function hasPureLineTrace(
|
|
137
|
+
data: readonly Plotly.Data[] | undefined,
|
|
138
|
+
): boolean {
|
|
139
|
+
if (!data) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return data.some((trace) => {
|
|
144
|
+
const traceType = (trace as { type?: unknown }).type;
|
|
145
|
+
const isScatterLike =
|
|
146
|
+
traceType === undefined || LINE_CLICK_TRACE_TYPES.has(String(traceType));
|
|
147
|
+
if (!isScatterLike) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return isPureLineMode((trace as { mode?: unknown }).mode);
|
|
55
151
|
});
|
|
56
152
|
}
|
|
57
153
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
154
|
+
function createDragmodeButton(
|
|
155
|
+
name: string,
|
|
156
|
+
title: string,
|
|
157
|
+
svg: string,
|
|
158
|
+
dragmode: Plotly.Layout["dragmode"],
|
|
159
|
+
setDragmode: (dragmode: Plotly.Layout["dragmode"]) => void,
|
|
160
|
+
): ModeBarButton {
|
|
161
|
+
return {
|
|
162
|
+
name,
|
|
163
|
+
title,
|
|
164
|
+
icon: { svg },
|
|
165
|
+
click: () => setDragmode(dragmode),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function lineSelectionButtons(
|
|
170
|
+
setDragmode: (dragmode: Plotly.Layout["dragmode"]) => void,
|
|
171
|
+
): ModeBarButton[] {
|
|
172
|
+
return [
|
|
173
|
+
createDragmodeButton(
|
|
174
|
+
"line-box-select",
|
|
175
|
+
"Box select",
|
|
176
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
177
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
178
|
+
<rect x="4" y="4" width="16" height="16" stroke-dasharray="2 2" />
|
|
179
|
+
</svg>`,
|
|
180
|
+
"select",
|
|
181
|
+
setDragmode,
|
|
182
|
+
),
|
|
183
|
+
createDragmodeButton(
|
|
184
|
+
"line-lasso-select",
|
|
185
|
+
"Lasso select",
|
|
186
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
187
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
188
|
+
<path d="M6 8c0-2.2 2.2-4 5-4s5 1.8 5 4-2.2 4-5 4-5 1.8-5 4 2.2 4 5 4" />
|
|
189
|
+
<circle cx="17.5" cy="16.5" r="1.5" />
|
|
190
|
+
</svg>`,
|
|
191
|
+
"lasso",
|
|
192
|
+
setDragmode,
|
|
193
|
+
),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function mergeModeBarButtonsToAdd(
|
|
198
|
+
defaults: readonly ModeBarButton[],
|
|
199
|
+
userButtons: readonly ModeBarButton[] | undefined,
|
|
200
|
+
): ModeBarButton[] {
|
|
201
|
+
const merged: ModeBarButton[] = [];
|
|
202
|
+
const seenStrings = new Set<string>();
|
|
203
|
+
|
|
204
|
+
const pushButton = (button: ModeBarButton) => {
|
|
205
|
+
if (typeof button === "string") {
|
|
206
|
+
if (seenStrings.has(button)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
seenStrings.add(button);
|
|
210
|
+
merged.push(button);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
merged.push(button);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
defaults.forEach(pushButton);
|
|
217
|
+
userButtons?.forEach(pushButton);
|
|
218
|
+
return merged;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function shouldHandleClickSelection(
|
|
222
|
+
points: readonly Plotly.PlotDatum[],
|
|
223
|
+
): boolean {
|
|
224
|
+
return points.some((point) => {
|
|
225
|
+
const type = getTraceSource(point).type;
|
|
226
|
+
return (
|
|
227
|
+
type === "bar" ||
|
|
228
|
+
type === "heatmap" ||
|
|
229
|
+
type === "histogram" ||
|
|
230
|
+
isLinePoint(point)
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function extractIndices(points: readonly Plotly.PlotDatum[]): number[] {
|
|
236
|
+
return points
|
|
237
|
+
.map(getPointIndex)
|
|
238
|
+
.filter((pointIndex): pointIndex is number => pointIndex !== undefined);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getIndexedValue(series: unknown, index: number): unknown {
|
|
242
|
+
if (Array.isArray(series) || ArrayBuffer.isView(series)) {
|
|
243
|
+
return (series as ArrayLike<unknown>)[index];
|
|
244
|
+
}
|
|
245
|
+
if (typeof series === "object" && series !== null && "length" in series) {
|
|
246
|
+
const maybeLength = Number(
|
|
247
|
+
(series as { length?: unknown }).length ?? Number.NaN,
|
|
248
|
+
);
|
|
249
|
+
if (Number.isFinite(maybeLength) && index >= 0 && index < maybeLength) {
|
|
250
|
+
return (series as Record<number, unknown>)[index];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function withInferredXY(
|
|
257
|
+
point: Plotly.PlotDatum,
|
|
258
|
+
fields: Record<AxisName, AxisDatum>,
|
|
259
|
+
): Record<AxisName, AxisDatum> {
|
|
260
|
+
// For some pure-line clicks Plotly provides index metadata but omits x/y.
|
|
261
|
+
// Recover x/y from trace arrays so Python gets a stable payload.
|
|
262
|
+
if (fields.x !== undefined && fields.y !== undefined) {
|
|
263
|
+
return fields;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const pointIndex = getPointIndex(point);
|
|
267
|
+
if (pointIndex === undefined) {
|
|
268
|
+
return fields;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const nextFields: Record<AxisName, AxisDatum> = { ...fields };
|
|
272
|
+
if (nextFields.pointIndex === undefined) {
|
|
273
|
+
nextFields.pointIndex = pointIndex;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const trace = getTraceSource(point);
|
|
277
|
+
if (nextFields.x === undefined) {
|
|
278
|
+
const inferredX = getIndexedValue(trace.x, pointIndex);
|
|
279
|
+
if (inferredX !== undefined) {
|
|
280
|
+
nextFields.x = inferredX;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (nextFields.y === undefined) {
|
|
284
|
+
const inferredY = getIndexedValue(trace.y, pointIndex);
|
|
285
|
+
if (inferredY !== undefined) {
|
|
286
|
+
nextFields.y = inferredY;
|
|
287
|
+
}
|
|
68
288
|
}
|
|
69
289
|
|
|
290
|
+
return nextFields;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function extractPoints(
|
|
294
|
+
points: readonly Plotly.PlotDatum[],
|
|
295
|
+
): Record<AxisName, AxisDatum>[] {
|
|
70
296
|
let parser: PlotlyTemplateParser | undefined;
|
|
71
297
|
|
|
72
298
|
return points.map((point) => {
|
|
73
|
-
const standardPointFields =
|
|
299
|
+
const standardPointFields = withInferredXY(
|
|
300
|
+
point,
|
|
301
|
+
pick(point, STANDARD_POINT_KEYS),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const trace = getTraceSource(point);
|
|
74
305
|
|
|
75
306
|
// Get the first hovertemplate
|
|
76
|
-
const hovertemplate = Array.isArray(
|
|
77
|
-
?
|
|
78
|
-
:
|
|
307
|
+
const hovertemplate = Array.isArray(trace.hovertemplate)
|
|
308
|
+
? trace.hovertemplate[0]
|
|
309
|
+
: trace.hovertemplate;
|
|
79
310
|
|
|
80
311
|
// For chart types with standard point keys (e.g. heatmaps),
|
|
81
312
|
// or when there's no hovertemplate, pick keys directly from the point.
|
|
82
|
-
if (!hovertemplate ||
|
|
313
|
+
if (!hovertemplate || trace.type === "heatmap") {
|
|
83
314
|
return standardPointFields;
|
|
84
315
|
}
|
|
85
316
|
|
|
86
|
-
// Update or create a parser
|
|
87
317
|
parser = parser
|
|
88
318
|
? parser.update(hovertemplate)
|
|
89
319
|
: createParser(hovertemplate);
|
|
@@ -94,22 +324,10 @@ export function extractPoints(
|
|
|
94
324
|
});
|
|
95
325
|
}
|
|
96
326
|
|
|
97
|
-
export function
|
|
98
|
-
|
|
99
|
-
):
|
|
100
|
-
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const points = evt.points.filter(isClickSelectablePoint);
|
|
105
|
-
if (points.length === 0) {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
selections: Arrays.EMPTY,
|
|
111
|
-
points: extractPoints(points),
|
|
112
|
-
indices: extractIndices(points),
|
|
113
|
-
range: undefined,
|
|
114
|
-
};
|
|
327
|
+
export function extractSunburstPoints(
|
|
328
|
+
points: readonly Plotly.PlotDatum[],
|
|
329
|
+
): Record<string, unknown>[] {
|
|
330
|
+
return points.map((point) => pick(point, SUNBURST_DATA_KEYS));
|
|
115
331
|
}
|
|
332
|
+
|
|
333
|
+
export const extractTreemapPoints = extractSunburstPoints;
|