@marimo-team/islands 0.25.1-dev2 → 0.25.1-dev20

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.
Files changed (112) hide show
  1. package/dist/{ConnectedDataExplorerComponent-KVhjDMSe.js → ConnectedDataExplorerComponent-CwMXfDQR.js} +3 -3
  2. package/dist/{ErrorBoundary-DO0ie20N.js → ErrorBoundary-BU3l6Hi2.js} +1 -1
  3. package/dist/{abnfDiagram-VCTEODGH-CpN0FRda.js → abnfDiagram-VCTEODGH-AFhOQTt8.js} +3 -3
  4. package/dist/{architectureDiagram-5GKGNRK7-DDyj5SRB.js → architectureDiagram-5GKGNRK7-BngksAAQ.js} +5 -5
  5. package/dist/{blockDiagram-I7D4REHJ-BbXEXJRu.js → blockDiagram-I7D4REHJ-CvBmppdy.js} +9 -9
  6. package/dist/{c4Diagram-7LVT6UL2-DcLsbQmR.js → c4Diagram-7LVT6UL2-EceL4_jH.js} +7 -7
  7. package/dist/{cells-DYIkSxs6.js → cells-Cksce8br.js} +4123 -4150
  8. package/dist/{channel-B5XnTarr.js → channel-z7HZXHwv.js} +1 -1
  9. package/dist/{chat-ui-BXj3uYjy.js → chat-ui-ruucqBTA.js} +45 -45
  10. package/dist/{chunk-4HAMMTFA-D2qv8E0X.js → chunk-4HAMMTFA-BPTTOOC5.js} +5 -5
  11. package/dist/{chunk-75Z2AOVW-5_HsvXwr.js → chunk-75Z2AOVW-C4AOleKf.js} +1 -1
  12. package/dist/{chunk-DU6HZSFF-BUgAHvnM.js → chunk-DU6HZSFF-1TwWR6gh.js} +1 -1
  13. package/dist/{chunk-F27PBJKO-DHmYWZXr.js → chunk-F27PBJKO-DMI1PBe4.js} +1 -1
  14. package/dist/{chunk-GMAD6QVW-Czynz8aA.js → chunk-GMAD6QVW-vStrMz2P.js} +3 -3
  15. package/dist/{chunk-GVQU2GXP-yQEdulRS.js → chunk-GVQU2GXP-2HwWgs4u.js} +2 -2
  16. package/dist/{chunk-IMKFNOWR-BfwTebD0.js → chunk-IMKFNOWR-UOVZuZVn.js} +5 -5
  17. package/dist/{chunk-L3NEJ4N5-DwRWnDDK.js → chunk-L3NEJ4N5-RhC7i0I2.js} +5 -5
  18. package/dist/{chunk-OSK3NFVY-BBKU_Ign.js → chunk-OSK3NFVY-CZV09N-U.js} +6 -6
  19. package/dist/{chunk-P2QGCYS3-C0iGU29N.js → chunk-P2QGCYS3-0TAWai9C.js} +1 -1
  20. package/dist/{chunk-POPQ4Y6H-BCbS3Ozc.js → chunk-POPQ4Y6H-Cc3_56GX.js} +1 -1
  21. package/dist/{chunk-PWAF6VOD-CTFCL0jI.js → chunk-PWAF6VOD-lNxB9k8z.js} +1 -1
  22. package/dist/{chunk-SHT3W25Y-BspJEJ5H.js → chunk-SHT3W25Y-CrEPyWy1.js} +7 -7
  23. package/dist/{chunk-SVP7TREG-DX3rFNTe.js → chunk-SVP7TREG-uIyyySp6.js} +2 -2
  24. package/dist/{chunk-TICWLB2K-fVF7Uujg.js → chunk-TICWLB2K-DdXM3eg6.js} +5 -5
  25. package/dist/classDiagram-ZZMXUADV-BiATbBE4.js +30 -0
  26. package/dist/classDiagram-v2-VYDZK3BY-JnH97GYw.js +30 -0
  27. package/dist/{code-visibility-64_ACQvC.js → code-visibility-DDDujOI0.js} +17 -17
  28. package/dist/{common-LlPE7GR6.js → common-BkYqr2IQ.js} +620 -620
  29. package/dist/{cynefinDiagram-5FMLGOSQ-CFQrqzSX.js → cynefinDiagram-5FMLGOSQ-zknaAsGy.js} +3 -3
  30. package/dist/{dagre-GXQ25YYZ-xHNAUTyG.js → dagre-GXQ25YYZ-BhMOoMGJ.js} +10 -10
  31. package/dist/{data-grid-overlay-editor-BUvmnptP.js → data-grid-overlay-editor-3APq_6Z0.js} +1 -1
  32. package/dist/{diagram-S7CK7UJ4-Bt1rPbAx.js → diagram-S7CK7UJ4-V5-Z-EDY.js} +4 -4
  33. package/dist/{diagram-UQ7AKVKN-D1sq5FoZ.js → diagram-UQ7AKVKN-qw8AjDal.js} +3 -3
  34. package/dist/{diagram-VSXAHHWV-BKpmG23x.js → diagram-VSXAHHWV-CR7BJygZ.js} +2 -2
  35. package/dist/{diagram-VX7I27RA-BMwGFVD6.js → diagram-VX7I27RA-DfuqFb1E.js} +5 -5
  36. package/dist/{diagram-Z3DM3KII--KLDR9Pl.js → diagram-Z3DM3KII-DpWwlYZB.js} +3 -3
  37. package/dist/{dist-BaHjhkpZ.js → dist-CcyKTBTF.js} +1 -1
  38. package/dist/{ebnfDiagram-PWID7BFC-WnG4cvSQ.js → ebnfDiagram-PWID7BFC-C20OnNMZ.js} +3 -3
  39. package/dist/{erDiagram-RLTQ6QDP-DhzKiYHc.js → erDiagram-RLTQ6QDP-DBXLWO9v.js} +12 -12
  40. package/dist/{error-entries-C0NsUqXl.js → error-entries-CvCFdpY2.js} +18 -18
  41. package/dist/{feedback-button-B98mEc3M.js → feedback-button-DWSXRUAw.js} +19 -19
  42. package/dist/flowDiagram-HODETNUW-DL52nMkt.js +16 -0
  43. package/dist/{formats-BR5iGb4s.js → formats-Cl_67ghI.js} +2 -2
  44. package/dist/{ganttDiagram-EL5Y4UJY-DbzEBLbl.js → ganttDiagram-EL5Y4UJY-cRRaDt88.js} +2 -2
  45. package/dist/{gitGraphDiagram-WWUBYQGX-BMf0iKi1.js → gitGraphDiagram-WWUBYQGX-Pox5k9-Y.js} +2 -2
  46. package/dist/{glide-data-editor-CBIPbkXG.js → glide-data-editor-DGmuodBB.js} +3 -3
  47. package/dist/{globals-VpAtGRbg.js → globals-ciicQhoq.js} +1 -1
  48. package/dist/{html-to-image-CeYYvQNb.js → html-to-image-CcfkLMrx.js} +32 -31
  49. package/dist/{infoDiagram-27XIBGKW-BagtCX_5.js → infoDiagram-27XIBGKW-ntpxMfrq.js} +2 -2
  50. package/dist/{ishikawaDiagram-5VMMS53U-DByDehYy.js → ishikawaDiagram-5VMMS53U-BllIlHE5.js} +3 -3
  51. package/dist/{journeyDiagram-3NMN7TZE-D2LcKDUO.js → journeyDiagram-3NMN7TZE-EPJDPlt0.js} +2 -2
  52. package/dist/{kanban-definition-UXKFOSKX-BxwkbBI_.js → kanban-definition-UXKFOSKX-GYtNMjtS.js} +9 -9
  53. package/dist/{katex-C6D4cX_N.js → katex-BNCmKUol.js} +983 -1120
  54. package/dist/{loader-D2W4oM-3.js → loader-DEZSzvma.js} +253 -248
  55. package/dist/main.js +622 -620
  56. package/dist/{mermaid-DPNuhemn.js → mermaid-Dnzoxjt1.js} +49 -49
  57. package/dist/{mhchem-DWCF06w9.js → mhchem-BEc7A1or.js} +1 -1
  58. package/dist/{mindmap-definition-YA3MSWOX-Cgvojw3y.js → mindmap-definition-YA3MSWOX-Cth76xgH.js} +11 -11
  59. package/dist/{number-overlay-editor-ClZmv3Qw.js → number-overlay-editor-CcdYLWWR.js} +1 -1
  60. package/dist/{pegDiagram-XKGWAZYB-DahHhYyN.js → pegDiagram-XKGWAZYB-Cu4uOXii.js} +3 -3
  61. package/dist/{pieDiagram-E7YTZNPT-CCtX0oCK.js → pieDiagram-E7YTZNPT-Bh3eDa9s.js} +3 -3
  62. package/dist/{process-output-BCTjlyuH.js → process-output-C6apL85F.js} +2 -2
  63. package/dist/{quadrantDiagram-AXDQQJYC-DftINo6g.js → quadrantDiagram-AXDQQJYC--kqzFC3w.js} +1 -1
  64. package/dist/{railroadDiagram-O6MQD6OU-Du5NsPUb.js → railroadDiagram-O6MQD6OU-DG-M6Rnc.js} +3 -3
  65. package/dist/{react-vega-5AcMW8gG.js → react-vega-ewj7q_U5.js} +1 -1
  66. package/dist/{requirementDiagram-BXWQKSXE-0pmmxL6_.js → requirementDiagram-BXWQKSXE-DCwsThh0.js} +11 -11
  67. package/dist/{reveal-component-C1s_K6O8.js → reveal-component-DUPORxps.js} +744 -743
  68. package/dist/{sankeyDiagram-P5KCCOFB-CDKR5TJH.js → sankeyDiagram-P5KCCOFB-BdzhT0BM.js} +1 -1
  69. package/dist/{sequenceDiagram-WJ2MYXX4-DB-ChWwU.js → sequenceDiagram-WJ2MYXX4-CbXB1_PU.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-orN6AwMx.js → stateDiagram-D77RDMKH-A-Zf3Sb2.js} +13 -13
  71. package/dist/stateDiagram-v2-MP3YSRHH-D9YMnUGx.js +30 -0
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-BkSDcqDT.js → swimlanes-42K2YHIH-B_pMf0J4.js} +10 -10
  74. package/dist/swimlanesDiagram-VR7AAH4N-Ctgy-yGP.js +30 -0
  75. package/dist/{timeline-definition-24CTP7MA-BuqJUIRZ.js → timeline-definition-24CTP7MA-DTXFjTTM.js} +3 -3
  76. package/dist/{toDate-WXg1yLcI.js → toDate-C67bxMWn.js} +139 -111
  77. package/dist/{vega-component-D0QXopU8.js → vega-component-tFOzgarw.js} +12 -12
  78. package/dist/{vega-embed-container-CbnTccZi.js → vega-embed-container-sSR5hAsy.js} +2 -2
  79. package/dist/{vega-loader.browser-D95WfUcj.js → vega-loader.browser-DNST6uVf.js} +2 -2
  80. package/dist/{vennDiagram-4TSXK5OY-CTUlJ2PK.js → vennDiagram-4TSXK5OY-BPHjZo0R.js} +3 -3
  81. package/dist/{wardleyDiagram-VM6X3IG4-3evLUb0M.js → wardleyDiagram-VM6X3IG4-6fTLYBuD.js} +3 -3
  82. package/dist/{xychartDiagram-S5SC5T6Z-DL2E3K8-.js → xychartDiagram-S5SC5T6Z-BYjwjrzE.js} +5 -5
  83. package/package.json +1 -2
  84. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +286 -0
  85. package/src/components/data-table/charts/charts.tsx +26 -13
  86. package/src/components/editor/chrome/components/contribute-snippet-button.tsx +6 -1
  87. package/src/components/editor/notebook-cell.tsx +2 -2
  88. package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
  89. package/src/components/editor/output/VideoOutput.tsx +1 -1
  90. package/src/components/editor/output/ansi-reduce.ts +8 -4
  91. package/src/components/editor/output/console/text-rendering.tsx +3 -1
  92. package/src/components/home/components.tsx +1 -0
  93. package/src/components/pages/run-page.tsx +1 -0
  94. package/src/components/static-html/share-modal.tsx +2 -1
  95. package/src/components/static-html/static-banner.tsx +2 -2
  96. package/src/components/ui/links.tsx +6 -1
  97. package/src/components/ui/typography.tsx +16 -8
  98. package/src/core/kernel/__tests__/session.test.ts +110 -1
  99. package/src/core/kernel/session.ts +46 -15
  100. package/src/css/globals.css +18 -21
  101. package/src/css/katex.min.css +260 -241
  102. package/src/plugins/impl/vega/__tests__/utils.test.ts +16 -1
  103. package/src/plugins/impl/vega/__tests__/vega.test.ts +37 -0
  104. package/src/plugins/impl/vega/loader.ts +23 -1
  105. package/src/plugins/impl/vega/utils.ts +5 -4
  106. package/src/plugins/layout/RoutesPlugin.tsx +4 -6
  107. package/dist/classDiagram-ZZMXUADV-Fe-V14Sc.js +0 -30
  108. package/dist/classDiagram-v2-VYDZK3BY-xTDg7lOs.js +0 -30
  109. package/dist/flowDiagram-HODETNUW-6dMdB8pR.js +0 -16
  110. package/dist/katex-DFBiSucW.js +0 -21840
  111. package/dist/stateDiagram-v2-MP3YSRHH-o05_rJN8.js +0 -30
  112. package/dist/swimlanesDiagram-VR7AAH4N-D_nFQfRc.js +0 -30
