@marimo-team/frontend 0.22.1-dev33 → 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-CNt4LxkV.js → JsonOutput-DboWEw2n.js} +2 -2
- package/dist/assets/{add-connection-dialog-Ca1Tc12W.js → add-connection-dialog-DiHC8_uD.js} +1 -1
- package/dist/assets/{agent-panel-BYAJ_EOf.js → agent-panel-CWWFNdAZ.js} +1 -1
- package/dist/assets/{cell-editor-CSxVMYfU.js → cell-editor-CKnV9MwH.js} +2 -2
- package/dist/assets/{column-preview-B92BXVH1.js → column-preview-BvDPfcdF.js} +1 -1
- package/dist/assets/{command-palette-BHXQKZ5s.js → command-palette-CPRmmNjA.js} +1 -1
- package/dist/assets/common-CRBlPqv5.js +1 -0
- package/dist/assets/{dependency-graph-panel-CABb99dr.js → dependency-graph-panel-BBmN-Vc7.js} +1 -1
- package/dist/assets/{edit-page-DtYaxOp-.js → edit-page-B-kCXJQD.js} +4 -4
- package/dist/assets/{file-explorer-panel-DJgn_eST.js → file-explorer-panel-DyDct4o3.js} +1 -1
- package/dist/assets/{form-BFjzbGu5.js → form-Byef3KYr.js} +1 -1
- package/dist/assets/{hooks-C2LN7xdC.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-Cb75LpRE.js → layout-BjQtqcnj.js} +2 -2
- package/dist/assets/{panels-C-PHvQEf.js → panels-aBXT7_tR.js} +1 -1
- package/dist/assets/{run-page-BZ-VoP12.js → run-page-yA3m6QEA.js} +1 -1
- package/dist/assets/{scratchpad-panel-CCcUfWUz.js → scratchpad-panel-Cul3iUG0.js} +1 -1
- package/dist/assets/{session-panel-DaLr35Wc.js → session-panel-CzPIEKo8.js} +1 -1
- package/dist/assets/{snippets-panel-qn7otI-U.js → snippets-panel-DQgE3W8I.js} +1 -1
- package/dist/assets/{state-q7CKuEm6.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/components/editor/cell/code/cell-editor.tsx +1 -1
- package/src/components/editor/chrome/panels/snippets-panel.tsx +1 -1
- package/src/components/editor/links/cell-link-list.tsx +1 -1
- package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +2 -3
- package/src/components/editor/navigation/navigation.ts +2 -2
- package/src/components/editor/output/console/ConsoleOutput.tsx +1 -1
- 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/src/utils/mime-types.ts +1 -1
- package/dist/assets/common-B5GX57h6.js +0 -1
- package/dist/assets/index-BG82ditz.js +0 -35
- package/dist/assets/useNotebookActions-DB5vGtvM.js +0 -1
|
@@ -14,11 +14,16 @@ import useEvent from "react-use-event-hook";
|
|
|
14
14
|
import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
|
|
15
15
|
import { useScript } from "@/hooks/useScript";
|
|
16
16
|
import { Arrays } from "@/utils/arrays";
|
|
17
|
-
import { Objects } from "@/utils/objects";
|
|
18
17
|
import {
|
|
19
|
-
extractClickSelection,
|
|
20
18
|
extractIndices,
|
|
21
19
|
extractPoints,
|
|
20
|
+
extractSunburstPoints,
|
|
21
|
+
extractTreemapPoints,
|
|
22
|
+
hasPureLineTrace,
|
|
23
|
+
lineSelectionButtons,
|
|
24
|
+
type ModeBarButton,
|
|
25
|
+
mergeModeBarButtonsToAdd,
|
|
26
|
+
shouldHandleClickSelection,
|
|
22
27
|
} from "./selection";
|
|
23
28
|
import { usePlotlyLayout } from "./usePlotlyLayout";
|
|
24
29
|
|
|
@@ -35,6 +40,10 @@ type T =
|
|
|
35
40
|
x?: number[];
|
|
36
41
|
y?: number[];
|
|
37
42
|
};
|
|
43
|
+
lasso?: {
|
|
44
|
+
x?: unknown[];
|
|
45
|
+
y?: unknown[];
|
|
46
|
+
};
|
|
38
47
|
// These are kept in the state to persist selections across re-renders
|
|
39
48
|
// on the frontend, but likely not used in the backend.
|
|
40
49
|
selections?: unknown[];
|
|
@@ -77,23 +86,6 @@ const LazyPlot = lazy(() =>
|
|
|
77
86
|
import("./Plot").then((mod) => ({ default: mod.Plot })),
|
|
78
87
|
);
|
|
79
88
|
|
|
80
|
-
const SUNBURST_DATA_KEYS: (keyof Plotly.SunburstPlotDatum)[] = [
|
|
81
|
-
"color",
|
|
82
|
-
"curveNumber",
|
|
83
|
-
"entry",
|
|
84
|
-
"hovertext",
|
|
85
|
-
"id",
|
|
86
|
-
"label",
|
|
87
|
-
"parent",
|
|
88
|
-
"percentEntry",
|
|
89
|
-
"percentParent",
|
|
90
|
-
"percentRoot",
|
|
91
|
-
"pointNumber",
|
|
92
|
-
"root",
|
|
93
|
-
"value",
|
|
94
|
-
] as const;
|
|
95
|
-
const TREE_MAP_DATA_KEYS = SUNBURST_DATA_KEYS;
|
|
96
|
-
|
|
97
89
|
export const PlotlyComponent = memo(
|
|
98
90
|
({ figure: originalFigure, value, setValue, config }: PlotlyPluginProps) => {
|
|
99
91
|
// Used for rendering LaTeX. TODO: Serve this library from Marimo
|
|
@@ -102,7 +94,7 @@ export const PlotlyComponent = memo(
|
|
|
102
94
|
);
|
|
103
95
|
const isScriptLoaded = scriptStatus === "ready";
|
|
104
96
|
|
|
105
|
-
const { figure, layout, handleReset } = usePlotlyLayout({
|
|
97
|
+
const { figure, layout, setLayout, handleReset } = usePlotlyLayout({
|
|
106
98
|
originalFigure,
|
|
107
99
|
initialValue: value,
|
|
108
100
|
isScriptLoaded,
|
|
@@ -112,31 +104,48 @@ export const PlotlyComponent = memo(
|
|
|
112
104
|
handleReset();
|
|
113
105
|
setValue({});
|
|
114
106
|
});
|
|
107
|
+
const handleSetDragmode = useEvent(
|
|
108
|
+
(dragmode: Plotly.Layout["dragmode"]) => {
|
|
109
|
+
setLayout((prev) => ({ ...prev, dragmode }));
|
|
110
|
+
setValue((prev) => ({ ...prev, dragmode }));
|
|
111
|
+
},
|
|
112
|
+
);
|
|
115
113
|
|
|
116
114
|
const configMemo = useDeepCompareMemoize(config);
|
|
117
115
|
const plotlyConfig = useMemo((): Partial<Plotly.Config> => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
svg: `
|
|
116
|
+
const hasPureLine = hasPureLineTrace(figure.data);
|
|
117
|
+
const defaultButtons: ModeBarButton[] = [
|
|
118
|
+
// Custom button to reset the state
|
|
119
|
+
{
|
|
120
|
+
name: "reset",
|
|
121
|
+
title: "Reset state",
|
|
122
|
+
icon: {
|
|
123
|
+
svg: `
|
|
127
124
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
128
125
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw">
|
|
129
126
|
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
130
127
|
<path d="M3 3v5h5" />
|
|
131
128
|
</svg>`,
|
|
132
|
-
},
|
|
133
|
-
click: handleResetWithClear,
|
|
134
129
|
},
|
|
135
|
-
|
|
130
|
+
click: handleResetWithClear,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
if (hasPureLine) {
|
|
134
|
+
defaultButtons.push(...lineSelectionButtons(handleSetDragmode));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
displaylogo: false,
|
|
136
139
|
// Prioritize user's config
|
|
137
140
|
...configMemo,
|
|
141
|
+
modeBarButtonsToAdd: mergeModeBarButtonsToAdd(
|
|
142
|
+
defaultButtons,
|
|
143
|
+
configMemo.modeBarButtonsToAdd as
|
|
144
|
+
| readonly ModeBarButton[]
|
|
145
|
+
| undefined,
|
|
146
|
+
),
|
|
138
147
|
};
|
|
139
|
-
}, [handleResetWithClear, configMemo]);
|
|
148
|
+
}, [handleResetWithClear, handleSetDragmode, configMemo, figure.data]);
|
|
140
149
|
|
|
141
150
|
return (
|
|
142
151
|
<LazyPlot
|
|
@@ -171,6 +180,7 @@ export const PlotlyComponent = memo(
|
|
|
171
180
|
points: Arrays.EMPTY,
|
|
172
181
|
indices: Arrays.EMPTY,
|
|
173
182
|
range: undefined,
|
|
183
|
+
lasso: undefined,
|
|
174
184
|
};
|
|
175
185
|
});
|
|
176
186
|
})}
|
|
@@ -181,9 +191,7 @@ export const PlotlyComponent = memo(
|
|
|
181
191
|
|
|
182
192
|
setValue((prev) => ({
|
|
183
193
|
...prev,
|
|
184
|
-
points: evt.points
|
|
185
|
-
Objects.pick(point, TREE_MAP_DATA_KEYS),
|
|
186
|
-
),
|
|
194
|
+
points: extractTreemapPoints(evt.points),
|
|
187
195
|
}));
|
|
188
196
|
})}
|
|
189
197
|
onSunburstClick={useEvent((evt: Readonly<Plotly.PlotMouseEvent>) => {
|
|
@@ -193,9 +201,7 @@ export const PlotlyComponent = memo(
|
|
|
193
201
|
|
|
194
202
|
setValue((prev) => ({
|
|
195
203
|
...prev,
|
|
196
|
-
points: evt.points
|
|
197
|
-
Objects.pick(point, SUNBURST_DATA_KEYS),
|
|
198
|
-
),
|
|
204
|
+
points: extractSunburstPoints(evt.points),
|
|
199
205
|
}));
|
|
200
206
|
})}
|
|
201
207
|
config={plotlyConfig}
|
|
@@ -203,13 +209,21 @@ export const PlotlyComponent = memo(
|
|
|
203
209
|
if (!evt) {
|
|
204
210
|
return;
|
|
205
211
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!
|
|
212
|
+
// Handle clicks for chart types where box/lasso selection
|
|
213
|
+
// is limited or unavailable (e.g. bar, heatmaps, histograms, pure line traces).
|
|
214
|
+
if (!shouldHandleClickSelection(evt.points)) {
|
|
209
215
|
return;
|
|
210
216
|
}
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
const extractedPoints = extractPoints(evt.points);
|
|
218
|
+
const extractedIndices = extractIndices(evt.points);
|
|
219
|
+
setValue((prev) => ({
|
|
220
|
+
...prev,
|
|
221
|
+
selections: Arrays.EMPTY,
|
|
222
|
+
range: undefined,
|
|
223
|
+
lasso: undefined,
|
|
224
|
+
points: extractedPoints,
|
|
225
|
+
indices: extractedIndices,
|
|
226
|
+
}));
|
|
213
227
|
})}
|
|
214
228
|
onSelected={useEvent((evt: Readonly<Plotly.PlotSelectionEvent>) => {
|
|
215
229
|
if (!evt) {
|
|
@@ -223,6 +237,10 @@ export const PlotlyComponent = memo(
|
|
|
223
237
|
points: extractPoints(evt.points),
|
|
224
238
|
indices: extractIndices(evt.points),
|
|
225
239
|
range: evt.range,
|
|
240
|
+
lasso:
|
|
241
|
+
"lassoPoints" in evt
|
|
242
|
+
? (evt.lassoPoints as { x?: unknown[]; y?: unknown[] })
|
|
243
|
+
: undefined,
|
|
226
244
|
}));
|
|
227
245
|
})}
|
|
228
246
|
className="w-full"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { Suspense } from "react";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { SetupMocks } from "@/__mocks__/common";
|
|
7
|
+
import type { Setter } from "@/plugins/types";
|
|
8
|
+
import { PlotlyComponent } from "../PlotlyPlugin";
|
|
9
|
+
|
|
10
|
+
SetupMocks.resizeObserver();
|
|
11
|
+
|
|
12
|
+
type CapturedPlotProps = {
|
|
13
|
+
onClick?: (event: {
|
|
14
|
+
points: {
|
|
15
|
+
data?: { type?: string };
|
|
16
|
+
x?: string | number;
|
|
17
|
+
y?: string | number;
|
|
18
|
+
pointIndex?: number;
|
|
19
|
+
pointNumber?: number;
|
|
20
|
+
curveNumber?: number;
|
|
21
|
+
}[];
|
|
22
|
+
}) => void;
|
|
23
|
+
} | null;
|
|
24
|
+
|
|
25
|
+
let capturedPlotProps: CapturedPlotProps = null;
|
|
26
|
+
|
|
27
|
+
vi.mock("../Plot", () => ({
|
|
28
|
+
Plot: (props: CapturedPlotProps) => {
|
|
29
|
+
capturedPlotProps = props;
|
|
30
|
+
return <div data-testid="plotly-mock" />;
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../usePlotlyLayout", () => ({
|
|
35
|
+
usePlotlyLayout: ({
|
|
36
|
+
originalFigure,
|
|
37
|
+
}: {
|
|
38
|
+
originalFigure: {
|
|
39
|
+
data: unknown[];
|
|
40
|
+
layout: Record<string, unknown>;
|
|
41
|
+
frames: unknown[] | null;
|
|
42
|
+
};
|
|
43
|
+
}) => ({
|
|
44
|
+
figure: originalFigure,
|
|
45
|
+
layout: originalFigure.layout,
|
|
46
|
+
handleReset: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
vi.mock("@/hooks/useScript", () => ({
|
|
51
|
+
useScript: () => "ready",
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock("react-use-event-hook", () => ({
|
|
55
|
+
default: <T,>(callback: T) => callback,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
describe("PlotlyPlugin", () => {
|
|
59
|
+
it("clicking a bar selects that bar", async () => {
|
|
60
|
+
const setValue = vi.fn<Setter<unknown>>();
|
|
61
|
+
|
|
62
|
+
render(
|
|
63
|
+
<Suspense fallback={null}>
|
|
64
|
+
<PlotlyComponent
|
|
65
|
+
figure={{
|
|
66
|
+
data: [{ type: "bar" }],
|
|
67
|
+
layout: {},
|
|
68
|
+
frames: null,
|
|
69
|
+
}}
|
|
70
|
+
value={undefined}
|
|
71
|
+
setValue={setValue}
|
|
72
|
+
host={document.createElement("div")}
|
|
73
|
+
config={{}}
|
|
74
|
+
/>
|
|
75
|
+
</Suspense>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(capturedPlotProps).not.toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
capturedPlotProps?.onClick?.({
|
|
84
|
+
points: [
|
|
85
|
+
{
|
|
86
|
+
data: { type: "bar" },
|
|
87
|
+
x: "Feb",
|
|
88
|
+
y: 18,
|
|
89
|
+
pointIndex: 1,
|
|
90
|
+
pointNumber: 1,
|
|
91
|
+
curveNumber: 0,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(setValue).toHaveBeenCalledTimes(1);
|
|
98
|
+
const updater = setValue.mock.calls[0][0] as (value: unknown) => unknown;
|
|
99
|
+
expect(updater({})).toEqual({
|
|
100
|
+
selections: [],
|
|
101
|
+
points: [
|
|
102
|
+
{
|
|
103
|
+
x: "Feb",
|
|
104
|
+
y: 18,
|
|
105
|
+
curveNumber: 0,
|
|
106
|
+
pointNumber: 1,
|
|
107
|
+
pointIndex: 1,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
indices: [1],
|
|
111
|
+
range: undefined,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|