@marimo-team/frontend 0.22.6-dev9 → 0.23.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.
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-DXwhLiKr.js"></script>
69
+ <script type="module" crossorigin src="./assets/index-DW6VcSzY.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/frontend",
3
- "version": "0.22.6-dev9",
3
+ "version": "0.23.0",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -19,6 +19,7 @@ import {
19
19
  extractPoints,
20
20
  extractSunburstPoints,
21
21
  extractTreemapPoints,
22
+ hasAreaTrace,
22
23
  hasPureLineTrace,
23
24
  lineSelectionButtons,
24
25
  type ModeBarButton,
@@ -113,7 +114,8 @@ export const PlotlyComponent = memo(
113
114
 
114
115
  const configMemo = useDeepCompareMemoize(config);
115
116
  const plotlyConfig = useMemo((): Partial<Plotly.Config> => {
116
- const hasPureLine = hasPureLineTrace(figure.data);
117
+ const hasLineOrAreaTrace =
118
+ hasPureLineTrace(figure.data) || hasAreaTrace(figure.data);
117
119
  const defaultButtons: ModeBarButton[] = [
118
120
  // Custom button to reset the state
119
121
  {
@@ -130,7 +132,7 @@ export const PlotlyComponent = memo(
130
132
  click: handleResetWithClear,
131
133
  },
132
134
  ];
133
- if (hasPureLine) {
135
+ if (hasLineOrAreaTrace) {
134
136
  defaultButtons.push(...lineSelectionButtons(handleSetDragmode));
135
137
  }
136
138
 
@@ -111,4 +111,54 @@ describe("PlotlyPlugin", () => {
111
111
  range: undefined,
112
112
  });
113
113
  });
114
+
115
+ it("clicking a violin element triggers onClick", async () => {
116
+ const setValue = vi.fn<Setter<unknown>>();
117
+
118
+ render(
119
+ <Suspense fallback={null}>
120
+ <PlotlyComponent
121
+ figure={{
122
+ data: [{ type: "violin" }],
123
+ layout: {},
124
+ frames: null,
125
+ }}
126
+ value={undefined}
127
+ setValue={setValue}
128
+ host={document.createElement("div")}
129
+ config={{}}
130
+ />
131
+ </Suspense>,
132
+ );
133
+
134
+ await waitFor(() => {
135
+ expect(capturedPlotProps).not.toBeNull();
136
+ });
137
+
138
+ act(() => {
139
+ capturedPlotProps?.onClick?.({
140
+ points: [
141
+ {
142
+ data: { type: "violin" },
143
+ x: "Group A",
144
+ y: 3,
145
+ pointIndex: 0,
146
+ pointNumber: 0,
147
+ curveNumber: 0,
148
+ },
149
+ ],
150
+ });
151
+ });
152
+
153
+ expect(setValue).toHaveBeenCalledTimes(1);
154
+ const updater = setValue.mock.calls[0][0] as (value: unknown) => unknown;
155
+ expect(updater({})).toEqual({
156
+ selections: [],
157
+ points: [
158
+ { x: "Group A", y: 3, curveNumber: 0, pointNumber: 0, pointIndex: 0 },
159
+ ],
160
+ indices: [0],
161
+ range: undefined,
162
+ });
163
+ });
114
164
  });
@@ -5,6 +5,7 @@ import { describe, expect, it, vi } from "vitest";
5
5
  import {
6
6
  extractIndices,
7
7
  extractPoints,
8
+ hasAreaTrace,
8
9
  hasPureLineTrace,
9
10
  lineSelectionButtons,
10
11
  type ModeBarButton,
@@ -101,6 +102,14 @@ describe("shouldHandleClickSelection", () => {
101
102
  expect(shouldHandleClickSelection([heatmapPoint])).toBe(true);
102
103
  });
103
104
 
105
+ it("accepts violin clicks", () => {
106
+ const violinPoint = createPlotDatum({
107
+ data: { type: "violin" },
108
+ });
109
+
110
+ expect(shouldHandleClickSelection([violinPoint])).toBe(true);
111
+ });
112
+
104
113
  it("accepts histogram clicks", () => {
105
114
  const histogramPoint = createPlotDatum({
106
115
  data: { type: "histogram" },
@@ -219,3 +228,67 @@ describe("extractPoints", () => {
219
228
  ]);
220
229
  });
221
230
  });
231
+
232
+ describe("hasAreaTrace", () => {
233
+ it("detects scatter trace with tozeroy fill", () => {
234
+ expect(
235
+ hasAreaTrace([createTrace({ type: "scatter", fill: "tozeroy" })]),
236
+ ).toBe(true);
237
+ });
238
+
239
+ it("detects scatter trace with tonexty fill", () => {
240
+ expect(
241
+ hasAreaTrace([createTrace({ type: "scatter", fill: "tonexty" })]),
242
+ ).toBe(true);
243
+ });
244
+
245
+ it("detects scatter trace with stackgroup (px.area pattern)", () => {
246
+ expect(
247
+ hasAreaTrace([
248
+ createTrace({ type: "scatter", mode: "lines", stackgroup: "one" }),
249
+ ]),
250
+ ).toBe(true);
251
+ });
252
+
253
+ it("detects area traces with mode=none (fill-only, no visible line)", () => {
254
+ expect(
255
+ hasAreaTrace([
256
+ createTrace({ type: "scatter", fill: "tozeroy", mode: "none" }),
257
+ ]),
258
+ ).toBe(true);
259
+ });
260
+
261
+ it("ignores scatter traces with no fill and no stackgroup", () => {
262
+ expect(
263
+ hasAreaTrace([
264
+ createTrace({ type: "scatter", mode: "lines" }),
265
+ createTrace({ type: "scatter", mode: "markers" }),
266
+ ]),
267
+ ).toBe(false);
268
+ });
269
+
270
+ it("ignores scatter traces with fill=none", () => {
271
+ expect(hasAreaTrace([createTrace({ type: "scatter", fill: "none" })])).toBe(
272
+ false,
273
+ );
274
+ });
275
+
276
+ it("ignores scatter traces with fill=empty string", () => {
277
+ expect(
278
+ hasAreaTrace([createTrace({ type: "scatter", fill: "" as "none" })]),
279
+ ).toBe(false);
280
+ });
281
+
282
+ it("ignores non-scatter traces", () => {
283
+ expect(
284
+ hasAreaTrace([
285
+ createTrace({ type: "bar" }),
286
+ createTrace({ type: "heatmap" }),
287
+ ]),
288
+ ).toBe(false);
289
+ });
290
+
291
+ it("returns false for undefined data", () => {
292
+ expect(hasAreaTrace(undefined)).toBe(false);
293
+ });
294
+ });
@@ -141,13 +141,44 @@ export function hasPureLineTrace(
141
141
  }
142
142
 
143
143
  return data.some((trace) => {
144
- const traceType = (trace as { type?: unknown }).type;
144
+ const t = trace as Record<string, unknown>;
145
145
  const isScatterLike =
146
- traceType === undefined || LINE_CLICK_TRACE_TYPES.has(String(traceType));
146
+ t.type === undefined || LINE_CLICK_TRACE_TYPES.has(String(t.type));
147
147
  if (!isScatterLike) {
148
148
  return false;
149
149
  }
150
- return isPureLineMode((trace as { mode?: unknown }).mode);
150
+ return isPureLineMode(t.mode);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Return true when any scatter/scattergl trace has a non-empty fill or a
156
+ * stackgroup, i.e. it is an area chart.
157
+ *
158
+ * Area traces built with `mode="none"` have no visible line or markers, so
159
+ * `hasPureLineTrace` returns false for them even though they need select/lasso
160
+ * buttons just as much as `mode="lines"` area charts. This function covers
161
+ * that gap and is OR-ed with `hasPureLineTrace` in the config builder.
162
+ */
163
+ export function hasAreaTrace(
164
+ data: readonly Plotly.Data[] | undefined,
165
+ ): boolean {
166
+ if (!data) {
167
+ return false;
168
+ }
169
+
170
+ return data.some((trace) => {
171
+ const t = trace as Record<string, unknown>;
172
+ // Only scatter/scattergl can be area traces.
173
+ if (t.type !== undefined && !LINE_CLICK_TRACE_TYPES.has(String(t.type))) {
174
+ return false;
175
+ }
176
+ // A trace is an area trace when fill is a non-empty string other than
177
+ // "none", OR it belongs to a stackgroup (px.area always sets stackgroup).
178
+ return (
179
+ (typeof t.fill === "string" && t.fill !== "" && t.fill !== "none") ||
180
+ t.stackgroup != null
181
+ );
151
182
  });
152
183
  }
153
184
 
@@ -228,6 +259,7 @@ export function shouldHandleClickSelection(
228
259
  type === "heatmap" ||
229
260
  type === "histogram" ||
230
261
  type === "waterfall" ||
262
+ type === "violin" ||
231
263
  isLinePoint(point)
232
264
  );
233
265
  });