matterviz 0.4.0 → 0.4.2

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 (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  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 +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  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 +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  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 +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -2,14 +2,15 @@
2
2
  lang="ts"
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >
5
- import type { D3ColorSchemeName, D3InterpolateName } from '../../colors'
6
- import { format_value } from '../../labels'
5
+ import type { D3InterpolateName } from '../../colors'
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'
10
10
  import type {
11
11
  AxisLoadError,
12
12
  BasePlotProps,
13
+ ColorScaleConfig,
13
14
  ControlsConfig,
14
15
  DataLoaderFn,
15
16
  DataSeries,
@@ -17,7 +18,6 @@
17
18
  FillHandlerEvent,
18
19
  FillRegion,
19
20
  HoverConfig,
20
- InitialRanges,
21
21
  InternalPoint,
22
22
  LabelPlacementConfig,
23
23
  LegendConfig,
@@ -26,9 +26,9 @@
26
26
  Point,
27
27
  RefLine,
28
28
  RefLineEvent,
29
- ScaleType,
30
29
  ScatterHandlerEvent,
31
30
  ScatterHandlerProps,
31
+ SizeScaleConfig,
32
32
  StyleOverrides,
33
33
  UserContentProps,
34
34
  } from '..'
@@ -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,23 +48,33 @@
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,
55
64
  } from '../core/auto-place'
56
- import type { AxisChangeState } from '../core/axis-utils'
57
- import { AXIS_DEFAULTS, create_axis_loader } from '../core/axis-utils'
58
- import { get_series_color, get_series_symbol } from '../core/data-transform'
59
65
  import {
60
- create_dimension_tracker,
61
- create_hover_lock,
62
- } from '../core/hover-lock.svelte'
66
+ AXIS_DEFAULTS,
67
+ type AxisChangeState,
68
+ create_axis_loader,
69
+ } from '../core/axis-utils'
70
+ import { get_series_color, get_series_symbol } from '../core/data-transform'
71
+ import { create_placed_tween } from '../core/placed-tween.svelte'
63
72
  import {
73
+ COLOR_BAR_DEFAULTS,
64
74
  DEFAULT_MARKERS,
65
75
  get_scale_type_name,
66
76
  is_time_scale,
77
+ SCALE_DEFAULTS,
67
78
  } from '../core/types'
68
79
  import { compute_label_positions } from '../core/utils/label-placement'
69
80
  import { create_legend_visibility } from '../core/utils/series-visibility'
@@ -73,7 +84,7 @@
73
84
  import type { ComponentProps, Snippet } from 'svelte'
74
85
  import { onDestroy, untrack } from 'svelte'
75
86
  import type { HTMLAttributes } from 'svelte/elements'
76
- import { Tween, type TweenOptions } from 'svelte/motion'
87
+ import type { TweenOptions } from 'svelte/motion'
77
88
  import { SvelteSet } from 'svelte/reactivity'
78
89
  import type { Pt } from '../core/fill-utils'
79
90
  import {
@@ -84,18 +95,14 @@
84
95
  import {
85
96
  expand_range_if_needed,
86
97
  get_relative_coords,
87
- MIN_TOUCH_DISTANCE_PIXELS,
98
+ invert_rect_range,
88
99
  normalize_y2_sync,
89
- pan_range_by_pixels,
90
- PINCH_ZOOM_THRESHOLD,
91
- remove_drag_listeners,
92
- sorted_range,
93
100
  sync_y2_range,
94
- to_epoch_num,
95
- zoom_range_by_factor,
96
101
  } from '../core/interactions'
102
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
97
103
  import type { Rect, Sides } from '../core/layout'
98
104
  import {
105
+ AXIS_TITLE_OFFSET,
99
106
  calc_auto_padding,
100
107
  filter_padding,
101
108
  LABEL_GAP_DEFAULT,
@@ -114,11 +121,10 @@
114
121
  get_nice_data_range,
115
122
  } from '../core/scales'
116
123
  import { resolve_line_tween, unique_id } from '../core/utils'
117
- import {
118
- build_legend_data,
119
- filter_series_to_ranges,
120
- pick_tooltip_bg,
121
- } from './scatter-data'
124
+ import { build_legend_data, filter_series_to_ranges, pick_tooltip_bg } from './scatter-data'
125
+
126
+ // Plots with at least this many visible markers get smaller default point radii
127
+ const DENSE_MARKER_COUNT = 100
122
128
 
123
129
  let {
124
130
  series = $bindable([]),
@@ -130,7 +136,7 @@
130
136
  styles: styles_init = {},
131
137
  controls: controls_init = {},
132
138
  padding = {},
133
- range_padding = 0.05,
139
+ range_padding = 0,
134
140
  current_x_value = null,
135
141
  tooltip_point = $bindable(null),
136
142
  selected_point = null,
@@ -138,13 +144,9 @@
138
144
  tooltip,
139
145
  user_content,
140
146
  change = () => {},
141
- color_scale = {
142
- type: `linear`,
143
- scheme: `interpolateViridis`,
144
- value_range: undefined,
145
- },
147
+ color_scale = SCALE_DEFAULTS.color,
146
148
  color_bar = {},
147
- size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
149
+ size_scale = SCALE_DEFAULTS.size,
148
150
  label_placement_config = {},
149
151
  hover_config = {},
150
152
  legend = {},
@@ -171,113 +173,99 @@
171
173
  on_axis_change,
172
174
  on_error,
173
175
  pan = {},
176
+ marginals = false,
174
177
  ...rest
175
- }: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, `change`> & PlotConfig & {
176
- series?: DataSeries<Metadata>[]
177
- styles?: StyleOverrides
178
- controls?: ControlsConfig
179
- current_x_value?: number | null
180
- tooltip_point?: InternalPoint<Metadata> | null
181
- selected_point?: { series_idx: number; point_idx: number } | null
182
- tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
183
- user_content?: Snippet<[UserContentProps]>
184
- header_controls?: Snippet<
185
- [{ height: number; width: number; fullscreen: boolean }]
186
- >
187
- controls_extra?: Snippet<
188
- [
189
- & { styles: StyleOverrides; selected_series_idx: number }
190
- & Required<PlotConfig>,
191
- ]
192
- >
193
- change?: (
194
- data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
195
- ) => void
196
- color_scale?: {
197
- type?: ScaleType
198
- scheme?: D3ColorSchemeName | D3InterpolateName
199
- value_range?: [number, number]
200
- } | D3InterpolateName
201
- size_scale?: {
202
- type?: ScaleType
203
- radius_range?: [number, number]
204
- value_range?: [number, number]
205
- }
206
- color_bar?:
207
- | (ComponentProps<typeof ColorBar> & {
208
- margin?: number | Sides
209
- tween?: TweenOptions<Point2D>
210
- responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
211
- axis_clearance?: number // Min distance kept from plot edges/axes (default: 15)
212
- })
213
- | null
214
- label_placement_config?: Partial<LabelPlacementConfig>
215
- hover_config?: Partial<HoverConfig>
216
- legend?: LegendConfig | null
217
- point_tween?: TweenOptions<Point2D>
218
- line_tween?: TweenOptions<string>
219
- point_events?: Record<
220
- string,
221
- (payload: { point: InternalPoint<Metadata>; event: Event }) => void
222
- >
223
- on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
224
- on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
225
- fill_regions?: FillRegion[] // Bindable for legend toggle support
226
- error_bands?: ErrorBand[]
227
- on_fill_click?: (event: FillHandlerEvent) => void
228
- on_fill_hover?: (event: FillHandlerEvent | null) => void
229
- ref_lines?: RefLine[] // Bindable for legend toggle support
230
- on_ref_line_click?: (event: RefLineEvent) => void
231
- on_ref_line_hover?: (event: RefLineEvent | null) => void
232
- selected_series_idx?: number
233
- wrapper?: HTMLDivElement
234
- // Interactive axis props
235
- data_loader?: DataLoaderFn<Metadata>
236
- on_axis_change?: (
237
- axis: `x` | `x2` | `y` | `y2`,
238
- key: string,
239
- new_series: DataSeries<Metadata>[],
240
- ) => void
241
- on_error?: (error: AxisLoadError) => void
242
- pan?: PanConfig
243
- } = $props()
178
+ }: HTMLAttributes<HTMLDivElement> &
179
+ Omit<BasePlotProps, `change`> &
180
+ PlotConfig & {
181
+ series?: DataSeries<Metadata>[]
182
+ styles?: StyleOverrides
183
+ controls?: ControlsConfig
184
+ current_x_value?: number | null
185
+ tooltip_point?: InternalPoint<Metadata> | null
186
+ selected_point?: { series_idx: number; point_idx: number } | null
187
+ tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
188
+ user_content?: Snippet<[UserContentProps]>
189
+ header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
190
+ controls_extra?: Snippet<
191
+ [{ styles: StyleOverrides; selected_series_idx: number } & Required<PlotConfig>]
192
+ >
193
+ change?: (data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null) => void
194
+ color_scale?: ColorScaleConfig | D3InterpolateName
195
+ size_scale?: SizeScaleConfig
196
+ color_bar?:
197
+ | (ComponentProps<typeof ColorBar> & {
198
+ margin?: number | Sides
199
+ tween?: TweenOptions<Point2D>
200
+ responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
201
+ axis_clearance?: number // Min distance kept from plot edges/axes (default: 15)
202
+ })
203
+ | null
204
+ label_placement_config?: Partial<LabelPlacementConfig>
205
+ hover_config?: Partial<HoverConfig>
206
+ legend?: LegendConfig | null
207
+ point_tween?: TweenOptions<Point2D>
208
+ line_tween?: TweenOptions<string>
209
+ point_events?: Record<
210
+ string,
211
+ (payload: { point: InternalPoint<Metadata>; event: Event }) => void
212
+ >
213
+ on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
214
+ on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
215
+ fill_regions?: FillRegion[] // Bindable for legend toggle support
216
+ error_bands?: ErrorBand[]
217
+ on_fill_click?: (event: FillHandlerEvent) => void
218
+ on_fill_hover?: (event: FillHandlerEvent | null) => void
219
+ ref_lines?: RefLine[] // Bindable for legend toggle support
220
+ on_ref_line_click?: (event: RefLineEvent) => void
221
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
222
+ selected_series_idx?: number
223
+ wrapper?: HTMLDivElement
224
+ // Interactive axis props
225
+ data_loader?: DataLoaderFn<Metadata>
226
+ on_axis_change?: (
227
+ axis: `x` | `x2` | `y` | `y2`,
228
+ key: string,
229
+ new_series: DataSeries<Metadata>[],
230
+ ) => void
231
+ on_error?: (error: AxisLoadError) => void
232
+ pan?: PanConfig
233
+ marginals?: MarginalsProp
234
+ } = $props()
244
235
 
245
236
  // Merged axis/display values with defaults (use $derived to avoid breaking $bindable)
246
237
  const final_x_axis = $derived({
247
238
  ...AXIS_DEFAULTS,
248
- label_shift: { x: 0, y: -40 }, // x-axis needs different label position
239
+ label_shift: { x: 0, y: 0 },
249
240
  ...x_axis,
250
241
  })
251
242
  const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...y_axis })
252
243
  const final_x2_axis = $derived({
253
244
  ...AXIS_DEFAULTS,
254
- label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
245
+ label_shift: { x: 0, y: AXIS_TITLE_OFFSET }, // x2-axis label above top edge
255
246
  ...x2_axis,
256
247
  })
257
248
  const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...y2_axis })
258
249
  // Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
259
- let is_time_x = $derived(
260
- is_time_scale(final_x_axis.scale_type, final_x_axis.format),
261
- )
262
- let is_time_x2 = $derived(
263
- is_time_scale(final_x2_axis.scale_type, final_x2_axis.format),
264
- )
250
+ let is_time_x = $derived(is_time_scale(final_x_axis.scale_type, final_x_axis.format))
251
+ let is_time_x2 = $derived(is_time_scale(final_x2_axis.scale_type, final_x2_axis.format))
265
252
  const final_display = $derived({ ...DEFAULTS.scatter.display, ...display })
