matterviz 0.4.1 → 0.4.3

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 (499) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +48 -60
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/api/optimade.js +10 -7
  6. package/dist/app.css +68 -0
  7. package/dist/brillouin/BrillouinZone.svelte +90 -91
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
  9. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
  12. package/dist/brillouin/BrillouinZoneScene.svelte +46 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  14. package/dist/brillouin/compute.js +10 -16
  15. package/dist/brillouin/geometry.js +2 -2
  16. package/dist/chempot-diagram/ChemPotDiagram.svelte +48 -50
  17. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +61 -169
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +559 -755
  20. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
  21. package/dist/chempot-diagram/async-compute.svelte.js +13 -14
  22. package/dist/chempot-diagram/color.d.ts +21 -1
  23. package/dist/chempot-diagram/color.js +70 -1
  24. package/dist/chempot-diagram/compute.d.ts +7 -0
  25. package/dist/chempot-diagram/compute.js +85 -26
  26. package/dist/chempot-diagram/controls-state.svelte.js +1 -1
  27. package/dist/chempot-diagram/export.js +1 -6
  28. package/dist/chempot-diagram/temperature.js +9 -10
  29. package/dist/colors/index.d.ts +0 -4
  30. package/dist/colors/index.js +19 -20
  31. package/dist/composition/BarChart.svelte +30 -26
  32. package/dist/composition/BubbleChart.svelte +17 -17
  33. package/dist/composition/Composition.svelte +10 -19
  34. package/dist/composition/Formula.svelte +20 -27
  35. package/dist/composition/FormulaFilter.svelte +115 -184
  36. package/dist/composition/PieChart.svelte +55 -41
  37. package/dist/composition/format.js +7 -9
  38. package/dist/composition/index.d.ts +2 -0
  39. package/dist/composition/index.js +14 -0
  40. package/dist/composition/parse.d.ts +2 -2
  41. package/dist/composition/parse.js +61 -115
  42. package/dist/constants.d.ts +0 -1
  43. package/dist/constants.js +17 -26
  44. package/dist/convex-hull/ConvexHull.svelte +12 -15
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +93 -94
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +127 -186
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +131 -180
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
  53. package/dist/convex-hull/ConvexHullControls.svelte +46 -48
  54. package/dist/convex-hull/ConvexHullStats.svelte +193 -195
  55. package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
  56. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  57. package/dist/convex-hull/StructurePopup.svelte +18 -22
  58. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  59. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  60. package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
  61. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  62. package/dist/convex-hull/helpers.d.ts +8 -10
  63. package/dist/convex-hull/helpers.js +37 -43
  64. package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
  65. package/dist/convex-hull/thermodynamics.js +37 -44
  66. package/dist/coordination/CoordinationBarPlot.svelte +49 -53
  67. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  68. package/dist/coordination/calc-coordination.d.ts +1 -1
  69. package/dist/coordination/calc-coordination.js +28 -25
  70. package/dist/element/BohrAtom.svelte +5 -9
  71. package/dist/element/ElementHeading.svelte +4 -1
  72. package/dist/element/ElementPhoto.svelte +11 -9
  73. package/dist/element/ElementStats.svelte +12 -10
  74. package/dist/element/ElementTile.svelte +44 -67
  75. package/dist/element/Nucleus.svelte +4 -2
  76. package/dist/element/data.d.ts +3 -2
  77. package/dist/element/data.js +5 -1
  78. package/dist/element/helpers.d.ts +4 -0
  79. package/dist/element/helpers.js +18 -0
  80. package/dist/element/index.d.ts +2 -5
  81. package/dist/element/index.js +4 -19
  82. package/dist/feedback/DragOverlay.svelte +5 -5
  83. package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
  84. package/dist/feedback/StatusMessage.svelte +7 -4
  85. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  86. package/dist/fermi-surface/FermiSurface.svelte +35 -44
  87. package/dist/fermi-surface/FermiSurface.svelte.d.ts +7 -13
  88. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  89. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  90. package/dist/fermi-surface/FermiSurfaceScene.svelte +64 -136
  91. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  92. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  93. package/dist/fermi-surface/compute.js +25 -58
  94. package/dist/fermi-surface/parse.js +54 -88
  95. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  96. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  97. package/dist/file-viewer/PlotPanel.svelte +350 -0
  98. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  99. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  100. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  101. package/dist/file-viewer/detect.d.ts +11 -0
  102. package/dist/file-viewer/detect.js +381 -0
  103. package/dist/file-viewer/eligibility.d.ts +4 -0
  104. package/dist/file-viewer/eligibility.js +53 -0
  105. package/dist/file-viewer/host-protocol.d.ts +50 -0
  106. package/dist/file-viewer/host-transfer.d.ts +25 -0
  107. package/dist/file-viewer/host-transfer.js +42 -0
  108. package/dist/file-viewer/index.d.ts +5 -0
  109. package/dist/file-viewer/index.js +5 -0
  110. package/dist/file-viewer/main.d.ts +44 -0
  111. package/dist/file-viewer/main.js +535 -0
  112. package/dist/file-viewer/parse.d.ts +9 -0
  113. package/dist/file-viewer/parse.js +170 -0
  114. package/dist/file-viewer/plot-utils.d.ts +24 -0
  115. package/dist/file-viewer/plot-utils.js +191 -0
  116. package/dist/file-viewer/types.d.ts +4 -0
  117. package/dist/file-viewer/types.js +5 -0
  118. package/dist/heatmap-matrix/HeatmapMatrix.svelte +153 -219
  119. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
  120. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
  121. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  122. package/dist/heatmap-matrix/index.d.ts +5 -0
  123. package/dist/icons.d.ts +4 -0
  124. package/dist/icons.js +4 -0
  125. package/dist/index.d.ts +2 -2
  126. package/dist/index.js +1 -1
  127. package/dist/io/ExportPane.svelte +11 -7
  128. package/dist/io/decompress.d.ts +4 -1
  129. package/dist/io/decompress.js +24 -36
  130. package/dist/io/export.d.ts +11 -4
  131. package/dist/io/export.js +75 -19
  132. package/dist/io/fetch.d.ts +0 -3
  133. package/dist/io/fetch.js +0 -11
  134. package/dist/io/file-drop.js +60 -35
  135. package/dist/io/index.d.ts +0 -1
  136. package/dist/io/index.js +2 -9
  137. package/dist/io/is-binary.d.ts +9 -0
  138. package/dist/io/is-binary.js +45 -1
  139. package/dist/io/url-drop.d.ts +2 -0
  140. package/dist/io/url-drop.js +40 -40
  141. package/dist/isosurface/Isosurface.svelte +700 -215
  142. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  143. package/dist/isosurface/IsosurfaceControls.svelte +590 -116
  144. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  145. package/dist/isosurface/VolumeSlice.svelte +224 -0
  146. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  147. package/dist/isosurface/coloring.d.ts +20 -0
  148. package/dist/isosurface/coloring.js +118 -0
  149. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  150. package/dist/isosurface/geometry.worker.d.ts +1 -0
  151. package/dist/isosurface/geometry.worker.js +59 -0
  152. package/dist/isosurface/grid.d.ts +10 -0
  153. package/dist/isosurface/grid.js +31 -0
  154. package/dist/isosurface/index.d.ts +4 -0
  155. package/dist/isosurface/index.js +4 -0
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +6 -6
  159. package/dist/isosurface/profile.d.ts +12 -0
  160. package/dist/isosurface/profile.js +19 -0
  161. package/dist/isosurface/sampling.d.ts +34 -0
  162. package/dist/isosurface/sampling.js +408 -0
  163. package/dist/isosurface/slice-rendering.d.ts +13 -0
  164. package/dist/isosurface/slice-rendering.js +79 -0
  165. package/dist/isosurface/slice.d.ts +22 -2
  166. package/dist/isosurface/slice.js +200 -116
  167. package/dist/isosurface/types.d.ts +24 -1
  168. package/dist/isosurface/types.js +120 -8
  169. package/dist/json-path.d.ts +4 -0
  170. package/dist/json-path.js +89 -0
  171. package/dist/labels.d.ts +6 -2
  172. package/dist/labels.js +44 -14
  173. package/dist/layout/FullscreenButton.svelte +2 -1
  174. package/dist/layout/FullscreenToggle.svelte +10 -4
  175. package/dist/layout/InfoCard.svelte +8 -21
  176. package/dist/layout/InfoTag.svelte +1 -3
  177. package/dist/layout/NumberRangeInput.svelte +46 -0
  178. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  179. package/dist/layout/PropertyFilter.svelte +2 -6
  180. package/dist/layout/SettingsSection.svelte +57 -62
  181. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  182. package/dist/layout/SubpageGrid.svelte +3 -1
  183. package/dist/layout/ViewerChrome.svelte +16 -11
  184. package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
  185. package/dist/layout/fullscreen.js +3 -5
  186. package/dist/layout/fullscreen.svelte.js +1 -2
  187. package/dist/layout/index.d.ts +1 -0
  188. package/dist/layout/index.js +1 -0
  189. package/dist/layout/json-tree/JsonNode.svelte +22 -59
  190. package/dist/layout/json-tree/JsonTree.svelte +101 -172
  191. package/dist/layout/json-tree/JsonValue.svelte +9 -32
  192. package/dist/layout/json-tree/utils.d.ts +0 -3
  193. package/dist/layout/json-tree/utils.js +50 -188
  194. package/dist/marching-cubes.d.ts +8 -1
  195. package/dist/marching-cubes.js +115 -48
  196. package/dist/math.d.ts +5 -2
  197. package/dist/math.js +48 -38
  198. package/dist/overlays/ContextMenu.svelte +19 -34
  199. package/dist/overlays/CopyButton.svelte +11 -5
  200. package/dist/overlays/DragControlTab.svelte +6 -1
  201. package/dist/overlays/DraggablePane.svelte +110 -74
  202. package/dist/overlays/DraggablePane.svelte.d.ts +2 -1
  203. package/dist/overlays/GlassChip.svelte +32 -0
  204. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  205. package/dist/overlays/InfoPaneCards.svelte +11 -23
  206. package/dist/overlays/index.d.ts +7 -0
  207. package/dist/overlays/index.js +26 -0
  208. package/dist/overlays/portal.d.ts +2 -0
  209. package/dist/overlays/portal.js +14 -0
  210. package/dist/periodic-table/PeriodicTable.svelte +113 -145
  211. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  212. package/dist/periodic-table/PeriodicTableControls.svelte +37 -115
  213. package/dist/periodic-table/TableInset.svelte +9 -4
  214. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +68 -105
  215. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  216. package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
  217. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  218. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  219. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
  220. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  221. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  222. package/dist/phase-diagram/build-diagram.js +4 -9
  223. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  224. package/dist/phase-diagram/parse.js +15 -15
  225. package/dist/phase-diagram/svg-to-diagram.js +28 -27
  226. package/dist/phase-diagram/utils.js +2 -3
  227. package/dist/plot/bar/BarPlot.svelte +461 -426
  228. package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
  229. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  230. package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
  231. package/dist/plot/bar/SpacegroupBarPlot.svelte +46 -77
  232. package/dist/plot/bar/data.d.ts +5 -4
  233. package/dist/plot/bar/data.js +2 -1
  234. package/dist/plot/box/BoxPlot.svelte +322 -226
  235. package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
  236. package/dist/plot/box/Violin.svelte.d.ts +1 -0
  237. package/dist/plot/box/box-plot.js +1 -1
  238. package/dist/plot/core/auto-place.d.ts +1 -1
  239. package/dist/plot/core/auto-place.js +2 -2
  240. package/dist/plot/core/axis-utils.d.ts +12 -7
  241. package/dist/plot/core/axis-utils.js +48 -48
  242. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  243. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  244. package/dist/plot/core/components/ColorBar.svelte +101 -106
  245. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  246. package/dist/plot/core/components/FillArea.svelte +10 -17
  247. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  248. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  249. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  250. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  251. package/dist/plot/core/components/Line.svelte +18 -9
  252. package/dist/plot/core/components/Line.svelte.d.ts +2 -0
  253. package/dist/plot/core/components/PlotAxis.svelte +40 -17
  254. package/dist/plot/core/components/PlotControls.svelte +65 -41
  255. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  256. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  257. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  258. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  259. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  260. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  261. package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
  262. package/dist/plot/core/components/ReferencePlane.svelte +7 -8
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +27 -474
  271. package/dist/plot/core/data-transform.d.ts +1 -2
  272. package/dist/plot/core/data-transform.js +9 -15
  273. package/dist/plot/core/fill-utils.d.ts +3 -1
  274. package/dist/plot/core/fill-utils.js +28 -6
  275. package/dist/plot/core/index.d.ts +1 -0
  276. package/dist/plot/core/index.js +1 -0
  277. package/dist/plot/core/interactions.js +0 -2
  278. package/dist/plot/core/layout.d.ts +15 -6
  279. package/dist/plot/core/layout.js +57 -38
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  283. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  284. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -21
  286. package/dist/plot/core/reference-line.js +1 -1
  287. package/dist/plot/core/scales.d.ts +23 -0
  288. package/dist/plot/core/scales.js +39 -13
  289. package/dist/plot/core/svg.js +0 -1
  290. package/dist/plot/core/types/fills.d.ts +101 -0
  291. package/dist/plot/core/types/fills.js +15 -0
  292. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  293. package/dist/plot/core/types/plot-3d.js +4 -0
  294. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  295. package/dist/plot/core/types/reference-lines.js +9 -0
  296. package/dist/plot/core/types.d.ts +29 -324
  297. package/dist/plot/core/types.js +23 -20
  298. package/dist/plot/core/utils/hierarchy-chart.d.ts +52 -0
  299. package/dist/plot/core/utils/hierarchy-chart.js +191 -0
  300. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  301. package/dist/plot/core/utils/hierarchy-labels.js +100 -0
  302. package/dist/plot/core/utils/label-placement.js +2 -2
  303. package/dist/plot/histogram/Histogram.svelte +314 -340
  304. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  305. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  306. package/dist/plot/histogram/histogram.d.ts +31 -0
  307. package/dist/plot/histogram/histogram.js +70 -0
  308. package/dist/plot/histogram/index.d.ts +1 -0
  309. package/dist/plot/histogram/index.js +1 -0
  310. package/dist/plot/index.d.ts +1 -0
  311. package/dist/plot/index.js +1 -0
  312. package/dist/plot/sankey/Sankey.svelte +78 -69
  313. package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
  314. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  315. package/dist/plot/sankey/sankey.js +13 -9
  316. package/dist/plot/scatter/BinnedScatterPlot.svelte +311 -141
  317. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
  318. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  319. package/dist/plot/scatter/ScatterPlot.svelte +517 -410
  320. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
  321. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  322. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  323. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  324. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  325. package/dist/plot/scatter/adaptive-density.js +36 -20
  326. package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
  327. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  328. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  329. package/dist/plot/scatter/scatter-data.js +8 -7
  330. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
  331. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
  332. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
  333. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
  334. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
  335. package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
  336. package/dist/plot/sunburst/Sunburst.svelte +269 -347
  337. package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
  338. package/dist/plot/sunburst/index.d.ts +1 -1
  339. package/dist/plot/sunburst/index.js +2 -1
  340. package/dist/plot/sunburst/render.d.ts +2 -2
  341. package/dist/plot/sunburst/render.js +47 -22
  342. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  343. package/dist/plot/sunburst/sunburst.js +10 -3
  344. package/dist/plot/treemap/Treemap.svelte +1022 -0
  345. package/dist/plot/treemap/Treemap.svelte.d.ts +90 -0
  346. package/dist/plot/treemap/index.d.ts +4 -0
  347. package/dist/plot/treemap/index.js +4 -0
  348. package/dist/plot/treemap/labels.d.ts +38 -0
  349. package/dist/plot/treemap/labels.js +80 -0
  350. package/dist/plot/treemap/treemap.d.ts +28 -0
  351. package/dist/plot/treemap/treemap.js +102 -0
  352. package/dist/rdf/RdfPlot.svelte +37 -59
  353. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  354. package/dist/rdf/calc-rdf.js +63 -52
  355. package/dist/rdf/index.d.ts +0 -1
  356. package/dist/sanitize.js +85 -27
  357. package/dist/scene/SceneCamera.svelte +1 -1
  358. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  359. package/dist/scene/bind-renderer.svelte.js +14 -6
  360. package/dist/scene/index.d.ts +1 -2
  361. package/dist/scene/index.js +1 -1
  362. package/dist/scene/{props.js → props.svelte.js} +15 -1
  363. package/dist/settings.d.ts +19 -9
  364. package/dist/settings.js +229 -525
  365. package/dist/spectral/Bands.svelte +207 -221
  366. package/dist/spectral/BandsAndDos.svelte +11 -9
  367. package/dist/spectral/BrillouinBandsDos.svelte +19 -42
  368. package/dist/spectral/Dos.svelte +92 -93
  369. package/dist/spectral/helpers.d.ts +3 -3
  370. package/dist/spectral/helpers.js +65 -140
  371. package/dist/state.svelte.js +5 -18
  372. package/dist/structure/Arrow.svelte +5 -18
  373. package/dist/structure/ArrowInstances.svelte +160 -0
  374. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  375. package/dist/structure/AtomLegend.svelte +120 -85
  376. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  377. package/dist/structure/Bond.svelte +31 -23
  378. package/dist/structure/CanvasTooltip.svelte +15 -3
  379. package/dist/structure/CellSelect.svelte +45 -35
  380. package/dist/structure/Cylinder.svelte +24 -40
  381. package/dist/structure/InstancedAtoms.svelte +114 -0
  382. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  383. package/dist/structure/Lattice.svelte +85 -149
  384. package/dist/structure/SiteLabels.svelte +122 -0
  385. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  386. package/dist/structure/Structure.svelte +636 -504
  387. package/dist/structure/Structure.svelte.d.ts +4 -2
  388. package/dist/structure/StructureCarousel.svelte +637 -0
  389. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  390. package/dist/structure/StructureControls.svelte +165 -425
  391. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  392. package/dist/structure/StructureExportPane.svelte +17 -21
  393. package/dist/structure/StructureInfoPane.svelte +128 -133
  394. package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
  395. package/dist/structure/StructureScene.svelte +535 -578
  396. package/dist/structure/StructureScene.svelte.d.ts +6 -2
  397. package/dist/structure/StructureViewport.svelte +451 -0
  398. package/dist/structure/StructureViewport.svelte.d.ts +65 -0
  399. package/dist/structure/atom-label-placement.d.ts +16 -12
  400. package/dist/structure/atom-label-placement.js +70 -37
  401. package/dist/structure/atom-properties.d.ts +2 -0
  402. package/dist/structure/atom-properties.js +105 -38
  403. package/dist/structure/bond-order-perception.js +3 -8
  404. package/dist/structure/bonding.d.ts +9 -3
  405. package/dist/structure/bonding.js +95 -46
  406. package/dist/structure/edit-history.d.ts +6 -0
  407. package/dist/structure/edit-history.js +27 -0
  408. package/dist/structure/export.d.ts +1 -0
  409. package/dist/structure/export.js +89 -105
  410. package/dist/structure/format-detect.d.ts +1 -0
  411. package/dist/structure/format-detect.js +20 -0
  412. package/dist/structure/geometry.d.ts +9 -0
  413. package/dist/structure/geometry.js +27 -0
  414. package/dist/structure/index.d.ts +16 -1
  415. package/dist/structure/index.js +15 -10
  416. package/dist/structure/measure.d.ts +1 -1
  417. package/dist/structure/measure.js +6 -12
  418. package/dist/structure/parse.d.ts +2 -1
  419. package/dist/structure/parse.js +272 -313
  420. package/dist/structure/partial-occupancy.js +10 -5
  421. package/dist/structure/pbc.js +85 -107
  422. package/dist/structure/polyhedra.d.ts +0 -1
  423. package/dist/structure/polyhedra.js +7 -21
  424. package/dist/structure/prop-groups.d.ts +85 -0
  425. package/dist/structure/prop-groups.js +18 -0
  426. package/dist/structure/supercell.d.ts +1 -2
  427. package/dist/structure/supercell.js +7 -13
  428. package/dist/symmetry/SymmetryElements.svelte +11 -15
  429. package/dist/symmetry/SymmetryStats.svelte +55 -71
  430. package/dist/symmetry/WyckoffTable.svelte +10 -28
  431. package/dist/symmetry/cell-transform.js +6 -14
  432. package/dist/symmetry/index.d.ts +1 -0
  433. package/dist/symmetry/index.js +13 -10
  434. package/dist/symmetry/spacegroups.js +3 -2
  435. package/dist/table/HeatmapTable.svelte +1142 -447
  436. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  437. package/dist/table/ToggleMenu.svelte +168 -53
  438. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  439. package/dist/table/index.d.ts +21 -6
  440. package/dist/table/index.js +34 -21
  441. package/dist/theme/ThemeControl.svelte +16 -8
  442. package/dist/theme/embedded.d.ts +18 -0
  443. package/dist/theme/embedded.js +206 -0
  444. package/dist/theme/index.d.ts +5 -5
  445. package/dist/theme/index.js +5 -12
  446. package/dist/theme/themes.mjs +44 -66
  447. package/dist/tooltip/KCoords.svelte +4 -1
  448. package/dist/tooltip/TooltipContent.svelte +3 -6
  449. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  450. package/dist/tooltip/index.d.ts +8 -1
  451. package/dist/trajectory/Trajectory.svelte +613 -386
  452. package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
  453. package/dist/trajectory/TrajectoryError.svelte +12 -74
  454. package/dist/trajectory/TrajectoryExportPane.svelte +14 -32
  455. package/dist/trajectory/TrajectoryInfoPane.svelte +127 -164
  456. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  457. package/dist/trajectory/extract.js +61 -49
  458. package/dist/trajectory/format-detect.d.ts +2 -0
  459. package/dist/trajectory/format-detect.js +17 -13
  460. package/dist/trajectory/frame-reader.d.ts +2 -0
  461. package/dist/trajectory/frame-reader.js +35 -32
  462. package/dist/trajectory/helpers.d.ts +2 -0
  463. package/dist/trajectory/helpers.js +34 -56
  464. package/dist/trajectory/index.d.ts +1 -0
  465. package/dist/trajectory/index.js +5 -8
  466. package/dist/trajectory/parse/ase.js +1 -1
  467. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  468. package/dist/trajectory/parse/h5-utils.js +109 -0
  469. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  470. package/dist/trajectory/parse/hdf5.js +116 -106
  471. package/dist/trajectory/parse/index.d.ts +6 -6
  472. package/dist/trajectory/parse/index.js +25 -33
  473. package/dist/trajectory/parse/lammps.js +8 -12
  474. package/dist/trajectory/parse/vasp.js +10 -7
  475. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  476. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  477. package/dist/trajectory/parse/vaspout-h5.d.ts +4 -0
  478. package/dist/trajectory/parse/vaspout-h5.js +281 -0
  479. package/dist/trajectory/parse/xyz.js +59 -9
  480. package/dist/trajectory/plotting.d.ts +8 -9
  481. package/dist/trajectory/plotting.js +113 -64
  482. package/dist/utils.d.ts +3 -0
  483. package/dist/utils.js +7 -0
  484. package/dist/xrd/XrdPlot.svelte +79 -118
  485. package/dist/xrd/broadening.js +2 -1
  486. package/dist/xrd/calc-xrd.js +24 -62
  487. package/dist/xrd/parse.d.ts +0 -1
  488. package/dist/xrd/parse.js +69 -95
  489. package/package.json +94 -34
  490. package/readme.md +6 -5
  491. package/dist/api/mp.d.ts +0 -6
  492. package/dist/api/mp.js +0 -22
  493. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  494. package/dist/scene/types.d.ts +0 -26
  495. package/dist/tooltip/types.d.ts +0 -8
  496. package/dist/trajectory/constants.d.ts +0 -6
  497. package/dist/trajectory/constants.js +0 -11
  498. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  499. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -3,7 +3,7 @@
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >
5
5
  import type { D3InterpolateName } from '../../colors'