@@ -237,8 +237,8 @@ function Ye(e) {
237
237
  if ("spec" in e) return Ye(e.spec);
238
238
  }
239
239
  }
240
- function Xe(t) {
241
- return !t || Object.keys(t).length === 0 ? "auto" : e.mapValues(t, (e) => e === "geometry" || e === "date" || e === "time" ? "string" : e === "datetime" ? "date" : e);
240
+ function Xe(t, { parseDates: n = !1 } = {}) {
241
+ return !t || Object.keys(t).length === 0 ? "auto" : e.mapValues(t, (e) => e === "geometry" ? "string" : e === "date" ? n ? "date" : "string" : e === "time" ? "string" : e === "datetime" ? "date" : e);
242
242
  }
243
243
  //#endregion
244
244
  //#region ../node_modules/.pnpm/d3-dsv@3.0.1/node_modules/d3-dsv/src/dsv.js
@@ -1,9 +1,9 @@
1
- import { i as e } from "./chunk-75Z2AOVW-5_HsvXwr.js";
1
+ import { i as e } from "./chunk-75Z2AOVW-C4AOleKf.js";
2
2
  import { t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
- import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-BUgAHvnM.js";
4
+ import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-1TwWR6gh.js";
5
5
  import { t as _ } from "./rough.esm-CtV9AnV1.js";
6
- import { p as v } from "./mermaid-DPNuhemn.js";
6
+ import { p as v } from "./mermaid-Dnzoxjt1.js";
7
7
  //#region ../node_modules/.pnpm/khroma@2.1.0/node_modules/khroma/dist/methods/transparentize.js
8
8
  var y = (e, t) => p(e, "a", -t), b = 1e-10;
9
9
  function x(e, t) {
@@ -1,8 +1,8 @@
1
- import { i as e } from "./chunk-75Z2AOVW-5_HsvXwr.js";
1
+ import { i as e } from "./chunk-75Z2AOVW-C4AOleKf.js";
2
2
  import { n as t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
- import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-BUgAHvnM.js";
5
- import { p as m } from "./mermaid-DPNuhemn.js";
4
+ import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-1TwWR6gh.js";
5
+ import { p as m } from "./mermaid-Dnzoxjt1.js";
6
6
  import { n as h } from "./mermaid-parser.core-DxuSfYwJ.js";
7
7
  import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
8
8
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
@@ -2,14 +2,14 @@ import { t as e } from "./linear-Cnaq3Tfb.js";
2
2
  import { n as t } from "./ordinal-c7efuCSS.js";
3
3
  import { t as n } from "./range-BAIvbVHp.js";
4
4
  import { q as r } from "./cubic-OaEBhQPK.js";
5
- import { i } from "./chunk-75Z2AOVW-5_HsvXwr.js";
5
+ import { i } from "./chunk-75Z2AOVW-C4AOleKf.js";
6
6
  import { n as a } from "./init-Ix4LXMlt.js";
7
7
  import { n as o } from "./src-CA_m4ONl.js";
8
8
  import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
- import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BUgAHvnM.js";
10
- import "./chunk-PWAF6VOD-CTFCL0jI.js";
11
- import { t as b } from "./chunk-GMAD6QVW-Czynz8aA.js";
12
- import { p as x } from "./mermaid-DPNuhemn.js";
9
+ import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-1TwWR6gh.js";
10
+ import "./chunk-PWAF6VOD-lNxB9k8z.js";
11
+ import { t as b } from "./chunk-GMAD6QVW-vStrMz2P.js";
12
+ import { p as x } from "./mermaid-Dnzoxjt1.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.1-dev2",
3
+ "version": "0.25.1-dev20",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -198,7 +198,6 @@
198
198
  "@testing-library/jest-dom": "^7.0.1",
199
199
  "@testing-library/react": "^16.3.2",
200
200
  "@types/dompurify": "^3.2.0",
201
- "@types/katex": "^0.16.8",
202
201
  "@types/lodash-es": "^4.17.12",
203
202
  "@types/node": "^24.13.3",
204
203
  "@types/plotly.js": "^2.35.16",
@@ -0,0 +1,286 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, waitFor } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { Tooltip } from "radix-ui";
6
+ import type { ComponentProps } from "react";
7
+ import { beforeAll, describe, expect, it, vi } from "vitest";
8
+ import { SetupMocks } from "@/__mocks__/common";
9
+ import { cellId } from "@/__tests__/branded";
10
+ import { LazyVegaEmbed } from "@/components/charts/lazy";
11
+ import { vegaLoader } from "@/plugins/impl/vega/loader";
12
+ import { ChartPanel, TablePanel, type TablePanelProps } from "../charts";
13
+ import { getChartTabName, tabsStorageAtom } from "../storage";
14
+ import { ChartType, NONE_VALUE } from "../types";
15
+
16
+ vi.mock("@/components/charts/lazy", () => ({
17
+ LazyVegaEmbed: vi.fn(() => null),
18
+ }));
19
+
20
+ beforeAll(() => {
21
+ SetupMocks.resizeObserver();
22
+ });
23
+
24
+ describe("ChartPanel", () => {
25
+ it.each([undefined, null, []] as const)(
26
+ "infers the full CSV through TablePanel without backend metadata (%s)",
27
+ async (fieldTypes) => {
28
+ vi.spyOn(vegaLoader, "load").mockResolvedValue(
29
+ "value,index\n,0\n2.5,1\n",
30
+ );
31
+ const store = createStore();
32
+ const id = cellId("csv-inference");
33
+ const tabName = getChartTabName(0, ChartType.BAR);
34
+ store.set(
35
+ tabsStorageAtom,
36
+ new Map([
37
+ [
38
+ id,
39
+ [
40
+ {
41
+ tabName,
42
+ chartType: ChartType.BAR,
43
+ config: {
44
+ general: {
45
+ xColumn: { field: "value", type: "number" },
46
+ yColumn: {
47
+ field: "index",
48
+ type: "integer",
49
+ aggregate: NONE_VALUE,
50
+ },
51
+ },
52
+ },
53
+ },
54
+ ],
55
+ ],
56
+ ]),
57
+ );
58
+ const sample = [{ value: null }];
59
+ const SampleTable = (_props: { data: typeof sample }) => null;
60
+ const props: TablePanelProps = {
61
+ cellId: id,
62
+ data: sample,
63
+ dataTable: <SampleTable data={sample} />,
64
+ totalRows: 2,
65
+ columns: 1,
66
+ displayHeader: true,
67
+ fieldTypes: fieldTypes == null ? fieldTypes : [...fieldTypes],
68
+ rowHeaders: [["index", ["integer", "int64"]]],
69
+ getDataUrl: vi
70
+ .fn()
71
+ .mockResolvedValue({ data_url: "chart.csv", format: "csv" }),
72
+ };
73
+ const { getByRole } = render(
74
+ <Provider store={store}>
75
+ <Tooltip.Provider>
76
+ <TablePanel {...props} />
77
+ </Tooltip.Provider>
78
+ </Provider>,
79
+ );
80
+ fireEvent.click(getByRole("tab", { name: tabName }));
81
+ await waitFor(() => {
82
+ expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual(
83
+ expect.objectContaining({
84
+ data: {
85
+ values: [
86
+ { value: null, index: 0 },
87
+ { value: 2.5, index: 1 },
88
+ ],
89
+ },
90
+ }),
91
+ );
92
+ });
93
+ },
94
+ );
95
+
96
+ it("reloads CSV only when column types change, not their array identity", async () => {
97
+ vi.spyOn(vegaLoader, "load").mockResolvedValue("value\n001\n");
98
+ const getDataUrl = vi.fn().mockResolvedValue({
99
+ data_url: "chart.csv",
100
+ format: "csv",
101
+ });
102
+ const props: ComponentProps<typeof ChartPanel> = {
103
+ tableData: [{ value: "001" }],
104
+ chartConfig: {
105
+ general: {
106
+ xColumn: { field: "value", type: "string" },
107
+ yColumn: { field: "value", type: "integer", aggregate: NONE_VALUE },
108
+ },
109
+ },
110
+ chartType: ChartType.BAR,
111
+ saveChart: vi.fn(),
112
+ saveChartType: vi.fn(),
113
+ getDataUrl,
114
+ isLargeDataset: false,
115
+ };
116
+ const panel = (
117
+ fieldTypes: ComponentProps<typeof ChartPanel>["fieldTypes"],
118
+ ) => (
119
+ <Tooltip.Provider>
120
+ <ChartPanel {...props} fieldTypes={fieldTypes} />
121
+ </Tooltip.Provider>
122
+ );
123
+ const expectValues = async (value: string | number) => {
124
+ await waitFor(() => {
125
+ expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual(
126
+ expect.objectContaining({ data: { values: [{ value }] } }),
127
+ );
128
+ });
129
+ };
130
+
131
+ const { rerender } = render(panel([["value", ["string", "object"]]]));
132
+ await expectValues("001");
133
+ expect(getDataUrl).toHaveBeenCalledTimes(1);
134
+
135
+ rerender(panel([["value", ["string", "object"]]]));
136
+ await expectValues("001");
137
+ expect(getDataUrl).toHaveBeenCalledTimes(1);
138
+
139
+ rerender(panel([["value", ["integer", "int64"]]]));
140
+ await expectValues(1);
141
+ expect(getDataUrl).toHaveBeenCalledTimes(2);
142
+ });
143
+
144
+ it("uses column types to parse numeric CSV values without coercing text", async () => {
145
+ vi.spyOn(vegaLoader, "load").mockResolvedValue(
146
+ "a.b,n,label,day,timestamp,active,duration\ninf,1,inf,2024-01-01,2024-01-01T12:00:00Z,True,1 days\n-inf,2,001,2024-01-02,2024-01-02T12:00:00Z,False,2 days\n2.5,3,2024-01-03,2024-01-03,2024-01-03T12:00:00Z,True,3 days\n,4,,,,,\n",
147
+ );
148
+
149
+ render(
150
+ <Tooltip.Provider>
151
+ <ChartPanel
152
+ tableData={[{ "a.b": 1, n: 1 }]}
153
+ chartConfig={{
154
+ general: {
155
+ xColumn: { field: "a.b", type: "number" },
156
+ yColumn: { field: "n", type: "integer", aggregate: NONE_VALUE },
157
+ },
158
+ }}
159
+ chartType={ChartType.BAR}
160
+ saveChart={vi.fn()}
161
+ saveChartType={vi.fn()}
162
+ getDataUrl={vi.fn().mockResolvedValue({
163
+ data_url: "chart.csv",
164
+ format: "csv",
165
+ })}
166
+ fieldTypes={[
167
+ ["a.b", ["number", "float64"]],
168
+ ["n", ["integer", "int64"]],
169
+ ["label", ["string", "object"]],
170
+ ["day", ["date", "date"]],
171
+ ["timestamp", ["datetime", "datetime64[ns]"]],
172
+ ["active", ["boolean", "bool"]],
173
+ ["duration", ["unknown", "timedelta64[ns]"]],
174
+ ]}
175
+ isLargeDataset={false}
176
+ />
177
+ </Tooltip.Provider>,
178
+ );
179
+
180
+ await waitFor(() => {
181
+ expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual(
182
+ expect.objectContaining({
183
+ data: {
184
+ values: [
185
+ {
186
+ "a.b": Infinity,
187
+ n: 1,
188
+ label: "inf",
189
+ day: new Date("2024-01-01"),
190
+ timestamp: new Date("2024-01-01T12:00:00Z"),
191
+ active: true,
192
+ duration: "1 days",
193
+ },
194
+ {
195
+ "a.b": -Infinity,
196
+ n: 2,
197
+ label: "001",
198
+ day: new Date("2024-01-02"),
199
+ timestamp: new Date("2024-01-02T12:00:00Z"),
200
+ active: false,
201
+ duration: "2 days",
202
+ },
203
+ {
204
+ "a.b": 2.5,
205
+ n: 3,
206
+ label: "2024-01-03",
207
+ day: new Date("2024-01-03"),
208
+ timestamp: new Date("2024-01-03T12:00:00Z"),
209
+ active: true,
210
+ duration: "3 days",
211
+ },
212
+ {
213
+ "a.b": null,
214
+ n: 4,
215
+ label: null,
216
+ day: "",
217
+ timestamp: "",
218
+ active: null,
219
+ duration: "",
220
+ },
221
+ ],
222
+ },
223
+ }),
224
+ );
225
+ });
226
+ });
227
+
228
+ it.each([true, false])(
229
+ "keeps dotted CSV columns aligned with the chart encodings (schema: %s)",
230
+ async (hasSchema) => {
231
+ vi.spyOn(vegaLoader, "load").mockResolvedValue("a.b,n\n1,1\n2,2\n3,3\n");
232
+
233
+ render(
234
+ <Tooltip.Provider>
235
+ <ChartPanel
236
+ tableData={[{ "a.b": 1, n: 1 }]}
237
+ chartConfig={{
238
+ general: {
239
+ xColumn: { field: "a.b", type: "number" },
240
+ yColumn: {
241
+ field: "n",
242
+ type: "number",
243
+ aggregate: NONE_VALUE,
244
+ },
245
+ },
246
+ }}
247
+ chartType={ChartType.BAR}
248
+ saveChart={vi.fn()}
249
+ saveChartType={vi.fn()}
250
+ getDataUrl={vi.fn().mockResolvedValue({
251
+ data_url: "chart.csv",
252
+ format: "csv",
253
+ })}
254
+ fieldTypes={
255
+ hasSchema
256
+ ? [
257
+ ["a.b", ["number", "float64"]],
258
+ ["n", ["integer", "int64"]],
259
+ ]
260
+ : undefined
261
+ }
262
+ isLargeDataset={false}
263
+ />
264
+ </Tooltip.Provider>,
265
+ );
266
+
267
+ await waitFor(() => {
268
+ expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual(
269
+ expect.objectContaining({
270
+ data: {
271
+ values: [
272
+ { "a.b": 1, n: 1 },
273
+ { "a.b": 2, n: 2 },
274
+ { "a.b": 3, n: 3 },
275
+ ],
276
+ },
277
+ encoding: expect.objectContaining({
278
+ x: expect.objectContaining({ field: "a\\.b" }),
279
+ y: expect.objectContaining({ field: "n" }),
280
+ }),
281
+ }),
282
+ );
283
+ });
284
+ },
285
+ );
286
+ });
@@ -23,9 +23,11 @@ import { Form } from "@/components/ui/form";
23
23
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
24
24
  import type { CellId } from "@/core/cells/ids";
25
25
  import { useAsyncData } from "@/hooks/useAsyncData";
26
+ import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
26
27
  import { useDebouncedCallback } from "@/hooks/useDebounce";
27
28
  import type { GetDataUrl } from "@/plugins/impl/DataTablePlugin";
28
29
  import { vegaLoadData } from "@/plugins/impl/vega/loader";
30
+ import { getVegaFieldTypes } from "@/plugins/impl/vega/utils";
29
31
  import { useTheme } from "@/theme/useTheme";
30
32
  import { uniqueBy } from "@/utils/arrays";
31
33
  import { inferFieldTypes } from "../columns";
@@ -284,6 +286,7 @@ export const TablePanel: React.FC<TablePanelProps> = ({
284
286
  saveChart={saveChart}
285
287
  saveChartType={saveChartType}
286
288
  getDataUrl={getDataUrl}
289
+ hasSchema={Boolean(fieldTypes?.length)}
287
290
  fieldTypes={mergeIndexFields(
288
291
  fieldTypes ?? inferFieldTypes(dataTable.props.data),
289
292
  rowHeaders,
@@ -307,6 +310,7 @@ export const ChartPanel: React.FC<{
307
310
  saveChartType: (chartType: ChartType) => void;
308
311
  getDataUrl?: GetDataUrl;
309
312
  fieldTypes?: FieldTypesWithExternalType | null;
313
+ hasSchema?: boolean;
310
314
  isLargeDataset: boolean;
311
315
  }> = ({
312
316
  tableData,
@@ -316,6 +320,7 @@ export const ChartPanel: React.FC<{
316
320
  saveChartType,
317
321
  getDataUrl,
318
322
  fieldTypes,
323
+ hasSchema = Boolean(fieldTypes?.length),
319
324
  isLargeDataset,
320
325
  }) => {
321
326
  const { theme } = useTheme();
@@ -331,6 +336,8 @@ export const ChartPanel: React.FC<{
331
336
  const [renderLargeCharts, setRenderLargeCharts] = useState(!isLargeDataset);
332
337
 
333
338
  const { ref: chartContainerRef } = useResizeObserver();
339
+ // Sample-inferred types are useful for controls, but not full CSV parsing.
340
+ const csvFieldTypes = useDeepCompareMemoize(hasSchema ? fieldTypes : null);
334
341
 
335
342
  const { data, isPending, error } = useAsyncData(async () => {
336
343
  if (!getDataUrl || tableData.length === 0 || !renderLargeCharts) {
@@ -342,20 +349,26 @@ export const ChartPanel: React.FC<{
342
349
  return response.data_url;
343
350
  }
344
351
 
345
- const chartData = await vegaLoadData(
346
- response.data_url,
347
- response.format === "arrow"
348
- ? { type: "arrow" }
349
- : response.format === "json"
350
- ? { type: "json" }
351
- : { type: "csv", parse: "auto" },
352
- {
353
- replacePeriod: true,
354
- },
355
- );
356
- return chartData;
352
+ let format: Parameters<typeof vegaLoadData>[1];
353
+ if (response.format === "arrow") {
354
+ format = { type: "arrow" };
355
+ } else if (response.format === "json") {
356
+ format = { type: "json" };
357
+ } else {
358
+ format = {
359
+ type: "csv",
360
+ parse: getVegaFieldTypes(
361
+ csvFieldTypes &&
362
+ Object.fromEntries(
363
+ csvFieldTypes.map(([name, [type]]) => [name, type]),
364
+ ),
365
+ { parseDates: true },
366
+ ),
367
+ };
368
+ }
369
+ return vegaLoadData(response.data_url, format);
357
370
  // Re-run when the data table changes
358
- }, [tableData, renderLargeCharts]);
371
+ }, [tableData, renderLargeCharts, csvFieldTypes]);
359
372
 
360
373
  const formValues = form.watch();
361
374
 
@@ -40,7 +40,12 @@ const ContributeSnippetModal: React.FC<{
40
40
  <DialogDescription>
41
41
  Have a useful snippet you want to share with the community? Make a
42
42
  pull request{" "}
43
- <a href={Constants.githubPage} target="_blank" className="underline">
43
+ <a
44
+ href={Constants.githubPage}
45
+ target="_blank"
46
+ rel="noreferrer"
47
+ className="underline"
48
+ >
44
49
  on GitHub
45
50
  </a>
46
51
  .
@@ -762,7 +762,7 @@ const EditableCellComponent = ({
762
762
  href="https://links.marimo.app/reusable-definitions"
763
763
  target="_blank"
764
764
  className="hover:underline text-muted-foreground text-xs font-bold"
765
- rel="noopener"
765
+ rel="noreferrer"
766
766
  >
767
767
  reusable
768
768
  </a>
@@ -801,7 +801,7 @@ const EditableCellComponent = ({
801
801
  <a
802
802
  href="https://links.marimo.app/reusable-definitions"
803
803
  target="_blank"
804
- rel="noopener"
804
+ rel="noreferrer"
805
805
  >
806
806
  <HelpCircleIcon
807
807
  size={16}
@@ -177,7 +177,7 @@ export const MarimoTracebackOutput = ({
177
177
  <a
178
178
  target="_blank"
179
179
  href="https://marimo.io/discord?ref=notebook"
180
- rel="noopener"
180
+ rel="noreferrer"
181
181
  >
182
182
  <MessageCircleIcon className="h-4 w-4 mr-2" />
183
183
  Ask in Discord
@@ -7,5 +7,5 @@ interface Props {
7
7
  }
8
8
 
9
9
  export const VideoOutput = ({ src, className }: Props): JSX.Element => {
10
- return <iframe className={className} src={src} />;
10
+ return <iframe className={className} src={src} title="Video output" />;
11
11
  };
@@ -13,8 +13,10 @@ export interface Cursor {
13
13
  export class TerminalBuffer {
14
14
  private lines: string[] = [""];
15
15
  private cursor: Cursor = { row: 0, col: 0 };
16
- // oxlint-ignore-next-line no-control-regex -- Needed for ANSI escape sequence parsing
17
- private static readonly ESCAPE_REGEX = /\u001B\[([0-9;]*)([A-DJKH])/u;
16
+ private static readonly ESCAPE_REGEX = new RegExp(
17
+ String.raw`\u001B\[([0-9;]*)([A-DJKH])`,
18
+ "u",
19
+ );
18
20
 
19
21
  /** Ensure the internal lines array is large enough. */
20
22
  private ensureLine(row: number) {
@@ -180,8 +182,10 @@ export class TerminalBuffer {
180
182
  */
181
183
  export class AnsiParser {
182
184
  // Matches both CSI sequences (ESC[...letter) and other escape sequences like character set selection (ESC(B)
183
- // oxlint-ignore-next-line no-control-regex -- Needed for ANSI parsing
184
- private ESC_REGEX = /\u001B(?:\[[0-9;]*[A-Za-z]|\([0-9A-Za-z])/gu;
185
+ private ESC_REGEX = new RegExp(
186
+ String.raw`\u001B(?:\[[0-9;]*[A-Za-z]|\([0-9A-Za-z])`,
187
+ "gu",
188
+ );
185
189
 
186
190
  parse(input: string): { type: "text" | "escape"; value: string }[] {
187
191
  const tokens: { type: "text" | "escape"; value: string }[] = [];
@@ -256,6 +256,7 @@ export const pipInstallReplacer: Replacer = (domNode: DOMNode) => {
256
256
  );
257
257
  }
258
258
 
259
+ // oxlint-disable-next-line react/jsx-no-useless-fragment -- html-react-parser replacers must return a React element.
259
260
  return <>{segments}</>;
260
261
  };
261
262
 
@@ -283,6 +284,7 @@ export const urlReplacer: Replacer = (domNode: DOMNode) => {
283
284
  return undefined;
284
285
  }
285
286
 
287
+ // oxlint-disable-next-line react/jsx-no-useless-fragment -- html-react-parser replacers must return a React element.
286
288
  return <>{result}</>;
287
289
  };
288
290
 
@@ -331,5 +333,5 @@ export const RenderTextWithLinks = ({ text }: { text: string }) => {
331
333
  );
332
334
  }, [text]);
333
335
 
334
- return <>{content}</>;
336
+ return content;
335
337
  };
@@ -170,6 +170,7 @@ export const ResourceLinks: React.FC = () => {
170
170
  key={resource.title}
171
171
  href={resource.url}
172
172
  target="_blank"
173
+ rel="noreferrer"
173
174
  aria-label={`${resource.title}: ${resource.description}`}
174
175
  className="flex items-start gap-3 py-3 px-3 rounded-lg border hover:bg-accent/20 transition-colors shadow-xs"
175
176
  >
@@ -50,6 +50,7 @@ const Watermark = () => {
50
50
  <a
51
51
  href={Constants.githubPage}
52
52
  target="_blank"
53
+ rel="noreferrer"
53
54
  className="text-sm text-(--grass-11) font-bold tracking-wide transition-colors bg-(--grass-4) hover:bg-(--grass-5) border-t border-l border-(--grass-8) px-3 py-1 rounded-tl-md flex items-center gap-2"
54
55
  >
55
56
  <span>made with marimo</span>
@@ -74,6 +74,7 @@ export const ShareStaticNotebookModal: React.FC<{
74
74
  <a
75
75
  href={Constants.issuesPage}
76
76
  target="_blank"
77
+ rel="noreferrer"
77
78
  className="underline"
78
79
  >
79
80
  GitHub
@@ -102,7 +103,7 @@ export const ShareStaticNotebookModal: React.FC<{
102
103
  <DialogDescription>
103
104
  You can publish a static, non-interactive version of this notebook
104
105
  to the public web. We will create a link for you that lives on{" "}
105
- <a href={BASE_URL} target="_blank">
106
+ <a href={BASE_URL} target="_blank" rel="noreferrer">
106
107
  {BASE_URL}
107
108
  </a>
108
109
  .
@@ -1,6 +1,4 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- /* oxlint-disable react/jsx-no-comment-textnodes */
3
- /* oxlint-disable react/jsx-no-target-blank */
4
2
 
5
3
  import { useAtomValue } from "jotai";
6
4
  import { CopyIcon, DownloadIcon } from "lucide-react";
@@ -49,6 +47,7 @@ export const StaticBanner: React.FC = () => {
49
47
  <a
50
48
  href={Constants.githubPage}
51
49
  target="_blank"
50
+ rel="noreferrer"
52
51
  className="text-(--sky-11) font-medium underline"
53
52
  >
54
53
  marimo
@@ -93,6 +92,7 @@ const StaticBannerDialog = ({ code }: { code: string }) => {
93
92
  <a
94
93
  href={Constants.githubPage}
95
94
  target="_blank"
95
+ rel="noreferrer"
96
96
  className="text-(--sky-11) hover:underline font-medium"
97
97
  >
98
98
  marimo
@@ -21,7 +21,12 @@ export const ExternalLink = ({
21
21
  children: React.ReactNode;
22
22
  }) => {
23
23
  return (
24
- <a href={href} target="_blank" className="text-link hover:underline">
24
+ <a
25
+ href={href}
26
+ target="_blank"
27
+ rel="noreferrer"
28
+ className="text-link hover:underline"
29
+ >
25
30
  {children}
26
31
  </a>
27
32
  );