266
253
  // Local state for styles (initialized from prop, owned by this component for controls)
267
254
  // Using $state because styles has bindings in ScatterPlotControls
268
255
  // untrack() explicitly captures initial prop value (intentional - props provide initial config)
269
- let styles = $state(untrack(() => ({
270
- show_points: DEFAULTS.scatter.show_points,
271
- show_lines: DEFAULTS.scatter.show_lines,
272
- point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
273
- line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
274
- ...styles_init,
275
- })))
256
+ let styles = $state(
257
+ untrack(() => ({
258
+ show_points: DEFAULTS.scatter.show_points,
259
+ show_lines: DEFAULTS.scatter.show_lines,
260
+ point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
261
+ line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
262
+ ...styles_init,
263
+ })),
264
+ )
276
265
  let controls = $derived({ show: true, open: false, ...controls_init })
277
266
 
278
267
  let [width, height] = $state([0, 0])
279
268
  let svg_element: SVGElement | null = $state(null) // Bind the SVG element
280
- let svg_bounding_box: DOMRect | null = $state(null) // Store SVG bounds during drag
281
269
 
282
270
  // Track which specific control properties user has modified
283
271
  let touched = new SvelteSet<string>()
@@ -296,20 +284,18 @@
296
284
  }),
297
285
  )
298
286
 
299
- // State for rectangle zoom selection
300
- let drag_start_coords = $state<Point2D | null>(null)
301
- let drag_current_coords = $state<Point2D | null>(null)
302
-
303
287
  // Zoom/pan state - track both initial (data-driven) and current (after pan/zoom) ranges
304
- let initial_x_range = $state<[number, number]>([0, 1])
305
- let initial_x2_range = $state<[number, number]>([0, 1])
306
- let initial_y_range = $state<[number, number]>([0, 1])
307
- let initial_y2_range = $state<[number, number]>([0, 1])
308
- let zoom_x_range = $state<[number, number]>([0, 1])
309
- let zoom_x2_range = $state<[number, number]>([0, 1])
310
- let zoom_y_range = $state<[number, number]>([0, 1])
311
- let zoom_y2_range = $state<[number, number]>([0, 1])
312
- const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
288
+ let ranges = $state<{
289
+ initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
290
+ current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
291
+ }>({
292
+ initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
293
+ current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
294
+ })
295
+ const legend_vis = create_legend_visibility(
296
+ () => series,
297
+ (next) => (series = next),
298
+ )
313
299
 
314
300
  // Y2 axis sync configuration
315
301
  let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
@@ -319,7 +305,7 @@
319
305
  // Helper to compute synced y2 range or return fallback when sync disabled
320
306
  const get_synced_y2 = (y1_range: Vec2, fallback: Vec2): Vec2 =>
321
307
  y2_sync_config.mode !== `none`
322
- ? sync_y2_range(y1_range, initial_y2_range, y2_sync_config)
308
+ ? sync_y2_range(y1_range, ranges.initial.y2, y2_sync_config)
323
309
  : fallback
324
310
 
325
311
  // Effect to update y2 range when sync mode changes - use $effect.pre to capture
@@ -330,25 +316,15 @@
330
316
  if (mode !== prev_sync_mode) {
331
317
  // When sync mode becomes enabled (or changes), apply sync immediately
332
318
  if (mode !== `none`) {
333
- zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
319
+ ranges.current.y2 = sync_y2_range(ranges.current.y, ranges.initial.y2, y2_sync_config)
334
320
  } else {
335
321
  // When switching to independent mode, reset Y2 to its data range
336
- zoom_y2_range = [...initial_y2_range] as [number, number]
322
+ ranges.current.y2 = [...ranges.initial.y2] as Vec2
337
323
  }
338
324
  prev_sync_mode = mode
339
325
  }
340
326
  })
341
327
 
342
- // Pan state
343
- let is_focused = $state(false)
344
- let shift_held = $state(false)
345
- let pan_drag_state = $state<
346
- InitialRanges & { start: { x: number; y: number } } | null
347
- >(null)
348
- let touch_state = $state<
349
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
350
- >(null)
351
-
352
328
  // Fill region hover state
353
329
  let hovered_fill_key = $state<string | null>(null)
354
330
 
@@ -370,17 +346,6 @@
370
346
  // State for legend/colorbar placement stability
371
347
  let legend_element = $state<HTMLDivElement | undefined>()
372
348
  let colorbar_element = $state<HTMLDivElement | undefined>()
373
- const legend_hover = create_hover_lock()
374
- const colorbar_hover = create_hover_lock()
375
- const dim_tracker = create_dimension_tracker()
376
- let has_initial_legend_placement = $state(false)
377
- let has_initial_colorbar_placement = $state(false)
378
-
379
- // Clear pending hover lock timeouts on unmount
380
- $effect(() => () => {
381
- legend_hover.cleanup()
382
- colorbar_hover.cleanup()
383
- })
384
349
 
385
350
  // Module-level constants to avoid repeated allocations
386
351
  // Create and categorize points in a single pass (instead of 3 separate iterations)
@@ -393,8 +358,13 @@
393
358
 
394
359
  for (const srs of series_with_ids) {
395
360
  if (!srs) continue
396
- const { x: xs, y: ys, visible = true, y_axis: series_y_axis = `y1`, x_axis: x_ax = `x1` } =
397
- srs as DataSeries
361
+ const {
362
+ x: xs,
363
+ y: ys,
364
+ visible = true,
365
+ y_axis: series_y_axis = `y1`,
366
+ x_axis: x_ax = `x1`,
367
+ } = srs as DataSeries
398
368
  for (let idx = 0; idx < xs.length; idx++) {
399
369
  const point = { x: xs[idx], y: ys[idx] }
400
370
  all.push(point)
@@ -419,37 +389,44 @@
419
389
 
420
390
  // Update padding when format or ticks change
421
391
  $effect(() => {
422
- const new_pad = width && height &&
423
- (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
424
- ? calc_auto_padding({
425
- padding,
426
- default_padding,
427
- x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
428
- y_axis: { ...final_y_axis, tick_values: y_tick_values },
429
- y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
430
- })
431
- : filter_padding(padding, default_padding)
392
+ const new_pad =
393
+ width &&
394
+ height &&
395
+ (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
396
+ ? calc_auto_padding({
397
+ padding,
398
+ default_padding,
399
+ x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
400
+ y_axis: { ...final_y_axis, tick_values: y_tick_values },
401
+ y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
402
+ })
403
+ : filter_padding(padding, default_padding)
432
404
 
433
405
  if (
434
406
  base_pad.t !== new_pad.t ||
435
407
  base_pad.b !== new_pad.b ||
436
408
  base_pad.l !== new_pad.l ||
437
409
  base_pad.r !== new_pad.r
438
- ) base_pad = new_pad
410
+ )
411
+ base_pad = new_pad
439
412
  })
440
413
 
441
414
  // === Auto-move legend/colorbar outside the plot when interior overlap is unavoidable ===
442
415
  // (shared logic lives in auto-place.ts so every 2D plot reuses it)
443
416
  // ColorBar's orientation prop defaults to horizontal, so treat unset as horizontal too
444
- const colorbar_is_horizontal = $derived((color_bar?.orientation ?? `horizontal`) === `horizontal`)
417
+ const colorbar_is_horizontal = $derived(
418
+ (color_bar?.orientation ?? `horizontal`) === `horizontal`,
419
+ )
445
420
  const colorbar_footprint = $derived(
446
421
  colorbar_element?.offsetWidth && colorbar_element?.offsetHeight
447
422
  ? measure_full_footprint(colorbar_element)
448
423
  : colorbar_is_horizontal
449
- ? { width: 220, height: 56 }
450
- : { width: 56, height: 100 },
424
+ ? COLOR_BAR_DEFAULTS.horizontal_footprint
425
+ : COLOR_BAR_DEFAULTS.vertical_footprint,
426
+ )
427
+ const legend_footprint = $derived(
428
+ measured_footprint(legend_element, { width: 120, height: 80 }),
451
429
  )
452
- const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 80 }))
453
430
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
454
431
 
455
432
  // Plot-specific obstacle field: series points/lines normalized to [0,1] (y=0 at top)
@@ -459,7 +436,9 @@
459
436
  const base_h = height - base_pad.t - base_pad.b
460
437
  if (base_w <= 0 || base_h <= 0) return []
461
438
  const norm_x = is_time_x
462
- ? scaleTime().domain([new Date(x_min), new Date(x_max)]).range([0, 1])
439
+ ? scaleTime()
440
+ .domain([new Date(x_min), new Date(x_max)])
441
+ .range([0, 1])
463
442
  : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1])
464
443
  const norm_y = create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [0, 1])
465
444
  return build_obstacles_norm(
@@ -485,23 +464,58 @@
485
464
  obstacles_norm,
486
465
  // gate on legend_element (the actual render signal) not legend_data, whose fill entries read
487
466
  // computed_fills -> pad and would make this derived reference itself
488
- legend: legend != null && legend_element != null &&
489
- !legend_has_explicit_pos && !legend_is_dragging && !legend_manual_position
490
- ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
491
- : null,
467
+ legend:
468
+ legend != null &&
469
+ legend_element != null &&
470
+ !legend_has_explicit_pos &&
471
+ !legend_is_dragging &&
472
+ !legend_manual_position
473
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
474
+ : null,
492
475
  // gate on a measured colorbar: its outside style stretches it to full width, so deciding from
493
476
  // the (wide) pre-measure fallback would flip-flop placement between interior and outside
494
- colorbar: Boolean(color_bar) && all_color_values.length > 0 && !color_bar?.wrapper_style &&
495
- (colorbar_element?.offsetWidth ?? 0) > 0 && (colorbar_element?.offsetHeight ?? 0) > 0
496
- ? {
497
- footprint: colorbar_footprint,
498
- horizontal: colorbar_is_horizontal,
499
- clearance: color_bar?.axis_clearance,
500
- }
501
- : null,
502
- })
477
+ colorbar:
478
+ Boolean(color_bar) &&
479
+ all_color_values.length > 0 &&
480
+ !color_bar?.wrapper_style &&
481
+ (colorbar_element?.offsetWidth ?? 0) > 0 &&
482
+ (colorbar_element?.offsetHeight ?? 0) > 0
483
+ ? {
484
+ footprint: colorbar_footprint,
485
+ horizontal: colorbar_is_horizontal,
486
+ clearance: color_bar?.axis_clearance,
487
+ }
488
+ : null,
489
+ }),
490
+ )
491
+ // Resolve marginals and reserve outer-band padding so the plot shrinks to make room
492
+ const resolved_marginals = $derived(
493
+ normalize_marginals(marginals, { top: true, right: true }),
494
+ )
495
+ const pad = $derived(add_sides(decor.pad, reserve_marginal_pad(resolved_marginals)))
496
+ // Map series to the generic marginal input, reusing the line/legend color fallback
497
+ const marginal_series = $derived<MarginalSeriesInput[]>(
498
+ series_with_ids.map((srs, idx) => {
499
+ const point_fill = Array.isArray(srs?.point_style)
500
+ ? srs.point_style[0]?.fill
501
+ : srs?.point_style?.fill
502
+ return {
503
+ x: srs?.x ?? [],
504
+ y: srs?.y ?? [],
505
+ color:
506
+ srs?.line_style?.stroke ??
507
+ point_fill ??
508
+ get_series_color(srs?.orig_series_idx ?? idx),
509
+ label: srs?.label,
510
+ visible: srs?.visible ?? true,
511
+ x_axis: srs?.x_axis,
512
+ y_axis: srs?.y_axis,
513
+ }
514
+ }),
515
+ )
516
+ const marginal_has_axis = $derived(
517
+ marginal_axis_presence(x2_points.length > 0, y2_points.length > 0),
503
518
  )