6
- import { format_value } from '../../labels'
6
+ import { format_value, format_value_or_num } from '../../labels'
7
7
  import { sanitize_html } from '../../sanitize'
8
8
  import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
9
9
  import type { Point2D, Vec2 } from '../../math'
@@ -40,6 +40,7 @@
40
40
  Line,
41
41
  PlotAxis,
42
42
  PlotLegend,
43
+ PlotMarginals,
43
44
  PlotTooltip,
44
45
  ReferenceLine,
45
46
  ScatterPlotControls,
@@ -47,20 +48,34 @@
47
48
  ZeroLines,
48
49
  ZoomRect,
49
50
  } from '..'
51
+ import type { MarginalSeriesInput, MarginalsProp } from '../core/marginals'
52
+ import {
53
+ add_sides,
54
+ marginal_axis,
55
+ marginal_axis_presence,
56
+ normalize_marginals,
57
+ reserve_marginal_pad,
58
+ } from '../core/marginals'
50
59
  import {
51
60
  build_obstacles_norm,
52
61
  has_explicit_position,
53
62
  measured_footprint,
54
63
  place_decorations,
64
+ placed_coords,
55
65
  } from '../core/auto-place'
56
- import type { AxisChangeState } from '../core/axis-utils'
57
- import { AXIS_DEFAULTS, create_axis_loader } from '../core/axis-utils'
66
+ import {
67
+ AXIS_DEFAULTS,
68
+ type AxisChangeState,
69
+ create_axis_loader,
70
+ } from '../core/axis-utils'
58
71
  import { get_series_color, get_series_symbol } from '../core/data-transform'
