@marimo-team/islands 0.22.1-dev27 → 0.22.1-dev28
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/_basePickBy-Sow3pJjS.js +41 -0
- package/dist/{_baseUniq--7il0Js0.js → _baseUniq-C87CckHL.js} +14 -53
- package/dist/{architecture-7HQA4BMR-CSK94-xM.js → architecture-7HQA4BMR-BHdkAMvZ.js} +2 -2
- package/dist/{architectureDiagram-VXUJARFQ-Cw5_EP5P.js → architectureDiagram-VXUJARFQ-B3YQo9At.js} +9 -9
- package/dist/{blockDiagram-VD42YOAC-BiuOHEQv.js → blockDiagram-VD42YOAC-CpQ3TKEN.js} +2 -2
- package/dist/{chat-ui-MFxd7AGf.js → chat-ui-Wi1Lm6y4.js} +3 -3
- package/dist/{chunk-4F5CHEZ2-B0Jbisw2.js → chunk-4F5CHEZ2-D5mClyDv.js} +1 -1
- package/dist/{chunk-B2363JML-mJ3Q9WfR.js → chunk-B2363JML-Br0eA2T3.js} +1 -1
- package/dist/{chunk-B4BG7PRW-C4MiRcFI.js → chunk-B4BG7PRW-4BjV11Br.js} +1 -1
- package/dist/{chunk-DI55MBZ5-CMx27S12.js → chunk-DI55MBZ5-DITY3EyP.js} +1 -1
- package/dist/{chunk-FRFDVMJY-IPSTV3Ua.js → chunk-FRFDVMJY-DnEvEFRR.js} +1 -1
- package/dist/{chunk-N4CR4FBY-DYub3dan.js → chunk-N4CR4FBY-CpZSuGSU.js} +1 -1
- package/dist/{chunk-PL6DKKU2-DZiAP4HM.js → chunk-PL6DKKU2-DnId6G-x.js} +1 -1
- package/dist/{chunk-SJTYNZTY-s-hMTFeC.js → chunk-SJTYNZTY-BsBZnJUj.js} +1 -1
- package/dist/{chunk-TCCFYFTB-D7iQP6Bp.js → chunk-TCCFYFTB-Clbl-fTg.js} +5 -4
- package/dist/{chunk-TQ3KTPDO-BmFTqg51.js → chunk-TQ3KTPDO-CFkSQ30e.js} +1 -1
- package/dist/{chunk-UMXZTB3W-AUirFhbD.js → chunk-UMXZTB3W-D-A834Bq.js} +1 -1
- package/dist/{classDiagram-2ON5EDUG-BJCfK6hn.js → classDiagram-2ON5EDUG-C8-zE3Zv.js} +2 -2
- package/dist/{classDiagram-v2-WZHVMYZB-aaIrLBHp.js → classDiagram-v2-WZHVMYZB-DrmbGANl.js} +2 -2
- package/dist/{clone-BsAy1q8B.js → clone-DZFQCtFJ.js} +1 -1
- package/dist/{constants-CMDkKrpC.js → constants-CvyfaCvs.js} +1 -1
- package/dist/{dagre-6UL2VRFP-CLYtUWgC.js → dagre-6UL2VRFP-OMItEBnY.js} +5 -5
- package/dist/{dagre-oeMGMQA0.js → dagre-QVd-lCXU.js} +10 -20
- package/dist/{diagram-PSM6KHXK-D_6t-W86.js → diagram-PSM6KHXK-CkKbohWI.js} +9 -9
- package/dist/{diagram-QEK2KX5R-DyPwbpot.js → diagram-QEK2KX5R-DjUMpVcx.js} +9 -9
- package/dist/{diagram-S2PKOQOG-nwsLe-2u.js → diagram-S2PKOQOG-b-c0d-wZ.js} +9 -9
- package/dist/{erDiagram-Q2GNP2WA-C3PXcCCI.js → erDiagram-Q2GNP2WA-CDhLaOZ1.js} +1 -1
- package/dist/{flowDiagram-NV44I4VS-DFDCBPb3.js → flowDiagram-NV44I4VS-BDi4O4CL.js} +1 -1
- package/dist/{gitGraph-G5XIXVHT-CuhmzmB1.js → gitGraph-G5XIXVHT-B_c6xFJv.js} +2 -2
- package/dist/{gitGraphDiagram-V2S2FVAM-B3inBgCa.js → gitGraphDiagram-V2S2FVAM-iQnXzbPM.js} +9 -9
- package/dist/{glide-data-editor-kjT1twjd.js → glide-data-editor-D8O9AS1C.js} +2 -2
- package/dist/{graphlib-B05vp8B3.js → graphlib-BV1_gi0C.js} +2 -1
- package/dist/hasIn-DnfJcYpY.js +108 -0
- package/dist/{info-VBDWY6EO-CHQo46SB.js → info-VBDWY6EO-BTyzxmhr.js} +2 -2
- package/dist/{infoDiagram-HS3SLOUP-WHNM1sDJ.js → infoDiagram-HS3SLOUP-OYrX6uO3.js} +9 -9
- package/dist/{input-DONWC1s4.js → input-BeQSGpld.js} +1 -1
- package/dist/main.js +95 -73
- package/dist/{mermaid-BdEvqBXn.js → mermaid-808LPVim.js} +19 -19
- package/dist/{mermaid-parser.core-C2Dti_2f.js → mermaid-parser.core-ntCgyx0x.js} +8 -8
- package/dist/min-Ds3gG0Ff.js +96 -0
- package/dist/{mindmap-definition-VGOIOE7T-B02Y_l4r.js → mindmap-definition-VGOIOE7T-CxEUZZvY.js} +1 -1
- package/dist/{packet-DYOGHKS2-DjPkocnd.js → packet-DYOGHKS2-BhvnpoGi.js} +2 -2
- package/dist/{pie-VRWISCQL-B9pA8cOD.js → pie-VRWISCQL-dILuA3iG.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-Bzhvjry9.js → pieDiagram-ADFJNKIX-U3LrUqAS.js} +9 -9
- package/dist/{process-output-LVENbROu.js → process-output-BvZAAk1w.js} +3 -3
- package/dist/{radar-ZZBFDIW7-C2obWVPx.js → radar-ZZBFDIW7-DwFrOJDj.js} +2 -2
- package/dist/range-fJeId9Ri.js +30 -0
- package/dist/{requirementDiagram-UZGBJVZJ-B7RuV-90.js → requirementDiagram-UZGBJVZJ-D0zpQnKC.js} +1 -1
- package/dist/{stateDiagram-FKZM4ZOC-QeUJrF7u.js → stateDiagram-FKZM4ZOC-B1S8jGMn.js} +4 -4
- package/dist/{stateDiagram-v2-4FDKWEC3-eOW_mDCq.js → stateDiagram-v2-4FDKWEC3-BH5ozUbc.js} +2 -2
- package/dist/{toDate-CamIA0ND.js → toDate-O4H9dZVC.js} +1 -1
- package/dist/{treemap-GDKQZRPO-DWMsd3D8.js → treemap-GDKQZRPO-bx2ngsgN.js} +2 -2
- package/dist/{types-IIG7e4M2.js → types-D_ntCXg0.js} +1 -1
- package/dist/{useDeepCompareMemoize-DAfQftmI.js → useDeepCompareMemoize-B8DwRVrX.js} +1 -1
- package/dist/{vega-component-CuWVhTG9.js → vega-component-C-fsM9rL.js} +3 -3
- package/package.json +1 -1
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +43 -15
- package/src/plugins/impl/plotly/PlotlyPlugin.tsx +12 -68
- package/src/plugins/impl/plotly/__tests__/selection.test.ts +237 -0
- package/src/plugins/impl/plotly/selection.ts +118 -0
- package/dist/_basePickBy-CsPbmRlg.js +0 -110
- package/dist/_baseSet-Cs7YTRXk.js +0 -27
- package/dist/get-Wu1vTRhN.js +0 -68
- package/dist/range-CNXr10o1.js +0 -17
- /package/dist/{now-Dh_IQA36.js → now-nrrrOr01.js} +0 -0
|
@@ -9,6 +9,35 @@ import { store } from "@/core/state/jotai";
|
|
|
9
9
|
import type { IPluginProps } from "../../types";
|
|
10
10
|
import { SliderPlugin } from "../SliderPlugin";
|
|
11
11
|
|
|
12
|
+
vi.mock("@/components/ui/slider", () => ({
|
|
13
|
+
Slider: ({
|
|
14
|
+
disabled,
|
|
15
|
+
onValueChange,
|
|
16
|
+
onValueCommit,
|
|
17
|
+
value,
|
|
18
|
+
}: {
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onValueChange?: (value: number[]) => void;
|
|
21
|
+
onValueCommit?: (value: number[]) => void;
|
|
22
|
+
value: number[];
|
|
23
|
+
}) => (
|
|
24
|
+
<div>
|
|
25
|
+
<button
|
|
26
|
+
aria-label="Slider change"
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={() => onValueChange?.([value[0] + 1])}
|
|
29
|
+
type="button"
|
|
30
|
+
/>
|
|
31
|
+
<button
|
|
32
|
+
aria-label="Slider commit"
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
onClick={() => onValueCommit?.(value)}
|
|
35
|
+
type="button"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
|
|
12
41
|
SetupMocks.resizeObserver();
|
|
13
42
|
|
|
14
43
|
describe("SliderPlugin", () => {
|
|
@@ -51,46 +80,45 @@ describe("SliderPlugin", () => {
|
|
|
51
80
|
const plugin = new SliderPlugin();
|
|
52
81
|
const setValue = vi.fn();
|
|
53
82
|
const props = createProps(false, false, setValue);
|
|
54
|
-
const {
|
|
83
|
+
const { getByRole } = render(plugin.render(props));
|
|
55
84
|
|
|
56
85
|
act(() => {
|
|
57
86
|
vi.advanceTimersByTime(0);
|
|
58
87
|
});
|
|
59
88
|
|
|
60
|
-
const
|
|
61
|
-
expect(thumb).toBeTruthy();
|
|
89
|
+
const changeButton = getByRole("button", { name: "Slider change" });
|
|
62
90
|
|
|
63
|
-
// Radix UI Slider updates on keyboard ArrowRight/ArrowLeft
|
|
64
91
|
act(() => {
|
|
65
|
-
(
|
|
66
|
-
fireEvent.keyDown(thumb!, { key: "ArrowRight" });
|
|
92
|
+
fireEvent.click(changeButton);
|
|
67
93
|
});
|
|
68
94
|
|
|
69
95
|
expect(setValue).toHaveBeenCalledWith(6);
|
|
70
96
|
});
|
|
71
97
|
|
|
72
|
-
it("slider
|
|
98
|
+
it("slider waits until commit before calling setValue when debounce is true", () => {
|
|
73
99
|
const plugin = new SliderPlugin();
|
|
74
100
|
const setValue = vi.fn();
|
|
75
101
|
const props = createProps(true, false, setValue);
|
|
76
|
-
const {
|
|
102
|
+
const { getByRole } = render(plugin.render(props));
|
|
77
103
|
|
|
78
104
|
act(() => {
|
|
79
105
|
vi.advanceTimersByTime(0);
|
|
80
106
|
});
|
|
81
107
|
|
|
82
|
-
const
|
|
108
|
+
const changeButton = getByRole("button", { name: "Slider change" });
|
|
109
|
+
const commitButton = getByRole("button", { name: "Slider commit" });
|
|
83
110
|
|
|
84
111
|
act(() => {
|
|
85
|
-
(
|
|
86
|
-
// Simulate just a programmatic change that Radix would trigger via pointer move
|
|
87
|
-
// which fires onValueChange but not onValueCommit yet
|
|
88
|
-
// Because we can't easily separated Radix's internal pointer events in jsdom, we
|
|
89
|
-
// test the main issue: editable input. We can trust Radix's onValueChange vs onValueCommit.
|
|
112
|
+
fireEvent.click(changeButton);
|
|
90
113
|
});
|
|
91
114
|
|
|
92
|
-
// We verified above that NumberField works when debounce=true
|
|
93
115
|
expect(setValue).not.toHaveBeenCalled();
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
fireEvent.click(commitButton);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(setValue).toHaveBeenCalledWith(6);
|
|
94
122
|
});
|
|
95
123
|
|
|
96
124
|
it("editable input triggers setValue immediately even when slider debounce is true", () => {
|
|
@@ -15,7 +15,11 @@ import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
|
|
|
15
15
|
import { useScript } from "@/hooks/useScript";
|
|
16
16
|
import { Arrays } from "@/utils/arrays";
|
|
17
17
|
import { Objects } from "@/utils/objects";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
extractClickSelection,
|
|
20
|
+
extractIndices,
|
|
21
|
+
extractPoints,
|
|
22
|
+
} from "./selection";
|
|
19
23
|
import { usePlotlyLayout } from "./usePlotlyLayout";
|
|
20
24
|
|
|
21
25
|
interface Data {
|
|
@@ -23,12 +27,9 @@ interface Data {
|
|
|
23
27
|
config: Partial<Plotly.Config>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
type AxisName = string;
|
|
27
|
-
type AxisDatum = unknown;
|
|
28
|
-
|
|
29
30
|
type T =
|
|
30
31
|
| {
|
|
31
|
-
points?: Record<
|
|
32
|
+
points?: Record<string, unknown>[] | Plotly.PlotDatum[];
|
|
32
33
|
indices?: number[];
|
|
33
34
|
range?: {
|
|
34
35
|
x?: number[];
|
|
@@ -202,19 +203,13 @@ export const PlotlyComponent = memo(
|
|
|
202
203
|
if (!evt) {
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
(point) => point.data?.type === "heatmap",
|
|
209
|
-
);
|
|
210
|
-
if (!isHeatmap) {
|
|
206
|
+
|
|
207
|
+
const clickSelection = extractClickSelection(evt);
|
|
208
|
+
if (!clickSelection) {
|
|
211
209
|
return;
|
|
212
210
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
points: extractPoints(evt.points),
|
|
216
|
-
indices: evt.points.map((point) => point.pointIndex),
|
|
217
|
-
}));
|
|
211
|
+
|
|
212
|
+
setValue((prev) => ({ ...prev, ...clickSelection }));
|
|
218
213
|
})}
|
|
219
214
|
onSelected={useEvent((evt: Readonly<Plotly.PlotSelectionEvent>) => {
|
|
220
215
|
if (!evt) {
|
|
@@ -226,7 +221,7 @@ export const PlotlyComponent = memo(
|
|
|
226
221
|
selections:
|
|
227
222
|
"selections" in evt ? (evt.selections as unknown[]) : [],
|
|
228
223
|
points: extractPoints(evt.points),
|
|
229
|
-
indices: evt.points
|
|
224
|
+
indices: extractIndices(evt.points),
|
|
230
225
|
range: evt.range,
|
|
231
226
|
}));
|
|
232
227
|
})}
|
|
@@ -241,54 +236,3 @@ export const PlotlyComponent = memo(
|
|
|
241
236
|
},
|
|
242
237
|
);
|
|
243
238
|
PlotlyComponent.displayName = "PlotlyComponent";
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* This is a hack to extract the points with their original keys,
|
|
247
|
-
* instead of the ones that Plotly uses internally,
|
|
248
|
-
* by using the hovertemplate.
|
|
249
|
-
*/
|
|
250
|
-
const STANDARD_POINT_KEYS: string[] = [
|
|
251
|
-
"x",
|
|
252
|
-
"y",
|
|
253
|
-
"z",
|
|
254
|
-
"lat",
|
|
255
|
-
"lon",
|
|
256
|
-
"curveNumber",
|
|
257
|
-
"pointNumber",
|
|
258
|
-
"pointNumbers",
|
|
259
|
-
"pointIndex",
|
|
260
|
-
];
|
|
261
|
-
|
|
262
|
-
function extractPoints(
|
|
263
|
-
points: Plotly.PlotDatum[],
|
|
264
|
-
): Record<AxisName, AxisDatum>[] {
|
|
265
|
-
if (!points) {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
let parser: PlotlyTemplateParser | undefined;
|
|
270
|
-
|
|
271
|
-
return points.map((point) => {
|
|
272
|
-
const standardPointFields = Objects.pick(point, STANDARD_POINT_KEYS);
|
|
273
|
-
|
|
274
|
-
// Get the first hovertemplate
|
|
275
|
-
const hovertemplate = Array.isArray(point.data.hovertemplate)
|
|
276
|
-
? point.data.hovertemplate[0]
|
|
277
|
-
: point.data.hovertemplate;
|
|
278
|
-
|
|
279
|
-
// For chart types with standard point keys (e.g. heatmaps),
|
|
280
|
-
// or when there's no hovertemplate, pick keys directly from the point.
|
|
281
|
-
if (!hovertemplate || point.data?.type === "heatmap") {
|
|
282
|
-
return standardPointFields;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Update or create a parser
|
|
286
|
-
parser = parser
|
|
287
|
-
? parser.update(hovertemplate)
|
|
288
|
-
: createParser(hovertemplate);
|
|
289
|
-
return {
|
|
290
|
-
...standardPointFields,
|
|
291
|
-
...parser.parse(point),
|
|
292
|
-
};
|
|
293
|
-
});
|
|
294
|
-
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Plotly from "plotly.js";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
extractClickSelection,
|
|
7
|
+
extractIndices,
|
|
8
|
+
extractPoints,
|
|
9
|
+
} from "../selection";
|
|
10
|
+
|
|
11
|
+
interface PlotlyPointInput {
|
|
12
|
+
data: {
|
|
13
|
+
type: string;
|
|
14
|
+
hovertemplate?: string | string[];
|
|
15
|
+
};
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makePoint(point: PlotlyPointInput): Plotly.PlotDatum {
|
|
20
|
+
return point as unknown as Plotly.PlotDatum;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeClickEvent(
|
|
24
|
+
points: Plotly.PlotDatum[],
|
|
25
|
+
): Readonly<Plotly.PlotMouseEvent> {
|
|
26
|
+
return { points } as unknown as Readonly<Plotly.PlotMouseEvent>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("extractIndices", () => {
|
|
30
|
+
it("prefers pointIndex and falls back to pointNumber", () => {
|
|
31
|
+
const points = [
|
|
32
|
+
makePoint({
|
|
33
|
+
pointIndex: 2,
|
|
34
|
+
pointNumber: 99,
|
|
35
|
+
data: { type: "scatter" },
|
|
36
|
+
}),
|
|
37
|
+
makePoint({
|
|
38
|
+
pointNumber: 4,
|
|
39
|
+
data: { type: "scattergl" },
|
|
40
|
+
}),
|
|
41
|
+
makePoint({
|
|
42
|
+
data: { type: "heatmap" },
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
expect(extractIndices(points)).toEqual([2, 4]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("extractPoints", () => {
|
|
51
|
+
it("extracts parsed scatter payload fields from the hovertemplate", () => {
|
|
52
|
+
const points = [
|
|
53
|
+
makePoint({
|
|
54
|
+
x: 3,
|
|
55
|
+
y: 7,
|
|
56
|
+
curveNumber: 0,
|
|
57
|
+
pointIndex: 1,
|
|
58
|
+
customdata: ["B"],
|
|
59
|
+
data: {
|
|
60
|
+
type: "scatter",
|
|
61
|
+
hovertemplate:
|
|
62
|
+
"label=%{customdata[0]}<br>x=%{x}<br>y=%{y}<extra></extra>",
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
expect(extractPoints(points)).toEqual([
|
|
68
|
+
{
|
|
69
|
+
x: 3,
|
|
70
|
+
y: 7,
|
|
71
|
+
curveNumber: 0,
|
|
72
|
+
pointIndex: 1,
|
|
73
|
+
label: "B",
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps standard heatmap keys without hovertemplate parsing", () => {
|
|
79
|
+
const points = [
|
|
80
|
+
makePoint({
|
|
81
|
+
x: "B",
|
|
82
|
+
y: "Row 2",
|
|
83
|
+
z: 6,
|
|
84
|
+
curveNumber: 0,
|
|
85
|
+
pointIndex: 5,
|
|
86
|
+
data: { type: "heatmap", hovertemplate: "ignored=%{z}" },
|
|
87
|
+
}),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
expect(extractPoints(points)).toEqual([
|
|
91
|
+
{
|
|
92
|
+
x: "B",
|
|
93
|
+
y: "Row 2",
|
|
94
|
+
z: 6,
|
|
95
|
+
curveNumber: 0,
|
|
96
|
+
pointIndex: 5,
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("extractClickSelection", () => {
|
|
103
|
+
it("returns undefined for unsupported trace types", () => {
|
|
104
|
+
const event = makeClickEvent([
|
|
105
|
+
makePoint({
|
|
106
|
+
x: "A",
|
|
107
|
+
y: 10,
|
|
108
|
+
pointIndex: 0,
|
|
109
|
+
data: { type: "bar" },
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
expect(extractClickSelection(event)).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns undefined when all points are non-click-selectable trace types", () => {
|
|
117
|
+
// scatter and scattergl use onSelected (box/lasso) for selection, not onClick.
|
|
118
|
+
// Clicks on these traces fire both plotly_click and plotly_selected; the
|
|
119
|
+
// latter provides a range and must be the authoritative source.
|
|
120
|
+
const event = makeClickEvent([
|
|
121
|
+
makePoint({
|
|
122
|
+
x: "ignore",
|
|
123
|
+
y: 1,
|
|
124
|
+
pointIndex: 0,
|
|
125
|
+
data: { type: "bar" },
|
|
126
|
+
}),
|
|
127
|
+
makePoint({
|
|
128
|
+
x: 2,
|
|
129
|
+
y: 5,
|
|
130
|
+
curveNumber: 1,
|
|
131
|
+
pointIndex: 3,
|
|
132
|
+
data: { type: "scatter" },
|
|
133
|
+
}),
|
|
134
|
+
makePoint({
|
|
135
|
+
x: 4,
|
|
136
|
+
y: 12,
|
|
137
|
+
curveNumber: 2,
|
|
138
|
+
pointNumber: 5,
|
|
139
|
+
data: { type: "scattergl" },
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
expect(extractClickSelection(event)).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("filters unsupported points and preserves supported click payloads", () => {
|
|
147
|
+
// bar is unsupported; histogram is supported and its pointNumbers must be
|
|
148
|
+
// forwarded so the backend can recover the exact sample rows.
|
|
149
|
+
const event = makeClickEvent([
|
|
150
|
+
makePoint({
|
|
151
|
+
x: "ignore",
|
|
152
|
+
y: 1,
|
|
153
|
+
pointIndex: 0,
|
|
154
|
+
data: { type: "bar" },
|
|
155
|
+
}),
|
|
156
|
+
makePoint({
|
|
157
|
+
x: 8,
|
|
158
|
+
y: 3,
|
|
159
|
+
curveNumber: 1,
|
|
160
|
+
pointNumber: 2,
|
|
161
|
+
pointNumbers: [4, 5, 6],
|
|
162
|
+
data: { type: "histogram" },
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
expect(extractClickSelection(event)).toEqual({
|
|
167
|
+
selections: [],
|
|
168
|
+
range: undefined,
|
|
169
|
+
indices: [2],
|
|
170
|
+
points: [
|
|
171
|
+
{
|
|
172
|
+
x: 8,
|
|
173
|
+
y: 3,
|
|
174
|
+
curveNumber: 1,
|
|
175
|
+
pointNumber: 2,
|
|
176
|
+
pointNumbers: [4, 5, 6],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("preserves histogram pointNumbers for backend row extraction", () => {
|
|
183
|
+
const event = makeClickEvent([
|
|
184
|
+
makePoint({
|
|
185
|
+
x: 8,
|
|
186
|
+
y: 2,
|
|
187
|
+
curveNumber: 0,
|
|
188
|
+
pointNumber: 3,
|
|
189
|
+
pointNumbers: [6, 7],
|
|
190
|
+
data: { type: "histogram" },
|
|
191
|
+
}),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
expect(extractClickSelection(event)).toEqual({
|
|
195
|
+
selections: [],
|
|
196
|
+
range: undefined,
|
|
197
|
+
indices: [3],
|
|
198
|
+
points: [
|
|
199
|
+
{
|
|
200
|
+
x: 8,
|
|
201
|
+
y: 2,
|
|
202
|
+
curveNumber: 0,
|
|
203
|
+
pointNumber: 3,
|
|
204
|
+
pointNumbers: [6, 7],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("preserves standard heatmap click payloads", () => {
|
|
211
|
+
const event = makeClickEvent([
|
|
212
|
+
makePoint({
|
|
213
|
+
x: "C",
|
|
214
|
+
y: "Row 3",
|
|
215
|
+
z: 11,
|
|
216
|
+
curveNumber: 0,
|
|
217
|
+
pointIndex: 10,
|
|
218
|
+
data: { type: "heatmap" },
|
|
219
|
+
}),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
expect(extractClickSelection(event)).toEqual({
|
|
223
|
+
selections: [],
|
|
224
|
+
range: undefined,
|
|
225
|
+
indices: [10],
|
|
226
|
+
points: [
|
|
227
|
+
{
|
|
228
|
+
x: "C",
|
|
229
|
+
y: "Row 3",
|
|
230
|
+
z: 11,
|
|
231
|
+
curveNumber: 0,
|
|
232
|
+
pointIndex: 10,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { pick } from "lodash-es";
|
|
4
|
+
import type * as Plotly from "plotly.js";
|
|
5
|
+
import { Arrays } from "@/utils/arrays";
|
|
6
|
+
import { createParser, type PlotlyTemplateParser } from "./parse-from-template";
|
|
7
|
+
|
|
8
|
+
type AxisName = string;
|
|
9
|
+
type AxisDatum = unknown;
|
|
10
|
+
|
|
11
|
+
export interface PlotlyClickSelection {
|
|
12
|
+
points: Record<AxisName, AxisDatum>[] | Plotly.PlotDatum[];
|
|
13
|
+
indices: number[];
|
|
14
|
+
range: undefined;
|
|
15
|
+
selections: unknown[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CLICK_SELECTABLE_TRACE_TYPES = new Set([
|
|
19
|
+
"heatmap",
|
|
20
|
+
"histogram",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const STANDARD_POINT_KEYS: string[] = [
|
|
24
|
+
"x",
|
|
25
|
+
"y",
|
|
26
|
+
"z",
|
|
27
|
+
"lat",
|
|
28
|
+
"lon",
|
|
29
|
+
"curveNumber",
|
|
30
|
+
"pointNumber",
|
|
31
|
+
"pointNumbers",
|
|
32
|
+
"pointIndex",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
function getPointIndex(point: Plotly.PlotDatum): number | undefined {
|
|
36
|
+
if (typeof point.pointIndex === "number") {
|
|
37
|
+
return point.pointIndex;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof point.pointNumber === "number") {
|
|
41
|
+
return point.pointNumber;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isClickSelectablePoint(point: Plotly.PlotDatum): boolean {
|
|
48
|
+
const traceType = point.data?.type;
|
|
49
|
+
return typeof traceType === "string"
|
|
50
|
+
? CLICK_SELECTABLE_TRACE_TYPES.has(traceType)
|
|
51
|
+
: false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function extractIndices(points: Plotly.PlotDatum[]): number[] {
|
|
55
|
+
return points.flatMap((point) => {
|
|
56
|
+
const index = getPointIndex(point);
|
|
57
|
+
return typeof index === "number" ? [index] : [];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This is a hack to extract the points with their original keys,
|
|
63
|
+
* instead of the ones that Plotly uses internally,
|
|
64
|
+
* by using the hovertemplate.
|
|
65
|
+
*/
|
|
66
|
+
export function extractPoints(
|
|
67
|
+
points: Plotly.PlotDatum[],
|
|
68
|
+
): Record<AxisName, AxisDatum>[] {
|
|
69
|
+
if (!points) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parser: PlotlyTemplateParser | undefined;
|
|
74
|
+
|
|
75
|
+
return points.map((point) => {
|
|
76
|
+
const standardPointFields = pick(point, STANDARD_POINT_KEYS);
|
|
77
|
+
|
|
78
|
+
// Get the first hovertemplate
|
|
79
|
+
const hovertemplate = Array.isArray(point.data.hovertemplate)
|
|
80
|
+
? point.data.hovertemplate[0]
|
|
81
|
+
: point.data.hovertemplate;
|
|
82
|
+
|
|
83
|
+
// For chart types with standard point keys (e.g. heatmaps),
|
|
84
|
+
// or when there's no hovertemplate, pick keys directly from the point.
|
|
85
|
+
if (!hovertemplate || point.data?.type === "heatmap") {
|
|
86
|
+
return standardPointFields;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update or create a parser
|
|
90
|
+
parser = parser
|
|
91
|
+
? parser.update(hovertemplate)
|
|
92
|
+
: createParser(hovertemplate);
|
|
93
|
+
return {
|
|
94
|
+
...standardPointFields,
|
|
95
|
+
...parser.parse(point),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function extractClickSelection(
|
|
101
|
+
evt: Readonly<Plotly.PlotMouseEvent>,
|
|
102
|
+
): PlotlyClickSelection | undefined {
|
|
103
|
+
if (!evt.points?.length) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const points = evt.points.filter(isClickSelectablePoint);
|
|
108
|
+
if (points.length === 0) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
selections: Arrays.EMPTY,
|
|
114
|
+
points: extractPoints(points),
|
|
115
|
+
indices: extractIndices(points),
|
|
116
|
+
range: undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { B as isArray_default, C as isArrayLike_default, H as _baseGetTag_default, R as identity_default, S as _isIterateeCall_default, T as _baseRest_default, V as isObjectLike_default, f as keysIn_default, k as eq_default } from "./isArrayLikeObject-LXbTYiBa.js";
|
|
2
|
-
import { t as isSymbol_default } from "./isSymbol-DCbjQG_U.js";
|
|
3
|
-
import { i as _castPath_default, n as _baseGet_default, s as _arrayMap_default } from "./get-Wu1vTRhN.js";
|
|
4
|
-
import { n as toFinite_default, t as _baseSet_default } from "./_baseSet-Cs7YTRXk.js";
|
|
5
|
-
import { C as keys_default, E as _baseFindIndex_default, S as _baseFlatten_default, f as _baseIteratee_default, h as _hasPath_default, u as _baseEach_default } from "./_baseUniq--7il0Js0.js";
|
|
6
|
-
function toInteger(f) {
|
|
7
|
-
var V = toFinite_default(f), H = V % 1;
|
|
8
|
-
return V === V ? H ? V - H : V : 0;
|
|
9
|
-
}
|
|
10
|
-
var toInteger_default = toInteger;
|
|
11
|
-
function flatten(f) {
|
|
12
|
-
return f != null && f.length ? _baseFlatten_default(f, 1) : [];
|
|
13
|
-
}
|
|
14
|
-
var flatten_default = flatten, objectProto = Object.prototype, hasOwnProperty$1 = objectProto.hasOwnProperty, defaults_default = _baseRest_default(function(f, V) {
|
|
15
|
-
f = Object(f);
|
|
16
|
-
var H = -1, U = V.length, G = U > 2 ? V[2] : void 0;
|
|
17
|
-
for (G && _isIterateeCall_default(V[0], V[1], G) && (U = 1); ++H < U; ) for (var K = V[H], Y = keysIn_default(K), X = -1, Z = Y.length; ++X < Z; ) {
|
|
18
|
-
var Q = Y[X], $ = f[Q];
|
|
19
|
-
($ === void 0 || eq_default($, objectProto[Q]) && !hasOwnProperty$1.call(f, Q)) && (f[Q] = K[Q]);
|
|
20
|
-
}
|
|
21
|
-
return f;
|
|
22
|
-
});
|
|
23
|
-
function last(f) {
|
|
24
|
-
var V = f == null ? 0 : f.length;
|
|
25
|
-
return V ? f[V - 1] : void 0;
|
|
26
|
-
}
|
|
27
|
-
var last_default = last;
|
|
28
|
-
function createFind(f) {
|
|
29
|
-
return function(H, U, W) {
|
|
30
|
-
var G = Object(H);
|
|
31
|
-
if (!isArrayLike_default(H)) {
|
|
32
|
-
var K = _baseIteratee_default(U, 3);
|
|
33
|
-
H = keys_default(H), U = function(f2) {
|
|
34
|
-
return K(G[f2], f2, G);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
var q = f(H, U, W);
|
|
38
|
-
return q > -1 ? G[K ? H[q] : q] : void 0;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
var _createFind_default = createFind, nativeMax = Math.max;
|
|
42
|
-
function findIndex(f, V, H) {
|
|
43
|
-
var U = f == null ? 0 : f.length;
|
|
44
|
-
if (!U) return -1;
|
|
45
|
-
var W = H == null ? 0 : toInteger_default(H);
|
|
46
|
-
return W < 0 && (W = nativeMax(U + W, 0)), _baseFindIndex_default(f, _baseIteratee_default(V, 3), W);
|
|
47
|
-
}
|
|
48
|
-
var find_default = _createFind_default(findIndex);
|
|
49
|
-
function baseMap(f, H) {
|
|
50
|
-
var U = -1, W = isArrayLike_default(f) ? Array(f.length) : [];
|
|
51
|
-
return _baseEach_default(f, function(f2, V, G) {
|
|
52
|
-
W[++U] = H(f2, V, G);
|
|
53
|
-
}), W;
|
|
54
|
-
}
|
|
55
|
-
var _baseMap_default = baseMap;
|
|
56
|
-
function map(V, H) {
|
|
57
|
-
return (isArray_default(V) ? _arrayMap_default : _baseMap_default)(V, _baseIteratee_default(H, 3));
|
|
58
|
-
}
|
|
59
|
-
var map_default = map, hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
60
|
-
function baseHas(f, V) {
|
|
61
|
-
return f != null && hasOwnProperty.call(f, V);
|
|
62
|
-
}
|
|
63
|
-
var _baseHas_default = baseHas;
|
|
64
|
-
function has(f, V) {
|
|
65
|
-
return f != null && _hasPath_default(f, V, _baseHas_default);
|
|
66
|
-
}
|
|
67
|
-
var has_default = has, stringTag = "[object String]";
|
|
68
|
-
function isString(V) {
|
|
69
|
-
return typeof V == "string" || !isArray_default(V) && isObjectLike_default(V) && _baseGetTag_default(V) == stringTag;
|
|
70
|
-
}
|
|
71
|
-
var isString_default = isString;
|
|
72
|
-
function baseLt(f, V) {
|
|
73
|
-
return f < V;
|
|
74
|
-
}
|
|
75
|
-
var _baseLt_default = baseLt;
|
|
76
|
-
function baseExtremum(f, V, H) {
|
|
77
|
-
for (var U = -1, W = f.length; ++U < W; ) {
|
|
78
|
-
var G = f[U], K = V(G);
|
|
79
|
-
if (K != null && (q === void 0 ? K === K && !isSymbol_default(K) : H(K, q))) var q = K, J = G;
|
|
80
|
-
}
|
|
81
|
-
return J;
|
|
82
|
-
}
|
|
83
|
-
var _baseExtremum_default = baseExtremum;
|
|
84
|
-
function min(f) {
|
|
85
|
-
return f && f.length ? _baseExtremum_default(f, identity_default, _baseLt_default) : void 0;
|
|
86
|
-
}
|
|
87
|
-
var min_default = min;
|
|
88
|
-
function basePickBy(f, V, H) {
|
|
89
|
-
for (var U = -1, W = V.length, G = {}; ++U < W; ) {
|
|
90
|
-
var K = V[U], q = _baseGet_default(f, K);
|
|
91
|
-
H(q, K) && _baseSet_default(G, _castPath_default(K, f), q);
|
|
92
|
-
}
|
|
93
|
-
return G;
|
|
94
|
-
}
|
|
95
|
-
var _basePickBy_default = basePickBy;
|
|
96
|
-
export {
|
|
97
|
-
isString_default as a,
|
|
98
|
-
_baseMap_default as c,
|
|
99
|
-
defaults_default as d,
|
|
100
|
-
flatten_default as f,
|
|
101
|
-
_baseLt_default as i,
|
|
102
|
-
find_default as l,
|
|
103
|
-
min_default as n,
|
|
104
|
-
has_default as o,
|
|
105
|
-
toInteger_default as p,
|
|
106
|
-
_baseExtremum_default as r,
|
|
107
|
-
map_default as s,
|
|
108
|
-
_basePickBy_default as t,
|
|
109
|
-
last_default as u
|
|
110
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { O as _assignValue_default, j as _isIndex_default, z as isObject_default } from "./isArrayLikeObject-LXbTYiBa.js";
|
|
2
|
-
import { i as _castPath_default, r as _toKey_default } from "./get-Wu1vTRhN.js";
|
|
3
|
-
import { t as toNumber_default } from "./toNumber-55tjPCWr.js";
|
|
4
|
-
var INFINITY = Infinity, MAX_INTEGER = 17976931348623157e292;
|
|
5
|
-
function toFinite(e) {
|
|
6
|
-
return e ? (e = toNumber_default(e), e === INFINITY || e === -INFINITY ? (e < 0 ? -1 : 1) * MAX_INTEGER : e === e ? e : 0) : e === 0 ? e : 0;
|
|
7
|
-
}
|
|
8
|
-
var toFinite_default = toFinite;
|
|
9
|
-
function baseSet(a, o, s, c) {
|
|
10
|
-
if (!isObject_default(a)) return a;
|
|
11
|
-
o = _castPath_default(o, a);
|
|
12
|
-
for (var l = -1, u = o.length, d = u - 1, f = a; f != null && ++l < u; ) {
|
|
13
|
-
var p = _toKey_default(o[l]), m = s;
|
|
14
|
-
if (p === "__proto__" || p === "constructor" || p === "prototype") return a;
|
|
15
|
-
if (l != d) {
|
|
16
|
-
var h = f[p];
|
|
17
|
-
m = c ? c(h, p, f) : void 0, m === void 0 && (m = isObject_default(h) ? h : _isIndex_default(o[l + 1]) ? [] : {});
|
|
18
|
-
}
|
|
19
|
-
_assignValue_default(f, p, m), f = f[p];
|
|
20
|
-
}
|
|
21
|
-
return a;
|
|
22
|
-
}
|
|
23
|
-
var _baseSet_default = baseSet;
|
|
24
|
-
export {
|
|
25
|
-
toFinite_default as n,
|
|
26
|
-
_baseSet_default as t
|
|
27
|
-
};
|