504
- const pad = $derived(decor.pad)
505
519
  const legend_auto_outside = $derived(decor.legend_outside)
506
520
  const legend_outside_x = $derived(decor.legend_pos.x)
507
521
  const legend_outside_y = $derived(decor.legend_pos.y)
@@ -514,9 +528,7 @@
514
528
  x: pad.l || 0,
515
529
  y: pad.t || 0,
516
530
  width: isFinite(width - pad.l - pad.r) ? Math.max(1, width - pad.l - pad.r) : 1,
517
- height: isFinite(height - pad.t - pad.b)
518
- ? Math.max(1, height - pad.t - pad.b)
519
- : 1,
531
+ height: isFinite(height - pad.t - pad.b) ? Math.max(1, height - pad.t - pad.b) : 1,
520
532
  })
521
533
 
522
534
  // Calculate plot area center coordinates
@@ -530,8 +542,12 @@
530
542
  for (const srs of series_with_ids) {
531
543
  if (!srs) continue
532
544
  const { color_values: cvs, size_values: svs } = srs as DataSeries
533
- if (cvs) { for (const val of cvs) if (val != null) color_values.push(val) }
534
- if (svs) { for (const val of svs) if (val != null) size_values.push(val) }
545
+ if (cvs) {
546
+ for (const val of cvs) if (val != null) color_values.push(val)
547
+ }
548
+ if (svs) {
549
+ for (const val of svs) if (val != null) size_values.push(val)
550
+ }
535
551
  }
536
552
  return { color_values, size_values }
537
553
  })
@@ -596,77 +612,68 @@
596
612
  return { explicit, range }
597
613
  }
598
614
 
599
- const x = get_range(final_x_axis, auto_x_range)
600
- const x2 = get_range(final_x2_axis, auto_x2_range)
601
- const y = get_range(final_y_axis, auto_y_range)
602
- const y2 = get_range(final_y2_axis, auto_y2_range)
603
-
604
- // X axis: explicit → direct, auto → lazy expand
605
- if (x.explicit) {
606
- zoom_x_range = x.range
607
- } else {
608
- const result = expand_range_if_needed(initial_x_range, x.range)
609
- if (result.changed) {
610
- ;[initial_x_range, zoom_x_range] = [result.range, result.range]
611
- }
615
+ const resolved = {
616
+ x: get_range(final_x_axis, auto_x_range),
617
+ x2: get_range(final_x2_axis, auto_x2_range),
618
+ y: get_range(final_y_axis, auto_y_range),
619
+ y2: get_range(final_y2_axis, auto_y2_range),
612
620
  }
613
621
 
614
- // X2 axis: explicit direct, auto lazy expand
615
- if (x2.explicit) {
616
- zoom_x2_range = x2.range
617
- } else {
618
- const result = expand_range_if_needed(initial_x2_range, x2.range)
619
- if (result.changed) {
620
- ;[initial_x2_range, zoom_x2_range] = [result.range, result.range]
621
- }
622
- }
623
-
624
- // Y axis: explicit → direct, auto → lazy expand
625
- if (y.explicit) {
626
- zoom_y_range = y.range
627
- } else {
628
- const result = expand_range_if_needed(initial_y_range, y.range)
629
- if (result.changed) {
630
- ;[initial_y_range, zoom_y_range] = [result.range, result.range]
622
+ // untrack reads of `ranges`: this effect also writes it, and tracked reads of the
623
+ // deep proxy would re-trigger the effect on every current/initial write
624
+ for (const axis of [`x`, `x2`, `y`] as const) {
625
+ const { explicit, range } = resolved[axis]
626
+ if (explicit) {
627
+ ranges.current[axis] = range
628
+ } else {
629
+ const result = expand_range_if_needed(
630
+ untrack(() => ranges.initial[axis]),
631
+ range,
632
+ )
633
+ if (result.changed) {
634
+ ranges.initial[axis] = result.range
635
+ ranges.current[axis] = result.range
636
+ }
631
637
  }
632
638
  }
633
639
 
634
640
  // Y2 axis: explicit → direct, else expand initial range then optionally sync
635
- if (y2.explicit) {
636
- zoom_y2_range = y2.range
641
+ if (resolved.y2.explicit) {
642
+ ranges.current.y2 = resolved.y2.range
637
643
  } else {
638
- const result = expand_range_if_needed(initial_y2_range, y2.range)
639
- if (result.changed) initial_y2_range = result.range
644
+ const result = expand_range_if_needed(
645
+ untrack(() => ranges.initial.y2),
646
+ resolved.y2.range,
647
+ )
648
+ if (result.changed) ranges.initial.y2 = result.range
640
649
  // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
641
650
  if (y2_sync_config.mode !== `none`) {
642
- // untrack the read of zoom_y_range: this effect also writes it (fresh array per
643
- // run when y.explicit), so a tracked read would loop until
644
- // effect_update_depth_exceeded. Pan/zoom handlers sync y2 themselves.
645
- zoom_y2_range = sync_y2_range(
646
- untrack(() => zoom_y_range),
647
- initial_y2_range,
651
+ // Pan/zoom handlers sync y2 themselves.
652
+ ranges.current.y2 = sync_y2_range(
653
+ untrack(() => ranges.current.y),
654
+ untrack(() => ranges.initial.y2),
648
655
  y2_sync_config,
649
656
  )
650
657
  } else if (result.changed) {
651
- zoom_y2_range = result.range
658
+ ranges.current.y2 = result.range
652
659
  }
653
660
  }
654
661
  })
655
662
 
656
- let [x_min, x_max] = $derived(zoom_x_range)
657
- let [x2_min, x2_max] = $derived(zoom_x2_range)
658
- let [y_min, y_max] = $derived(zoom_y_range)
659
- let [y2_min, y2_max] = $derived(zoom_y2_range)
663
+ let [x_min, x_max] = $derived(ranges.current.x)
664
+ let [x2_min, x2_max] = $derived(ranges.current.x2)
665
+ let [y_min, y_max] = $derived(ranges.current.y)
666
+ let [y2_min, y2_max] = $derived(ranges.current.y2)
660
667
 
661
668
  // Create auto color range
662
669
  let auto_color_range = $derived(
663
670
  // Ensure we only calculate extent on actual numbers, filtering out nulls/undefined
664
671
  all_color_values.length > 0
665
672
  ? extent(
666
- all_color_values.filter((color_val: number | null): color_val is number =>
667
- typeof color_val === `number`
668
- ),
669
- )
673
+ all_color_values.filter(
674
+ (color_val: number | null): color_val is number => typeof color_val === `number`,
675
+ ),
676
+ )
670
677
  : [0, 1],
671
678
  ) as Vec2
672
679
 
@@ -675,37 +682,37 @@
675
682
  let x_scale_fn = $derived(
676
683
  is_time_x
677
684
  ? scaleTime()
678
- .domain([new Date(x_min), new Date(x_max)])
679
- .range([pad.l, width - pad.r])
680
- : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [
681
- pad.l,
682
- width - pad.r,
683
- ]),
685
+ .domain([new Date(x_min), new Date(x_max)])
686
+ .range([pad.l, width - pad.r])
687
+ : create_scale(
688
+ final_x_axis.scale_type ?? `linear`,
689
+ [x_min, x_max],
690
+ [pad.l, width - pad.r],
691
+ ),
684
692
  )
685
693
 
686
694
  let x2_scale_fn = $derived(
687
695
  is_time_x2
688
696
  ? scaleTime()
689
- .domain([new Date(x2_min), new Date(x2_max)])
690
- .range([pad.l, width - pad.r])
691
- : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [
692
- pad.l,
693
- width - pad.r,
694
- ]),
697
+ .domain([new Date(x2_min), new Date(x2_max)])
698
+ .range([pad.l, width - pad.r])
699
+ : create_scale(
700
+ final_x2_axis.scale_type ?? `linear`,
701
+ [x2_min, x2_max],
702
+ [pad.l, width - pad.r],
703
+ ),
695
704
  )
696
705
 
697
706
  let y_scale_fn = $derived(
698
- create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [
699
- height - pad.b,
700
- pad.t,
701
- ]),
707
+ create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [height - pad.b, pad.t]),
702
708
  )
703
709
 
704
710
  let y2_scale_fn = $derived(
705
- create_scale(final_y2_axis.scale_type ?? `linear`, [y2_min, y2_max], [
706
- height - pad.b,
707
- pad.t,
708
- ]),
711
+ create_scale(
712
+ final_y2_axis.scale_type ?? `linear`,
713
+ [y2_min, y2_max],
714
+ [height - pad.b, pad.t],
715
+ ),
709
716
  )
710
717
 
711
718
  // All size values from series (for size scale) - extracted in series_value_arrays
@@ -742,6 +749,22 @@
742
749
  })
743
750
  let effective_line_tween = $derived(resolve_line_tween(line_tween, line_tween_load))
744
751
 
752
+ let visible_marker_count = $derived.by(() => {
753
+ if (!styles.show_points) return 0
754
+ let count = 0
755
+ for (const series_data of filtered_series ?? []) {
756
+ if ((series_data.markers ?? DEFAULT_MARKERS).includes(`points`)) {
757
+ count += series_data.filtered_data.length
758
+ }
759
+ }
760
+ return count
761
+ })
762
+
763
+ const default_point_radius = (markers: string): number => {
764
+ const [sparse_radius, dense_radius] = markers.includes(`line`) ? [2.5, 2] : [3, 2.5]
765
+ return visible_marker_count >= DENSE_MARKER_COUNT ? dense_radius : sparse_radius
766
+ }
767
+
745
768
  // Obstacle field for legend/colorbar auto-placement. Sampling only data points lets the
746
769
  // legend land on top of a steep connecting line whose markers are sparse (e.g. y=x^2), so
747
770
  // sample_series_obstacle_points also walks each drawn segment at a fixed pixel cadence.
@@ -757,14 +780,16 @@
757
780
  const active_x_scale = use_x2_scale ? x2_scale_fn : x_scale_fn
758
781
  const active_is_time_x = use_x2_scale ? is_time_x2 : is_time_x
759
782
  const active_y_scale = series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn
760
- const draws_line = styles.show_lines &&
761
- (series_data.markers ?? DEFAULT_MARKERS).includes(`line`)
783
+ const draws_line =
784
+ styles.show_lines && (series_data.markers ?? DEFAULT_MARKERS).includes(`line`)
762
785
 
763
786
  const pixel_points = series_data.filtered_data.map((point) => ({
764
787
  x: active_is_time_x ? active_x_scale(new Date(point.x)) : active_x_scale(point.x),
765
788
  y: active_y_scale(point.y),
766
789
  }))
767
- points.push(...sample_series_obstacle_points(pixel_points, draws_line, SEGMENT_SAMPLE_STEP))
790
+ points.push(
791
+ ...sample_series_obstacle_points(pixel_points, draws_line, SEGMENT_SAMPLE_STEP),
792
+ )
768
793
  }
769
794
  return points
770
795
  })
@@ -849,14 +874,16 @@
849
874
  }
850
875
 
851
876
  return all_regions