59
72
  import { create_placed_tween } from '../core/placed-tween.svelte'
60
73
  import {
74
+ COLOR_BAR_DEFAULTS,
61
75
  DEFAULT_MARKERS,
62
76
  get_scale_type_name,
63
77
  is_time_scale,
78
+ SCALE_DEFAULTS,
64
79
  } from '../core/types'
65
80
  import { compute_label_positions } from '../core/utils/label-placement'
66
81
  import { create_legend_visibility } from '../core/utils/series-visibility'
@@ -88,11 +103,12 @@
88
103
  import { create_pan_zoom } from '../core/pan-zoom.svelte'
89
104
  import type { Rect, Sides } from '../core/layout'
90
105
  import {
106
+ AXIS_TITLE_OFFSET,
91
107
  calc_auto_padding,
92
108
  filter_padding,
109
+ full_footprint_or,
93
110
  LABEL_GAP_DEFAULT,
94
111
  y2_axis_label_x,
95
- measure_full_footprint,
96
112
  measure_max_tick_width,
97
113
  sample_series_obstacle_points,
98
114
  } from '../core/layout'
@@ -106,11 +122,10 @@
106
122
  get_nice_data_range,
107
123
  } from '../core/scales'
108
124
  import { resolve_line_tween, unique_id } from '../core/utils'
109
- import {
110
- build_legend_data,
111
- filter_series_to_ranges,
112
- pick_tooltip_bg,
113
- } from './scatter-data'
125
+ import { build_legend_data, filter_series_to_ranges, pick_tooltip_bg } from './scatter-data'
126
+
127
+ // Plots with at least this many visible markers get smaller default point radii
128
+ const DENSE_MARKER_COUNT = 100
114
129
 
115
130
  let {
116
131
  series = $bindable([]),
@@ -122,7 +137,7 @@
122
137
  styles: styles_init = {},
123
138
  controls: controls_init = {},
124
139
  padding = {},
125
- range_padding = 0.05,
140
+ range_padding = 0,
126
141
  current_x_value = null,
127
142
  tooltip_point = $bindable(null),
128
143
  selected_point = null,
@@ -130,13 +145,9 @@
130
145
  tooltip,
131
146
  user_content,
132
147
  change = () => {},
133
- color_scale = {
134
- type: `linear`,
135
- scheme: `interpolateViridis`,
136
- value_range: undefined,
137
- },
148
+ color_scale = SCALE_DEFAULTS.color,
138
149
  color_bar = {},
139
- size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
150
+ size_scale = SCALE_DEFAULTS.size,
140
151
  label_placement_config = {},
141
152
  hover_config = {},
142
153
  legend = {},
@@ -163,100 +174,95 @@
163
174
  on_axis_change,
164
175
  on_error,
165
176
  pan = {},
177
+ marginals = false,
166
178
  ...rest
167
- }: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, `change`> & PlotConfig & {
168
- series?: DataSeries<Metadata>[]
169
- styles?: StyleOverrides
170
- controls?: ControlsConfig
171
- current_x_value?: number | null
172
- tooltip_point?: InternalPoint<Metadata> | null
173
- selected_point?: { series_idx: number; point_idx: number } | null
174
- tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
175
- user_content?: Snippet<[UserContentProps]>
176
- header_controls?: Snippet<
177
- [{ height: number; width: number; fullscreen: boolean }]
178
- >
179
- controls_extra?: Snippet<
180
- [
181
- & { styles: StyleOverrides; selected_series_idx: number }
182
- & Required<PlotConfig>,
183
- ]
184
- >
185
- change?: (
186
- data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
187
- ) => void
188
- color_scale?: ColorScaleConfig | D3InterpolateName
189
- size_scale?: SizeScaleConfig
190
- color_bar?:
191
- | (ComponentProps<typeof ColorBar> & {
192
- margin?: number | Sides
193
- tween?: TweenOptions<Point2D>
194
- responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
195
- axis_clearance?: number // Min distance kept from plot edges/axes (default: 15)
196
- })
197
- | null
198
- label_placement_config?: Partial<LabelPlacementConfig>
199
- hover_config?: Partial<HoverConfig>
200
- legend?: LegendConfig | null
201
- point_tween?: TweenOptions<Point2D>
202
- line_tween?: TweenOptions<string>
203
- point_events?: Record<
204
- string,
205
- (payload: { point: InternalPoint<Metadata>; event: Event }) => void
206
- >
207
- on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
208
- on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
209
- fill_regions?: FillRegion[] // Bindable for legend toggle support
210
- error_bands?: ErrorBand[]
211
- on_fill_click?: (event: FillHandlerEvent) => void
212
- on_fill_hover?: (event: FillHandlerEvent | null) => void
213
- ref_lines?: RefLine[] // Bindable for legend toggle support
214
- on_ref_line_click?: (event: RefLineEvent) => void
215
- on_ref_line_hover?: (event: RefLineEvent | null) => void
216
- selected_series_idx?: number
217
- wrapper?: HTMLDivElement
218
- // Interactive axis props
219
- data_loader?: DataLoaderFn<Metadata>
220
- on_axis_change?: (
221
- axis: `x` | `x2` | `y` | `y2`,
222
- key: string,
223
- new_series: DataSeries<Metadata>[],
224
- ) => void
225
- on_error?: (error: AxisLoadError) => void
226
- pan?: PanConfig
227
- } = $props()
179
+ }: HTMLAttributes<HTMLDivElement> &
180
+ Omit<BasePlotProps, `change`> &
181
+ PlotConfig & {
182
+ series?: DataSeries<Metadata>[]
183
+ styles?: StyleOverrides
184
+ controls?: ControlsConfig
185
+ current_x_value?: number | null
186
+ tooltip_point?: InternalPoint<Metadata> | null
187
+ selected_point?: { series_idx: number; point_idx: number } | null
188
+ tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
189
+ user_content?: Snippet<[UserContentProps]>
190
+ header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
191
+ controls_extra?: Snippet<
192
+ [{ styles: StyleOverrides; selected_series_idx: number } & Required<PlotConfig>]
193
+ >
194
+ change?: (data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null) => void
195
+ color_scale?: ColorScaleConfig | D3InterpolateName
196
+ size_scale?: SizeScaleConfig
197
+ color_bar?:
198
+ | (ComponentProps<typeof ColorBar> & {
199
+ margin?: number | Sides
200
+ tween?: TweenOptions<Point2D>
201
+ responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
202
+ axis_clearance?: number // Min distance kept from plot edges/axes (default: 15)
203
+ })
204
+ | null
205
+ label_placement_config?: Partial<LabelPlacementConfig>
206
+ hover_config?: Partial<HoverConfig>
207
+ legend?: LegendConfig | null
208
+ point_tween?: TweenOptions<Point2D>
209
+ line_tween?: TweenOptions<string>
210
+ point_events?: Record<
211
+ string,
212
+ (payload: { point: InternalPoint<Metadata>; event: Event }) => void
213
+ >
214
+ on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
215
+ on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
216
+ fill_regions?: FillRegion[] // Bindable for legend toggle support
217
+ error_bands?: ErrorBand[]
218
+ on_fill_click?: (event: FillHandlerEvent) => void
219
+ on_fill_hover?: (event: FillHandlerEvent | null) => void
220
+ ref_lines?: RefLine[] // Bindable for legend toggle support
221
+ on_ref_line_click?: (event: RefLineEvent) => void
222
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
223
+ selected_series_idx?: number
224
+ wrapper?: HTMLDivElement
225
+ // Interactive axis props
226
+ data_loader?: DataLoaderFn<Metadata>
227
+ on_axis_change?: (
228
+ axis: `x` | `x2` | `y` | `y2`,
229
+ key: string,
230
+ new_series: DataSeries<Metadata>[],
231
+ ) => void
232
+ on_error?: (error: AxisLoadError) => void
233
+ pan?: PanConfig
234
+ marginals?: MarginalsProp
235
+ } = $props()
228
236
 
229
237
  // Merged axis/display values with defaults (use $derived to avoid breaking $bindable)
230
238
  const final_x_axis = $derived({
231
239
  ...AXIS_DEFAULTS,
232
- label_shift: { x: 0, y: -40 }, // x-axis needs different label position
240
+ label_shift: { x: 0, y: 0 },
233
241
  ...x_axis,
234
242
  })
235
243
  const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...y_axis })
236
244
  const final_x2_axis = $derived({
237
245
  ...AXIS_DEFAULTS,
238
- label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
246
+ label_shift: { x: 0, y: AXIS_TITLE_OFFSET }, // x2-axis label above top edge
239
247
  ...x2_axis,
240
248
  })
