@marimo-team/frontend 0.23.16-dev26 → 0.23.16-dev29
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/{CellStatus-DwV4gbV3.js → CellStatus-CmojVYzm.js} +1 -1
- package/dist/assets/{JsonOutput-BG5T3DG2.js → JsonOutput-BwwRpByA.js} +12 -12
- package/dist/assets/{MarimoErrorOutput-C2HrF0bD.js → MarimoErrorOutput-BWte8vjk.js} +5 -5
- package/dist/assets/{RenderHTML-DoCFSir5.js → RenderHTML-DRltV71H.js} +1 -1
- package/dist/assets/{RunButton-COtmJuup.js → RunButton-DIegd2mN.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BBAhBpT1.js → add-cell-with-ai-BBFD-M9V.js} +12 -12
- package/dist/assets/{add-connection-dialog-DFD5BLGa.js → add-connection-dialog-CECJ8507.js} +1 -1
- package/dist/assets/{agent-panel-TJGdsxOX.js → agent-panel-YaS0WXP1.js} +5 -5
- package/dist/assets/{ai-model-dropdown-BMS30HMu.js → ai-model-dropdown-BpVBUZnz.js} +4 -4
- package/dist/assets/{app-config-button-Cankwixh.js → app-config-button-BW-w01aF.js} +1 -1
- package/dist/assets/{cell-editor-sBYL591r.js → cell-editor-EV-1IccQ.js} +10 -10
- package/dist/assets/{cell-link-D1nZ5myN.js → cell-link-C5gpAn3R.js} +1 -1
- package/dist/assets/{cells-D9tCNe6o.js → cells-6yd8nOAu.js} +45 -45
- package/dist/assets/{chat-display-ZB0n2r4U.js → chat-display-8BbC5B-D.js} +1 -1
- package/dist/assets/{chat-panel-BKqRvvhb.js → chat-panel-00V3uFDx.js} +1 -1
- package/dist/assets/{chat-ui-B3Pz1Tc4.js → chat-ui-DWhJ2Ozy.js} +1 -1
- package/dist/assets/{column-preview-CSVQtCQS.js → column-preview-DeXESYpM.js} +1 -1
- package/dist/assets/{command-palette-DiUt8L1K.js → command-palette-DZ2rNzzf.js} +1 -1
- package/dist/assets/{common-XBd1qRDk.js → common-XVELED_O.js} +1 -1
- package/dist/assets/{components-D-aPO6md.js → components-BLHpmhXx.js} +1 -1
- package/dist/assets/{datasource-Pl4KKOUa.js → datasource-CTqa8bf-.js} +2 -2
- package/dist/assets/{dependency-graph-panel-rRXAafZ4.js → dependency-graph-panel-C4Nzcddp.js} +3 -3
- package/dist/assets/documentation-panel-Bym4G7IS.js +1 -0
- package/dist/assets/{download-Benw6kUY.js → download-HKgqJX4l.js} +3 -3
- package/dist/assets/{edit-page-CxNNXB4Y.js → edit-page-BPBDN3FB.js} +6 -6
- package/dist/assets/{error-entries-CmfGHGq1.js → error-entries-CxyvaxEM.js} +2 -2
- package/dist/assets/{error-panel-BKb_UksO.js → error-panel-CfSy7uUr.js} +1 -1
- package/dist/assets/{file-explorer-panel-C9I7XP84.js → file-explorer-panel-Dd5Xn2TK.js} +4 -4
- package/dist/assets/{file-icons-Czm7CWoc.js → file-icons-CSjJ3JC6.js} +1 -1
- package/dist/assets/{file-name-input-CjWG0FWT.js → file-name-input-CrKtrXMl.js} +1 -1
- package/dist/assets/{floating-outline-BWk7h_Uf.js → floating-outline-ByQ8vvif.js} +1 -1
- package/dist/assets/{focus-DTmSq3hN.js → focus-CVONl-Dy.js} +1 -1
- package/dist/assets/{form-DrCvDcRK.js → form-Ca3yd_Od.js} +1 -1
- package/dist/assets/{home-page-DwtZlFRD.js → home-page-FpAawj9s.js} +2 -2
- package/dist/assets/{html-to-image-1vflBYah.js → html-to-image-C-4ysbGe.js} +1 -1
- package/dist/assets/{index-DK7_phcn.js → index-BJDqUUF2.js} +6 -6
- package/dist/assets/{layout-Cdpz2zk4.js → layout-Dtql_o_T.js} +5 -5
- package/dist/assets/{logs-panel-BojuPimp.js → logs-panel-5u1wUBn1.js} +1 -1
- package/dist/assets/{markdown-renderer-C9vdWclk.js → markdown-renderer-dABDqZDw.js} +1 -1
- package/dist/assets/{name-cell-input-BSFsnuTp.js → name-cell-input-Y4knmEIU.js} +1 -1
- package/dist/assets/{outline-panel-B1f5a2yI.js → outline-panel-CQ_KWLip.js} +1 -1
- package/dist/assets/{packages-panel-kj52vQTZ.js → packages-panel-Ijt7d51P.js} +1 -1
- package/dist/assets/{panels-Bs7O_Hac.js → panels-Nt5IZl-9.js} +2 -2
- package/dist/assets/{process-output-Csd6PmBH.js → process-output-Dd11YI04.js} +1 -1
- package/dist/assets/{radio-group-CdR34W0K.js → radio-group-22eWJ9fv.js} +1 -1
- package/dist/assets/{readonly-code-display-XBAHhCYR.js → readonly-code-display-BjK-TEri.js} +1 -1
- package/dist/assets/{readonly-python-code-3UXLXM0g.js → readonly-python-code-n_TrLmsR.js} +1 -1
- package/dist/assets/{reveal-component-DTcc6OYd.js → reveal-component-CCLTV1q6.js} +42 -42
- package/dist/assets/{run-page-BgzVSOZh.js → run-page-CtYo0xcq.js} +1 -1
- package/dist/assets/scratchpad-panel-C2wAEv5n.js +1 -0
- package/dist/assets/{session-panel-Cs9YKkJc.js → session-panel-BZGNcZmp.js} +1 -1
- package/dist/assets/{skeleton-BOtI6k5P.js → skeleton-BzlKcq21.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-ACMhA8oL.js → snippets-panel-DNaNtkR0.js} +1 -1
- package/dist/assets/{state-Dj8qTUAp.js → state-B7LxUcAy.js} +3 -3
- package/dist/assets/{state-N0U-K9F6.js → state-jCdh8Dcx.js} +1 -1
- package/dist/assets/{textarea-BQE_pb7P.js → textarea-DuEtSOCl.js} +1 -1
- package/dist/assets/{tracing-panel-CmMG0R-H.js → tracing-panel-rVAa3En0.js} +2 -2
- package/dist/assets/{tracing-CuJfR83E.js → tracing-ttUd3J7U.js} +1 -1
- package/dist/assets/{tree-actions-DJTGQWz0.js → tree-actions-UCr1tMHw.js} +1 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +1 -0
- package/dist/assets/{useDeleteCell-DsWH8wmn.js → useDeleteCell-PrgejW8A.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DjhiptCs.js → useDependencyPanelTab-5jc-_-hy.js} +1 -1
- package/dist/assets/{useNotebookActions-Bj0q_nkr.js → useNotebookActions-DG8f4m8p.js} +3 -3
- package/dist/assets/useRunCells-DHsd7HGL.js +1 -0
- package/dist/assets/{useSplitCell-CapBPXO1.js → useSplitCell-MZpER0Bh.js} +1 -1
- package/dist/index.html +22 -22
- package/package.json +1 -1
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/reveal-component.tsx +22 -17
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slides.css +4 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/utils.ts +28 -0
- package/dist/assets/documentation-panel-DSOMQtKU.js +0 -1
- package/dist/assets/scratchpad-panel-XnaU8Prb.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/useCellActionButton-Bglkc3w8.js +0 -1
- package/dist/assets/useRunCells-BpMmDGqa.js +0 -1
- /package/dist/assets/{swiper-component-Che9MNs4.js → swiper-component-g8EXE911.js} +0 -0
|
@@ -13,13 +13,18 @@ import type {
|
|
|
13
13
|
ValueCounts,
|
|
14
14
|
} from "../types";
|
|
15
15
|
import {
|
|
16
|
+
DEFAULT_CHART_HEIGHT,
|
|
16
17
|
getDataSpecAndSourceName,
|
|
17
18
|
getLegacyBooleanSpec,
|
|
18
19
|
getLegacyNumericSpec,
|
|
19
20
|
getLegacyTemporalSpec,
|
|
20
21
|
getScale,
|
|
21
22
|
} from "./legacy-chart-spec";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
calculateBinStep,
|
|
25
|
+
getPartialTimeTooltip,
|
|
26
|
+
isValidBinValue,
|
|
27
|
+
} from "./utils";
|
|
23
28
|
|
|
24
29
|
// We rely on vega's built-in binning to determine bar widths.
|
|
25
30
|
const MAX_BAR_HEIGHT = 20; // px
|
|
@@ -34,6 +39,81 @@ const BAR_COLOR = mint.mint11;
|
|
|
34
39
|
const UNHOVERED_BAR_OPACITY = 0.6;
|
|
35
40
|
const NULL_BAR_COLOR = orange.orange11;
|
|
36
41
|
|
|
42
|
+
// Full-height invisible hit targets (pixel coords). Do not use
|
|
43
|
+
// `y: { aggregate: "max" }` without a field — Vega drops it (console warning)
|
|
44
|
+
// and `aggregate: "max"` *with* a field shrinks the hit area to the bar
|
|
45
|
+
// height, which causes hover flicker on short integer bars (#10303).
|
|
46
|
+
const FULL_HEIGHT_TOOLTIP_Y = {
|
|
47
|
+
y: { value: 0 },
|
|
48
|
+
y2: { value: CONCAT_CHART_HEIGHT },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const NULLS_TOOLTIP = [
|
|
52
|
+
{
|
|
53
|
+
field: "count",
|
|
54
|
+
type: "quantitative" as const,
|
|
55
|
+
title: "nulls",
|
|
56
|
+
format: ",d",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Visible null bar + full-height invisible tooltip / hover layer. */
|
|
61
|
+
function buildNullBarSpec(opts: {
|
|
62
|
+
width: number;
|
|
63
|
+
filterNullOnly?: boolean;
|
|
64
|
+
}): TopLevelFacetedUnitSpec {
|
|
65
|
+
return {
|
|
66
|
+
height: CONCAT_CHART_HEIGHT,
|
|
67
|
+
width: opts.width,
|
|
68
|
+
// @ts-expect-error 'layer' property not in TopLevelFacetedUnitSpec
|
|
69
|
+
layer: [
|
|
70
|
+
{
|
|
71
|
+
mark: {
|
|
72
|
+
type: "bar",
|
|
73
|
+
color: NULL_BAR_COLOR,
|
|
74
|
+
},
|
|
75
|
+
encoding: {
|
|
76
|
+
x: {
|
|
77
|
+
field: "bin_start",
|
|
78
|
+
type: "nominal",
|
|
79
|
+
axis: null,
|
|
80
|
+
},
|
|
81
|
+
y: {
|
|
82
|
+
field: "count",
|
|
83
|
+
type: "quantitative",
|
|
84
|
+
axis: null,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
mark: {
|
|
90
|
+
type: "bar",
|
|
91
|
+
opacity: 0,
|
|
92
|
+
},
|
|
93
|
+
encoding: {
|
|
94
|
+
x: {
|
|
95
|
+
field: "bin_start",
|
|
96
|
+
type: "nominal",
|
|
97
|
+
axis: null,
|
|
98
|
+
},
|
|
99
|
+
...FULL_HEIGHT_TOOLTIP_Y,
|
|
100
|
+
tooltip: NULLS_TOOLTIP,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
...(opts.filterNullOnly
|
|
105
|
+
? {
|
|
106
|
+
transform: [
|
|
107
|
+
{
|
|
108
|
+
filter:
|
|
109
|
+
"datum['bin_start'] === null && datum['bin_end'] === null",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
37
117
|
export class ColumnChartSpecModel<T> {
|
|
38
118
|
private columnStats = new Map<ColumnName, Partial<ColumnHeaderStats>>();
|
|
39
119
|
private columnBinValues = new Map<ColumnName, BinValues>();
|
|
@@ -114,7 +194,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
114
194
|
domain: false,
|
|
115
195
|
},
|
|
116
196
|
},
|
|
117
|
-
height:
|
|
197
|
+
height: DEFAULT_CHART_HEIGHT,
|
|
118
198
|
} as TopLevelFacetedUnitSpec;
|
|
119
199
|
}
|
|
120
200
|
|
|
@@ -122,22 +202,28 @@ export class ColumnChartSpecModel<T> {
|
|
|
122
202
|
const binValues = this.columnBinValues.get(column);
|
|
123
203
|
const valueCounts = this.columnValueCounts.get(column);
|
|
124
204
|
const hasValueCounts = valueCounts && valueCounts.length > 0;
|
|
125
|
-
|
|
126
|
-
let data = this.dataSpec as TopLevelFacetedUnitSpec["data"];
|
|
127
205
|
const stats = this.columnStats.get(column);
|
|
206
|
+
const nullCount = typeof stats?.nulls === "number" ? stats.nulls : 0;
|
|
128
207
|
|
|
208
|
+
// Never mutate stored bin values; filter NaN/null junk from hist edge cases.
|
|
209
|
+
const validBins = (binValues ?? []).filter(isValidBinValue);
|
|
210
|
+
|
|
211
|
+
let data = this.dataSpec as TopLevelFacetedUnitSpec["data"];
|
|
129
212
|
if (hasValueCounts) {
|
|
130
213
|
data = { values: valueCounts, name: "value_counts" };
|
|
131
|
-
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
214
|
+
} else if (binValues !== undefined) {
|
|
215
|
+
const binsForChart =
|
|
216
|
+
nullCount > 0
|
|
217
|
+
? [
|
|
218
|
+
...validBins,
|
|
219
|
+
{
|
|
220
|
+
bin_start: null,
|
|
221
|
+
bin_end: null,
|
|
222
|
+
count: nullCount,
|
|
223
|
+
},
|
|
224
|
+
]
|
|
225
|
+
: validBins;
|
|
226
|
+
data = { values: binsForChart, name: "bin_values" };
|
|
141
227
|
}
|
|
142
228
|
|
|
143
229
|
const base = this.createBase(data);
|
|
@@ -158,17 +244,26 @@ export class ColumnChartSpecModel<T> {
|
|
|
158
244
|
case "date":
|
|
159
245
|
case "datetime":
|
|
160
246
|
case "time": {
|
|
161
|
-
if (
|
|
247
|
+
if (binValues === undefined) {
|
|
162
248
|
const legacyBase = this.createBase(this.legacyDataSpec);
|
|
163
249
|
const scale = getScale(this.legacySourceName);
|
|
164
250
|
return getLegacyTemporalSpec(column, type, legacyBase, scale);
|
|
165
251
|
}
|
|
166
252
|
|
|
167
|
-
|
|
168
|
-
|
|
253
|
+
if (validBins.length === 0) {
|
|
254
|
+
if (nullCount === 0) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...base,
|
|
259
|
+
...buildNullBarSpec({ width: CONCAT_CHART_WIDTH }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const tooltip = getPartialTimeTooltip(validBins);
|
|
169
264
|
|
|
170
|
-
// Single
|
|
171
|
-
if (
|
|
265
|
+
// Single non-null bin, no nulls — full-width bar
|
|
266
|
+
if (validBins.length === 1 && nullCount === 0) {
|
|
172
267
|
return {
|
|
173
268
|
...base,
|
|
174
269
|
mark: { type: "bar", color: BAR_COLOR },
|
|
@@ -251,7 +346,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
251
346
|
},
|
|
252
347
|
},
|
|
253
348
|
|
|
254
|
-
// Invisible tooltip layer
|
|
349
|
+
// Invisible full-height tooltip / hover layer
|
|
255
350
|
{
|
|
256
351
|
mark: {
|
|
257
352
|
type: "bar",
|
|
@@ -265,11 +360,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
265
360
|
type: "ordinal",
|
|
266
361
|
axis: null,
|
|
267
362
|
},
|
|
268
|
-
|
|
269
|
-
aggregate: "max",
|
|
270
|
-
type: "quantitative",
|
|
271
|
-
axis: null,
|
|
272
|
-
},
|
|
363
|
+
...FULL_HEIGHT_TOOLTIP_Y,
|
|
273
364
|
tooltip: [
|
|
274
365
|
{
|
|
275
366
|
field: "bin_start",
|
|
@@ -305,16 +396,55 @@ export class ColumnChartSpecModel<T> {
|
|
|
305
396
|
// Create a histogram spec that properly handles null values
|
|
306
397
|
const format = type === "integer" ? ",d" : ".2f";
|
|
307
398
|
|
|
308
|
-
if (
|
|
399
|
+
if (binValues === undefined) {
|
|
309
400
|
const legacyBase = this.createBase(this.legacyDataSpec);
|
|
310
401
|
return getLegacyNumericSpec(column, format, legacyBase);
|
|
311
402
|
}
|
|
312
403
|
|
|
313
|
-
|
|
404
|
+
if (validBins.length === 0) {
|
|
405
|
+
if (nullCount === 0) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
// All-null: null bar only (no empty quantitative histogram) — #10303
|
|
409
|
+
return {
|
|
410
|
+
...base,
|
|
411
|
+
...buildNullBarSpec({ width: CONCAT_CHART_WIDTH }),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const nullBar = buildNullBarSpec({
|
|
416
|
+
width: CONCAT_NULL_BAR_WIDTH,
|
|
417
|
+
filterNullOnly: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const binStep = calculateBinStep(validBins);
|
|
421
|
+
const axisTickValues = [
|
|
422
|
+
stats?.min,
|
|
423
|
+
stats?.p25,
|
|
424
|
+
stats?.median,
|
|
425
|
+
stats?.p75,
|
|
426
|
+
stats?.p95,
|
|
427
|
+
stats?.max,
|
|
428
|
+
].filter(
|
|
429
|
+
(value): value is number =>
|
|
430
|
+
typeof value === "number" && Number.isFinite(value),
|
|
431
|
+
);
|
|
314
432
|
|
|
315
433
|
const histogram: TopLevelFacetedUnitSpec = {
|
|
316
434
|
height: CONCAT_CHART_HEIGHT,
|
|
317
435
|
width: CONCAT_CHART_WIDTH,
|
|
436
|
+
// When hconcat shares data with a null bin, exclude it from the
|
|
437
|
+
// quantitative histogram so Vega does not warn on null extents.
|
|
438
|
+
...(nullCount > 0
|
|
439
|
+
? {
|
|
440
|
+
transform: [
|
|
441
|
+
{
|
|
442
|
+
filter:
|
|
443
|
+
"datum['bin_start'] !== null && datum['bin_end'] !== null",
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
318
448
|
// @ts-expect-error 'layer' property not in TopLevelFacetedUnitSpec
|
|
319
449
|
layer: [
|
|
320
450
|
{
|
|
@@ -349,7 +479,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
349
479
|
},
|
|
350
480
|
},
|
|
351
481
|
|
|
352
|
-
//
|
|
482
|
+
// Full-height tooltip / hover layer (covers short bars + inter-bar gaps)
|
|
353
483
|
{
|
|
354
484
|
mark: {
|
|
355
485
|
type: "bar",
|
|
@@ -376,25 +506,13 @@ export class ColumnChartSpecModel<T> {
|
|
|
376
506
|
labelOpacity: 0.5,
|
|
377
507
|
labelExpr:
|
|
378
508
|
"(datum.value >= 10000 || datum.value <= -10000) ? format(datum.value, '.2e') : format(datum.value, '.2~f')",
|
|
379
|
-
|
|
380
|
-
values: [
|
|
381
|
-
stats?.min,
|
|
382
|
-
stats?.p25,
|
|
383
|
-
stats?.median,
|
|
384
|
-
stats?.p75,
|
|
385
|
-
stats?.p95,
|
|
386
|
-
stats?.max,
|
|
387
|
-
].filter((value): value is number => value !== undefined),
|
|
509
|
+
values: axisTickValues,
|
|
388
510
|
},
|
|
389
511
|
},
|
|
390
512
|
x2: {
|
|
391
513
|
field: "bin_end_extended",
|
|
392
514
|
},
|
|
393
|
-
|
|
394
|
-
aggregate: "max",
|
|
395
|
-
type: "quantitative",
|
|
396
|
-
axis: null,
|
|
397
|
-
},
|
|
515
|
+
...FULL_HEIGHT_TOOLTIP_Y,
|
|
398
516
|
tooltip: [
|
|
399
517
|
{
|
|
400
518
|
field: "bin_range",
|
|
@@ -424,68 +542,10 @@ export class ColumnChartSpecModel<T> {
|
|
|
424
542
|
],
|
|
425
543
|
};
|
|
426
544
|
|
|
427
|
-
const nullBar: TopLevelFacetedUnitSpec = {
|
|
428
|
-
height: CONCAT_CHART_HEIGHT,
|
|
429
|
-
width: CONCAT_NULL_BAR_WIDTH,
|
|
430
|
-
// @ts-expect-error 'layer' property not in TopLevelFacetedUnitSpec
|
|
431
|
-
layer: [
|
|
432
|
-
{
|
|
433
|
-
mark: {
|
|
434
|
-
type: "bar",
|
|
435
|
-
color: NULL_BAR_COLOR,
|
|
436
|
-
},
|
|
437
|
-
encoding: {
|
|
438
|
-
x: {
|
|
439
|
-
field: "bin_start",
|
|
440
|
-
type: "nominal",
|
|
441
|
-
axis: null,
|
|
442
|
-
},
|
|
443
|
-
y: {
|
|
444
|
-
field: "count",
|
|
445
|
-
type: "quantitative",
|
|
446
|
-
axis: null,
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
mark: {
|
|
452
|
-
type: "bar",
|
|
453
|
-
opacity: 0,
|
|
454
|
-
},
|
|
455
|
-
encoding: {
|
|
456
|
-
x: {
|
|
457
|
-
field: "bin_start",
|
|
458
|
-
type: "nominal",
|
|
459
|
-
axis: null,
|
|
460
|
-
},
|
|
461
|
-
y: {
|
|
462
|
-
aggregate: "max",
|
|
463
|
-
type: "quantitative",
|
|
464
|
-
axis: null,
|
|
465
|
-
},
|
|
466
|
-
tooltip: [
|
|
467
|
-
{
|
|
468
|
-
field: "count",
|
|
469
|
-
type: "quantitative",
|
|
470
|
-
title: "nulls",
|
|
471
|
-
format: ",d",
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
],
|
|
477
|
-
transform: [
|
|
478
|
-
{
|
|
479
|
-
filter:
|
|
480
|
-
"datum['bin_start'] === null && datum['bin_end'] === null",
|
|
481
|
-
},
|
|
482
|
-
],
|
|
483
|
-
};
|
|
484
|
-
|
|
485
545
|
let chart: TopLevelFacetedUnitSpec = histogram;
|
|
486
546
|
let numericBase = base;
|
|
487
547
|
|
|
488
|
-
if (
|
|
548
|
+
if (nullCount > 0) {
|
|
489
549
|
numericBase = {
|
|
490
550
|
...base,
|
|
491
551
|
config: {
|
|
@@ -508,7 +568,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
508
568
|
}
|
|
509
569
|
|
|
510
570
|
return {
|
|
511
|
-
...numericBase,
|
|
571
|
+
...numericBase,
|
|
512
572
|
...chart,
|
|
513
573
|
};
|
|
514
574
|
}
|
|
@@ -684,7 +744,7 @@ export class ColumnChartSpecModel<T> {
|
|
|
684
744
|
{
|
|
685
745
|
name: "hover_bar",
|
|
686
746
|
select: {
|
|
687
|
-
type: "point",
|
|
747
|
+
type: "point" as const,
|
|
688
748
|
on: "mouseover",
|
|
689
749
|
clear: "mouseout",
|
|
690
750
|
},
|
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
isDataURLString,
|
|
18
18
|
} from "@/utils/json/base64";
|
|
19
19
|
|
|
20
|
+
// Height (px) of the fallback/legacy summary charts. The invisible full-height
|
|
21
|
+
// hover layer's `y2` must match this so it covers the whole plot; shared with
|
|
22
|
+
// `createBase` so the two can't drift apart.
|
|
23
|
+
export const DEFAULT_CHART_HEIGHT = 100;
|
|
24
|
+
|
|
20
25
|
export function getLegacyNumericSpec(
|
|
21
26
|
column: string,
|
|
22
27
|
format: string,
|
|
@@ -47,7 +52,8 @@ export function getLegacyNumericSpec(
|
|
|
47
52
|
},
|
|
48
53
|
},
|
|
49
54
|
|
|
50
|
-
//
|
|
55
|
+
// Full-height invisible hit targets (pixel coords). Avoid
|
|
56
|
+
// `y: { aggregate: "max" }` without a field — Vega drops it (#10303).
|
|
51
57
|
{
|
|
52
58
|
mark: {
|
|
53
59
|
type: "bar",
|
|
@@ -67,9 +73,10 @@ export function getLegacyNumericSpec(
|
|
|
67
73
|
},
|
|
68
74
|
},
|
|
69
75
|
y: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
value: 0,
|
|
77
|
+
},
|
|
78
|
+
y2: {
|
|
79
|
+
value: DEFAULT_CHART_HEIGHT,
|
|
73
80
|
},
|
|
74
81
|
tooltip: [
|
|
75
82
|
{
|
|
@@ -136,8 +143,8 @@ export function getLegacyTemporalSpec(
|
|
|
136
143
|
},
|
|
137
144
|
},
|
|
138
145
|
|
|
139
|
-
//
|
|
140
|
-
//
|
|
146
|
+
// Full-height invisible hit targets (pixel coords). Avoid
|
|
147
|
+
// `y: { aggregate: "max" }` without a field — Vega drops it (#10303).
|
|
141
148
|
{
|
|
142
149
|
mark: {
|
|
143
150
|
type: "bar",
|
|
@@ -151,7 +158,8 @@ export function getLegacyTemporalSpec(
|
|
|
151
158
|
bin: true,
|
|
152
159
|
scale: scale,
|
|
153
160
|
},
|
|
154
|
-
y: {
|
|
161
|
+
y: { value: 0 },
|
|
162
|
+
y2: { value: DEFAULT_CHART_HEIGHT },
|
|
155
163
|
tooltip: [
|
|
156
164
|
{
|
|
157
165
|
// Can also use column, but this is more explicit
|
|
@@ -2,10 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import type { StringFieldDef } from "vega-lite/types_unstable/channeldef.js";
|
|
4
4
|
import { Logger } from "@/utils/Logger";
|
|
5
|
-
import type { BinValues } from "../types";
|
|
5
|
+
import type { BinValue, BinValues } from "../types";
|
|
6
6
|
|
|
7
7
|
const READABLE_TIME_FORMAT = "%-I:%M:%S %p"; // e.g., 1:02:30 AM (no leading zero on hour)
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* True when a bin has finite, non-null start/end suitable for quantitative /
|
|
11
|
+
* temporal encodings. Rejects nulls and NaN (all-NaN hist garbage).
|
|
12
|
+
*/
|
|
13
|
+
export function isValidBinValue(value: BinValue): boolean {
|
|
14
|
+
const { bin_start, bin_end } = value;
|
|
15
|
+
if (bin_start == null || bin_end == null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
// Reject NaN and ±Infinity; they break quantitative/temporal encodings.
|
|
19
|
+
if (typeof bin_start === "number" && !Number.isFinite(bin_start)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (typeof bin_end === "number" && !Number.isFinite(bin_end)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export function getPartialTimeTooltip(
|
|
10
29
|
values: BinValues,
|
|
11
30
|
): Partial<StringFieldDef<string>> {
|
|
@@ -13,9 +32,11 @@ export function getPartialTimeTooltip(
|
|
|
13
32
|
return {};
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
// Find non-null
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
// Find first valid bin. `isValidBinValue` guarantees a non-null, finite
|
|
36
|
+
// start, so only bail when no valid bin exists (nullish, not falsy — a
|
|
37
|
+
// numeric `0` / epoch timestamp is a legitimate value).
|
|
38
|
+
const value = values.find((v) => isValidBinValue(v))?.bin_start;
|
|
39
|
+
if (value == null) {
|
|
19
40
|
return {};
|
|
20
41
|
}
|
|
21
42
|
|
|
@@ -89,9 +110,7 @@ export function calculateBinStep(values: BinValues) {
|
|
|
89
110
|
return 1;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
const validValues = values.filter(
|
|
93
|
-
(v) => v.bin_start !== null && v.bin_end !== null,
|
|
94
|
-
);
|
|
113
|
+
const validValues = values.filter(isValidBinValue);
|
|
95
114
|
|
|
96
115
|
if (validValues.length === 0) {
|
|
97
116
|
return 1;
|
|
@@ -70,7 +70,7 @@ export function toFieldTypes(
|
|
|
70
70
|
return new Map(fieldTypes.map(([columnName, [type]]) => [columnName, type]));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
interface BinValue {
|
|
73
|
+
export interface BinValue {
|
|
74
74
|
bin_start: number | string | Date | null;
|
|
75
75
|
bin_end: number | string | Date | null;
|
|
76
76
|
count: number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
|
+
import { CellOutputId } from "@/core/cells/ids";
|
|
8
|
+
import { createCell, createCellRuntimeState } from "@/core/cells/types";
|
|
9
|
+
import { CellOutputBlock } from "../reveal-component";
|
|
10
|
+
|
|
11
|
+
describe("CellOutputBlock", () => {
|
|
12
|
+
it("renders the cell's data-cell-id and data-cell-name so it can be targeted by custom CSS", () => {
|
|
13
|
+
const id = cellId("test");
|
|
14
|
+
const cell = {
|
|
15
|
+
...createCell({ id, name: "title" }),
|
|
16
|
+
...createCellRuntimeState({
|
|
17
|
+
output: {
|
|
18
|
+
channel: "output",
|
|
19
|
+
mimetype: "text/plain",
|
|
20
|
+
data: "hello",
|
|
21
|
+
timestamp: 0,
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<TooltipProvider>
|
|
28
|
+
<CellOutputBlock cell={cell} />
|
|
29
|
+
</TooltipProvider>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const cellElement = container.querySelector(`[data-cell-id="${id}"]`);
|
|
33
|
+
expect(cellElement).toBeTruthy();
|
|
34
|
+
expect(cellElement?.getAttribute("data-cell-name")).toBe("title");
|
|
35
|
+
|
|
36
|
+
// The wrapper's `data-cell-id` must coexist with the output's own
|
|
37
|
+
// `CellOutputId`-derived id, without colliding.
|
|
38
|
+
expect(document.getElementById(CellOutputId.create(id))).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
5
|
+
import { SetupMocks } from "@/__mocks__/common";
|
|
6
|
+
import { cellId } from "@/__tests__/branded";
|
|
7
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
|
+
import { createCell, createCellRuntimeState } from "@/core/cells/types";
|
|
9
|
+
import { SlideCellReadOnlyView } from "../slide-cell-view";
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
SetupMocks.resizeObserver();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("SlideCellReadOnlyView", () => {
|
|
16
|
+
it("renders the cell's data-cell-id and data-cell-name so it can be targeted by custom CSS", () => {
|
|
17
|
+
const id = cellId("test");
|
|
18
|
+
const cell = {
|
|
19
|
+
...createCell({ id, name: "title", code: "x = 1" }),
|
|
20
|
+
...createCellRuntimeState(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const { container } = render(
|
|
24
|
+
<TooltipProvider>
|
|
25
|
+
<SlideCellReadOnlyView cell={cell} />
|
|
26
|
+
</TooltipProvider>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const cellElement = container.querySelector(`[data-cell-id="${id}"]`);
|
|
30
|
+
expect(cellElement).toBeTruthy();
|
|
31
|
+
expect(cellElement?.getAttribute("data-cell-name")).toBe("title");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -13,6 +13,7 @@ import useEvent from "react-use-event-hook";
|
|
|
13
13
|
import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
14
14
|
import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
|
|
15
15
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
|
16
|
+
import { cellDomProps } from "@/components/editor/common";
|
|
16
17
|
import { Slide as CellOutputSlide } from "@/components/slides/slide";
|
|
17
18
|
import { SlideScrollContainer } from "@/components/slides/slide-scroll-hint";
|
|
18
19
|
import { Button } from "@/components/ui/button";
|
|
@@ -304,6 +305,25 @@ function resolveSlideContentStyle(
|
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Renders a cell's output wrapped in a `data-cell-id`/`data-cell-name` div
|
|
310
|
+
* (see `cellDomProps`), so it can be targeted by custom CSS in the slides
|
|
311
|
+
* view just like it can in the vertical layout. The wrapper is required
|
|
312
|
+
* because `CellOutputSlide` sets its own `id` from `CellOutputId` (a
|
|
313
|
+
* different scheme than `HTMLCellId`) and its inner `OutputArea` uses
|
|
314
|
+
* `display: contents`, which can't be targeted for background/border styling.
|
|
315
|
+
*/
|
|
316
|
+
export const CellOutputBlock = ({ cell }: { cell: RuntimeCell }) => (
|
|
317
|
+
<div className="flex flex-col" {...cellDomProps(cell.id, cell.name)}>
|
|
318
|
+
<CellOutputSlide
|
|
319
|
+
cellId={cell.id}
|
|
320
|
+
status={cell.status}
|
|
321
|
+
output={cell.output}
|
|
322
|
+
stale={outputIsStale(cell, false)}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
|
|
307
327
|
const SubslideView = ({
|
|
308
328
|
subslide,
|
|
309
329
|
resolveShowCode,
|
|
@@ -340,15 +360,7 @@ const SubslideView = ({
|
|
|
340
360
|
{subslide.blocks.map((block, i) => {
|
|
341
361
|
const rendered = block.cells.map((cell) => {
|
|
342
362
|
if (!resolveShowCode(cell.id)) {
|
|
343
|
-
return
|
|
344
|
-
<CellOutputSlide
|
|
345
|
-
key={cell.id}
|
|
346
|
-
cellId={cell.id}
|
|
347
|
-
status={cell.status}
|
|
348
|
-
output={cell.output}
|
|
349
|
-
stale={outputIsStale(cell, false)}
|
|
350
|
-
/>
|
|
351
|
-
);
|
|
363
|
+
return <CellOutputBlock key={cell.id} cell={cell} />;
|
|
352
364
|
}
|
|
353
365
|
return isEditable ? (
|
|
354
366
|
<SlideCellView key={cell.id} cell={cell} />
|
|
@@ -406,14 +418,7 @@ const ParkedPreviewContent = ({
|
|
|
406
418
|
<SlideCellReadOnlyView cell={cell} />
|
|
407
419
|
);
|
|
408
420
|
}
|
|
409
|
-
return
|
|
410
|
-
<CellOutputSlide
|
|
411
|
-
cellId={cell.id}
|
|
412
|
-
status={cell.status}
|
|
413
|
-
output={cell.output}
|
|
414
|
-
stale={outputIsStale(cell, false)}
|
|
415
|
-
/>
|
|
416
|
-
);
|
|
421
|
+
return <CellOutputBlock cell={cell} />;
|
|
417
422
|
};
|
|
418
423
|
|
|
419
424
|
// There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
|
|
@@ -202,7 +202,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
202
202
|
);
|
|
203
203
|
|
|
204
204
|
const editor = (
|
|
205
|
-
<div className="marimo-cell">
|
|
205
|
+
<div className="marimo-cell" {...cellDomProps(cell.id, cell.name)}>
|
|
206
206
|
<ReadonlyCode
|
|
207
207
|
code={display.code}
|
|
208
208
|
language={display.language}
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
width: unset;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/* If the first output is the only child, make it flex 1
|
|
19
|
-
|
|
18
|
+
/* If the first output is the only child, make it flex 1. Cells are wrapped
|
|
19
|
+
in a `data-cell-id`/`data-cell-name` div (see `cellDomProps`), so `.output`
|
|
20
|
+
is a grandchild rather than a direct child here. */
|
|
21
|
+
> *:only-child:has(> .output:only-child, > * > .output:only-child) {
|
|
20
22
|
flex: 1;
|
|
21
23
|
}
|
|
22
24
|
|