@marimo-team/frontend 0.22.6-dev4 → 0.22.6-dev5
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/index.html
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
|
|
67
67
|
<!-- /TODO -->
|
|
68
68
|
<title>{{ title }}</title>
|
|
69
|
-
<script type="module" crossorigin src="./assets/index-
|
|
69
|
+
<script type="module" crossorigin src="./assets/index-qrM_kwoL.js"></script>
|
|
70
70
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-D2MJg03u.js">
|
|
71
71
|
<link rel="modulepreload" crossorigin href="./assets/chunk-LvLJmgfZ.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="./assets/react-Bj1aDYRI.js">
|
package/package.json
CHANGED
|
@@ -8,11 +8,15 @@ import {
|
|
|
8
8
|
computeLayoutUpdate,
|
|
9
9
|
computeOmitKeys,
|
|
10
10
|
createInitialLayout,
|
|
11
|
+
hasCompatibleTraces,
|
|
11
12
|
} from "../usePlotlyLayout";
|
|
12
13
|
|
|
13
|
-
function createFigure(
|
|
14
|
+
function createFigure(
|
|
15
|
+
layoutOverrides: Partial<Plotly.Layout> = {},
|
|
16
|
+
data: Plotly.Data[] = [],
|
|
17
|
+
): Figure {
|
|
14
18
|
return {
|
|
15
|
-
data
|
|
19
|
+
data,
|
|
16
20
|
layout: { ...layoutOverrides } as Plotly.Layout,
|
|
17
21
|
frames: null,
|
|
18
22
|
};
|
|
@@ -35,9 +39,46 @@ describe("createInitialLayout", () => {
|
|
|
35
39
|
});
|
|
36
40
|
});
|
|
37
41
|
|
|
42
|
+
describe("hasCompatibleTraces", () => {
|
|
43
|
+
it("returns true for same trace types", () => {
|
|
44
|
+
const a = createFigure({}, [{ type: "scatter" } as Plotly.Data]);
|
|
45
|
+
const b = createFigure({}, [{ type: "scatter" } as Plotly.Data]);
|
|
46
|
+
expect(hasCompatibleTraces(a, b)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns true for default scatter types (undefined type)", () => {
|
|
50
|
+
const a = createFigure({}, [{} as Plotly.Data]);
|
|
51
|
+
const b = createFigure({}, [{ type: "scatter" } as Plotly.Data]);
|
|
52
|
+
expect(hasCompatibleTraces(a, b)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns false for different trace types", () => {
|
|
56
|
+
const a = createFigure({}, [{ type: "scatter" } as Plotly.Data]);
|
|
57
|
+
const b = createFigure({}, [{ type: "histogram" } as Plotly.Data]);
|
|
58
|
+
expect(hasCompatibleTraces(a, b)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns false for different number of traces", () => {
|
|
62
|
+
const a = createFigure({}, [{ type: "scatter" } as Plotly.Data]);
|
|
63
|
+
const b = createFigure({}, [
|
|
64
|
+
{ type: "scatter" } as Plotly.Data,
|
|
65
|
+
{ type: "scatter" } as Plotly.Data,
|
|
66
|
+
]);
|
|
67
|
+
expect(hasCompatibleTraces(a, b)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns true for empty data arrays", () => {
|
|
71
|
+
const a = createFigure({}, []);
|
|
72
|
+
const b = createFigure({}, []);
|
|
73
|
+
expect(hasCompatibleTraces(a, b)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
38
77
|
describe("computeLayoutOnFigureChange", () => {
|
|
39
|
-
it("preserves only dragmode/xaxis/yaxis from previous layout (#7964)", () => {
|
|
40
|
-
const
|
|
78
|
+
it("preserves only dragmode/xaxis/yaxis from previous layout for compatible traces (#7964)", () => {
|
|
79
|
+
const scatterData = [{ type: "scatter" } as Plotly.Data];
|
|
80
|
+
const prevFigure = createFigure({}, scatterData);
|
|
81
|
+
const nextFigure = createFigure({ title: { text: "New" } }, scatterData);
|
|
41
82
|
const prevLayout: Partial<Plotly.Layout> = {
|
|
42
83
|
dragmode: "zoom",
|
|
43
84
|
xaxis: { range: [0, 10] },
|
|
@@ -46,7 +87,11 @@ describe("computeLayoutOnFigureChange", () => {
|
|
|
46
87
|
annotations: [{ text: "Old", x: 0, y: 0 }],
|
|
47
88
|
};
|
|
48
89
|
|
|
49
|
-
const result = computeLayoutOnFigureChange(
|
|
90
|
+
const result = computeLayoutOnFigureChange(
|
|
91
|
+
nextFigure,
|
|
92
|
+
prevFigure,
|
|
93
|
+
prevLayout,
|
|
94
|
+
);
|
|
50
95
|
|
|
51
96
|
// Preserved from prev
|
|
52
97
|
expect(result.dragmode).toBe("zoom");
|
|
@@ -59,15 +104,68 @@ describe("computeLayoutOnFigureChange", () => {
|
|
|
59
104
|
expect(result.annotations).toBeUndefined();
|
|
60
105
|
});
|
|
61
106
|
|
|
107
|
+
it("resets axis settings when trace types change (#5898)", () => {
|
|
108
|
+
const prevFigure = createFigure({}, [{ type: "histogram" } as Plotly.Data]);
|
|
109
|
+
const nextFigure = createFigure({ title: { text: "Bar" } }, [
|
|
110
|
+
{ type: "bar" } as Plotly.Data,
|
|
111
|
+
]);
|
|
112
|
+
const prevLayout: Partial<Plotly.Layout> = {
|
|
113
|
+
dragmode: "zoom",
|
|
114
|
+
xaxis: { range: [-3, 3] },
|
|
115
|
+
yaxis: { range: [0, 200] },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = computeLayoutOnFigureChange(
|
|
119
|
+
nextFigure,
|
|
120
|
+
prevFigure,
|
|
121
|
+
prevLayout,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Dragmode is still preserved
|
|
125
|
+
expect(result.dragmode).toBe("zoom");
|
|
126
|
+
// Axis settings are NOT preserved — they come from the new figure's layout
|
|
127
|
+
expect(result.xaxis).toBeUndefined();
|
|
128
|
+
expect(result.yaxis).toBeUndefined();
|
|
129
|
+
// New figure's layout is applied
|
|
130
|
+
expect(result.title).toEqual({ text: "Bar" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("preserves nextFigure dragmode when prevLayout has no dragmode", () => {
|
|
134
|
+
const prevFigure = createFigure({}, [{ type: "histogram" } as Plotly.Data]);
|
|
135
|
+
const nextFigure = createFigure({ dragmode: "lasso" }, [
|
|
136
|
+
{ type: "bar" } as Plotly.Data,
|
|
137
|
+
]);
|
|
138
|
+
const prevLayout: Partial<Plotly.Layout> = {
|
|
139
|
+
xaxis: { range: [0, 10] },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = computeLayoutOnFigureChange(
|
|
143
|
+
nextFigure,
|
|
144
|
+
prevFigure,
|
|
145
|
+
prevLayout,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// nextFigure.layout.dragmode should be preserved via base, not overwritten
|
|
149
|
+
expect(result.dragmode).toBe("lasso");
|
|
150
|
+
// Axis settings are NOT preserved for incompatible traces
|
|
151
|
+
expect(result.xaxis).toBeUndefined();
|
|
152
|
+
expect(result.yaxis).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
62
155
|
it("uses shapes from new figure, not previous layout", () => {
|
|
63
156
|
const nextFigure = createFigure({
|
|
64
157
|
shapes: [{ type: "circle", x0: 0, x1: 1, y0: 0, y1: 1 }],
|
|
65
158
|
});
|
|
159
|
+
const prevFigure = createFigure({});
|
|
66
160
|
const prevLayout: Partial<Plotly.Layout> = {
|
|
67
161
|
shapes: [{ type: "rect", x0: 0, x1: 1, y0: 0, y1: 1 }],
|
|
68
162
|
};
|
|
69
163
|
|
|
70
|
-
const result = computeLayoutOnFigureChange(
|
|
164
|
+
const result = computeLayoutOnFigureChange(
|
|
165
|
+
nextFigure,
|
|
166
|
+
prevFigure,
|
|
167
|
+
prevLayout,
|
|
168
|
+
);
|
|
71
169
|
|
|
72
170
|
expect(result.shapes).toHaveLength(1);
|
|
73
171
|
expect(result.shapes?.[0].type).toBe("circle");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { usePrevious } from "@uidotdev/usehooks";
|
|
4
4
|
import { dequal as isEqual } from "dequal";
|
|
5
5
|
import type * as Plotly from "plotly.js";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { Objects } from "@/utils/objects";
|
|
8
8
|
import type { Figure } from "./Plot";
|
|
9
9
|
|
|
@@ -39,18 +39,46 @@ export function createInitialLayout(figure: Figure): Partial<Plotly.Layout> {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if two figures have compatible trace types.
|
|
44
|
+
* When traces are incompatible (different types, count, or order), axis settings
|
|
45
|
+
* from the old figure should not be preserved as they would distort the
|
|
46
|
+
* new chart. See https://github.com/marimo-team/marimo/issues/5898
|
|
47
|
+
*/
|
|
48
|
+
export function hasCompatibleTraces(prev: Figure, next: Figure): boolean {
|
|
49
|
+
if (prev.data.length !== next.data.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return prev.data.every(
|
|
53
|
+
(trace, i) =>
|
|
54
|
+
(trace.type ?? "scatter") === (next.data[i]?.type ?? "scatter"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
/**
|
|
43
59
|
* Computes the updated layout when the figure changes.
|
|
44
60
|
* Preserves user-interaction values (dragmode, xaxis, yaxis) while
|
|
45
61
|
* taking everything else from the new figure's layout.
|
|
62
|
+
*
|
|
63
|
+
* When trace types change, only dragmode is preserved — axis settings
|
|
64
|
+
* are reset to let Plotly auto-compute ranges for the new chart type.
|
|
46
65
|
*/
|
|
47
66
|
export function computeLayoutOnFigureChange(
|
|
48
67
|
nextFigure: Figure,
|
|
68
|
+
prevFigure: Figure,
|
|
49
69
|
prevLayout: Partial<Plotly.Layout>,
|
|
50
70
|
): Partial<Plotly.Layout> {
|
|
71
|
+
const base = createInitialLayout(nextFigure);
|
|
72
|
+
if (hasCompatibleTraces(prevFigure, nextFigure)) {
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
...Objects.pick(prevLayout, PERSISTED_LAYOUT_KEYS),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Incompatible traces — only preserve dragmode, not axis settings
|
|
51
79
|
return {
|
|
52
|
-
...
|
|
53
|
-
...
|
|
80
|
+
...base,
|
|
81
|
+
...("dragmode" in prevLayout ? { dragmode: prevLayout.dragmode } : {}),
|
|
54
82
|
};
|
|
55
83
|
}
|
|
56
84
|
|
|
@@ -121,6 +149,9 @@ export function usePlotlyLayout({
|
|
|
121
149
|
return structuredClone(originalFigure);
|
|
122
150
|
});
|
|
123
151
|
|
|
152
|
+
// Track the previous figure to detect trace type changes
|
|
153
|
+
const prevFigureRef = useRef(figure);
|
|
154
|
+
|
|
124
155
|
const [layout, setLayout] = useState<Partial<Plotly.Layout>>(() => {
|
|
125
156
|
return {
|
|
126
157
|
...createInitialLayout(figure),
|
|
@@ -132,12 +163,15 @@ export function usePlotlyLayout({
|
|
|
132
163
|
// Update figure and layout when originalFigure changes
|
|
133
164
|
useEffect(() => {
|
|
134
165
|
const nextFigure = structuredClone(originalFigure);
|
|
166
|
+
const prevFig = prevFigureRef.current;
|
|
167
|
+
prevFigureRef.current = nextFigure;
|
|
135
168
|
setFigure(nextFigure);
|
|
136
169
|
// Start with the new figure's layout, then only preserve user-interaction
|
|
137
170
|
// values (dragmode, xaxis, yaxis) from the previous layout.
|
|
138
171
|
// We don't want to preserve other properties like `shapes` from the previous
|
|
139
172
|
// layout, as they should be fully controlled by the figure prop.
|
|
140
|
-
|
|
173
|
+
// When trace types change, axis settings are reset to avoid distortion (#5898).
|
|
174
|
+
setLayout((prev) => computeLayoutOnFigureChange(nextFigure, prevFig, prev));
|
|
141
175
|
}, [originalFigure, isScriptLoaded]);
|
|
142
176
|
|
|
143
177
|
const prevFigure = usePrevious(figure) ?? figure;
|