241
249
  const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...y2_axis })
242
250
  // Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
243
- let is_time_x = $derived(
244
- is_time_scale(final_x_axis.scale_type, final_x_axis.format),
245
- )
246
- let is_time_x2 = $derived(
247
- is_time_scale(final_x2_axis.scale_type, final_x2_axis.format),
248
- )
251
+ let is_time_x = $derived(is_time_scale(final_x_axis.scale_type, final_x_axis.format))
252
+ let is_time_x2 = $derived(is_time_scale(final_x2_axis.scale_type, final_x2_axis.format))
249
253
  const final_display = $derived({ ...DEFAULTS.scatter.display, ...display })
250
254
  // Local state for styles (initialized from prop, owned by this component for controls)
251
255
  // Using $state because styles has bindings in ScatterPlotControls
252
256
  // untrack() explicitly captures initial prop value (intentional - props provide initial config)
253
- let styles = $state(untrack(() => ({
254
- show_points: DEFAULTS.scatter.show_points,
255
- show_lines: DEFAULTS.scatter.show_lines,
256
- point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
257
- line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
258
- ...styles_init,
259
- })))
257
+ let styles = $state(
258
+ untrack(() => ({
259
+ show_points: DEFAULTS.scatter.show_points,
260
+ show_lines: DEFAULTS.scatter.show_lines,
261
+ point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
262
+ line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
263
+ ...styles_init,
264
+ })),
265
+ )
260
266
  let controls = $derived({ show: true, open: false, ...controls_init })
261
267
 
262
268
  let [width, height] = $state([0, 0])
@@ -287,7 +293,10 @@
287
293
  initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
288
294
  current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
289
295
  })
290
- const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
296
+ const legend_vis = create_legend_visibility(
297
+ () => series,
298
+ (next) => (series = next),
299
+ )
291
300
 
292
301
  // Y2 axis sync configuration
293
302
  let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
@@ -350,8 +359,13 @@
350
359
 
351
360
  for (const srs of series_with_ids) {
352
361
  if (!srs) continue
353
- const { x: xs, y: ys, visible = true, y_axis: series_y_axis = `y1`, x_axis: x_ax = `x1` } =
354
- srs as DataSeries
362
+ const {
363
+ x: xs,
364
+ y: ys,
365
+ visible = true,
366
+ y_axis: series_y_axis = `y1`,
367
+ x_axis: x_ax = `x1`,
368
+ } = srs as DataSeries
355
369
  for (let idx = 0; idx < xs.length; idx++) {
356
370
  const point = { x: xs[idx], y: ys[idx] }
357
371
  all.push(point)
@@ -376,37 +390,53 @@
376
390
 
377
391
  // Update padding when format or ticks change
378
392
  $effect(() => {
379
- const new_pad = width && height &&
380
- (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
381
- ? calc_auto_padding({
382
- padding,
383
- default_padding,
384
- x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
385
- y_axis: { ...final_y_axis, tick_values: y_tick_values },
386
- y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
387
- })
388
- : filter_padding(padding, default_padding)
393
+ const new_pad =
394
+ width &&
395
+ height &&
396
+ (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
397
+ ? calc_auto_padding({
398
+ padding,
399
+ default_padding,
400
+ x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
401
+ y_axis: { ...final_y_axis, tick_values: y_tick_values },
402
+ y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
403
+ })
404
+ : filter_padding(padding, default_padding)
389
405
 
390
406
  if (
391
407
  base_pad.t !== new_pad.t ||
392
408
  base_pad.b !== new_pad.b ||
393
409
  base_pad.l !== new_pad.l ||
394
410
  base_pad.r !== new_pad.r
395
- ) base_pad = new_pad
411
+ )
412
+ base_pad = new_pad
396
413
  })
397
414
 
398
415
  // === Auto-move legend/colorbar outside the plot when interior overlap is unavoidable ===
399
416
  // (shared logic lives in auto-place.ts so every 2D plot reuses it)
400
417
  // ColorBar's orientation prop defaults to horizontal, so treat unset as horizontal too
401
- const colorbar_is_horizontal = $derived((color_bar?.orientation ?? `horizontal`) === `horizontal`)
402
- const colorbar_footprint = $derived(
403
- colorbar_element?.offsetWidth && colorbar_element?.offsetHeight
404
- ? measure_full_footprint(colorbar_element)
405
- : colorbar_is_horizontal
406
- ? { width: 220, height: 56 }
407
- : { width: 56, height: 100 },
418
+ const colorbar_is_horizontal = $derived(
419
+ (color_bar?.orientation ?? `horizontal`) === `horizontal`,
420
+ )
421
+ // Fallback estimate (with room for tick labels) used before the colorbar first
422
+ // renders; compute_element_placement measures the real footprint once it's laid out
423
+ const colorbar_fallback_size = $derived(
424
+ colorbar_is_horizontal
425
+ ? COLOR_BAR_DEFAULTS.horizontal_footprint
426
+ : COLOR_BAR_DEFAULTS.vertical_footprint,
408
427
  )
409
- const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 80 }))
428
+ // full footprint (not the offset box): colorbar tick labels are absolutely
429
+ // positioned outside the bar and must count toward reserved margins
430
+ let colorbar_size_revision = $state(0)
431
+ let legend_size_revision = $state(0)
432
+ const colorbar_footprint = $derived.by(() => {
433
+ void colorbar_size_revision
434
+ return full_footprint_or(colorbar_element, colorbar_fallback_size)
435
+ })
436
+ const legend_footprint = $derived.by(() => {
437
+ void legend_size_revision
438
+ return measured_footprint(legend_element, { width: 120, height: 80 })
439
+ })
410
440
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
411
441
 
412
442
  // Plot-specific obstacle field: series points/lines normalized to [0,1] (y=0 at top)
@@ -416,7 +446,9 @@
416
446
  const base_h = height - base_pad.t - base_pad.b
417
447
  if (base_w <= 0 || base_h <= 0) return []
418
448
  const norm_x = is_time_x
419
- ? scaleTime().domain([new Date(x_min), new Date(x_max)]).range([0, 1])
449
+ ? scaleTime()
450
+ .domain([new Date(x_min), new Date(x_max)])
451
+ .range([0, 1])
420
452
  : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1])
421
453
  const norm_y = create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [0, 1])
422
454
  return build_obstacles_norm(
@@ -442,23 +474,58 @@
442
474
  obstacles_norm,
443
475
  // gate on legend_element (the actual render signal) not legend_data, whose fill entries read
444
476
  // computed_fills -> pad and would make this derived reference itself
445
- legend: legend != null && legend_element != null &&
446
- !legend_has_explicit_pos && !legend_is_dragging && !legend_manual_position
447
- ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
448
- : null,
477
+ legend:
478
+ legend != null &&
479
+ legend_element != null &&
480
+ !legend_has_explicit_pos &&
481
+ !legend_is_dragging &&
482
+ !legend_manual_position
483
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
484
+ : null,
449
485
  // gate on a measured colorbar: its outside style stretches it to full width, so deciding from
450
486
  // the (wide) pre-measure fallback would flip-flop placement between interior and outside
451
- colorbar: Boolean(color_bar) && all_color_values.length > 0 && !color_bar?.wrapper_style &&
452
- (colorbar_element?.offsetWidth ?? 0) > 0 && (colorbar_element?.offsetHeight ?? 0) > 0
453
- ? {
454
- footprint: colorbar_footprint,
455
- horizontal: colorbar_is_horizontal,
456
- clearance: color_bar?.axis_clearance,
457
- }
458
- : null,
459
- })
487
+ colorbar:
488
+ Boolean(color_bar) &&
489
+ all_color_values.length > 0 &&
490
+ !color_bar?.wrapper_style &&
491
+ (colorbar_element?.offsetWidth ?? 0) > 0 &&
492
+ (colorbar_element?.offsetHeight ?? 0) > 0
493
+ ? {
494
+ footprint: colorbar_footprint,
495
+ horizontal: colorbar_is_horizontal,
496
+ clearance: color_bar?.axis_clearance,
497
+ }
498
+ : null,
499
+ }),
500
+ )
501
+ // Resolve marginals and reserve outer-band padding so the plot shrinks to make room
502
+ const resolved_marginals = $derived(
503
+ normalize_marginals(marginals, { top: true, right: true }),
504
+ )
505
+ const pad = $derived(add_sides(decor.pad, reserve_marginal_pad(resolved_marginals)))
506
+ // Map series to the generic marginal input, reusing the line/legend color fallback
507
+ const marginal_series = $derived<MarginalSeriesInput[]>(
508
+ series_with_ids.map((srs, idx) => {
509
+ const point_fill = Array.isArray(srs?.point_style)
510
+ ? srs.point_style[0]?.fill
511
+ : srs?.point_style?.fill
512
+ return {
513
+ x: srs?.x ?? [],
514
+ y: srs?.y ?? [],
515
+ color:
516
+ srs?.line_style?.stroke ??
517
+ point_fill ??
518
+ get_series_color(srs?.orig_series_idx ?? idx),
519
+ label: srs?.label,
520
+ visible: srs?.visible ?? true,
521
+ x_axis: srs?.x_axis,
522
+ y_axis: srs?.y_axis,
523
+ }
524
+ }),
525
+ )
526
+ const marginal_has_axis = $derived(
527
+ marginal_axis_presence(x2_points.length > 0, y2_points.length > 0),
460
528
  )
461
- const pad = $derived(decor.pad)
462
529
  const legend_auto_outside = $derived(decor.legend_outside)
463
530
  const legend_outside_x = $derived(decor.legend_pos.x)
464
531
  const legend_outside_y = $derived(decor.legend_pos.y)
@@ -471,9 +538,7 @@
471
538
  x: pad.l || 0,
472
539
  y: pad.t || 0,
473
540
  width: isFinite(width - pad.l - pad.r) ? Math.max(1, width - pad.l - pad.r) : 1,
474
- height: isFinite(height - pad.t - pad.b)
475
- ? Math.max(1, height - pad.t - pad.b)
476
- : 1,
541
+ height: isFinite(height - pad.t - pad.b) ? Math.max(1, height - pad.t - pad.b) : 1,
477
542
  })
478
543
 
479
544
  // Calculate plot area center coordinates
@@ -487,8 +552,12 @@
487
552
  for (const srs of series_with_ids) {
488
553
  if (!srs) continue
489
554
  const { color_values: cvs, size_values: svs } = srs as DataSeries
490
- if (cvs) { for (const val of cvs) if (val != null) color_values.push(val) }
491
- if (svs) { for (const val of svs) if (val != null) size_values.push(val) }
555
+ if (cvs) {
556
+ for (const val of cvs) if (val != null) color_values.push(val)
557
+ }
558
+ if (svs) {
559
+ for (const val of svs) if (val != null) size_values.push(val)
560
+ }
492
561
  }
493
562
  return { color_values, size_values }
494
563
  })