852
- .filter((
853
- entry,
854
- ): entry is {
855
- region: FillRegion
856
- source_type: `fill_region` | `error_band`
857
- source_idx: number
858
- hover_key: string
859
- } => entry.region !== null)
877
+ .filter(
878
+ (
879
+ entry,
880
+ ): entry is {
881
+ region: FillRegion
882
+ source_type: `fill_region` | `error_band`
883
+ source_idx: number
884
+ hover_key: string
885
+ } => entry.region !== null,
886
+ )
860
887
  .map(({ region, source_type, source_idx, hover_key }, idx) => {
861
888
  // Hidden fills keep their entry (with empty path_segments -> nothing renders) so the
862
889
  // legend item persists greyed-out and can be toggled back on.
@@ -864,15 +891,15 @@
864
891
  const path_segments = hidden
865
892
  ? []
866
893
  : compute_fill_segments(region, series_with_ids, domains)
867
- .map((seg) =>
868
- generate_fill_path(
869
- seg.upper.map(to_px),
870
- seg.lower.map(to_px),
871
- seg.upper_curve,
872
- seg.lower_curve,
894
+ .map((seg) =>
895
+ generate_fill_path(
896
+ seg.upper.map(to_px),
897
+ seg.lower.map(to_px),
898
+ seg.upper_curve,
899
+ seg.lower_curve,
900
+ ),
873
901
  )
874
- )
875
- .filter((path) => path.length > 0)
902
+ .filter((path) => path.length > 0)
876
903
 
877
904
  // Drop only visible fills with no geometry; keep hidden ones for the legend
878
905
  if (!hidden && path_segments.length === 0) return null
@@ -906,13 +933,12 @@
906
933
  })
907
934
 
908
935
  // Compute ref_lines with index and group by z-index (using shared utilities)
909
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
910
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
936
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
911
937
 
912
938
  // Calculate best legend placement using continuous grid sampling
913
939
  let legend_placement = $derived.by(() => {
914
- const should_place = legend != null &&
915
- (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
940
+ const should_place =
941
+ legend != null && (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
916
942
 
917
943
  if (!should_place || !width || !height) return null
918
944
 
@@ -940,10 +966,9 @@
940
966
 
941
967
  // Fallback estimate (with room for tick labels) used before the colorbar first
942
968
  // renders; compute_element_placement measures the real footprint once it's laid out
943
- const is_horizontal = (color_bar.orientation ?? `horizontal`) === `horizontal`
944
- const colorbar_size = is_horizontal
945
- ? { width: 220, height: 56 }
946
- : { width: 56, height: 100 }
969
+ const colorbar_size = colorbar_is_horizontal
970
+ ? COLOR_BAR_DEFAULTS.horizontal_footprint
971
+ : COLOR_BAR_DEFAULTS.vertical_footprint
947
972
 
948
973
  // Build exclusion rects (avoid legend if it's placed)
949
974
  const exclude_rects: Rect[] = []
@@ -968,75 +993,28 @@
968
993
  })
969
994
  })
970
995
 
971
- // Active legend placement (null if user set explicit position)
972
- let active_legend_placement = $derived.by(() => {
973
- if (!legend_placement) return null
974
-
975
- // Skip auto-placement if user set explicit position in style
976
- const legend_style = legend?.style ?? ``
977
- if (
978
- /(^|[;{]\s*)(top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(
979
- legend_style,
980
- )
981
- ) return null
982
-
983
- return legend_placement
984
- })
985
-
986
- // Initialize tweened values for color bar position - create once, update target via effect
987
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
988
- const tweened_colorbar_coords = new Tween(
989
- { x: 0, y: 0 },
990
- untrack(() => ({ duration: 400, ...color_bar?.tween })),
991
- )
992
- // Initialize tweened values for legend position - create once, update target via effect
993
- const tweened_legend_coords = new Tween(
994
- { x: 0, y: 0 },
995
- untrack(() => ({ duration: 400, ...legend?.tween })),
996
+ // Active legend placement (null if user set explicit position in style)
997
+ let active_legend_placement = $derived(
998
+ legend_placement && !legend_has_explicit_pos ? legend_placement : null,
996
999
  )
997
1000
 
998
- // Update placement positions (with animation and stability checks)
999
- $effect(() => {
1000
- if (!width || !height) return
1001
-
1002
- // Track dimensions for resize detection
1003
- const dims_changed = dim_tracker.has_changed(width, height)
1004
- if (dims_changed) dim_tracker.update(width, height)
1005
-
1006
- // Update colorbar position (stable after initial placement unless responsive)
1007
- if (color_bar_placement) {
1008
- const is_responsive = color_bar?.responsive ?? false
1009
- const should_update = dims_changed || (!colorbar_hover.is_locked.current &&
1010
- (is_responsive || !has_initial_colorbar_placement))
1011
-
1012
- if (should_update) {
1013
- tweened_colorbar_coords.set(
1014
- { x: color_bar_placement.x, y: color_bar_placement.y },
1015
- has_initial_colorbar_placement ? undefined : { duration: 0 },
1016
- )
1017
- if (colorbar_element && !has_initial_colorbar_placement) {
1018
- has_initial_colorbar_placement = true
1019
- }
1020
- }
1021
- }
1022
-
1023
- // Update legend position (stable after initial placement unless responsive)
1024
- if (legend_manual_position && !legend_is_dragging) {
1025
- // Immediate update (no animation) for manually dragged positions
1026
- tweened_legend_coords.set(legend_manual_position, { duration: 0 })
1027
- } else if (active_legend_placement && !legend_is_dragging) {
1028
- const is_responsive = legend?.responsive ?? false
1029
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
1030
- (is_responsive || !has_initial_legend_placement))
1031
-
1032
- if (should_update) {
1033
- tweened_legend_coords.set(
1034
- { x: active_legend_placement.x, y: active_legend_placement.y },
1035
- has_initial_legend_placement ? undefined : { duration: 0 },
1036
- )
1037
- if (legend_element) has_initial_legend_placement = true
1038
- }
1039
- }
1001
+ // Tweened colorbar/legend coordinates with shared placement stability gating
1002
+ const colorbar_tween = create_placed_tween({
1003
+ placement: () => color_bar_placement,
1004
+ dims: () => ({ width, height }),
1005
+ responsive: () => color_bar?.responsive ?? false,
1006
+ element: () => colorbar_element,
1007
+ tween: () => color_bar?.tween,
1008
+ })
1009
+ const legend_tween = create_placed_tween({
1010
+ placement: () => active_legend_placement,
1011
+ dims: () => ({ width, height }),
1012
+ responsive: () => legend?.responsive ?? false,
1013
+ element: () => legend_element,
1014
+ tween: () => legend?.tween,
1015
+ // Leave coords alone mid-drag; once dragged, the manual position wins permanently
1016
+ suspended: () => legend_is_dragging,
1017
+ manual_position: () => legend_manual_position,
1040
1018
  })
1041
1019
 
1042
1020
  // Generate axis ticks - consolidated into single derived for efficiency
@@ -1061,15 +1039,16 @@
1061
1039
  x_scale_for_ticks,
1062
1040
  { format: final_x_axis.format },
1063
1041
  ),
1064
- x2: x2_points.length > 0
1065
- ? generate_ticks(
1066
- [x2_min, x2_max],
1067
- final_x2_axis.scale_type ?? `linear`,
1068
- final_x2_axis.ticks,
1069
- x2_scale_for_ticks,
1070
- { format: final_x2_axis.format },
1071
- )
1072
- : [],
1042
+ x2:
1043
+ x2_points.length > 0
1044
+ ? generate_ticks(
1045
+ [x2_min, x2_max],
1046
+ final_x2_axis.scale_type ?? `linear`,
1047
+ final_x2_axis.ticks,
1048
+ x2_scale_for_ticks,
1049
+ { format: final_x2_axis.format },
1050
+ )
1051
+ : [],
1073
1052
  y: generate_ticks(
1074
1053
  [y_min, y_max],
1075
1054
  final_y_axis.scale_type ?? `linear`,
@@ -1077,15 +1056,16 @@
1077
1056
  y_scale_fn,
1078
1057
  { default_count: 5 },
1079
1058
  ),
1080
- y2: 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
- : [],
1059
+ y2:
1060
+ y2_points.length > 0
1061
+ ? generate_ticks(
1062
+ [y2_min, y2_max],
1063
+ final_y2_axis.scale_type ?? `linear`,
1064
+ final_y2_axis.ticks,
1065
+ y2_scale_fn,
1066
+ { default_count: 5 },
1067
+ )
1068
+ : [],
1089
1069
  }
1090
1070
  })
1091
1071
 
@@ -1102,250 +1082,76 @@
1102
1082
  y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
1103
1083
  })
1104
1084
 
