@marimo-team/frontend 0.23.16-dev26 → 0.23.16-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/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-KWSR-llo.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-BfyjcLEp.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-j4gnjFei.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-BYAv-jBF.js} +6 -6
- package/dist/assets/{layout-Cdpz2zk4.js → layout-Cyvip7OR.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-EhYQAy06.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-CkFCS99a.js} +2 -2
- package/dist/assets/{run-page-BgzVSOZh.js → run-page-ClhbSbzk.js} +1 -1
- package/dist/assets/scratchpad-panel-BH7tCYvw.js +1 -0
- package/dist/assets/{session-panel-Cs9YKkJc.js → session-panel-BZGNcZmp.js} +1 -1
- package/dist/assets/{skeleton-BOtI6k5P.js → skeleton-DiD_znnO.js} +1 -1
- 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-C5R8yLEx.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/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/useCellActionButton-Bglkc3w8.js +0 -1
- package/dist/assets/useRunCells-BpMmDGqa.js +0 -1
|
@@ -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;
|
|
@@ -20,6 +20,26 @@ vi.mock("@/core/cells/cells", () => ({
|
|
|
20
20
|
getAllEditorViews: () => mockGetAllEditorViews(),
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A view stub that only carries what navigate needs
|
|
25
|
+
*/
|
|
26
|
+
function createMockView(state: EditorState): EditorView {
|
|
27
|
+
const cell = document.createElement("div");
|
|
28
|
+
cell.className = "marimo-cell";
|
|
29
|
+
// jsdom doesn't implement scrollIntoView.
|
|
30
|
+
cell.scrollIntoView = vi.fn();
|
|
31
|
+
const dom = document.createElement("div");
|
|
32
|
+
cell.append(dom);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
state,
|
|
36
|
+
dom,
|
|
37
|
+
// Don't actually apply changes; it's complex to mock properly and the
|
|
38
|
+
// assertions only care that dispatch was called.
|
|
39
|
+
dispatch: vi.fn(),
|
|
40
|
+
} as unknown as EditorView;
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
describe("navigate", () => {
|
|
24
44
|
let view1: EditorView;
|
|
25
45
|
let view2: EditorView;
|
|
@@ -36,20 +56,8 @@ describe("navigate", () => {
|
|
|
36
56
|
});
|
|
37
57
|
|
|
38
58
|
// Create mock views that track dispatch calls
|
|
39
|
-
view1 =
|
|
40
|
-
|
|
41
|
-
dispatch: vi.fn(() => {
|
|
42
|
-
// Mock dispatch - in real tests we just need to verify it was called
|
|
43
|
-
// Don't actually apply changes as it's complex to mock properly
|
|
44
|
-
}),
|
|
45
|
-
} as unknown as EditorView;
|
|
46
|
-
|
|
47
|
-
view2 = {
|
|
48
|
-
state: state2,
|
|
49
|
-
dispatch: vi.fn(() => {
|
|
50
|
-
// Mock dispatch - in real tests we just need to verify it was called
|
|
51
|
-
}),
|
|
52
|
-
} as unknown as EditorView;
|
|
59
|
+
view1 = createMockView(state1);
|
|
60
|
+
view2 = createMockView(state2);
|
|
53
61
|
|
|
54
62
|
mockViews = [view1, view2];
|
|
55
63
|
mockGetAllEditorViews.mockReturnValue(mockViews);
|
|
@@ -234,6 +242,22 @@ describe("navigate", () => {
|
|
|
234
242
|
|
|
235
243
|
expect(result).toBe(false);
|
|
236
244
|
});
|
|
245
|
+
|
|
246
|
+
it("should scroll the cell owning the match into view", async () => {
|
|
247
|
+
const cell = view1.dom.closest(".marimo-cell");
|
|
248
|
+
invariant(cell, "mock view should be inside a cell");
|
|
249
|
+
|
|
250
|
+
expect(findNext()).toBeTruthy();
|
|
251
|
+
|
|
252
|
+
// The scroll is deferred a frame.
|
|
253
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
254
|
+
|
|
255
|
+
expect(cell.scrollIntoView).toHaveBeenCalledWith({
|
|
256
|
+
behavior: "instant",
|
|
257
|
+
block: "nearest",
|
|
258
|
+
inline: "nearest",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
237
261
|
});
|
|
238
262
|
|
|
239
263
|
describe("findPrev", () => {
|
|
@@ -319,10 +343,7 @@ describe("navigate", () => {
|
|
|
319
343
|
extensions: [EditorState.readOnly.of(true)],
|
|
320
344
|
});
|
|
321
345
|
|
|
322
|
-
const readOnlyView =
|
|
323
|
-
state: readOnlyState,
|
|
324
|
-
dispatch: vi.fn(),
|
|
325
|
-
} as unknown as EditorView;
|
|
346
|
+
const readOnlyView = createMockView(readOnlyState);
|
|
326
347
|
|
|
327
348
|
// Replace view1 with read-only view temporarily
|
|
328
349
|
mockGetAllEditorViews.mockReturnValueOnce([readOnlyView, view2]);
|
|
@@ -568,10 +589,7 @@ describe("navigate", () => {
|
|
|
568
589
|
doc: Text.of(["hello world hello there hello"]),
|
|
569
590
|
});
|
|
570
591
|
|
|
571
|
-
const testView =
|
|
572
|
-
state: testDoc,
|
|
573
|
-
dispatch: vi.fn(),
|
|
574
|
-
} as unknown as EditorView;
|
|
592
|
+
const testView = createMockView(testDoc);
|
|
575
593
|
|
|
576
594
|
mockGetAllEditorViews.mockReturnValue([testView]);
|
|
577
595
|
|
|
@@ -613,10 +631,7 @@ describe("navigate", () => {
|
|
|
613
631
|
doc: Text.of([""]),
|
|
614
632
|
});
|
|
615
633
|
|
|
616
|
-
const emptyView =
|
|
617
|
-
state: emptyState,
|
|
618
|
-
dispatch: vi.fn(),
|
|
619
|
-
} as unknown as EditorView;
|
|
634
|
+
const emptyView = createMockView(emptyState);
|
|
620
635
|
|
|
621
636
|
mockGetAllEditorViews.mockReturnValue([emptyView]);
|
|
622
637
|
|
|
@@ -629,10 +644,7 @@ describe("navigate", () => {
|
|
|
629
644
|
doc: Text.of(["orphan content"]),
|
|
630
645
|
});
|
|
631
646
|
|
|
632
|
-
const orphanView =
|
|
633
|
-
state: orphanState,
|
|
634
|
-
dispatch: vi.fn(),
|
|
635
|
-
} as unknown as EditorView;
|
|
647
|
+
const orphanView = createMockView(orphanState);
|
|
636
648
|
|
|
637
649
|
store.set(findReplaceAtom, {
|
|
638
650
|
type: "setCurrentView",
|
|
@@ -6,6 +6,7 @@ import { EditorView } from "@codemirror/view";
|
|
|
6
6
|
import { getAllEditorViews } from "@/core/cells/cells";
|
|
7
7
|
import { replaceEditorContent } from "@/core/codemirror/replace-editor-content";
|
|
8
8
|
import { store } from "@/core/state/jotai";
|
|
9
|
+
import { scrollOwnerCell } from "../utils";
|
|
9
10
|
import { asQueryCreator, type QueryType } from "./query";
|
|
10
11
|
import { findReplaceAtom } from "./state";
|
|
11
12
|
|
|
@@ -85,6 +86,7 @@ const findInDirection = (direction: "next" | "prev") =>
|
|
|
85
86
|
effects: [EditorView.scrollIntoView(selection.main, { y: "center" })],
|
|
86
87
|
userEvent: "select.search",
|
|
87
88
|
});
|
|
89
|
+
scrollOwnerCell(view);
|
|
88
90
|
store.set(findReplaceAtom, {
|
|
89
91
|
type: "setCurrentView",
|
|
90
92
|
view,
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import { python } from "@codemirror/lang-python";
|
|
4
4
|
import { EditorState } from "@codemirror/state";
|
|
5
5
|
import { EditorView } from "@codemirror/view";
|
|
6
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
7
7
|
import { goToVariableDefinition } from "../commands";
|
|
8
8
|
|
|
9
9
|
async function tick(): Promise<void> {
|
|
10
10
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function createEditor(content: string) {
|
|
13
|
+
function createEditor(content: string, parent: HTMLElement = document.body) {
|
|
14
14
|
const state = EditorState.create({
|
|
15
15
|
doc: content,
|
|
16
16
|
extensions: [python()],
|
|
@@ -18,7 +18,7 @@ function createEditor(content: string) {
|
|
|
18
18
|
|
|
19
19
|
const view = new EditorView({
|
|
20
20
|
state,
|
|
21
|
-
parent
|
|
21
|
+
parent,
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
return view;
|
|
@@ -383,6 +383,27 @@ print('myVar')`);
|
|
|
383
383
|
"
|
|
384
384
|
`);
|
|
385
385
|
});
|
|
386
|
+
|
|
387
|
+
test("scrolls the cell owning the definition into view", async () => {
|
|
388
|
+
const cell = document.createElement("div");
|
|
389
|
+
cell.className = "marimo-cell";
|
|
390
|
+
// jsdom doesn't implement scrollIntoView.
|
|
391
|
+
cell.scrollIntoView = vi.fn();
|
|
392
|
+
document.body.append(cell);
|
|
393
|
+
|
|
394
|
+
view = createEditor("myVar = 10\nprint(myVar)", cell);
|
|
395
|
+
expect(goToVariableDefinition(view, "myVar")).toBe(true);
|
|
396
|
+
// The jump and the scroll are both deferred to the next frame.
|
|
397
|
+
await tick();
|
|
398
|
+
|
|
399
|
+
expect(cell.scrollIntoView).toHaveBeenCalledWith({
|
|
400
|
+
behavior: "instant",
|
|
401
|
+
block: "nearest",
|
|
402
|
+
inline: "nearest",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
cell.remove();
|
|
406
|
+
});
|
|
386
407
|
});
|
|
387
408
|
|
|
388
409
|
/**
|