@@ -567,7 +636,10 @@
567
636
  if (explicit) {
568
637
  ranges.current[axis] = range
569
638
  } else {
570
- const result = expand_range_if_needed(untrack(() => ranges.initial[axis]), range)
639
+ const result = expand_range_if_needed(
640
+ untrack(() => ranges.initial[axis]),
641
+ range,
642
+ )
571
643
  if (result.changed) {
572
644
  ranges.initial[axis] = result.range
573
645
  ranges.current[axis] = result.range
@@ -579,7 +651,10 @@
579
651
  if (resolved.y2.explicit) {
580
652
  ranges.current.y2 = resolved.y2.range
581
653
  } else {
582
- const result = expand_range_if_needed(untrack(() => ranges.initial.y2), resolved.y2.range)
654
+ const result = expand_range_if_needed(
655
+ untrack(() => ranges.initial.y2),
656
+ resolved.y2.range,
657
+ )
583
658
  if (result.changed) ranges.initial.y2 = result.range
584
659
  // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
585
660
  if (y2_sync_config.mode !== `none`) {
@@ -605,10 +680,10 @@
605
680
  // Ensure we only calculate extent on actual numbers, filtering out nulls/undefined
606
681
  all_color_values.length > 0
607
682
  ? extent(
608
- all_color_values.filter((color_val: number | null): color_val is number =>
609
- typeof color_val === `number`
610
- ),
611
- )
683
+ all_color_values.filter(
684
+ (color_val: number | null): color_val is number => typeof color_val === `number`,
685
+ ),
686
+ )
612
687
  : [0, 1],
613
688
  ) as Vec2
614
689
 
@@ -617,37 +692,37 @@
617
692
  let x_scale_fn = $derived(
618
693
  is_time_x
619
694
  ? scaleTime()
620
- .domain([new Date(x_min), new Date(x_max)])
621
- .range([pad.l, width - pad.r])
622
- : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [
623
- pad.l,
624
- width - pad.r,
625
- ]),
695
+ .domain([new Date(x_min), new Date(x_max)])
696
+ .range([pad.l, width - pad.r])
697
+ : create_scale(
698
+ final_x_axis.scale_type ?? `linear`,
699
+ [x_min, x_max],
700
+ [pad.l, width - pad.r],
701
+ ),
626
702
  )
627
703
 
628
704
  let x2_scale_fn = $derived(
629
705
  is_time_x2
630
706
  ? scaleTime()
631
- .domain([new Date(x2_min), new Date(x2_max)])
632
- .range([pad.l, width - pad.r])
633
- : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [
634
- pad.l,
635
- width - pad.r,
636
- ]),
707
+ .domain([new Date(x2_min), new Date(x2_max)])
708
+ .range([pad.l, width - pad.r])
709
+ : create_scale(
710
+ final_x2_axis.scale_type ?? `linear`,
711
+ [x2_min, x2_max],
712
+ [pad.l, width - pad.r],
713
+ ),
637
714
  )
638
715
 
639
716
  let y_scale_fn = $derived(
640
- create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [
641
- height - pad.b,
642
- pad.t,
643
- ]),
717
+ create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [height - pad.b, pad.t]),
644
718
  )
645
719
 
646
720
  let y2_scale_fn = $derived(
647
- create_scale(final_y2_axis.scale_type ?? `linear`, [y2_min, y2_max], [
648
- height - pad.b,
649
- pad.t,
650
- ]),
721
+ create_scale(
722
+ final_y2_axis.scale_type ?? `linear`,
723
+ [y2_min, y2_max],
724
+ [height - pad.b, pad.t],
725
+ ),
651
726
  )
652
727
 
653
728
  // All size values from series (for size scale) - extracted in series_value_arrays
@@ -684,6 +759,22 @@
684
759
  })
685
760
  let effective_line_tween = $derived(resolve_line_tween(line_tween, line_tween_load))
686
761
 
762
+ let visible_marker_count = $derived.by(() => {
763
+ if (!styles.show_points) return 0
764
+ let count = 0
765
+ for (const series_data of filtered_series ?? []) {
766
+ if ((series_data.markers ?? DEFAULT_MARKERS).includes(`points`)) {
767
+ count += series_data.filtered_data.length
768
+ }
769
+ }
770
+ return count
771
+ })
772
+
773
+ const default_point_radius = (markers: string): number => {
774
+ const [sparse_radius, dense_radius] = markers.includes(`line`) ? [2.5, 2] : [3, 2.5]
775
+ return visible_marker_count >= DENSE_MARKER_COUNT ? dense_radius : sparse_radius
776
+ }
777
+
687
778
  // Obstacle field for legend/colorbar auto-placement. Sampling only data points lets the
688
779
  // legend land on top of a steep connecting line whose markers are sparse (e.g. y=x^2), so
689
780
  // sample_series_obstacle_points also walks each drawn segment at a fixed pixel cadence.
@@ -699,14 +790,16 @@
699
790
  const active_x_scale = use_x2_scale ? x2_scale_fn : x_scale_fn
700
791
  const active_is_time_x = use_x2_scale ? is_time_x2 : is_time_x
701
792
  const active_y_scale = series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn
702
- const draws_line = styles.show_lines &&
703
- (series_data.markers ?? DEFAULT_MARKERS).includes(`line`)
793
+ const draws_line =
794
+ styles.show_lines && (series_data.markers ?? DEFAULT_MARKERS).includes(`line`)
704
795
 
705
796
  const pixel_points = series_data.filtered_data.map((point) => ({
706
797
  x: active_is_time_x ? active_x_scale(new Date(point.x)) : active_x_scale(point.x),
707
798
  y: active_y_scale(point.y),
708
799
  }))
709
- points.push(...sample_series_obstacle_points(pixel_points, draws_line, SEGMENT_SAMPLE_STEP))
800
+ points.push(
801
+ ...sample_series_obstacle_points(pixel_points, draws_line, SEGMENT_SAMPLE_STEP),
802
+ )
710
803
  }
711
804
  return points
712
805
  })
@@ -791,14 +884,16 @@
791
884
  }
792
885
 
793
886
  return all_regions