1105
- // Define global handlers reference for adding/removing listeners
1106
- const on_window_mouse_move = (evt: MouseEvent) => {
1107
- if (!drag_start_coords || !svg_bounding_box) return // Exit if not dragging or no bounds
1108
-
1109
- // Calculate mouse position relative to the stored SVG bounding box
1110
- const current_x = evt.clientX - svg_bounding_box.left
1111
- const current_y = evt.clientY - svg_bounding_box.top
1112
- drag_current_coords = { x: current_x, y: current_y }
1113
-
1114
- // Optional: update tooltip only if inside SVG bounds
1115
- const is_inside_svg = current_x >= 0 &&
1116
- current_x <= svg_bounding_box.width &&
1117
- current_y >= 0 &&
1118
- current_y <= svg_bounding_box.height
1119
-
1120
- if (is_inside_svg) {
1121
- // Use the already calculated relative coordinates
1122
- update_tooltip_point(current_x, current_y)
1123
- } else tooltip_point = null // Clear tooltip if outside
1124
- }
1125
-
1126
- const on_window_mouse_up = (_evt: MouseEvent) => {
1127
- if (drag_start_coords && drag_current_coords) {
1128
- // Use current scales to invert screen coords to data coords
1129
- const start_data_x_val = x_scale_fn.invert(drag_start_coords.x)
1130
- const end_data_x_val = x_scale_fn.invert(drag_current_coords.x)
1131
- const start_data_y_val = y_scale_fn.invert(drag_start_coords.y)
1132
- const end_data_y_val = y_scale_fn.invert(drag_current_coords.y)
1133
-
1134
- // Same scale inverts both coords, so both are numbers or both are Dates
1135
- const [x1, x2] = [to_epoch_num(start_data_x_val), to_epoch_num(end_data_x_val)]
1136
- const next_x_range = sorted_range(x1, x2)
1137
- // Y axis is always number
1138
- const next_y_range = sorted_range(start_data_y_val, end_data_y_val)
1139
-
1140
- // Check for minuscule zoom box (e.g. accidental click)
1141
- const min_zoom_size = 5 // Minimum pixels to trigger zoom
1142
- const dx = Math.abs(drag_start_coords.x - drag_current_coords.x)
1143
- const dy = Math.abs(drag_start_coords.y - drag_current_coords.y)
1144
-
1145
- if (
1146
- dx > min_zoom_size &&
1147
- dy > min_zoom_size &&
1148
- next_x_range[0] !== next_x_range[1] &&
1149
- next_y_range[0] !== next_y_range[1]
1150
- ) {
1151
- // Update axis ranges to trigger reactivity (like BarPlot/Histogram do)
1152
- x_axis = { ...x_axis, range: next_x_range }
1153
- y_axis = { ...y_axis, range: next_y_range }
1154
-
1155
- // X2 axis: invert screen coords using x2 scale
1156
- if (x2_points.length > 0) {
1157
- const x2_a = to_epoch_num(x2_scale_fn.invert(drag_start_coords.x))
1158
- const x2_b = to_epoch_num(x2_scale_fn.invert(drag_current_coords.x))
1159
- x2_axis = { ...x2_axis, range: sorted_range(x2_a, x2_b) }
1160
- }
1161
-
1162
- // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1163
- // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1164
- if (y2_points.length > 0 && y2_sync_config.mode === `none`) {
1165
- const y2_a = y2_scale_fn.invert(drag_start_coords.y)
1166
- const y2_b = y2_scale_fn.invert(drag_current_coords.y)
1167
- y2_axis = { ...y2_axis, range: sorted_range(y2_a, y2_b) }
1168
- }
1085
+ // Shared pan/zoom/touch/drag-rect interaction controller. set_range routes y2
1086
+ // writes through get_synced_y2 (write-order contract: y is written before y2, so
1087
+ // the sync reads the just-updated y range).
1088
+ const pan_zoom = create_pan_zoom({
1089
+ ranges: () => ranges.current,
1090
+ scale_type: (axis) =>
1091
+ ({ x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis })[axis]
1092
+ .scale_type,
1093
+ // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1094
+ plot_dims: () => ({
1095
+ width: Math.max(1, width - pad.l - pad.r),
1096
+ height: Math.max(1, height - pad.t - pad.b),
1097
+ }),
1098
+ pan: () => pan,
1099
+ set_range: (axis, range) => {
1100
+ if (axis === `y2`) ranges.current.y2 = get_synced_y2(ranges.current.y, range)
1101
+ else ranges.current[axis] = range
1102
+ },
1103
+ svg: () => svg_element,
1104
+ on_rect_zoom: (start, current) => {
1105
+ // Update axis ranges to trigger reactivity; both x and y must invert to valid
1106
+ // (finite, non-degenerate) ranges or the rect zoom is discarded entirely
1107
+ const next_x = invert_rect_range(x_scale_fn, start.x, current.x)
1108
+ const next_y = invert_rect_range(y_scale_fn, start.y, current.y)
1109
+ if (!next_x || !next_y) return
1110
+ x_axis = { ...x_axis, range: next_x }
1111
+ y_axis = { ...y_axis, range: next_y }
1112
+
1113
+ // X2 axis: invert screen coords using x2 scale
1114
+ const next_x2 =
1115
+ x2_points.length > 0 ? invert_rect_range(x2_scale_fn, start.x, current.x) : null
1116
+ if (next_x2) x2_axis = { ...x2_axis, range: next_x2 }
1117
+
1118
+ // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1119
+ // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1120
+ const next_y2 =
1121
+ y2_points.length > 0 && y2_sync_config.mode === `none`
1122
+ ? invert_rect_range(y2_scale_fn, start.y, current.y)
1123
+ : null
1124
+ if (next_y2) y2_axis = { ...y2_axis, range: next_y2 }
1125
+ },
1126
+ on_reset: () => {
1127
+ // Reset to current auto ranges (not stale initial ranges which may have expanded)
1128
+ // This ensures lazy expansion restarts fresh from current data bounds
1129
+ ranges.initial = {
1130
+ x: [...auto_x_range] as Vec2,
1131
+ x2: [...auto_x2_range] as Vec2,
1132
+ y: [...auto_y_range] as Vec2,
1133
+ y2: [...auto_y2_range] as Vec2,
1169
1134
  }
1170
- }
1171
-
1172
- // Reset states and remove listeners
1173
- drag_start_coords = null
1174
- drag_current_coords = null
1175
- svg_bounding_box = null
1176
- window.removeEventListener(`mousemove`, on_window_mouse_move)
1177
- window.removeEventListener(`mouseup`, on_window_mouse_up)
1178
- document.body.style.cursor = `default`
1179
- }
1180
-
1181
- // Pan/zoom all four axes from an interaction-start snapshot, each in its own
1182
- // scale's transform space (log axes pan by a constant factor, linear by a shift).
1183
- // Plot dims clamped to 1px so degenerate containers can't produce Infinity deltas.
1184
- const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
1185
- const plot_width = Math.max(1, width - pad.l - pad.r)
1186
- const plot_height = Math.max(1, height - pad.t - pad.b)
1187
- zoom_x_range = pan_range_by_pixels(init.initial_x_range, dx_px, plot_width, final_x_axis.scale_type)
1188
- zoom_x2_range = pan_range_by_pixels(init.initial_x2_range, dx_px, plot_width, final_x2_axis.scale_type)
1189
- zoom_y_range = pan_range_by_pixels(init.initial_y_range, dy_px, plot_height, final_y_axis.scale_type)
1190
- zoom_y2_range = get_synced_y2(
1191
- zoom_y_range,
1192
- pan_range_by_pixels(init.initial_y2_range, dy_px, plot_height, final_y2_axis.scale_type),
1193
- )
1194
- }
1195
- const zoom_all_axes = (init: InitialRanges, factor: number) => {
1196
- zoom_x_range = zoom_range_by_factor(init.initial_x_range, factor, final_x_axis.scale_type)
1197
- zoom_x2_range = zoom_range_by_factor(init.initial_x2_range, factor, final_x2_axis.scale_type)
1198
- zoom_y_range = zoom_range_by_factor(init.initial_y_range, factor, final_y_axis.scale_type)
1199
- zoom_y2_range = get_synced_y2(
1200
- zoom_y_range,
1201
- zoom_range_by_factor(init.initial_y2_range, factor, final_y2_axis.scale_type),
1202
- )
1203
- }
1204
-
1205
- // Pan drag handler (drag direction inverted on x for natural pan feel)
1206
- const on_pan_move = (evt: MouseEvent) => {
1207
- if (!pan_drag_state) return
1208
- const sensitivity = pan?.drag_sensitivity ?? 1
1209
- pan_all_axes(
1210
- pan_drag_state,
1211
- -(evt.clientX - pan_drag_state.start.x) * sensitivity,
1212
- (evt.clientY - pan_drag_state.start.y) * sensitivity,
1213
- )
1214
- }
1215
-
1216
- const on_pan_end = () => {
1217
- pan_drag_state = null
1218
- document.body.style.cursor = ``
1219
- window.removeEventListener(`mousemove`, on_pan_move)
1220
- window.removeEventListener(`mouseup`, on_pan_end)
1221
- }
1222
-
1223
- // Tear down any window listeners + cursor override if the component unmounts mid-drag
1224
- // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
1225
- // onDestroy also runs during SSR teardown, where window/document don't exist.
1226
- onDestroy(() => {
1227
- remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
1228
- drag_start_coords = null
1229
- drag_current_coords = null
1230
- svg_bounding_box = null
1231
- pan_drag_state = null
1232
- })
1233
-
1234
- function handle_mouse_down(evt: MouseEvent) {
1235
- if (!svg_element) return
1236
-
1237
- // Check if pan is enabled and shift is held for pan mode
1238
- const pan_enabled = pan?.enabled !== false
1239
- if (pan_enabled && evt.shiftKey) {
1240
- evt.preventDefault()
1241
- pan_drag_state = {
1242
- start: { x: evt.clientX, y: evt.clientY },
1243
- initial_x_range: [...zoom_x_range] as [number, number],
1244
- initial_x2_range: [...zoom_x2_range] as [number, number],
1245
- initial_y_range: [...zoom_y_range] as [number, number],
1246
- initial_y2_range: [...zoom_y2_range] as [number, number],
1135
+ ranges.current = {
1136
+ x: [...auto_x_range] as Vec2,
1137
+ x2: [...auto_x2_range] as Vec2,
1138
+ y: [...auto_y_range] as Vec2,
1139
+ y2: get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2),
1247
1140
  }
1248
- document.body.style.cursor = `grabbing`
1249
- window.addEventListener(`mousemove`, on_pan_move)
1250
- window.addEventListener(`mouseup`, on_pan_end)
1251
- return
1252
- }
1253
-
1254
- // Store bounding box first, then calculate coords using it
1255
- svg_bounding_box = svg_element.getBoundingClientRect()
1256
-
1257
- // Calculate initial coords using the same bounding box that will be used during drag
1258
- const initial_x = evt.clientX - svg_bounding_box.left
1259
- const initial_y = evt.clientY - svg_bounding_box.top
1260
- const coords = { x: initial_x, y: initial_y }
1261
-
1262
- drag_start_coords = coords
1263
- drag_current_coords = coords
1264
-
1265
- window.addEventListener(`mousemove`, on_window_mouse_move)
1266
- window.addEventListener(`mouseup`, on_window_mouse_up)
1267
- document.body.style.cursor = `crosshair`
1268
- evt.preventDefault()
1269
- }
1270
-
1271
- // Wheel handler for pan (requires focus and shift)
1272
- function handle_wheel(evt: WheelEvent) {
1273
- const pan_enabled = pan?.enabled !== false
1274
- // Only capture wheel when focused AND Shift is held
1275
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
1276
- if (!pan_enabled || !is_focused || !shift_held) return
1277
-
1278
- evt.preventDefault()
1279
-
1280
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1281
- const plot_width = Math.max(1, width - pad.l - pad.r)
1282
- const plot_height = Math.max(1, height - pad.t - pad.b)
1283
- const sensitivity = pan?.wheel_sensitivity ?? 1
1284
-
1285
- // Pan along the dominant wheel direction
1286
- // (deltaX for horizontal scroll on trackpads, deltaY for vertical)
1287
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
1288
- const dx = evt.deltaX * sensitivity
1289
- zoom_x_range = pan_range_by_pixels(zoom_x_range, dx, plot_width, final_x_axis.scale_type)
1290
- zoom_x2_range = pan_range_by_pixels(zoom_x2_range, dx, plot_width, final_x2_axis.scale_type)
1291
- } else {
1292
- const dy = evt.deltaY * sensitivity
1293
- zoom_y_range = pan_range_by_pixels(zoom_y_range, dy, plot_height, final_y_axis.scale_type)
1294
- zoom_y2_range = get_synced_y2(
1295
- zoom_y_range,
1296
- pan_range_by_pixels(zoom_y2_range, dy, plot_height, final_y2_axis.scale_type),
1297
- )
1298
- }
1299
- }
1300
-
1301
- // Touch handlers for pinch-zoom and two-finger pan
1302
- function handle_touch_start(evt: TouchEvent) {
1303
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
1304
- if (!touch_enabled || evt.touches.length !== 2) return
1305
-
1306
- evt.preventDefault()
1307
- const touches = Array.from(evt.touches)
1308
- touch_state = {
1309
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
1310
- initial_x_range: [...zoom_x_range] as [number, number],
1311
- initial_x2_range: [...zoom_x2_range] as [number, number],
1312
- initial_y_range: [...zoom_y_range] as [number, number],
1313
- initial_y2_range: [...zoom_y2_range] as [number, number],
1314
- }
1315
- }
1316
-
1317
- function handle_touch_move(evt: TouchEvent) {
1318
- if (!touch_state || evt.touches.length !== 2) return
1319
- evt.preventDefault()
1320
-
1321
- const [t1, t2] = Array.from(evt.touches)
1322
- const [s1, s2] = touch_state.start_touches
1323
-
1324
- // Calculate center movement for pan
1325
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
1326
- const curr_center = {
1327
- x: (t1.clientX + t2.clientX) / 2,
1328
- y: (t1.clientY + t2.clientY) / 2,
1329
- }
1330
- const dx = curr_center.x - start_center.x
1331
- const dy = curr_center.y - start_center.y
1332
-
1333
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
1334
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
1335
- // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
1336
- if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
1337
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
1338
- const scale = curr_dist / start_dist
1339
-
1340
- // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
1341
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
1342
- zoom_all_axes(touch_state, scale)
1343
- } else pan_all_axes(touch_state, -dx, dy)
1344
- }
1345
-
1346
- function handle_touch_end() {
1347
- touch_state = null
1348
- }
1141
+ // Also reset axis props so future data changes recalculate auto ranges
1142
+ x_axis = { ...x_axis, range: [null, null] }
1143
+ x2_axis = { ...x2_axis, range: [null, null] }
1144
+ y_axis = { ...y_axis, range: [null, null] }
1145
+ y2_axis = { ...y2_axis, range: [null, null] }
1146
+ },
1147
+ // Live tooltip while rect-dragging: update for the closest point inside the
1148
+ // plot bounds, clear when the cursor leaves the svg
1149
+ on_drag_move: (coords, inside_svg) => {
1150
+ if (inside_svg) update_tooltip_point(coords.x, coords.y)
1151
+ else tooltip_point = null
1152
+ },
1153
+ })
1154
+ onDestroy(() => pan_zoom.destroy())
1349
1155
 
