@marimo-team/frontend 0.23.16-dev3 → 0.23.16-dev31
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-BalYv0pK.js → CellStatus-CmojVYzm.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-BwwRpByA.js} +12 -12
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-BWte8vjk.js} +4 -4
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-DRltV71H.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-DIegd2mN.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-BBFD-M9V.js} +17 -17
- package/dist/assets/add-connection-dialog-CECJ8507.js +170 -0
- package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-YaS0WXP1.js} +5 -5
- package/dist/assets/ai-model-dropdown-BpVBUZnz.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-BW-w01aF.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-EV-1IccQ.js} +10 -10
- package/dist/assets/cell-link-C5gpAn3R.js +1 -0
- package/dist/assets/cells-6yd8nOAu.js +231 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-8BbC5B-D.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-00V3uFDx.js} +3 -3
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-DWhJ2Ozy.js} +1 -1
- package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-DeXESYpM.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-DZ2rNzzf.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-XVELED_O.js} +1 -1
- package/dist/assets/components-BLHpmhXx.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-CTqa8bf-.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-C4Nzcddp.js} +4 -4
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-Bym4G7IS.js} +1 -1
- package/dist/assets/download-HKgqJX4l.js +9 -0
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-DSEkwJ4O.js → edit-page-D80ascX2.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CxyvaxEM.js} +2 -2
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-CfSy7uUr.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/file-explorer-panel-Dd5Xn2TK.js +26 -0
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-CSjJ3JC6.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CrKtrXMl.js} +1 -1
- package/dist/assets/floating-outline-ByQ8vvif.js +1 -0
- package/dist/assets/focus-CVONl-Dy.js +1 -0
- package/dist/assets/{form-B6BkSCJb.js → form-Ca3yd_Od.js} +1 -1
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-FpAawj9s.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-C-4ysbGe.js} +2 -2
- package/dist/assets/index-4k5eCUzO.js +38 -0
- package/dist/assets/index-xs-zSSrX.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-CUlZijwG.js → layout-Dtql_o_T.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-5u1wUBn1.js} +1 -1
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-dABDqZDw.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/name-cell-input-Y4knmEIU.js +1 -0
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/outline-panel-CQ_KWLip.js +1 -0
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-Ijt7d51P.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-BUtnUuiE.js} +1 -1
- package/dist/assets/panels-DfU1Oikj.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-Dd11YI04.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-22eWJ9fv.js} +1 -1
- package/dist/assets/readonly-code-display-BjK-TEri.js +1 -0
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-n_TrLmsR.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-CCLTV1q6.js} +60 -60
- package/dist/assets/{run-page-B6pgDYEJ.js → run-page-DbezK7Qs.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-C2wAEv5n.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BZGNcZmp.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-BzlKcq21.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-DNaNtkR0.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/state-DFPAfLw7.js +3 -0
- package/dist/assets/{state-DBzEQtWz.js → state-jCdh8Dcx.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
- package/dist/assets/switch-OVajJBPR.js +1 -0
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-DuEtSOCl.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-rVAa3En0.js} +2 -2
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-ttUd3J7U.js} +1 -1
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-UCr1tMHw.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-PrgejW8A.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-5jc-_-hy.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/useNotebookActions-DG8f4m8p.js +12 -0
- package/dist/assets/useRunCells-DHsd7HGL.js +1 -0
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-MZpER0Bh.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +2 -2
- package/src/__mocks__/requests.ts +45 -4
- 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/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +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/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +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/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +25 -7
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/download-BomYt3nl.js +0 -9
- package/dist/assets/file-explorer-panel-BfXM2BOd.js +0 -26
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-Dbc5mg7a.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
- package/dist/assets/panels-Dj45fW-v.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
- package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/state-BZQc7xxL.js +0 -3
- package/dist/assets/switch-CvweaCXq.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
- package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
|
@@ -178,6 +178,104 @@ describe("ColumnChartSpecModel", () => {
|
|
|
178
178
|
expect(summary2.spec?.data?.values).toEqual(mockBinValues.integer);
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
it("should render only a null bar for all-null numeric columns", () => {
|
|
182
|
+
const allNullStats: Record<ColumnName, Partial<ColumnHeaderStats>> = {
|
|
183
|
+
number: { nulls: 12, total: 12 },
|
|
184
|
+
};
|
|
185
|
+
const allNullBins: Record<ColumnName, BinValues> = {
|
|
186
|
+
number: [],
|
|
187
|
+
};
|
|
188
|
+
const model = new ColumnChartSpecModel(
|
|
189
|
+
[],
|
|
190
|
+
mockFieldTypes,
|
|
191
|
+
allNullStats,
|
|
192
|
+
allNullBins,
|
|
193
|
+
{},
|
|
194
|
+
{ includeCharts: true },
|
|
195
|
+
);
|
|
196
|
+
const summary = model.getHeaderSummary("number");
|
|
197
|
+
expect(summary.spec).toBeDefined();
|
|
198
|
+
// @ts-expect-error hconcat should be available
|
|
199
|
+
expect(summary.spec?.hconcat).toBeUndefined();
|
|
200
|
+
// @ts-expect-error data.values should be available
|
|
201
|
+
expect(summary.spec?.data?.values).toEqual([
|
|
202
|
+
{ bin_start: null, bin_end: null, count: 12 },
|
|
203
|
+
]);
|
|
204
|
+
// Full-width null-only chart (not the thin hconcat null strip)
|
|
205
|
+
expect(summary.spec?.width).toBe(70);
|
|
206
|
+
// Full-height tooltip hit area uses pixel y/y2 (no aggregate:max)
|
|
207
|
+
// @ts-expect-error layer should be available
|
|
208
|
+
expect(summary.spec?.layer?.[1]?.encoding?.y).toEqual({ value: 0 });
|
|
209
|
+
// @ts-expect-error layer should be available
|
|
210
|
+
expect(summary.spec?.layer?.[1]?.encoding?.y2).toEqual({ value: 30 });
|
|
211
|
+
expect(summary.spec).toMatchSnapshot();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should ignore NaN bins and not mutate stored bin values", () => {
|
|
215
|
+
const nanOnlyBins: Record<ColumnName, BinValues> = {
|
|
216
|
+
number: [
|
|
217
|
+
{ bin_start: Number.NaN, bin_end: 0.1, count: 0 },
|
|
218
|
+
{ bin_start: Number.NaN, bin_end: 1, count: 0 },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
const stats: Record<ColumnName, Partial<ColumnHeaderStats>> = {
|
|
222
|
+
number: { nulls: 12, total: 12 },
|
|
223
|
+
};
|
|
224
|
+
const model = new ColumnChartSpecModel(
|
|
225
|
+
[],
|
|
226
|
+
mockFieldTypes,
|
|
227
|
+
stats,
|
|
228
|
+
nanOnlyBins,
|
|
229
|
+
{},
|
|
230
|
+
{ includeCharts: true },
|
|
231
|
+
);
|
|
232
|
+
const first = model.getHeaderSummary("number");
|
|
233
|
+
const second = model.getHeaderSummary("number");
|
|
234
|
+
// All NaN bins are invalid → null-only chart
|
|
235
|
+
// @ts-expect-error hconcat should be available
|
|
236
|
+
expect(first.spec?.hconcat).toBeUndefined();
|
|
237
|
+
expect(first.spec?.width).toBe(70);
|
|
238
|
+
// Stored bins must not grow across getHeaderSummary calls
|
|
239
|
+
expect(nanOnlyBins.number).toHaveLength(2);
|
|
240
|
+
expect(second.spec).toEqual(first.spec);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should render a null bar for all-null temporal columns", () => {
|
|
244
|
+
const model = new ColumnChartSpecModel(
|
|
245
|
+
[],
|
|
246
|
+
mockFieldTypes,
|
|
247
|
+
{ datetime: { nulls: 12, total: 12 } },
|
|
248
|
+
{ datetime: [] },
|
|
249
|
+
{},
|
|
250
|
+
{ includeCharts: true },
|
|
251
|
+
);
|
|
252
|
+
const summary = model.getHeaderSummary("datetime");
|
|
253
|
+
expect(summary.spec).toBeDefined();
|
|
254
|
+
// @ts-expect-error data.values
|
|
255
|
+
expect(summary.spec?.data?.values).toEqual([
|
|
256
|
+
{ bin_start: null, bin_end: null, count: 12 },
|
|
257
|
+
]);
|
|
258
|
+
expect(summary.spec?.width).toBe(70);
|
|
259
|
+
// Same full-height hover hit area as numeric all-null charts
|
|
260
|
+
// @ts-expect-error layer should be available
|
|
261
|
+
expect(summary.spec?.layer?.[1]?.encoding?.y).toEqual({ value: 0 });
|
|
262
|
+
// @ts-expect-error layer should be available
|
|
263
|
+
expect(summary.spec?.layer?.[1]?.encoding?.y2).toEqual({ value: 30 });
|
|
264
|
+
expect(summary.spec).toMatchSnapshot();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should return null when numeric bins are empty and there are no nulls", () => {
|
|
268
|
+
const model = new ColumnChartSpecModel(
|
|
269
|
+
[],
|
|
270
|
+
mockFieldTypes,
|
|
271
|
+
{ number: { min: 0, max: 1 } },
|
|
272
|
+
{ number: [] },
|
|
273
|
+
{},
|
|
274
|
+
{ includeCharts: true },
|
|
275
|
+
);
|
|
276
|
+
expect(model.getHeaderSummary("number").spec).toBeNull();
|
|
277
|
+
});
|
|
278
|
+
|
|
181
279
|
it("should handle datetime bin values", () => {
|
|
182
280
|
const model = new ColumnChartSpecModel(
|
|
183
281
|
[],
|
|
@@ -191,7 +289,10 @@ describe("ColumnChartSpecModel", () => {
|
|
|
191
289
|
const summary = model.getHeaderSummary("datetime");
|
|
192
290
|
expect(summary.spec).toBeDefined();
|
|
193
291
|
// @ts-expect-error data.values should be available
|
|
194
|
-
expect(summary.spec?.data?.values).toEqual(
|
|
292
|
+
expect(summary.spec?.data?.values).toEqual([
|
|
293
|
+
...mockBinValues.datetime,
|
|
294
|
+
{ bin_start: null, bin_end: null, count: 10 },
|
|
295
|
+
]);
|
|
195
296
|
|
|
196
297
|
expect(summary.spec).toMatchSnapshot();
|
|
197
298
|
|
|
@@ -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;
|
|
@@ -8,13 +8,11 @@ describe("runServerSidePDFDownload", () => {
|
|
|
8
8
|
const downloadPDF = vi.fn().mockResolvedValue(undefined);
|
|
9
9
|
|
|
10
10
|
await runServerSidePDFDownload({
|
|
11
|
-
filename: "slides.py",
|
|
12
11
|
preset: "document",
|
|
13
12
|
downloadPDF,
|
|
14
13
|
});
|
|
15
14
|
|
|
16
15
|
expect(downloadPDF).toHaveBeenCalledWith({
|
|
17
|
-
filename: "slides.py",
|
|
18
16
|
webpdf: false,
|
|
19
17
|
preset: "document",
|
|
20
18
|
includeInputs: true,
|
|
@@ -26,13 +24,11 @@ describe("runServerSidePDFDownload", () => {
|
|
|
26
24
|
const downloadPDF = vi.fn().mockResolvedValue(undefined);
|
|
27
25
|
|
|
28
26
|
await runServerSidePDFDownload({
|
|
29
|
-
filename: "slides.py",
|
|
30
27
|
preset: "slides",
|
|
31
28
|
downloadPDF,
|
|
32
29
|
});
|
|
33
30
|
|
|
34
31
|
expect(downloadPDF).toHaveBeenCalledWith({
|
|
35
|
-
filename: "slides.py",
|
|
36
32
|
webpdf: false,
|
|
37
33
|
preset: "slides",
|
|
38
34
|
includeInputs: true,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
type Preset = "document" | "slides";
|
|
4
4
|
type DownloadPDF = (opts: {
|
|
5
|
-
filename: string;
|
|
6
5
|
webpdf: boolean;
|
|
7
6
|
preset: Preset;
|
|
8
7
|
includeInputs: boolean;
|
|
@@ -10,14 +9,12 @@ type DownloadPDF = (opts: {
|
|
|
10
9
|
}) => Promise<void>;
|
|
11
10
|
|
|
12
11
|
export async function runServerSidePDFDownload(opts: {
|
|
13
|
-
filename: string;
|
|
14
12
|
preset: Preset;
|
|
15
13
|
downloadPDF: DownloadPDF;
|
|
16
14
|
}): Promise<void> {
|
|
17
|
-
const {
|
|
15
|
+
const { preset, downloadPDF } = opts;
|
|
18
16
|
|
|
19
17
|
await downloadPDF({
|
|
20
|
-
filename,
|
|
21
18
|
webpdf: false,
|
|
22
19
|
preset,
|
|
23
20
|
includeInputs: true,
|