794
- .filter((
795
- entry,
796
- ): entry is {
797
- region: FillRegion
798
- source_type: `fill_region` | `error_band`
799
- source_idx: number
800
- hover_key: string
801
- } => entry.region !== null)
887
+ .filter(
888
+ (
889
+ entry,
890
+ ): entry is {
891
+ region: FillRegion
892
+ source_type: `fill_region` | `error_band`
893
+ source_idx: number
894
+ hover_key: string
895
+ } => entry.region !== null,
896
+ )
802
897
  .map(({ region, source_type, source_idx, hover_key }, idx) => {
803
898
  // Hidden fills keep their entry (with empty path_segments -> nothing renders) so the
804
899
  // legend item persists greyed-out and can be toggled back on.
@@ -806,15 +901,15 @@
806
901
  const path_segments = hidden
807
902
  ? []
808
903
  : compute_fill_segments(region, series_with_ids, domains)
809
- .map((seg) =>
810
- generate_fill_path(
811
- seg.upper.map(to_px),
812
- seg.lower.map(to_px),
813
- seg.upper_curve,
814
- seg.lower_curve,
904
+ .map((seg) =>
905
+ generate_fill_path(
906
+ seg.upper.map(to_px),
907
+ seg.lower.map(to_px),
908
+ seg.upper_curve,
909
+ seg.lower_curve,
910
+ ),
815
911
  )
816
- )
817
- .filter((path) => path.length > 0)
912
+ .filter((path) => path.length > 0)
818
913
 
819
914
  // Drop only visible fills with no geometry; keep hidden ones for the legend
820
915
  if (!hidden && path_segments.length === 0) return null
@@ -848,13 +943,12 @@
848
943
  })
849
944
 
850
945
  // Compute ref_lines with index and group by z-index (using shared utilities)
851
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
852
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
946
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
853
947
 
854
948
  // Calculate best legend placement using continuous grid sampling
855
- let legend_placement = $derived.by(() => {
856
- const should_place = legend != null &&
857
- (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
949
+ const get_legend_placement = () => {
950
+ const should_place =
951
+ legend != null && (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
858
952
 
859
953
  if (!should_place || !width || !height) return null
860
954
 
@@ -871,24 +965,27 @@
871
965
  }
872
966
 
873
967
  return compute_element_placement(placement_config)
874
- })
968
+ }
875
969
 
876
970
  // Calculate color bar placement (coordinates with legend to avoid overlap)
877
- let color_bar_placement = $derived.by(() => {
971
+ const get_color_bar_placement = () => {
878
972
  if (!color_bar || all_color_values.length === 0 || !width || !height) return null
879
973
 
880
974
  const plot_width = width - pad.l - pad.r
881
975
  const plot_height = height - pad.t - pad.b
882
976
 
883
- // Fallback estimate (with room for tick labels) used before the colorbar first
884
- // renders; compute_element_placement measures the real footprint once it's laid out
885
- const is_horizontal = (color_bar.orientation ?? `horizontal`) === `horizontal`
886
- const colorbar_size = is_horizontal
887
- ? { width: 220, height: 56 }
888
- : { width: 56, height: 100 }
889
-
890
977
  // Build exclusion rects (avoid legend if it's placed)
891
978
  const exclude_rects: Rect[] = []
979
+ const legend_placement =
980
+ legend_is_dragging && legend_manual_position
981
+ ? legend_manual_position
982
+ : legend_has_explicit_pos && legend_element
983
+ ? { x: legend_element.offsetLeft, y: legend_element.offsetTop }
984
+ : legend_auto_outside
985
+ ? { x: legend_outside_x, y: legend_outside_y }
986
+ : legend_tween.placed()
987
+ ? legend_tween.coords.target
988
+ : get_legend_placement()
892
989
  if (legend_element && legend_placement) {
893
990
  exclude_rects.push({
894
991
  x: legend_placement.x,
@@ -901,48 +998,44 @@
901
998
  return compute_element_placement({
902
999
  plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
903
1000
  element: colorbar_element,
904
- element_size: colorbar_size,
1001
+ element_size: colorbar_fallback_size,
905
1002
  // Small gap from the corner; the full-footprint measurement reserves the tick
906
1003
  // labels, so this alone keeps the colorbar off the axes
907
1004
  axis_clearance: color_bar?.axis_clearance ?? 15,
908
1005
  exclude_rects,
909
1006
  points: plot_points_for_placement,
910
1007
  })
911
- })
912
-
913
- // Active legend placement (null if user set explicit position)
914
- let active_legend_placement = $derived.by(() => {
915
- if (!legend_placement) return null
1008
+ }
916
1009
 
917
- // Skip auto-placement if user set explicit position in style
918
- const legend_style = legend?.style ?? ``
919
- if (
920
- /(^|[;{]\s*)(top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(
921
- legend_style,
922
- )
923
- ) return null
924
-
925
- return legend_placement
926
- })
1010
+ // Active legend placement (null if user set explicit position in style)
1011
+ const get_active_legend_placement = () =>
1012
+ legend_has_explicit_pos ? null : get_legend_placement()
927
1013
 
928
1014
  // Tweened colorbar/legend coordinates with shared placement stability gating
929
- const colorbar_tween = create_placed_tween({
930
- placement: () => color_bar_placement,
931
- dims: () => ({ width, height }),
932
- responsive: () => color_bar?.responsive ?? false,
933
- element: () => colorbar_element,
934
- tween: () => color_bar?.tween,
935
- })
936
1015
  const legend_tween = create_placed_tween({
937
- placement: () => active_legend_placement,
1016
+ placement: get_active_legend_placement,
938
1017
  dims: () => ({ width, height }),
939
1018
  responsive: () => legend?.responsive ?? false,
940
1019
  element: () => legend_element,
941
1020
  tween: () => legend?.tween,
1021
+ on_element_resize: () => (legend_size_revision += 1),
1022
+ placement_revision: () => colorbar_size_revision,
942
1023
  // Leave coords alone mid-drag; once dragged, the manual position wins permanently
943
1024
  suspended: () => legend_is_dragging,
944
1025
  manual_position: () => legend_manual_position,
945
1026
  })
1027
+ const colorbar_tween = create_placed_tween({
1028
+ placement: get_color_bar_placement,
1029
+ dims: () => ({ width, height }),
1030
+ responsive: () => color_bar?.responsive ?? false,
1031
+ element: () => colorbar_element,
1032
+ tween: () => color_bar?.tween,
1033
+ on_element_resize: () => (colorbar_size_revision += 1),
1034
+ placement_revision: () =>
1035
+ `${legend_size_revision}:${legend_tween.coords.target.x}:${legend_tween.coords.target.y}:${
1036
+ legend_auto_outside ? `${legend_outside_x}:${legend_outside_y}` : `inside`
1037
+ }:${legend?.style ?? ``}`,
1038
+ })
946
1039
 
947
1040
  // Generate axis ticks - consolidated into single derived for efficiency
948
1041
  let axis_ticks = $derived.by(() => {
@@ -966,15 +1059,16 @@
966
1059
  x_scale_for_ticks,
967
1060
  { format: final_x_axis.format },
968
1061
  ),
969
- x2: x2_points.length > 0
970
- ? generate_ticks(
971
- [x2_min, x2_max],
972
- final_x2_axis.scale_type ?? `linear`,
973
- final_x2_axis.ticks,
974
- x2_scale_for_ticks,
975
- { format: final_x2_axis.format },
976
- )
977
- : [],
1062
+ x2:
1063
+ x2_points.length > 0
1064
+ ? generate_ticks(
1065
+ [x2_min, x2_max],
1066
+ final_x2_axis.scale_type ?? `linear`,
1067
+ final_x2_axis.ticks,
1068
+ x2_scale_for_ticks,
1069
+ { format: final_x2_axis.format },
1070
+ )
1071
+ : [],
978
1072
  y: generate_ticks(
979
1073
  [y_min, y_max],
980
1074
  final_y_axis.scale_type ?? `linear`,
@@ -982,15 +1076,16 @@
982
1076
  y_scale_fn,
983
1077
  { default_count: 5 },
984
1078
  ),
985
- y2: y2_points.length > 0
986
- ? generate_ticks(
987
- [y2_min, y2_max],
988
- final_y2_axis.scale_type ?? `linear`,
989
- final_y2_axis.ticks,
990
- y2_scale_fn,
991
- { default_count: 5 },
992
- )
993
- : [],
1079
+ y2:
1080
+ y2_points.length > 0
1081
+ ? generate_ticks(
1082
+ [y2_min, y2_max],
1083
+ final_y2_axis.scale_type ?? `linear`,
1084
+ final_y2_axis.ticks,
1085
+ y2_scale_fn,
1086
+ { default_count: 5 },
1087
+ )
1088
+ : [],
994
1089
  }
995
1090
  })
996
1091
 
@@ -1016,7 +1111,9 @@
1016
1111
  ({ x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis })[axis]
1017
1112
  .scale_type,
1018
1113
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1019
- plot_dims: () => ({
1114
+ plot_bounds: () => ({
1115
+ x: pad.l,
1116
+ y: pad.t,
1020
1117
  width: Math.max(1, width - pad.l - pad.r),
1021
1118
  height: Math.max(1, height - pad.t - pad.b),
1022
1119
  }),
@@ -1036,16 +1133,16 @@
1036
1133
  y_axis = { ...y_axis, range: next_y }
1037
1134
 
1038
1135
  // X2 axis: invert screen coords using x2 scale
1039
- const next_x2 = x2_points.length > 0
1040
- ? invert_rect_range(x2_scale_fn, start.x, current.x)
1041
- : null
1136
+ const next_x2 =
1137
+ x2_points.length > 0 ? invert_rect_range(x2_scale_fn, start.x, current.x) : null
1042
1138
  if (next_x2) x2_axis = { ...x2_axis, range: next_x2 }
1043
1139
 
1044
1140
  // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1045
1141
  // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1046
- const next_y2 = y2_points.length > 0 && y2_sync_config.mode === `none`
1047
- ? invert_rect_range(y2_scale_fn, start.y, current.y)
1048
- : null
1142
+ const next_y2 =
1143
+ y2_points.length > 0 && y2_sync_config.mode === `none`
1144
+ ? invert_rect_range(y2_scale_fn, start.y, current.y)
1145
+ : null
1049
1146
  if (next_y2) y2_axis = { ...y2_axis, range: next_y2 }
1050
1147
  },
1051
1148
  on_reset: () => {
@@ -1100,9 +1197,7 @@
1100
1197
  const point_cx = tooltip_is_time_x
1101
1198
  ? tooltip_x_scale(new Date(point.x))
1102
1199
  : tooltip_x_scale(point.x)
1103
- const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(
1104
- point.y,
1105
- )
1200
+ const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(point.y)
1106
1201
 
1107
1202
  // Calculate squared screen distance between mouse and point
1108
1203
  const screen_dx = x_rel - point_cx
@@ -1119,11 +1214,7 @@
1119
1214
  }
1120
1215
 
1121
1216
  // Check if the closest point is within the hover threshold
1122
- if (
1123
- closest_point &&
1124
- closest_series &&
1125
- min_screen_dist_sq <= hover_threshold_px_sq
1126
- ) {
1217
+ if (closest_point && closest_series && min_screen_dist_sq <= hover_threshold_px_sq) {
1127
1218
  // Construct handler props synchronously to avoid stale derived reads
1128
1219
  const props = construct_handler_props(closest_point)
1129
1220
  tooltip_point = closest_point
@@ -1198,8 +1289,8 @@
1198
1289
  const new_y = event.clientY - svg_rect.top - legend_drag_offset.y
1199
1290
 
1200
1291
  // Get actual legend dimensions for accurate bounds checking using the bound element reference
1201
- const { width: legend_width, height: legend_height } = legend_element
1202
- .getBoundingClientRect()
1292
+ const { width: legend_width, height: legend_height } =
1293
+ legend_element.getBoundingClientRect()
1203
1294
 
1204
1295
  // Constrain to plot bounds using measured legend size
1205
1296
  const constrained_x = Math.max(0, Math.min(width - legend_width, new_x))
@@ -1245,9 +1336,7 @@
1245
1336
  const cx = handler_is_time_x ? handler_x_scale(new Date(x)) : handler_x_scale(x)
1246
1337
  const cy = (hovered_series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(y)
1247
1338
  const active_x_config = handler_use_x2 ? final_x2_axis : final_x_axis
1248
- const active_y_config = hovered_series.y_axis === `y2`
1249
- ? final_y2_axis
1250
- : final_y_axis
1339
+ const active_y_config = hovered_series.y_axis === `y2` ? final_y2_axis : final_y_axis
1251
1340
  const coords = {
1252
1341
  x,
1253
1342
  y,
@@ -1264,8 +1353,8 @@
1264
1353
  metadata,
1265
1354
  label: hovered_series.label ?? null,
1266
1355
  series_idx,
1267
- x_formatted: format_value(x, active_x_config.format || `.3~s`),
1268
- y_formatted: format_value(y, active_y_config.format || `.3~s`),
1356
+ x_formatted: format_value_or_num(x, active_x_config.format),
1357
+ y_formatted: format_value_or_num(y, active_y_config.format),
1269
1358
  color_value: color_value ?? null,
1270
1359
  colorbar: {
1271
1360
  value: color_value ?? null,
@@ -1287,9 +1376,7 @@
1287
1376
  // Precompute non-click event names from point_events so we don't rebuild
1288
1377
  // the entries array on every point render.
1289
1378
  let point_event_names = $derived(
1290
- point_events
1291
- ? Object.keys(point_events).filter((name) => name !== `onclick`)
1292
- : [],
1379
+ point_events ? Object.keys(point_events).filter((name) => name !== `onclick`) : [],
1293
1380
  )
1294
1381
 
1295
1382
  // Set theme-aware background when entering fullscreen
@@ -1298,41 +1385,30 @@
1298
1385
  })
1299
1386
 
1300
1387
  // State accessors for shared axis change handler
1388
+ // Spread into existing state in each setter to preserve merged type structure
1301
1389
  const axis_state: AxisChangeState<DataSeries<Metadata>> = {
1302
- get_axis: (axis) => {
1303
- if (axis === `x`) return x_axis
1304
- if (axis === `x2`) return x2_axis
1305
- if (axis === `y`) return y_axis
1306
- return y2_axis
1390
+ axes: {
1391
+ x: { get: () => x_axis, set: (config) => (x_axis = { ...x_axis, ...config }) },
1392
+ x2: { get: () => x2_axis, set: (config) => (x2_axis = { ...x2_axis, ...config }) },
1393
+ y: { get: () => y_axis, set: (config) => (y_axis = { ...y_axis, ...config }) },
1394
+ y2: { get: () => y2_axis, set: (config) => (y2_axis = { ...y2_axis, ...config }) },
1307
1395
  },
1308
- set_axis: (axis, config) => {
1309
- // Spread into existing state to preserve merged type structure
1310
- if (axis === `x`) x_axis = { ...x_axis, ...config }
1311
- else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
1312
- else if (axis === `y`) y_axis = { ...y_axis, ...config }
1313
- else y2_axis = { ...y2_axis, ...config }
1314
- },
1315
- get_series: () => series,
1316
- set_series: (new_series) => (series = new_series),
1317
- get_loading: () => axis_loading,
1318
- set_loading: (axis) => (axis_loading = axis),
1396
+ series: { get: () => series, set: (next) => (series = next) },
1397
+ loading: { get: () => axis_loading, set: (axis) => (axis_loading = axis) },
1319
1398
  }
1320
1399
 
1321
1400
  // Shared handler + one-shot auto-load bound to this component's state
1322
- const { handle_axis_change, try_auto_load } = create_axis_loader(
1323
- axis_state,
1324
- () => ({ data_loader, on_axis_change, on_error }),
1325
- )
1401
+ const { handle_axis_change, try_auto_load } = create_axis_loader(axis_state, () => ({
1402
+ data_loader,
1403
+ on_axis_change,
1404
+ on_error,
1405
+ }))
1326
1406
  $effect(try_auto_load)
1327
1407
  </script>
1328
1408
 
1329
1409
  {#snippet fill_regions_layer(fills: typeof computed_fills)}
1330
1410
  {#each fills as fill (fill.hover_key)}
1331
- {#each fill.path_segments as
1332
- path_d,
1333
- segment_idx
1334
- (`${fill.id ?? fill.idx}-${segment_idx}`)
1335
- }
1411
+ {#each fill.path_segments as path_d, segment_idx (`${fill.id ?? fill.idx}-${segment_idx}`)}
1336
1412
  <FillArea
1337
1413
  region={fill}
1338
1414
  region_idx={fill.idx}
@@ -1399,7 +1475,7 @@
1399
1475
  bind:clientWidth={width}
1400
1476
  bind:clientHeight={height}
1401
1477
  {...rest}
1402
- class="scatter {rest.class ?? ``}"
1478
+ class={[`scatter`, rest.class]}
1403
1479
  class:fullscreen
1404
1480
  >
1405
1481
  {#if width && height}
@@ -1415,8 +1491,8 @@
1415
1491
  bind:this={svg_element}
1416
1492
  role="application"
1417
1493
  aria-label={rest[`aria-label`] ??
1418
- ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1419
- `Scatter plot`)}
1494
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1495
+ `Scatter plot`)}
1420
1496
  tabindex="0"
1421
1497
  onfocusin={() => pan_zoom.set_focused(true)}
1422
1498
  onfocusout={() => pan_zoom.set_focused(false)}
@@ -1473,7 +1549,7 @@
1473
1549
  domain={[x_min, x_max]}
1474
1550
  tick_label={(tick) => get_tick_label(tick, final_x_axis.ticks)}
1475
1551
  label_x={width / 2 + (final_x_axis.label_shift?.x ?? 0)}
1476
- label_y={height - pad.b - (final_x_axis.label_shift?.y ?? -40)}
1552
+ label_y={height - pad.b + AXIS_TITLE_OFFSET + (final_x_axis.label_shift?.y ?? 0)}
1477
1553
  axis_loading={axis_loading === `x`}
1478
1554
  on_axis_change={(key) => handle_axis_change(`x`, key)}
1479
1555
  />
@@ -1481,8 +1557,8 @@
1481
1557
  <!-- Current frame indicator -->
1482
1558
  {#if current_x_value != null}
1483
1559
  {@const current_pos_raw = is_time_x
1484
- ? x_scale_fn(new Date(current_x_value))
1485
- : x_scale_fn(current_x_value)}
1560
+ ? x_scale_fn(new Date(current_x_value))
1561
+ : x_scale_fn(current_x_value)}
1486
1562
  {#if isFinite(current_pos_raw)}
1487
1563
  {@const current_pos = current_pos_raw}
1488
1564
  {#if current_pos >= pad.l && current_pos <= width - pad.r}
@@ -1516,7 +1592,8 @@
1516
1592
  tick_label={(tick) => get_tick_label(tick, final_y_axis.ticks)}
1517
1593
  label_x={Math.max(
1518
1594
  12,
1519
- pad.l - (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1595
+ pad.l -
1596
+ (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1520
1597
  LABEL_GAP_DEFAULT,
1521
1598
  ) + (final_y_axis.label_shift?.x ?? 0)}
1522
1599
  label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y_axis.label_shift?.y ?? 0)}
@@ -1561,7 +1638,7 @@
1561
1638
  domain={[x2_min, x2_max]}
1562
1639
  tick_label={(tick) => get_tick_label(tick, final_x2_axis.ticks)}
1563
1640
  label_x={width / 2 + (final_x2_axis.label_shift?.x ?? 0)}
1564
- label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? 40))}
1641
+ label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? AXIS_TITLE_OFFSET))}
1565
1642
  axis_loading={axis_loading === `x2`}
1566
1643
  on_axis_change={(key) => handle_axis_change(`x2`, key)}
1567
1644
  />
@@ -1619,30 +1696,32 @@
1619
1696
  data-series-id={series_data._id}
1620
1697
  clip-path="url(#{clip_path_id})"
1621
1698
  opacity={hovered_legend_series_idx !== null &&
1622
- hovered_legend_series_idx !== series_data.orig_series_idx
1699
+ hovered_legend_series_idx !== series_data.orig_series_idx
1623
1700
  ? 0.25
1624
1701
  : 1}
1625
1702
  >
1626
- {#if series_markers?.includes(`line`)}
1703
+ {#if series_markers.includes(`line`)}
1627
1704
  {@const all_line_points = series_data.x.map((x, idx) => ({
1628
- x,
1629
- y: series_data.y[idx],
1630
- }))}
1705
+ x,
1706
+ y: series_data.y[idx],
1707
+ }))}
1631
1708
  {@const finite_screen_points = all_line_points
1632
- .map((point) => get_screen_coords(point, series_data))
1633
- .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
1634
- {@const apply_line_controls = controls.show &&
1635
- (!has_multiple_series ||
1636
- series_data._id === series_with_ids[selected_series_idx]?._id)}
1709
+ .map((point) => get_screen_coords(point, series_data))
1710
+ .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
1711
+ {@const apply_line_controls =
1712
+ controls.show &&
1713
+ (!has_multiple_series ||
1714
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
1637
1715
  {@const ls = series_data.line_style}
1638
1716
  {@const tc = (key: string) => apply_line_controls && touched.has(key)}
1639
- {@const color_fallback = ls?.stroke ??
1640
- (Array.isArray(series_data.point_style)
1641
- ? series_data.point_style[0]?.fill
1642
- : series_data.point_style?.fill) ??
1643
- (series_data.color_values?.[0] != null
1644
- ? color_scale_fn(series_data.color_values[0])
1645
- : series_default_color)}
1717
+ {@const color_fallback =
1718
+ ls?.stroke ??
1719
+ (Array.isArray(series_data.point_style)
1720
+ ? series_data.point_style[0]?.fill
1721
+ : series_data.point_style?.fill) ??
1722
+ (series_data.color_values?.[0] != null
1723
+ ? color_scale_fn(series_data.color_values[0])
1724
+ : series_default_color)}
1646
1725
  <Line
1647
1726
  points={finite_screen_points}
1648
1727
  origin={[
@@ -1650,8 +1729,11 @@
1650
1729
  series_data.y_axis === `y2` ? y2_scale_fn(y2_min) : y_scale_fn(y_min),
1651
1730
  ]}
1652
1731
  line_color={(tc(`line.color`) ? styles.line?.color : null) ?? color_fallback}
1653
- line_width={(tc(`line.width`) ? styles.line?.width : null) ?? ls?.stroke_width ?? 2}
1732
+ line_width={(tc(`line.width`) ? styles.line?.width : null) ??
1733
+ ls?.stroke_width ??
1734
+ 2}
1654
1735
  line_dash={(tc(`line.dash`) ? styles.line?.dash : null) ?? ls?.line_dash}
1736
+ curve={ls?.curve}
1655
1737
  area_color="transparent"
1656
1738
  line_tween={effective_line_tween}
1657
1739
  />
@@ -1672,53 +1754,60 @@
1672
1754
  {@const series_default_color = get_series_color(series_data.orig_series_idx ?? 0)}
1673
1755
  {@const series_default_symbol = get_series_symbol(series_data.orig_series_idx ?? 0)}
1674
1756
  <g data-series-id={series_data._id}>
1675
- {#if series_markers?.includes(`points`)}
1676
- {#each series_data.filtered_data as
1677
- point
1678
- (`${point.series_idx}-${point.point_idx}`)
1679
- }
1757
+ {#if series_markers.includes(`points`)}
1758
+ {#each series_data.filtered_data as point (`${point.series_idx}-${point.point_idx}`)}
1680
1759
  {@const label_id = `${point.series_idx}-${point.point_idx}`}
1681
1760
  {@const calculated_label_pos = label_positions[label_id]}
1682
1761
  {@const point_label = point.point_label ?? {}}
1683
- {@const label_style = point_label.auto_placement &&
1684
- actual_label_config.max_neighbors &&
1685
- !calculated_label_pos
1686
- ? {}
1687
- : point_label}
1762
+ {@const label_style =
1763
+ point_label.auto_placement &&
1764
+ actual_label_config.max_neighbors &&
1765
+ !calculated_label_pos
1766
+ ? {}
1767
+ : point_label}
1688
1768
  {@const final_label = calculated_label_pos
1689
- ? {
1690
- ...label_style,
1691
- offset: {
1692
- x: calculated_label_pos.x -
1693
- (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
1694
- y: calculated_label_pos.y - (series_data.y_axis === `y2`
1695
- ? y2_scale_fn(point.y)
1696
- : y_scale_fn(point.y)),
1697
- },
1698
- }
1699
- : label_style}
1769
+ ? {
1770
+ ...label_style,
1771
+ offset: {
1772
+ x:
1773
+ calculated_label_pos.x -
1774
+ (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
1775
+ y:
1776
+ calculated_label_pos.y -
1777
+ (series_data.y_axis === `y2`
1778
+ ? y2_scale_fn(point.y)
1779
+ : y_scale_fn(point.y)),
1780
+ },
1781
+ }
1782
+ : label_style}
1700
1783
  {@const [raw_screen_x, raw_screen_y] = get_screen_coords(point, series_data)}
1701
- {@const screen_x = isFinite(raw_screen_x) ? raw_screen_x : x_scale_fn.range()[0]}
1784
+ {@const screen_x = isFinite(raw_screen_x)
1785
+ ? raw_screen_x
1786
+ : x_scale_fn.range()[0]}
1702
1787
  {@const screen_y = isFinite(raw_screen_y)
1703
- ? raw_screen_y
1704
- : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
1705
- {@const apply_controls = controls.show &&
1706
- (!has_multiple_series ||
1707
- series_data._id === series_with_ids[selected_series_idx]?._id)}
1788
+ ? raw_screen_y
1789
+ : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
1790
+ {@const apply_controls =
1791
+ controls.show &&
1792
+ (!has_multiple_series ||
1793
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
1708
1794
  {@const pt = point.point_style}
1709
1795
  {@const tc = (key: string) => apply_controls && touched.has(key)}
1710
- {@const computed_radius = point.size_value != null
1711
- ? size_scale_fn(point.size_value)
1712
- : (tc(`point.size`) ? styles.point?.size : null) ?? pt?.radius ?? 4}
1796
+ {@const computed_radius =
1797
+ point.size_value != null
1798
+ ? size_scale_fn(point.size_value)
1799
+ : ((tc(`point.size`) ? styles.point?.size : null) ??
1800
+ pt?.radius ??
1801
+ default_point_radius(series_markers))}
1713
1802
  <ScatterPoint
1714
1803
  x={screen_x}
1715
1804
  y={screen_y}
1716
1805
  is_dimmed={hovered_legend_series_idx !== null &&
1717
1806
  hovered_legend_series_idx !== point.series_idx}
1718
1807
  is_hovered={tooltip_point?.series_idx === point.series_idx &&
1719
- tooltip_point?.point_idx === point.point_idx}
1808
+ tooltip_point?.point_idx === point.point_idx}
1720
1809
  is_selected={selected_point?.series_idx === point.series_idx &&
1721
- selected_point?.point_idx === point.point_idx}
1810
+ selected_point?.point_idx === point.point_idx}
1722
1811
  leader_line_threshold={actual_label_config.leader_line_threshold}
1723
1812
  style={{
1724
1813
  symbol_type: pt?.symbol_type ?? series_default_symbol,
@@ -1726,18 +1815,21 @@
1726
1815
  radius: computed_radius,
1727
1816
  stroke_width:
1728
1817
  (tc(`point.stroke_width`) ? styles.point?.stroke_width : null) ??
1729
- pt?.stroke_width ?? 1,
1818
+ pt?.stroke_width ??
1819
+ 1,
1730
1820
  stroke:
1731
1821
  (tc(`point.stroke_color`) ? styles.point?.stroke_color : null) ??
1732
- pt?.stroke ?? `#000`,
1822
+ pt?.stroke ??
1823
+ `#000`,
1733
1824
  stroke_opacity:
1734
1825
  (tc(`point.stroke_opacity`) ? styles.point?.stroke_opacity : null) ??
1735
- pt?.stroke_opacity ?? 1,
1736
- fill_opacity: (tc(`point.opacity`) ? styles.point?.opacity : null) ??
1737
- pt?.fill_opacity ?? 1,
1738
- cursor: (on_point_click || point_events?.onclick)
1739
- ? `pointer`
1740
- : undefined,
1826
+ pt?.stroke_opacity ??
1827
+ 1,
1828
+ fill_opacity:
1829
+ (tc(`point.opacity`) ? styles.point?.opacity : null) ??
1830
+ pt?.fill_opacity ??
1831
+ 1,
1832
+ cursor: on_point_click || point_events?.onclick ? `pointer` : undefined,
1741
1833
  }}
1742
1834
  hover={point.point_hover ?? {}}
1743
1835
  label={final_label}
@@ -1745,15 +1837,17 @@
1745
1837
  {point_tween}
1746
1838
  origin={{ x: plot_center_x, y: plot_center_y }}
1747
1839
  --point-fill-color={point.color_value != null
1748
- ? color_scale_fn(point.color_value)
1749
- : (tc(`point.color`) ? styles.point?.color : null) ?? pt?.fill ??
1750
- series_default_color}
1840
+ ? color_scale_fn(point.color_value)
1841
+ : ((tc(`point.color`) ? styles.point?.color : null) ??
1842
+ pt?.fill ??
1843
+ series_default_color)}
1751
1844
  {...point_events &&
1752
- Object.fromEntries(
1753
- point_event_names.map((name) => [name, (event: Event) =>
1754
- point_events?.[name]?.({ point, event })]
1755
- ),
1756
- )}
1845
+ Object.fromEntries(
1846
+ point_event_names.map((name) => [
1847
+ name,
1848
+ (event: Event) => point_events?.[name]?.({ point, event }),
1849
+ ]),
1850
+ )}
1757
1851
  onclick={(event: MouseEvent) => {
1758
1852
  // Call user-provided onclick handler first if it exists
1759
1853
  point_events?.onclick?.({ point, event })
@@ -1773,16 +1867,33 @@
1773
1867
  {@render fill_regions_layer(fills_by_z.above_all)}
1774
1868
  <!-- Reference lines: above all -->
1775
1869
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
1870
+
1871
+ <!-- Marginal distribution strips -->
1872
+ <PlotMarginals
1873
+ marginals={resolved_marginals}
1874
+ series={marginal_series}
1875
+ {width}
1876
+ {height}
1877
+ {pad}
1878
+ has_axis={marginal_has_axis}
1879
+ axes={{
1880
+ x1: marginal_axis(x_scale_fn, [x_min, x_max], final_x_axis),
1881
+ x2: marginal_axis(x2_scale_fn, [x2_min, x2_max], final_x2_axis),
1882
+ y1: marginal_axis(y_scale_fn, [y_min, y_max], final_y_axis),
1883
+ y2: marginal_axis(y2_scale_fn, [y2_min, y2_max], final_y2_axis),
1884
+ }}
1885
+ id={component_id}
1886
+ />
1776
1887
  </svg>
1777
1888
 
1778
1889
  <!-- Tooltip overlay above all plot overlays (legend, colorbar) -->
1779
1890
  {#if handler_props && hovered && tooltip_point}
1780
1891
  {@const { point_label, series_idx } = tooltip_point}
1781
1892
  {@const tooltip_bg_color = pick_tooltip_bg(
1782
- tooltip_point,
1783
- series_with_ids[series_idx],
1784
- color_scale_fn,
1785
- )}
1893
+ tooltip_point,
1894
+ series_with_ids[series_idx],
1895
+ color_scale_fn,
1896
+ )}
1786
1897
  <PlotTooltip
1787
1898
  x={handler_props.cx}
1788
1899
  y={handler_props.cy}
@@ -1800,9 +1911,10 @@
1800
1911
  {@html sanitize_html(hp.x_axis.label || `x`)}: {hp.x_formatted}<br />
1801
1912
  {@html sanitize_html(hp.y_axis.label || `y`)}: {hp.y_formatted}
1802
1913
  {#if hp.colorbar?.value != null}
1803
- <br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {
1804
- format_value(hp.colorbar.value, hp.colorbar.tick_format || `.3~g`)
1805
- }
1914
+ <br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {format_value(
1915
+ hp.colorbar.value,
1916
+ hp.colorbar.tick_format || `.3~g`,
1917
+ )}
1806
1918
  {/if}
1807
1919
  {/if}
1808
1920
  </PlotTooltip>
@@ -1813,10 +1925,9 @@
1813
1925
  <ScatterPlotControls
1814
1926
  toggle_props={{
1815
1927
  ...controls.toggle_props,
1816
- style:
1817
- `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
1818
- controls.toggle_props?.style ?? ``
1819
- }`,
1928
+ style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
1929
+ controls.toggle_props?.style ?? ``
1930
+ }`,
1820
1931
  }}
1821
1932
  pane_props={controls.pane_props}
1822
1933
  bind:x_axis
@@ -1839,13 +1950,13 @@
1839
1950
  {/if}
1840
1951
 
1841
1952
  <!-- Color Bar -->
1842
- {#if color_bar && all_color_values.length > 0 && color_bar_placement}
1953
+ {#if width > 0 && height > 0 && color_bar && all_color_values.length > 0}
1843
1954
  {@const color_domain = [
1844
- (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[0] ??
1845
- auto_color_range[0],
1846
- (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[1] ??
1847
- auto_color_range[1],
1848
- ] as Vec2}
1955
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[0] ??
1956
+ auto_color_range[0],
1957
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[1] ??
1958
+ auto_color_range[1],
1959
+ ] as Vec2}
1849
1960
  <div
1850
1961
  bind:this={colorbar_element}
1851
1962
  onmouseenter={() => colorbar_tween.set_locked(true)}
@@ -1854,9 +1965,10 @@
1854
1965
  role="img"
1855
1966
  aria-label="Color scale legend"
1856
1967
  style={`${
1857
- // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
1858
- effective_cbar_wrapper_style ??
1859
- `position: absolute; left: ${colorbar_tween.coords.current.x}px; top: ${colorbar_tween.coords.current.y}px`}; pointer-events: auto;`}
1968
+ // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
1969
+ effective_cbar_wrapper_style ??
1970
+ `position: absolute; left: ${colorbar_tween.coords.current.x}px; top: ${colorbar_tween.coords.current.y}px`
1971
+ }; pointer-events: auto;`}
1860
1972
  >
1861
1973
  <ColorBar
1862
1974
  tick_labels={4}
@@ -1865,7 +1977,8 @@
1865
1977
  color_scale_domain={color_domain}
1866
1978
  scale_type={typeof color_scale === `string` ? undefined : color_scale.type}
1867
1979
  range={color_domain?.every((val) => val != null) ? color_domain : undefined}
1868
- bar_style="width: 220px; height: 16px; {color_bar?.style ?? ``}"
1980
+ bar_style="width: {COLOR_BAR_DEFAULTS.width}px; height: {COLOR_BAR_DEFAULTS.horizontal_bar_height}px; {color_bar?.style ??
1981
+ ``}"
1869
1982
  {...color_bar}
1870
1983
  wrapper_style={effective_cbar_wrapper_style ? `height: 100%; width: 100%;` : ``}
1871
1984
  />
@@ -1874,24 +1987,18 @@
1874
1987
 
1875
1988
  <!-- Legend -->
1876
1989
  <!-- Only render if multiple series or if legend prop was explicitly provided by user (even if empty object) -->
1877
- {#if legend != null && legend_data.length > 0 &&
1878
- (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
1990
+ {#if legend != null && legend_data.length > 0 && (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
1879
1991
  {@const default_x = pad.l + 10}
1880
1992
  {@const default_y = pad.t + 10}
1881
- {@const current_x = legend_is_dragging && legend_manual_position
1882
- ? legend_manual_position.x
1883
- : legend_auto_outside
1884
- ? legend_outside_x
1885
- : legend_placement
1886
- ? legend_tween.coords.current.x
1887
- : default_x}
1888
- {@const current_y = legend_is_dragging && legend_manual_position
1889
- ? legend_manual_position.y
1890
- : legend_auto_outside
1891
- ? legend_outside_y
1892
- : legend_placement
1893
- ? legend_tween.coords.current.y
1894
- : default_y}
1993
+ {@const auto_position = placed_coords(
1994
+ legend_auto_outside,
1995
+ { x: legend_outside_x, y: legend_outside_y },
1996
+ legend_tween.placed(),
1997
+ legend_tween.coords.current,
1998
+ { x: default_x, y: default_y },
1999
+ )}
2000
+ {@const current_position =
2001
+ legend_is_dragging && legend_manual_position ? legend_manual_position : auto_position}
1895
2002
  <PlotLegend
1896
2003
  bind:root_element={legend_element}
1897
2004
  series_data={legend_data}
@@ -1907,15 +2014,14 @@
1907
2014
  hovered_fill_key = fill && fill.visible !== false ? fill.hover_key : null
1908
2015
  hovered_legend_series_idx = null
1909
2016
  } else {
1910
- hovered_legend_series_idx = item != null && item.series_idx >= 0
1911
- ? item.series_idx
1912
- : null
2017
+ hovered_legend_series_idx =
2018
+ item != null && item.series_idx >= 0 ? item.series_idx : null
1913
2019
  hovered_fill_key = null
1914
2020
  }
1915
2021
  }}
1916
2022
  active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
1917
- active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)?.idx ??
1918
- null}
2023
+ active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)
2024
+ ?.idx ?? null}
1919
2025
  draggable={legend?.draggable ?? true}
1920
2026
  {...legend}
1921
2027
  on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
@@ -1927,7 +2033,7 @@
1927
2033
  fill_regions = fill_regions.map((region, idx) =>
1928
2034
  idx === source_idx
1929
2035
  ? { ...region, visible: !(region.visible !== false) }
1930
- : region
2036
+ : region,
1931
2037
  )
1932
2038
  }
1933
2039
  }}
@@ -1938,7 +2044,9 @@
1938
2044
  // Only fill_regions can be toggled (error_bands are not bindable)
1939
2045
  if (source_type !== `fill_region`) return
1940
2046
  // Toggle: if only this fill is visible, show all; otherwise show only this one
1941
- const visible_count = fill_regions.filter((region) => region.visible !== false).length
2047
+ const visible_count = fill_regions.filter(
2048
+ (region) => region.visible !== false,
2049
+ ).length
1942
2050
  const this_visible = fill_regions[source_idx]?.visible !== false
1943
2051
  if (visible_count === 1 && this_visible) {
1944
2052
  // Show all fills
@@ -1953,8 +2061,8 @@
1953
2061
  }}
1954
2062
  style={`
1955
2063
  position: absolute;
1956
- left: ${current_x}px;
1957
- top: ${current_y}px;
2064
+ left: ${current_position.x}px;
2065
+ top: ${current_position.y}px;
1958
2066
  pointer-events: auto;
1959
2067
  ${legend?.style ?? ``}
1960
2068
  `}
@@ -2024,7 +2132,9 @@
2024
2132
  div.scatter :global(.pane-toggle),
2025
2133
  div.scatter .header-controls {
2026
2134
  opacity: 0;
2027
- transition: opacity 0.2s, background-color 0.2s;
2135
+ transition:
2136
+ opacity 0.2s,
2137
+ background-color 0.2s;
2028
2138
  }
2029
2139
  div.scatter:hover :global(.pane-toggle),
2030
2140
  div.scatter:hover .header-controls,
@@ -2051,10 +2161,7 @@
2051
2161
  color: var(--text-color);
2052
2162
  white-space: nowrap;
2053
2163
  /* Use line-height to center text vertically without flexbox */
2054
- line-height: var(
2055
- --scatter-axis-label-line-height,
2056
- 20px
2057
- ); /* Match foreignObject height */
2164
+ line-height: var(--scatter-axis-label-line-height, 20px); /* Match foreignObject height */
2058
2165
  display: block;
2059
2166
  }
2060
2167
  .current-frame-indicator {