1350
1156
  // tooltip logic: find closest point and update tooltip state
1351
1157
  function update_tooltip_point(x_rel: number, y_rel: number, evt?: MouseEvent) {
@@ -1369,9 +1175,7 @@
1369
1175
  const point_cx = tooltip_is_time_x
1370
1176
  ? tooltip_x_scale(new Date(point.x))
1371
1177
  : tooltip_x_scale(point.x)
1372
- const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(
1373
- point.y,
1374
- )
1178
+ const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(point.y)
1375
1179
 
1376
1180
  // Calculate squared screen distance between mouse and point
1377
1181
  const screen_dx = x_rel - point_cx
@@ -1388,11 +1192,7 @@
1388
1192
  }
1389
1193
 
1390
1194
  // Check if the closest point is within the hover threshold
1391
- if (
1392
- closest_point &&
1393
- closest_series &&
1394
- min_screen_dist_sq <= hover_threshold_px_sq
1395
- ) {
1195
+ if (closest_point && closest_series && min_screen_dist_sq <= hover_threshold_px_sq) {
1396
1196
  // Construct handler props synchronously to avoid stale derived reads
1397
1197
  const props = construct_handler_props(closest_point)
1398
1198
  tooltip_point = closest_point
@@ -1467,8 +1267,8 @@
1467
1267
  const new_y = event.clientY - svg_rect.top - legend_drag_offset.y
1468
1268
 
1469
1269
  // Get actual legend dimensions for accurate bounds checking using the bound element reference
1470
- const { width: legend_width, height: legend_height } = legend_element
1471
- .getBoundingClientRect()
1270
+ const { width: legend_width, height: legend_height } =
1271
+ legend_element.getBoundingClientRect()
1472
1272
 
1473
1273
  // Constrain to plot bounds using measured legend size
1474
1274
  const constrained_x = Math.max(0, Math.min(width - legend_width, new_x))
@@ -1477,7 +1277,7 @@
1477
1277
  legend_manual_position = { x: constrained_x, y: constrained_y }
1478
1278
  }
1479
1279
 
1480
- function get_screen_coords(point: Point, data_series?: DataSeries): [number, number] {
1280
+ function get_screen_coords(point: Point, data_series?: DataSeries): Vec2 {
1481
1281
  // convert data coordinates to potentially non-finite screen coordinates
1482
1282
  const use_x2 = data_series?.x_axis === `x2`
1483
1283
  const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
@@ -1514,9 +1314,7 @@
1514
1314
  const cx = handler_is_time_x ? handler_x_scale(new Date(x)) : handler_x_scale(x)
1515
1315
  const cy = (hovered_series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(y)
1516
1316
  const active_x_config = handler_use_x2 ? final_x2_axis : final_x_axis
1517
- const active_y_config = hovered_series.y_axis === `y2`
1518
- ? final_y2_axis
1519
- : final_y_axis
1317
+ const active_y_config = hovered_series.y_axis === `y2` ? final_y2_axis : final_y_axis
1520
1318
  const coords = {
1521
1319
  x,
1522
1320
  y,
@@ -1533,8 +1331,8 @@
1533
1331
  metadata,
1534
1332
  label: hovered_series.label ?? null,
1535
1333
  series_idx,
1536
- x_formatted: format_value(x, active_x_config.format || `.3~s`),
1537
- y_formatted: format_value(y, active_y_config.format || `.3~s`),
1334
+ x_formatted: format_value_or_num(x, active_x_config.format),
1335
+ y_formatted: format_value_or_num(y, active_y_config.format),
1538
1336
  color_value: color_value ?? null,
1539
1337
  colorbar: {
1540
1338
  value: color_value ?? null,
@@ -1556,9 +1354,7 @@
1556
1354
  // Precompute non-click event names from point_events so we don't rebuild
1557
1355
  // the entries array on every point render.
1558
1356
  let point_event_names = $derived(
1559
- point_events
1560
- ? Object.keys(point_events).filter((name) => name !== `onclick`)
1561
- : [],
1357
+ point_events ? Object.keys(point_events).filter((name) => name !== `onclick`) : [],
1562
1358
  )
1563
1359
 
1564
1360
  // Set theme-aware background when entering fullscreen
@@ -1567,41 +1363,30 @@
1567
1363
  })
1568
1364
 
1569
1365
  // State accessors for shared axis change handler
1366
+ // Spread into existing state in each setter to preserve merged type structure
1570
1367
  const axis_state: AxisChangeState<DataSeries<Metadata>> = {
1571
- get_axis: (axis) => {
1572
- if (axis === `x`) return x_axis
1573
- if (axis === `x2`) return x2_axis
1574
- if (axis === `y`) return y_axis
1575
- return y2_axis
1576
- },
1577
- set_axis: (axis, config) => {
1578
- // Spread into existing state to preserve merged type structure
1579
- if (axis === `x`) x_axis = { ...x_axis, ...config }
1580
- else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
1581
- else if (axis === `y`) y_axis = { ...y_axis, ...config }
1582
- else y2_axis = { ...y2_axis, ...config }
1368
+ axes: {
1369
+ x: { get: () => x_axis, set: (config) => (x_axis = { ...x_axis, ...config }) },
1370
+ x2: { get: () => x2_axis, set: (config) => (x2_axis = { ...x2_axis, ...config }) },
1371
+ y: { get: () => y_axis, set: (config) => (y_axis = { ...y_axis, ...config }) },
1372
+ y2: { get: () => y2_axis, set: (config) => (y2_axis = { ...y2_axis, ...config }) },
1583
1373
  },
1584
- get_series: () => series,
1585
- set_series: (new_series) => (series = new_series),
1586
- get_loading: () => axis_loading,
1587
- set_loading: (axis) => (axis_loading = axis),
1374
+ series: { get: () => series, set: (next) => (series = next) },
1375
+ loading: { get: () => axis_loading, set: (axis) => (axis_loading = axis) },
1588
1376
  }
1589
1377
 
1590
1378
  // Shared handler + one-shot auto-load bound to this component's state
1591
- const { handle_axis_change, try_auto_load } = create_axis_loader(
1592
- axis_state,
1593
- () => ({ data_loader, on_axis_change, on_error }),
1594
- )
1379
+ const { handle_axis_change, try_auto_load } = create_axis_loader(axis_state, () => ({
1380
+ data_loader,
1381
+ on_axis_change,
1382
+ on_error,
1383
+ }))
1595
1384
  $effect(try_auto_load)
1596
1385
  </script>
1597
1386
 
1598
1387
  {#snippet fill_regions_layer(fills: typeof computed_fills)}
1599
1388
  {#each fills as fill (fill.hover_key)}
1600
- {#each fill.path_segments as
1601
- path_d,
1602
- segment_idx
1603
- (`${fill.id ?? fill.idx}-${segment_idx}`)
1604
- }
1389
+ {#each fill.path_segments as path_d, segment_idx (`${fill.id ?? fill.idx}-${segment_idx}`)}
1605
1390
  <FillArea
1606
1391
  region={fill}
1607
1392
  region_idx={fill.idx}
@@ -1658,11 +1443,9 @@
1658
1443
  evt.preventDefault()
1659
1444
  fullscreen = false
1660
1445
  }
1661
- if (evt.key === `Shift`) shift_held = true
1662
- }}
1663
- onkeyup={(evt) => {
1664
- if (evt.key === `Shift`) shift_held = false
1446
+ pan_zoom.on_window_key_down(evt)
1665
1447
  }}
1448
+ onkeyup={pan_zoom.on_window_key_up}
1666
1449
  />
1667
1450
 
1668
1451
  <div
@@ -1670,7 +1453,7 @@
1670
1453
  bind:clientWidth={width}
1671
1454
  bind:clientHeight={height}
1672
1455
  {...rest}
1673
- class="scatter {rest.class ?? ``}"
1456
+ class={[`scatter`, rest.class]}
1674
1457
  class:fullscreen
1675
1458
  >
1676
1459
  {#if width && height}
@@ -1686,49 +1469,30 @@
1686
1469
  bind:this={svg_element}
1687
1470
  role="application"
1688
1471
  aria-label={rest[`aria-label`] ??
1689
- ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1690
- `Scatter plot`)}
1472
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1473
+ `Scatter plot`)}
1691
1474
  tabindex="0"
1692
- onfocusin={() => (is_focused = true)}
1693
- onfocusout={() => (is_focused = false)}
1475
+ onfocusin={() => pan_zoom.set_focused(true)}
1476
+ onfocusout={() => pan_zoom.set_focused(false)}
1694
1477
  onmouseenter={() => (hovered = true)}
1695
- onmousedown={handle_mouse_down}
1478
+ onmousedown={pan_zoom.on_mouse_down}
1696
1479
  onmousemove={(evt: MouseEvent) => {
1697
1480
  // Only find closest point if not actively dragging
1698
- if (!drag_start_coords && !pan_drag_state) on_mouse_move(evt)
1481
+ if (!pan_zoom.drag_start && !pan_zoom.is_pan_dragging) on_mouse_move(evt)
1699
1482
  }}
1700
1483
  onmouseleave={() => {
1701
1484
  hovered = false
1702
1485
  tooltip_point = null
1703
1486
  on_point_hover?.(null)
1704
1487
  }}
1705
- ondblclick={() => {
1706
- // Reset to current auto ranges (not stale initial_*_range which may have expanded)
1707
- // This ensures lazy expansion restarts fresh from current data bounds
1708
- initial_x_range = [...auto_x_range] as [number, number]
1709
- initial_x2_range = [...auto_x2_range] as [number, number]
1710
- initial_y_range = [...auto_y_range] as [number, number]
1711
- initial_y2_range = [...auto_y2_range] as [number, number]
1712
- zoom_x_range = [...auto_x_range] as [number, number]
1713
- zoom_x2_range = [...auto_x2_range] as [number, number]
1714
- zoom_y_range = [...auto_y_range] as [number, number]
1715
- zoom_y2_range = get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2)
1716
- // Also reset axis props so future data changes recalculate auto ranges
1717
- x_axis = { ...x_axis, range: [null, null] }
1718
- x2_axis = { ...x2_axis, range: [null, null] }
1719
- y_axis = { ...y_axis, range: [null, null] }
1720
- y2_axis = { ...y2_axis, range: [null, null] }
1721
- }}
1722
- onwheel={handle_wheel}
1723
- ontouchstart={handle_touch_start}
1724
- ontouchmove={handle_touch_move}
1725
- ontouchend={handle_touch_end}
1726
- ontouchcancel={handle_touch_end}
1727
- style:cursor={pan_drag_state
1728
- ? `grabbing`
1729
- : shift_held && pan?.enabled !== false
1730
- ? `grab`
1731
- : `crosshair`}
1488
+ ondblclick={pan_zoom.reset_view}
1489
+ onkeydown={pan_zoom.on_key_down}
1490
+ onwheel={pan_zoom.on_wheel}
1491
+ ontouchstart={pan_zoom.on_touch_start}
1492
+ ontouchmove={pan_zoom.on_touch_move}
1493
+ ontouchend={pan_zoom.on_touch_end}
1494
+ ontouchcancel={pan_zoom.on_touch_end}
1495
+ style:cursor={pan_zoom.cursor}
1732
1496
  >
1733
1497
  {@render user_content?.({
1734
1498
  height,
@@ -1763,7 +1527,7 @@
1763
1527
  domain={[x_min, x_max]}
1764
1528
  tick_label={(tick) => get_tick_label(tick, final_x_axis.ticks)}
1765
1529
  label_x={width / 2 + (final_x_axis.label_shift?.x ?? 0)}
1766
- label_y={height - pad.b - (final_x_axis.label_shift?.y ?? -40)}
1530
+ label_y={height - pad.b + AXIS_TITLE_OFFSET + (final_x_axis.label_shift?.y ?? 0)}
1767
1531
  axis_loading={axis_loading === `x`}
1768
1532
  on_axis_change={(key) => handle_axis_change(`x`, key)}
1769
1533
  />
@@ -1771,8 +1535,8 @@
1771
1535
  <!-- Current frame indicator -->
1772
1536
  {#if current_x_value != null}
1773
1537
  {@const current_pos_raw = is_time_x
1774
- ? x_scale_fn(new Date(current_x_value))
1775
- : x_scale_fn(current_x_value)}
1538
+ ? x_scale_fn(new Date(current_x_value))
1539
+ : x_scale_fn(current_x_value)}
1776
1540
  {#if isFinite(current_pos_raw)}
1777
1541
  {@const current_pos = current_pos_raw}
1778
1542
  {#if current_pos >= pad.l && current_pos <= width - pad.r}
@@ -1806,7 +1570,8 @@
1806
1570
  tick_label={(tick) => get_tick_label(tick, final_y_axis.ticks)}
1807
1571
  label_x={Math.max(
1808
1572
  12,
1809
- pad.l - (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1573
+ pad.l -
1574
+ (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1810
1575
  LABEL_GAP_DEFAULT,
1811
1576
  ) + (final_y_axis.label_shift?.x ?? 0)}
1812
1577
  label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y_axis.label_shift?.y ?? 0)}
@@ -1851,7 +1616,7 @@
1851
1616
  domain={[x2_min, x2_max]}
1852
1617
  tick_label={(tick) => get_tick_label(tick, final_x2_axis.ticks)}
1853
1618
  label_x={width / 2 + (final_x2_axis.label_shift?.x ?? 0)}
1854
- label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? 40))}
1619
+ label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? AXIS_TITLE_OFFSET))}
1855
1620
  axis_loading={axis_loading === `x2`}
1856
1621
  on_axis_change={(key) => handle_axis_change(`x2`, key)}
1857
1622
  />
@@ -1859,7 +1624,7 @@
1859
1624
 
1860
1625
  <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
1861
1626
 
1862
- <ZoomRect start={drag_start_coords} current={drag_current_coords} />
1627
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
1863
1628
 
1864
1629
  <ZeroLines
1865
1630
  display={final_display}
@@ -1867,10 +1632,10 @@
1867
1632
  {x2_scale_fn}
1868
1633
  {y_scale_fn}
1869
1634
  {y2_scale_fn}
1870
- x_range={zoom_x_range}
1871
- x2_range={zoom_x2_range}
1872
- y_range={zoom_y_range}
1873
- y2_range={zoom_y2_range}
1635
+ x_range={ranges.current.x}
1636
+ x2_range={ranges.current.x2}
1637
+ y_range={ranges.current.y}
1638
+ y2_range={ranges.current.y2}
1874
1639
  x_scale_type={final_x_axis.scale_type}
1875
1640
  x2_scale_type={final_x2_axis.scale_type}
1876
1641
  y_scale_type={final_y_axis.scale_type}
@@ -1909,30 +1674,32 @@
1909
1674
  data-series-id={series_data._id}
1910
1675
  clip-path="url(#{clip_path_id})"
1911
1676
  opacity={hovered_legend_series_idx !== null &&
1912
- hovered_legend_series_idx !== series_data.orig_series_idx
1677
+ hovered_legend_series_idx !== series_data.orig_series_idx
1913
1678
  ? 0.25
1914
1679
  : 1}
1915
1680
  >
1916
- {#if series_markers?.includes(`line`)}
1681
+ {#if series_markers.includes(`line`)}
1917
1682
  {@const all_line_points = series_data.x.map((x, idx) => ({
1918
- x,
1919
- y: series_data.y[idx],
1920
- }))}
1683
+ x,
1684
+ y: series_data.y[idx],
1685
+ }))}
1921
1686
  {@const finite_screen_points = all_line_points
1922
- .map((point) => get_screen_coords(point, series_data))
1923
- .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
1924
- {@const apply_line_controls = controls.show &&
1925
- (!has_multiple_series ||
1926
- series_data._id === series_with_ids[selected_series_idx]?._id)}
1687
+ .map((point) => get_screen_coords(point, series_data))
1688
+ .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
1689
+ {@const apply_line_controls =
1690
+ controls.show &&
1691
+ (!has_multiple_series ||
1692
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
1927
1693
  {@const ls = series_data.line_style}
1928
1694
  {@const tc = (key: string) => apply_line_controls && touched.has(key)}
1929
- {@const color_fallback = ls?.stroke ??
1930
- (Array.isArray(series_data.point_style)
1931
- ? series_data.point_style[0]?.fill
1932
- : series_data.point_style?.fill) ??
1933
- (series_data.color_values?.[0] != null
1934
- ? color_scale_fn(series_data.color_values[0])
1935
- : series_default_color)}
1695
+ {@const color_fallback =
1696
+ ls?.stroke ??
1697
+ (Array.isArray(series_data.point_style)
1698
+ ? series_data.point_style[0]?.fill
1699
+ : series_data.point_style?.fill) ??
1700
+ (series_data.color_values?.[0] != null
1701
+ ? color_scale_fn(series_data.color_values[0])
1702
+ : series_default_color)}
1936
1703
  <Line
1937
1704
  points={finite_screen_points}
1938
1705
  origin={[
@@ -1940,8 +1707,11 @@
1940
1707
  series_data.y_axis === `y2` ? y2_scale_fn(y2_min) : y_scale_fn(y_min),
1941
1708
  ]}
1942
1709
  line_color={(tc(`line.color`) ? styles.line?.color : null) ?? color_fallback}
1943
- line_width={(tc(`line.width`) ? styles.line?.width : null) ?? ls?.stroke_width ?? 2}
1710
+ line_width={(tc(`line.width`) ? styles.line?.width : null) ??
1711
+ ls?.stroke_width ??
1712
+ 2}
1944
1713
  line_dash={(tc(`line.dash`) ? styles.line?.dash : null) ?? ls?.line_dash}
1714
+ curve={ls?.curve}
1945
1715
  area_color="transparent"
1946
1716
  line_tween={effective_line_tween}
1947
1717
  />
@@ -1962,53 +1732,60 @@
1962
1732
  {@const series_default_color = get_series_color(series_data.orig_series_idx ?? 0)}
1963
1733
  {@const series_default_symbol = get_series_symbol(series_data.orig_series_idx ?? 0)}
1964
1734
  <g data-series-id={series_data._id}>
1965
- {#if series_markers?.includes(`points`)}
1966
- {#each series_data.filtered_data as
1967
- point
1968
- (`${point.series_idx}-${point.point_idx}`)
1969
- }
1735
+ {#if series_markers.includes(`points`)}
1736
+ {#each series_data.filtered_data as point (`${point.series_idx}-${point.point_idx}`)}
1970
1737
  {@const label_id = `${point.series_idx}-${point.point_idx}`}
1971
1738
  {@const calculated_label_pos = label_positions[label_id]}
1972
1739
  {@const point_label = point.point_label ?? {}}
1973
- {@const label_style = point_label.auto_placement &&
1974
- actual_label_config.max_neighbors &&
1975
- !calculated_label_pos
1976
- ? {}
1977
- : point_label}
1740
+ {@const label_style =
1741
+ point_label.auto_placement &&
1742
+ actual_label_config.max_neighbors &&
1743
+ !calculated_label_pos
1744
+ ? {}
1745
+ : point_label}
1978
1746
  {@const final_label = calculated_label_pos
1979
- ? {
1980
- ...label_style,
1981
- offset: {
1982
- x: calculated_label_pos.x -
1983
- (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
1984
- y: calculated_label_pos.y - (series_data.y_axis === `y2`
1985
- ? y2_scale_fn(point.y)
1986
- : y_scale_fn(point.y)),
1987
- },
1988
- }
1989
- : label_style}
1747
+ ? {
1748
+ ...label_style,
1749
+ offset: {
1750
+ x:
1751
+ calculated_label_pos.x -
1752
+ (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
1753
+ y:
1754
+ calculated_label_pos.y -
1755
+ (series_data.y_axis === `y2`
1756
+ ? y2_scale_fn(point.y)
1757
+ : y_scale_fn(point.y)),
1758
+ },
1759
+ }
1760
+ : label_style}
1990
1761
  {@const [raw_screen_x, raw_screen_y] = get_screen_coords(point, series_data)}
1991
- {@const screen_x = isFinite(raw_screen_x) ? raw_screen_x : x_scale_fn.range()[0]}
1762
+ {@const screen_x = isFinite(raw_screen_x)
1763
+ ? raw_screen_x
1764
+ : x_scale_fn.range()[0]}
1992
1765
  {@const screen_y = isFinite(raw_screen_y)
1993
- ? raw_screen_y
1994
- : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
1995
- {@const apply_controls = controls.show &&
1996
- (!has_multiple_series ||
1997
- series_data._id === series_with_ids[selected_series_idx]?._id)}
1766
+ ? raw_screen_y
1767
+ : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
1768
+ {@const apply_controls =
1769
+ controls.show &&
1770
+ (!has_multiple_series ||
1771
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
1998
1772
  {@const pt = point.point_style}
1999
1773
  {@const tc = (key: string) => apply_controls && touched.has(key)}
2000
- {@const computed_radius = point.size_value != null
2001
- ? size_scale_fn(point.size_value)
2002
- : (tc(`point.size`) ? styles.point?.size : null) ?? pt?.radius ?? 4}
1774
+ {@const computed_radius =
1775
+ point.size_value != null
1776
+ ? size_scale_fn(point.size_value)
1777
+ : ((tc(`point.size`) ? styles.point?.size : null) ??
1778
+ pt?.radius ??
1779
+ default_point_radius(series_markers))}
2003
1780
  <ScatterPoint
2004
1781
  x={screen_x}
2005
1782
  y={screen_y}
2006
1783
  is_dimmed={hovered_legend_series_idx !== null &&
2007
1784
  hovered_legend_series_idx !== point.series_idx}
2008
1785
  is_hovered={tooltip_point?.series_idx === point.series_idx &&
2009
- tooltip_point?.point_idx === point.point_idx}
1786
+ tooltip_point?.point_idx === point.point_idx}
2010
1787
  is_selected={selected_point?.series_idx === point.series_idx &&
2011
- selected_point?.point_idx === point.point_idx}
1788
+ selected_point?.point_idx === point.point_idx}
2012
1789
  leader_line_threshold={actual_label_config.leader_line_threshold}
2013
1790
  style={{
2014
1791
  symbol_type: pt?.symbol_type ?? series_default_symbol,
@@ -2016,18 +1793,21 @@
2016
1793
  radius: computed_radius,
2017
1794
  stroke_width:
2018
1795
  (tc(`point.stroke_width`) ? styles.point?.stroke_width : null) ??
2019
- pt?.stroke_width ?? 1,
1796
+ pt?.stroke_width ??
1797
+ 1,
2020
1798
  stroke:
2021
1799
  (tc(`point.stroke_color`) ? styles.point?.stroke_color : null) ??
2022
- pt?.stroke ?? `#000`,
1800
+ pt?.stroke ??
1801
+ `#000`,
2023
1802
  stroke_opacity:
2024
1803
  (tc(`point.stroke_opacity`) ? styles.point?.stroke_opacity : null) ??
2025
- pt?.stroke_opacity ?? 1,
2026
- fill_opacity: (tc(`point.opacity`) ? styles.point?.opacity : null) ??
2027
- pt?.fill_opacity ?? 1,
2028
- cursor: (on_point_click || point_events?.onclick)
2029
- ? `pointer`
2030
- : undefined,
1804
+ pt?.stroke_opacity ??
1805
+ 1,
1806
+ fill_opacity:
1807
+ (tc(`point.opacity`) ? styles.point?.opacity : null) ??
1808
+ pt?.fill_opacity ??
1809
+ 1,
1810
+ cursor: on_point_click || point_events?.onclick ? `pointer` : undefined,
2031
1811
  }}
2032
1812
  hover={point.point_hover ?? {}}
2033
1813
  label={final_label}
@@ -2035,15 +1815,17 @@
2035
1815
  {point_tween}
2036
1816
  origin={{ x: plot_center_x, y: plot_center_y }}
2037
1817
  --point-fill-color={point.color_value != null
2038
- ? color_scale_fn(point.color_value)
2039
- : (tc(`point.color`) ? styles.point?.color : null) ?? pt?.fill ??
2040
- series_default_color}
1818
+ ? color_scale_fn(point.color_value)
1819
+ : ((tc(`point.color`) ? styles.point?.color : null) ??
1820
+ pt?.fill ??
1821
+ series_default_color)}
2041
1822
  {...point_events &&
2042
- Object.fromEntries(
2043
- point_event_names.map((name) => [name, (event: Event) =>
2044
- point_events?.[name]?.({ point, event })]
2045
- ),
2046
- )}
1823
+ Object.fromEntries(
1824
+ point_event_names.map((name) => [
1825
+ name,
1826
+ (event: Event) => point_events?.[name]?.({ point, event }),
1827
+ ]),
1828
+ )}
2047
1829
  onclick={(event: MouseEvent) => {
2048
1830
  // Call user-provided onclick handler first if it exists
2049
1831
  point_events?.onclick?.({ point, event })
@@ -2063,16 +1845,33 @@
2063
1845
  {@render fill_regions_layer(fills_by_z.above_all)}
2064
1846
  <!-- Reference lines: above all -->
2065
1847
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
1848
+
1849
+ <!-- Marginal distribution strips -->
1850
+ <PlotMarginals
1851
+ marginals={resolved_marginals}
1852
+ series={marginal_series}
1853
+ {width}
1854
+ {height}
1855
+ {pad}
1856
+ has_axis={marginal_has_axis}
1857
+ axes={{
1858
+ x1: marginal_axis(x_scale_fn, [x_min, x_max], final_x_axis),
1859
+ x2: marginal_axis(x2_scale_fn, [x2_min, x2_max], final_x2_axis),
1860
+ y1: marginal_axis(y_scale_fn, [y_min, y_max], final_y_axis),
1861
+ y2: marginal_axis(y2_scale_fn, [y2_min, y2_max], final_y2_axis),
1862
+ }}
1863
+ id={component_id}
1864
+ />
2066
1865
  </svg>
2067
1866
 
2068
1867
  <!-- Tooltip overlay above all plot overlays (legend, colorbar) -->
2069
1868
  {#if handler_props && hovered && tooltip_point}
2070
1869
  {@const { point_label, series_idx } = tooltip_point}
2071
1870
  {@const tooltip_bg_color = pick_tooltip_bg(
2072
- tooltip_point,
2073
- series_with_ids[series_idx],
2074
- color_scale_fn,
2075
- )}
1871
+ tooltip_point,
1872
+ series_with_ids[series_idx],
1873
+ color_scale_fn,
1874
+ )}
2076
1875
  <PlotTooltip
2077
1876
  x={handler_props.cx}
2078
1877
  y={handler_props.cy}
@@ -2090,9 +1889,10 @@
2090
1889
  {@html sanitize_html(hp.x_axis.label || `x`)}: {hp.x_formatted}<br />
2091
1890
  {@html sanitize_html(hp.y_axis.label || `y`)}: {hp.y_formatted}
2092
1891
  {#if hp.colorbar?.value != null}
2093
- <br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {
2094
- format_value(hp.colorbar.value, hp.colorbar.tick_format || `.3~g`)
2095
- }
1892
+ <br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {format_value(
1893
+ hp.colorbar.value,
1894
+ hp.colorbar.tick_format || `.3~g`,
1895
+ )}
2096
1896
  {/if}
2097
1897
  {/if}
2098
1898
  </PlotTooltip>
@@ -2103,10 +1903,9 @@
2103
1903
  <ScatterPlotControls
2104
1904
  toggle_props={{
2105
1905
  ...controls.toggle_props,
2106
- style:
2107
- `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
2108
- controls.toggle_props?.style ?? ``
2109
- }`,
1906
+ style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
1907
+ controls.toggle_props?.style ?? ``
1908
+ }`,
2110
1909
  }}
2111
1910
  pane_props={controls.pane_props}
2112
1911
  bind:x_axis
@@ -2131,22 +1930,23 @@
2131
1930
  <!-- Color Bar -->
2132
1931
  {#if color_bar && all_color_values.length > 0 && color_bar_placement}
2133
1932
  {@const color_domain = [
2134
- (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[0] ??
2135
- auto_color_range[0],
2136
- (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[1] ??
2137
- auto_color_range[1],
2138
- ] as Vec2}
1933
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[0] ??
1934
+ auto_color_range[0],
1935
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[1] ??
1936
+ auto_color_range[1],
1937
+ ] as Vec2}
2139
1938
  <div
2140
1939
  bind:this={colorbar_element}
2141
- onmouseenter={() => colorbar_hover.set_locked(true)}
2142
- onmouseleave={() => colorbar_hover.set_locked(false)}
1940
+ onmouseenter={() => colorbar_tween.set_locked(true)}
1941
+ onmouseleave={() => colorbar_tween.set_locked(false)}
2143
1942
  class="colorbar-wrapper"
2144
1943
  role="img"
2145
1944
  aria-label="Color scale legend"
2146
1945
  style={`${
2147
- // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
2148
- effective_cbar_wrapper_style ??
2149
- `position: absolute; left: ${tweened_colorbar_coords.current.x}px; top: ${tweened_colorbar_coords.current.y}px`}; pointer-events: auto;`}
1946
+ // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
1947
+ effective_cbar_wrapper_style ??
1948
+ `position: absolute; left: ${colorbar_tween.coords.current.x}px; top: ${colorbar_tween.coords.current.y}px`
1949
+ }; pointer-events: auto;`}
2150
1950
  >
2151
1951
  <ColorBar
2152
1952
  tick_labels={4}
@@ -2155,7 +1955,8 @@
2155
1955
  color_scale_domain={color_domain}
2156
1956
  scale_type={typeof color_scale === `string` ? undefined : color_scale.type}
2157
1957
  range={color_domain?.every((val) => val != null) ? color_domain : undefined}
2158
- bar_style="width: 220px; height: 16px; {color_bar?.style ?? ``}"
1958
+ bar_style="width: {COLOR_BAR_DEFAULTS.width}px; height: {COLOR_BAR_DEFAULTS.horizontal_bar_height}px; {color_bar?.style ??
1959
+ ``}"
2159
1960
  {...color_bar}
2160
1961
  wrapper_style={effective_cbar_wrapper_style ? `height: 100%; width: 100%;` : ``}
2161
1962
  />
@@ -2164,31 +1965,32 @@
2164
1965
 
2165
1966
  <!-- Legend -->
2166
1967
  <!-- Only render if multiple series or if legend prop was explicitly provided by user (even if empty object) -->
2167
- {#if legend != null && legend_data.length > 0 &&
2168
- (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
1968
+ {#if legend != null && legend_data.length > 0 && (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
2169
1969
  {@const default_x = pad.l + 10}
2170
1970
  {@const default_y = pad.t + 10}
2171
- {@const current_x = legend_is_dragging && legend_manual_position
2172
- ? legend_manual_position.x
2173
- : legend_auto_outside
2174
- ? legend_outside_x
2175
- : legend_placement
2176
- ? tweened_legend_coords.current.x
2177
- : default_x}
2178
- {@const current_y = legend_is_dragging && legend_manual_position
2179
- ? legend_manual_position.y
2180
- : legend_auto_outside
2181
- ? legend_outside_y
2182
- : legend_placement
2183
- ? tweened_legend_coords.current.y
2184
- : default_y}
1971
+ {@const current_x =
1972
+ legend_is_dragging && legend_manual_position
1973
+ ? legend_manual_position.x
1974
+ : legend_auto_outside
1975
+ ? legend_outside_x
1976
+ : legend_placement
1977
+ ? legend_tween.coords.current.x
1978
+ : default_x}
1979
+ {@const current_y =
1980
+ legend_is_dragging && legend_manual_position
1981
+ ? legend_manual_position.y
1982
+ : legend_auto_outside
1983
+ ? legend_outside_y
1984
+ : legend_placement
1985
+ ? legend_tween.coords.current.y
1986
+ : default_y}
2185
1987
  <PlotLegend
2186
1988
  bind:root_element={legend_element}
2187
1989
  series_data={legend_data}
2188
1990
  on_drag_start={handle_legend_drag_start}
2189
1991
  on_drag={handle_legend_drag}
2190
1992
  on_drag_end={() => (legend_is_dragging = false)}
2191
- on_hover_change={legend_hover.set_locked}
1993
+ on_hover_change={legend_tween.set_locked}
2192
1994
  on_item_hover={(item) => {
2193
1995
  if (item?.item_type === `fill`) {
2194
1996
  // highlight the matching fill in the plot (same state plot fill-hover uses), but skip
@@ -2197,15 +1999,14 @@
2197
1999
  hovered_fill_key = fill && fill.visible !== false ? fill.hover_key : null
2198
2000
  hovered_legend_series_idx = null
2199
2001
  } else {
2200
- hovered_legend_series_idx = item != null && item.series_idx >= 0
2201
- ? item.series_idx
2202
- : null
2002
+ hovered_legend_series_idx =
2003
+ item != null && item.series_idx >= 0 ? item.series_idx : null
2203
2004
  hovered_fill_key = null
2204
2005
  }
2205
2006
  }}
2206
2007
  active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
2207
- active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)?.idx ??
2208
- null}
2008
+ active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)
2009
+ ?.idx ?? null}
2209
2010
  draggable={legend?.draggable ?? true}
2210
2011
  {...legend}
2211
2012
  on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
@@ -2217,7 +2018,7 @@
2217
2018
  fill_regions = fill_regions.map((region, idx) =>
2218
2019
  idx === source_idx
2219
2020
  ? { ...region, visible: !(region.visible !== false) }
2220
- : region
2021
+ : region,
2221
2022
  )
2222
2023
  }
2223
2024
  }}
@@ -2228,7 +2029,9 @@
2228
2029
  // Only fill_regions can be toggled (error_bands are not bindable)
2229
2030
  if (source_type !== `fill_region`) return
2230
2031
  // Toggle: if only this fill is visible, show all; otherwise show only this one
2231
- const visible_count = fill_regions.filter((region) => region.visible !== false).length
2032
+ const visible_count = fill_regions.filter(
2033
+ (region) => region.visible !== false,
2034
+ ).length
2232
2035
  const this_visible = fill_regions[source_idx]?.visible !== false
2233
2036
  if (visible_count === 1 && this_visible) {
2234
2037
  // Show all fills
@@ -2314,7 +2117,9 @@
2314
2117
  div.scatter :global(.pane-toggle),
2315
2118
  div.scatter .header-controls {
2316
2119
  opacity: 0;
2317
- transition: opacity 0.2s, background-color 0.2s;
2120
+ transition:
2121
+ opacity 0.2s,
2122
+ background-color 0.2s;
2318
2123
  }
2319
2124
  div.scatter:hover :global(.pane-toggle),
2320
2125
  div.scatter:hover .header-controls,
@@ -2341,10 +2146,7 @@
2341
2146
  color: var(--text-color);
2342
2147
  white-space: nowrap;
2343
2148
  /* Use line-height to center text vertically without flexbox */
2344
- line-height: var(
2345
- --scatter-axis-label-line-height,
2346
- 20px
2347
- ); /* Match foreignObject height */
2149
+ line-height: var(--scatter-axis-label-line-height, 20px); /* Match foreignObject height */
2348
2150
  display: block;
2349
2151
  }
2350
2152
  .current-frame-indicator {