matterviz 0.3.7 → 0.4.1

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 (486) hide show
  1. package/dist/Icon.svelte +7 -4
  2. package/dist/MillerIndexInput.svelte +1 -1
  3. package/dist/api/optimade.js +32 -26
  4. package/dist/app.css +0 -3
  5. package/dist/brillouin/BrillouinZone.svelte +76 -148
  6. package/dist/brillouin/BrillouinZone.svelte.d.ts +6 -14
  7. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  10. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  11. package/dist/brillouin/BrillouinZoneScene.svelte +97 -205
  12. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +4 -23
  13. package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
  14. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  15. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  16. package/dist/brillouin/compute.d.ts +2 -0
  17. package/dist/brillouin/compute.js +89 -90
  18. package/dist/brillouin/geometry.d.ts +8 -0
  19. package/dist/brillouin/geometry.js +57 -0
  20. package/dist/brillouin/index.d.ts +2 -0
  21. package/dist/brillouin/index.js +2 -0
  22. package/dist/brillouin/types.d.ts +2 -2
  23. package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  25. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +109 -203
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  27. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +180 -470
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  29. package/dist/chempot-diagram/async-compute.svelte.js +3 -1
  30. package/dist/chempot-diagram/chempot-worker.js +2 -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 +4 -4
  34. package/dist/chempot-diagram/compute.js +20 -20
  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.js +8 -7
  45. package/dist/composition/FormulaFilter.svelte +18 -11
  46. package/dist/composition/PieChart.svelte +11 -10
  47. package/dist/composition/chem-sys.d.ts +8 -0
  48. package/dist/composition/chem-sys.js +86 -0
  49. package/dist/composition/format.js +7 -4
  50. package/dist/composition/index.d.ts +1 -0
  51. package/dist/composition/index.js +1 -0
  52. package/dist/composition/parse.d.ts +0 -1
  53. package/dist/composition/parse.js +41 -31
  54. package/dist/controls.d.ts +1 -0
  55. package/dist/controls.js +0 -1
  56. package/dist/convex-hull/ConvexHull.svelte +8 -10
  57. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  58. package/dist/convex-hull/ConvexHull2D.svelte +106 -185
  59. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  60. package/dist/convex-hull/ConvexHull3D.svelte +179 -683
  61. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  62. package/dist/convex-hull/ConvexHull4D.svelte +183 -687
  63. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  64. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  65. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  66. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  67. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  68. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  69. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  70. package/dist/convex-hull/ConvexHullStats.svelte +36 -175
  71. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  72. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  73. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  74. package/dist/convex-hull/GasPressureControls.svelte +4 -4
  75. package/dist/convex-hull/TemperatureSlider.svelte +2 -2
  76. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  77. package/dist/convex-hull/barycentric-coords.js +6 -33
  78. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  79. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  80. package/dist/convex-hull/demo-temperature.d.ts +1 -1
  81. package/dist/convex-hull/demo-temperature.js +20 -22
  82. package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
  83. package/dist/convex-hull/gas-thermodynamics.js +22 -30
  84. package/dist/convex-hull/helpers.d.ts +42 -7
  85. package/dist/convex-hull/helpers.js +171 -78
  86. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  87. package/dist/convex-hull/hull-state.svelte.js +124 -0
  88. package/dist/convex-hull/index.d.ts +10 -8
  89. package/dist/convex-hull/index.js +7 -2
  90. package/dist/convex-hull/thermodynamics.js +136 -960
  91. package/dist/convex-hull/types.d.ts +13 -5
  92. package/dist/convex-hull/types.js +12 -0
  93. package/dist/coordination/CoordinationBarPlot.svelte +27 -34
  94. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  95. package/dist/element/BohrAtom.svelte +2 -1
  96. package/dist/element/index.d.ts +4 -0
  97. package/dist/element/index.js +18 -0
  98. package/dist/feedback/DragOverlay.svelte +3 -1
  99. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  100. package/dist/feedback/StatusMessage.svelte +13 -3
  101. package/dist/fermi-surface/FermiSlice.svelte +13 -5
  102. package/dist/fermi-surface/FermiSurface.svelte +78 -151
  103. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  104. package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
  105. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  106. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -221
  107. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  108. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
  109. package/dist/fermi-surface/compute.js +67 -66
  110. package/dist/fermi-surface/export.js +6 -16
  111. package/dist/fermi-surface/index.d.ts +0 -1
  112. package/dist/fermi-surface/index.js +0 -1
  113. package/dist/fermi-surface/parse.d.ts +1 -1
  114. package/dist/fermi-surface/parse.js +71 -79
  115. package/dist/fermi-surface/types.d.ts +3 -2
  116. package/dist/heatmap-matrix/HeatmapMatrix.svelte +69 -52
  117. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  118. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  119. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  120. package/dist/heatmap-matrix/index.d.ts +3 -2
  121. package/dist/heatmap-matrix/index.js +1 -1
  122. package/dist/index.d.ts +1 -0
  123. package/dist/index.js +1 -0
  124. package/dist/io/ExportPane.svelte +166 -0
  125. package/dist/io/ExportPane.svelte.d.ts +17 -0
  126. package/dist/io/decompress.js +5 -4
  127. package/dist/io/export.d.ts +9 -5
  128. package/dist/io/export.js +77 -51
  129. package/dist/io/fetch.d.ts +2 -1
  130. package/dist/io/fetch.js +5 -1
  131. package/dist/io/file-drop.d.ts +8 -1
  132. package/dist/io/file-drop.js +48 -36
  133. package/dist/io/index.d.ts +2 -0
  134. package/dist/io/index.js +10 -0
  135. package/dist/io/types.d.ts +13 -0
  136. package/dist/io/url-drop.js +64 -33
  137. package/dist/isosurface/parse.js +52 -51
  138. package/dist/isosurface/slice.js +5 -4
  139. package/dist/isosurface/types.js +1 -1
  140. package/dist/keyboard.d.ts +3 -0
  141. package/dist/keyboard.js +23 -0
  142. package/dist/labels.d.ts +1 -1
  143. package/dist/labels.js +9 -8
  144. package/dist/layout/FullscreenButton.svelte +33 -0
  145. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  146. package/dist/layout/FullscreenToggle.svelte +8 -14
  147. package/dist/layout/PropertyFilter.svelte +3 -2
  148. package/dist/layout/SettingsSection.svelte +1 -1
  149. package/dist/layout/ViewerChrome.svelte +116 -0
  150. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  151. package/dist/layout/fullscreen.d.ts +4 -0
  152. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  153. package/dist/layout/fullscreen.svelte.js +37 -0
  154. package/dist/layout/index.d.ts +3 -0
  155. package/dist/layout/index.js +3 -0
  156. package/dist/layout/json-tree/JsonNode.svelte +1 -1
  157. package/dist/layout/json-tree/JsonTree.svelte +2 -2
  158. package/dist/layout/json-tree/utils.js +5 -4
  159. package/dist/marching-cubes.js +8 -13
  160. package/dist/math.d.ts +12 -4
  161. package/dist/math.js +42 -30
  162. package/dist/overlays/DraggablePane.svelte +4 -4
  163. package/dist/overlays/index.d.ts +4 -0
  164. package/dist/periodic-table/PeriodicTable.svelte +27 -15
  165. package/dist/periodic-table/PropertySelect.svelte +1 -0
  166. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
  167. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  168. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  169. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  170. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  171. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  172. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  173. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  174. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
  175. package/dist/phase-diagram/build-diagram.js +2 -2
  176. package/dist/phase-diagram/colors.js +1 -1
  177. package/dist/phase-diagram/parse.d.ts +2 -1
  178. package/dist/phase-diagram/parse.js +6 -5
  179. package/dist/phase-diagram/types.d.ts +1 -1
  180. package/dist/phase-diagram/utils.d.ts +3 -3
  181. package/dist/phase-diagram/utils.js +8 -12
  182. package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +246 -841
  183. package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +8 -16
  184. package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
  185. package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
  186. package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +8 -7
  187. package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
  188. package/dist/plot/bar/data.d.ts +40 -0
  189. package/dist/plot/bar/data.js +154 -0
  190. package/dist/plot/bar/geometry.d.ts +39 -0
  191. package/dist/plot/bar/geometry.js +60 -0
  192. package/dist/plot/bar/index.d.ts +3 -0
  193. package/dist/plot/bar/index.js +3 -0
  194. package/dist/plot/box/BoxPlot.svelte +1292 -0
  195. package/dist/plot/box/BoxPlot.svelte.d.ts +95 -0
  196. package/dist/plot/box/BoxPlotControls.svelte +109 -0
  197. package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
  198. package/dist/plot/box/Violin.svelte +14 -0
  199. package/dist/plot/box/Violin.svelte.d.ts +70 -0
  200. package/dist/plot/box/box-plot.d.ts +56 -0
  201. package/dist/plot/box/box-plot.js +129 -0
  202. package/dist/plot/box/index.d.ts +5 -0
  203. package/dist/plot/box/index.js +5 -0
  204. package/dist/plot/box/kde.d.ts +17 -0
  205. package/dist/plot/box/kde.js +160 -0
  206. package/dist/plot/box/quantile.d.ts +3 -0
  207. package/dist/plot/box/quantile.js +53 -0
  208. package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +1 -1
  209. package/dist/plot/{auto-place.js → core/auto-place.js} +6 -3
  210. package/dist/plot/core/axis-utils.d.ts +46 -0
  211. package/dist/plot/core/axis-utils.js +110 -0
  212. package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
  213. package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
  214. package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +41 -38
  215. package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +7 -6
  216. package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
  217. package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
  218. package/dist/plot/core/components/ControlPane.svelte +46 -0
  219. package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
  220. package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
  221. package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
  222. package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
  223. package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
  224. package/dist/plot/{Line.svelte → core/components/Line.svelte} +33 -15
  225. package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +3 -2
  226. package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +9 -6
  227. package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +5 -3
  228. package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
  229. package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
  230. package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
  231. package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
  232. package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
  233. package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
  234. package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
  235. package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
  236. package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
  237. package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +5 -5
  238. package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +5 -5
  239. package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +8 -8
  240. package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +5 -5
  241. package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
  242. package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
  243. package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
  244. package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
  245. package/dist/plot/core/components/index.d.ts +17 -0
  246. package/dist/plot/core/components/index.js +17 -0
  247. package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
  248. package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +21 -23
  249. package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
  250. package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
  251. package/dist/plot/core/fill-utils.d.ts +34 -0
  252. package/dist/plot/core/fill-utils.js +391 -0
  253. package/dist/plot/core/index.d.ts +10 -0
  254. package/dist/plot/core/index.js +11 -0
  255. package/dist/plot/core/interactions.d.ts +39 -0
  256. package/dist/plot/core/interactions.js +209 -0
  257. package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
  258. package/dist/plot/{layout.js → core/layout.js} +16 -8
  259. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  260. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  261. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  262. package/dist/plot/core/placed-tween.svelte.js +68 -0
  263. package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +11 -11
  264. package/dist/plot/{reference-line.js → core/reference-line.js} +29 -42
  265. package/dist/plot/core/scales.d.ts +40 -0
  266. package/dist/plot/{scales.js → core/scales.js} +94 -93
  267. package/dist/plot/core/svg.d.ts +3 -0
  268. package/dist/plot/core/svg.js +41 -0
  269. package/dist/plot/{types.d.ts → core/types.d.ts} +36 -85
  270. package/dist/plot/{types.js → core/types.js} +1 -1
  271. package/dist/plot/{utils → core/utils}/label-placement.d.ts +3 -3
  272. package/dist/plot/{utils → core/utils}/label-placement.js +3 -3
  273. package/dist/plot/core/utils/series-visibility.d.ts +26 -0
  274. package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
  275. package/dist/plot/core/utils.d.ts +12 -0
  276. package/dist/plot/core/utils.js +27 -0
  277. package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +174 -551
  278. package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
  279. package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
  280. package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
  281. package/dist/plot/histogram/index.d.ts +2 -0
  282. package/dist/plot/histogram/index.js +2 -0
  283. package/dist/plot/index.d.ts +8 -41
  284. package/dist/plot/index.js +10 -39
  285. package/dist/plot/sankey/Sankey.svelte +697 -0
  286. package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
  287. package/dist/plot/sankey/SankeyControls.svelte +98 -0
  288. package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
  289. package/dist/plot/sankey/index.d.ts +4 -0
  290. package/dist/plot/sankey/index.js +3 -0
  291. package/dist/plot/sankey/sankey-types.d.ts +42 -0
  292. package/dist/plot/sankey/sankey-types.js +4 -0
  293. package/dist/plot/sankey/sankey.d.ts +52 -0
  294. package/dist/plot/sankey/sankey.js +189 -0
  295. package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +64 -64
  296. package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +6 -6
  297. package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
  298. package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
  299. package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +297 -1008
  300. package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +10 -18
  301. package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
  302. package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +2 -2
  303. package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
  304. package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
  305. package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
  306. package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
  307. package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +5 -12
  308. package/dist/plot/scatter/index.d.ts +7 -0
  309. package/dist/plot/scatter/index.js +5 -0
  310. package/dist/plot/scatter/scatter-data.d.ts +19 -0
  311. package/dist/plot/scatter/scatter-data.js +212 -0
  312. package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +25 -34
  313. package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +9 -17
  314. package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +14 -14
  315. package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +6 -6
  316. package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +129 -128
  317. package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +6 -15
  318. package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +7 -6
  319. package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +5 -4
  320. package/dist/plot/scatter-3d/index.d.ts +4 -0
  321. package/dist/plot/scatter-3d/index.js +4 -0
  322. package/dist/plot/sunburst/Sunburst.svelte +1041 -0
  323. package/dist/plot/sunburst/Sunburst.svelte.d.ts +97 -0
  324. package/dist/plot/sunburst/SunburstControls.svelte +200 -0
  325. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
  326. package/dist/plot/sunburst/index.d.ts +4 -0
  327. package/dist/plot/sunburst/index.js +4 -0
  328. package/dist/plot/sunburst/render.d.ts +34 -0
  329. package/dist/plot/sunburst/render.js +122 -0
  330. package/dist/plot/sunburst/sunburst.d.ts +62 -0
  331. package/dist/plot/sunburst/sunburst.js +269 -0
  332. package/dist/rdf/RdfPlot.svelte +2 -1
  333. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  334. package/dist/rdf/calc-rdf.js +11 -24
  335. package/dist/sanitize.js +14 -3
  336. package/dist/scene/SceneCamera.svelte +62 -0
  337. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  338. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  339. package/dist/scene/bind-renderer.svelte.js +14 -0
  340. package/dist/scene/index.d.ts +4 -0
  341. package/dist/scene/index.js +5 -0
  342. package/dist/scene/props.js +52 -0
  343. package/dist/scene/types.d.ts +26 -0
  344. package/dist/scene/types.js +1 -0
  345. package/dist/settings.d.ts +79 -3
  346. package/dist/settings.js +321 -1
  347. package/dist/spectral/Bands.svelte +47 -36
  348. package/dist/spectral/Bands.svelte.d.ts +6 -6
  349. package/dist/spectral/BandsAndDos.svelte +23 -25
  350. package/dist/spectral/BrillouinBandsDos.svelte +42 -30
  351. package/dist/spectral/Dos.svelte +15 -23
  352. package/dist/spectral/Dos.svelte.d.ts +4 -3
  353. package/dist/spectral/helpers.d.ts +8 -6
  354. package/dist/spectral/helpers.js +137 -65
  355. package/dist/state.svelte.d.ts +0 -7
  356. package/dist/state.svelte.js +0 -6
  357. package/dist/structure/Arrow.svelte +2 -4
  358. package/dist/structure/AtomLegend.svelte +8 -9
  359. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  360. package/dist/structure/CanvasTooltip.svelte +1 -0
  361. package/dist/structure/CellSelect.svelte +12 -5
  362. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  363. package/dist/structure/Cylinder.svelte +12 -8
  364. package/dist/structure/Cylinder.svelte.d.ts +4 -1
  365. package/dist/structure/Lattice.svelte +2 -2
  366. package/dist/structure/Structure.svelte +365 -423
  367. package/dist/structure/Structure.svelte.d.ts +5 -15
  368. package/dist/structure/StructureControls.svelte +217 -2
  369. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  370. package/dist/structure/StructureExportPane.svelte +54 -156
  371. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  372. package/dist/structure/StructureInfoPane.svelte +10 -9
  373. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  374. package/dist/structure/StructureScene.svelte +376 -208
  375. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  376. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  377. package/dist/structure/{label-placement.js → atom-label-placement.js} +15 -5
  378. package/dist/structure/atom-properties.d.ts +1 -1
  379. package/dist/structure/atom-properties.js +17 -22
  380. package/dist/structure/bond-order-perception.js +3 -5
  381. package/dist/structure/bonding.d.ts +4 -0
  382. package/dist/structure/bonding.js +134 -63
  383. package/dist/structure/export.d.ts +24 -4
  384. package/dist/structure/export.js +89 -143
  385. package/dist/structure/index.d.ts +4 -4
  386. package/dist/structure/index.js +3 -3
  387. package/dist/structure/measure.d.ts +3 -2
  388. package/dist/structure/measure.js +6 -5
  389. package/dist/structure/parse.d.ts +3 -2
  390. package/dist/structure/parse.js +419 -438
  391. package/dist/structure/partial-occupancy.d.ts +0 -1
  392. package/dist/structure/partial-occupancy.js +1 -1
  393. package/dist/structure/pbc.d.ts +1 -1
  394. package/dist/structure/pbc.js +190 -13
  395. package/dist/structure/polyhedra.d.ts +41 -0
  396. package/dist/structure/polyhedra.js +602 -0
  397. package/dist/structure/site.d.ts +4 -0
  398. package/dist/structure/site.js +1 -0
  399. package/dist/structure/supercell.js +3 -2
  400. package/dist/structure/validation.js +5 -6
  401. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  402. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  403. package/dist/symmetry/SymmetryElements.svelte +354 -0
  404. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  405. package/dist/symmetry/SymmetryStats.svelte +113 -8
  406. package/dist/symmetry/WyckoffTable.svelte +68 -7
  407. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  408. package/dist/symmetry/cell-transform.js +7 -14
  409. package/dist/symmetry/index.d.ts +14 -4
  410. package/dist/symmetry/index.js +291 -72
  411. package/dist/symmetry/spacegroups.d.ts +12 -1
  412. package/dist/symmetry/spacegroups.js +63 -14
  413. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  414. package/dist/symmetry/symmetry-elements.js +521 -0
  415. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  416. package/dist/symmetry/wyckoff-db.js +87 -0
  417. package/dist/table/HeatmapTable.svelte +66 -25
  418. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  419. package/dist/table/index.d.ts +1 -3
  420. package/dist/table/index.js +1 -1
  421. package/dist/theme/index.js +8 -8
  422. package/dist/tooltip/KCoords.svelte +45 -0
  423. package/dist/tooltip/KCoords.svelte.d.ts +8 -0
  424. package/dist/tooltip/index.d.ts +1 -0
  425. package/dist/tooltip/index.js +1 -0
  426. package/dist/trajectory/Trajectory.svelte +123 -100
  427. package/dist/trajectory/Trajectory.svelte.d.ts +11 -22
  428. package/dist/trajectory/TrajectoryExportPane.svelte +17 -25
  429. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  430. package/dist/trajectory/TrajectoryInfoPane.svelte +5 -3
  431. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  432. package/dist/trajectory/constants.js +6 -2
  433. package/dist/trajectory/extract.js +17 -37
  434. package/dist/trajectory/format-detect.d.ts +1 -1
  435. package/dist/trajectory/format-detect.js +27 -19
  436. package/dist/trajectory/frame-reader.d.ts +0 -1
  437. package/dist/trajectory/frame-reader.js +63 -162
  438. package/dist/trajectory/helpers.d.ts +10 -2
  439. package/dist/trajectory/helpers.js +56 -36
  440. package/dist/trajectory/index.js +1 -1
  441. package/dist/trajectory/parse/ase.d.ts +9 -1
  442. package/dist/trajectory/parse/ase.js +47 -32
  443. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  444. package/dist/trajectory/parse/diagnostics.js +14 -0
  445. package/dist/trajectory/parse/hdf5.js +1 -1
  446. package/dist/trajectory/parse/index.d.ts +1 -1
  447. package/dist/trajectory/parse/index.js +65 -105
  448. package/dist/trajectory/parse/lammps.d.ts +0 -2
  449. package/dist/trajectory/parse/lammps.js +8 -6
  450. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  451. package/dist/trajectory/parse/pymatgen.js +74 -0
  452. package/dist/trajectory/parse/vasp.js +38 -18
  453. package/dist/trajectory/parse/xyz.d.ts +13 -1
  454. package/dist/trajectory/parse/xyz.js +102 -94
  455. package/dist/trajectory/plotting.d.ts +1 -2
  456. package/dist/trajectory/plotting.js +16 -113
  457. package/dist/utils.d.ts +2 -0
  458. package/dist/utils.js +7 -5
  459. package/dist/xrd/XrdPlot.svelte +16 -30
  460. package/dist/xrd/broadening.d.ts +2 -1
  461. package/dist/xrd/calc-xrd.js +18 -20
  462. package/dist/xrd/index.d.ts +2 -2
  463. package/dist/xrd/parse.js +2 -2
  464. package/package.json +43 -26
  465. package/dist/element/data.json +0 -11864
  466. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  467. package/dist/fermi-surface/marching-cubes.js +0 -2
  468. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  469. package/dist/plot/axis-utils.d.ts +0 -19
  470. package/dist/plot/axis-utils.js +0 -78
  471. package/dist/plot/defaults.d.ts +0 -19
  472. package/dist/plot/defaults.js +0 -9
  473. package/dist/plot/fill-utils.d.ts +0 -46
  474. package/dist/plot/fill-utils.js +0 -322
  475. package/dist/plot/hover-lock.svelte.d.ts +0 -14
  476. package/dist/plot/hover-lock.svelte.js +0 -46
  477. package/dist/plot/interactions.d.ts +0 -12
  478. package/dist/plot/interactions.js +0 -101
  479. package/dist/plot/scales.d.ts +0 -48
  480. package/dist/plot/svg.d.ts +0 -1
  481. package/dist/plot/svg.js +0 -11
  482. package/dist/plot/utils/series-visibility.d.ts +0 -15
  483. package/dist/plot/utils.d.ts +0 -1
  484. package/dist/plot/utils.js +0 -14
  485. /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
  486. /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
@@ -2,15 +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 type { D3SymbolName } from '../labels'
7
- import { format_value, symbol_names } from '../labels'
8
- import { sanitize_html } from '../sanitize'
9
- import { FullscreenToggle, set_fullscreen_bg } from '../layout'
10
- import type { Point2D, Vec2 } from '../math'
5
+ import type { D3InterpolateName } from '../../colors'
6
+ import { format_value } from '../../labels'
7
+ import { sanitize_html } from '../../sanitize'
8
+ import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
9
+ import type { Point2D, Vec2 } from '../../math'
11
10
  import type {
12
11
  AxisLoadError,
13
12
  BasePlotProps,
13
+ ColorScaleConfig,
14
14
  ControlsConfig,
15
15
  DataLoaderFn,
16
16
  DataSeries,
@@ -18,7 +18,6 @@
18
18
  FillHandlerEvent,
19
19
  FillRegion,
20
20
  HoverConfig,
21
- InitialRanges,
22
21
  InternalPoint,
23
22
  LabelPlacementConfig,
24
23
  LegendConfig,
@@ -27,12 +26,12 @@
27
26
  Point,
28
27
  RefLine,
29
28
  RefLineEvent,
30
- ScaleType,
31
29
  ScatterHandlerEvent,
32
30
  ScatterHandlerProps,
31
+ SizeScaleConfig,
33
32
  StyleOverrides,
34
33
  UserContentProps,
35
- } from './'
34
+ } from '..'
36
35
  import {
37
36
  ColorBar,
38
37
  compute_element_placement,
@@ -47,86 +46,71 @@
47
46
  ScatterPoint,
48
47
  ZeroLines,
49
48
  ZoomRect,
50
- } from './'
49
+ } from '..'
51
50
  import {
52
51
  build_obstacles_norm,
53
52
  has_explicit_position,
54
53
  measured_footprint,
55
54
  place_decorations,
56
- } from './auto-place'
57
- import type { AxisChangeState } from './axis-utils'
58
- import { create_axis_change_handler } from './axis-utils'
59
- import {
60
- get_series_color,
61
- get_series_symbol,
62
- process_prop,
63
- } from './data-transform'
64
- import { AXIS_DEFAULTS } from './defaults'
65
- import {
66
- create_dimension_tracker,
67
- create_hover_lock,
68
- } from './hover-lock.svelte'
55
+ } 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
+ import { create_placed_tween } from '../core/placed-tween.svelte'
69
60
  import {
70
61
  DEFAULT_MARKERS,
71
62
  get_scale_type_name,
72
63
  is_time_scale,
73
- } from './types'
74
- import { compute_label_positions } from './utils/label-placement'
75
- import type { SeriesVisibilitySnapshot } from './utils/series-visibility'
76
- import {
77
- handle_legend_double_click,
78
- toggle_group_visibility,
79
- toggle_series_visibility,
80
- } from './utils/series-visibility'
81
- import { DEFAULTS } from '../settings'
64
+ } from '../core/types'
65
+ import { compute_label_positions } from '../core/utils/label-placement'
66
+ import { create_legend_visibility } from '../core/utils/series-visibility'
67
+ import { DEFAULTS } from '../../settings'
82
68
  import { extent } from 'd3-array'
83
69
  import { scaleTime } from 'd3-scale'
84
70
  import type { ComponentProps, Snippet } from 'svelte'
85
- import { untrack } from 'svelte'
71
+ import { onDestroy, untrack } from 'svelte'
86
72
  import type { HTMLAttributes } from 'svelte/elements'
87
- import { Tween, type TweenOptions } from 'svelte/motion'
73
+ import type { TweenOptions } from 'svelte/motion'
88
74
  import { SvelteSet } from 'svelte/reactivity'
89
- import type { FillPathPoint } from './fill-utils'
75
+ import type { Pt } from '../core/fill-utils'
90
76
  import {
91
- apply_range_constraints,
92
- apply_where_condition,
93
- clamp_for_log_scale,
77
+ compute_fill_segments,
94
78
  convert_error_band_to_fill_region,
95
79
  generate_fill_path,
96
- is_fill_gradient,
97
- resolve_boundary,
98
- } from './fill-utils'
80
+ } from '../core/fill-utils'
99
81
  import {
100
82
  expand_range_if_needed,
101
83
  get_relative_coords,
84
+ invert_rect_range,
102
85
  normalize_y2_sync,
103
- pan_range,
104
- PINCH_ZOOM_THRESHOLD,
105
- pixels_to_data_delta,
106
86
  sync_y2_range,
107
- } from './interactions'
108
- import type { Rect, Sides } from './layout'
87
+ } from '../core/interactions'
88
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
89
+ import type { Rect, Sides } from '../core/layout'
109
90
  import {
110
91
  calc_auto_padding,
111
- constrain_tooltip_position,
112
92
  filter_padding,
113
93
  LABEL_GAP_DEFAULT,
94
+ y2_axis_label_x,
114
95
  measure_full_footprint,
115
96
  measure_max_tick_width,
116
97
  sample_series_obstacle_points,
117
- } from './layout'
118
- import type { IndexedRefLine } from './reference-line'
119
- import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
98
+ } from '../core/layout'
99
+ import type { IndexedRefLine } from '../core/reference-line'
100
+ import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
120
101
  import {
121
102
  create_color_scale,
122
103
  create_scale,
123
104
  create_size_scale,
124
105
  generate_ticks,
125
106
  get_nice_data_range,
126
- } from './scales'
127
-
128
- const in_range = (val: number | null | undefined, lo: number, hi: number) =>
129
- val != null && !isNaN(val) && val >= Math.min(lo, hi) && val <= Math.max(lo, hi)
107
+ } from '../core/scales'
108
+ import { resolve_line_tween, unique_id } from '../core/utils'
109
+ import {
110
+ build_legend_data,
111
+ filter_series_to_ranges,
112
+ pick_tooltip_bg,
113
+ } from './scatter-data'
130
114
 
131
115
  let {
132
116
  series = $bindable([]),
@@ -201,16 +185,8 @@
201
185
  change?: (
202
186
  data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
203
187
  ) => void
204
- color_scale?: {
205
- type?: ScaleType
206
- scheme?: D3ColorSchemeName | D3InterpolateName
207
- value_range?: [number, number]
208
- } | D3InterpolateName
209
- size_scale?: {
210
- type?: ScaleType
211
- radius_range?: [number, number]
212
- value_range?: [number, number]
213
- }
188
+ color_scale?: ColorScaleConfig | D3InterpolateName
189
+ size_scale?: SizeScaleConfig
214
190
  color_bar?:
215
191
  | (ComponentProps<typeof ColorBar> & {
216
192
  margin?: number | Sides
@@ -285,13 +261,12 @@
285
261
 
286
262
  let [width, height] = $state([0, 0])
287
263
  let svg_element: SVGElement | null = $state(null) // Bind the SVG element
288
- let svg_bounding_box: DOMRect | null = $state(null) // Store SVG bounds during drag
289
264
 
290
265
  // Track which specific control properties user has modified
291
266
  let touched = new SvelteSet<string>()
292
267
 
293
268
  // Unique component ID to avoid clipPath conflicts between multiple instances
294
- let component_id = $state(`scatter-${crypto.randomUUID()}`)
269
+ let component_id = $state(unique_id(`scatter`))
295
270
  let clip_path_id = $derived(`plot-area-clip-${component_id}`)
296
271
 
297
272
  // Assign stable IDs to series for keying
@@ -304,20 +279,15 @@
304
279
  }),
305
280
  )
306
281
 
307
- // State for rectangle zoom selection
308
- let drag_start_coords = $state<Point2D | null>(null)
309
- let drag_current_coords = $state<Point2D | null>(null)
310
-
311
282
  // Zoom/pan state - track both initial (data-driven) and current (after pan/zoom) ranges
312
- let initial_x_range = $state<[number, number]>([0, 1])
313
- let initial_x2_range = $state<[number, number]>([0, 1])
314
- let initial_y_range = $state<[number, number]>([0, 1])
315
- let initial_y2_range = $state<[number, number]>([0, 1])
316
- let zoom_x_range = $state<[number, number]>([0, 1])
317
- let zoom_x2_range = $state<[number, number]>([0, 1])
318
- let zoom_y_range = $state<[number, number]>([0, 1])
319
- let zoom_y2_range = $state<[number, number]>([0, 1])
320
- let prev_series_visibility: SeriesVisibilitySnapshot | null = $state(null)
283
+ let ranges = $state<{
284
+ initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
285
+ current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
286
+ }>({
287
+ initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
288
+ current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
289
+ })
290
+ const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
321
291
 
322
292
  // Y2 axis sync configuration
323
293
  let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
@@ -327,7 +297,7 @@
327
297
  // Helper to compute synced y2 range or return fallback when sync disabled
328
298
  const get_synced_y2 = (y1_range: Vec2, fallback: Vec2): Vec2 =>
329
299
  y2_sync_config.mode !== `none`
330
- ? sync_y2_range(y1_range, initial_y2_range, y2_sync_config)
300
+ ? sync_y2_range(y1_range, ranges.initial.y2, y2_sync_config)
331
301
  : fallback
332
302
 
333
303
  // Effect to update y2 range when sync mode changes - use $effect.pre to capture
@@ -338,25 +308,15 @@
338
308
  if (mode !== prev_sync_mode) {
339
309
  // When sync mode becomes enabled (or changes), apply sync immediately
340
310
  if (mode !== `none`) {
341
- zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
311
+ ranges.current.y2 = sync_y2_range(ranges.current.y, ranges.initial.y2, y2_sync_config)
342
312
  } else {
343
313
  // When switching to independent mode, reset Y2 to its data range
344
- zoom_y2_range = [...initial_y2_range] as [number, number]
314
+ ranges.current.y2 = [...ranges.initial.y2] as Vec2
345
315
  }
346
316
  prev_sync_mode = mode
347
317
  }
348
318
  })
349
319
 
350
- // Pan state
351
- let is_focused = $state(false)
352
- let shift_held = $state(false)
353
- let pan_drag_state = $state<
354
- InitialRanges & { start: { x: number; y: number } } | null
355
- >(null)
356
- let touch_state = $state<
357
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
358
- >(null)
359
-
360
320
  // Fill region hover state
361
321
  let hovered_fill_key = $state<string | null>(null)
362
322
 
@@ -378,20 +338,6 @@
378
338
  // State for legend/colorbar placement stability
379
339
  let legend_element = $state<HTMLDivElement | undefined>()
380
340
  let colorbar_element = $state<HTMLDivElement | undefined>()
381
- const legend_hover = create_hover_lock()
382
- const colorbar_hover = create_hover_lock()
383
- const dim_tracker = create_dimension_tracker()
384
- let has_initial_legend_placement = $state(false)
385
- let has_initial_colorbar_placement = $state(false)
386
-
387
- // Clear pending hover lock timeouts on unmount
388
- $effect(() => () => {
389
- legend_hover.cleanup()
390
- colorbar_hover.cleanup()
391
- })
392
-
393
- // Tooltip element reference for dynamic sizing
394
- let tooltip_el = $state<HTMLDivElement | undefined>()
395
341
 
396
342
  // Module-level constants to avoid repeated allocations
397
343
  // Create and categorize points in a single pass (instead of 3 separate iterations)
@@ -431,7 +377,7 @@
431
377
  // Update padding when format or ticks change
432
378
  $effect(() => {
433
379
  const new_pad = width && height &&
434
- (y_tick_values.length || y2_tick_values.length || x2_tick_values.length)
380
+ (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
435
381
  ? calc_auto_padding({
436
382
  padding,
437
383
  default_padding,
@@ -607,60 +553,52 @@
607
553
  return { explicit, range }
608
554
  }
609
555
 
610
- const x = get_range(final_x_axis, auto_x_range)
611
- const x2 = get_range(final_x2_axis, auto_x2_range)
612
- const y = get_range(final_y_axis, auto_y_range)
613
- const y2 = get_range(final_y2_axis, auto_y2_range)
614
-
615
- // X axis: explicit → direct, auto → lazy expand
616
- if (x.explicit) {
617
- zoom_x_range = x.range
618
- } else {
619
- const result = expand_range_if_needed(initial_x_range, x.range)
620
- if (result.changed) {
621
- ;[initial_x_range, zoom_x_range] = [result.range, result.range]
622
- }
623
- }
624
-
625
- // X2 axis: explicit → direct, auto → lazy expand
626
- if (x2.explicit) {
627
- zoom_x2_range = x2.range
628
- } else {
629
- const result = expand_range_if_needed(initial_x2_range, x2.range)
630
- if (result.changed) {
631
- ;[initial_x2_range, zoom_x2_range] = [result.range, result.range]
632
- }
556
+ const resolved = {
557
+ x: get_range(final_x_axis, auto_x_range),
558
+ x2: get_range(final_x2_axis, auto_x2_range),
559
+ y: get_range(final_y_axis, auto_y_range),
560
+ y2: get_range(final_y2_axis, auto_y2_range),
633
561
  }
634
562
 
635
- // Y axis: explicit direct, auto lazy expand
636
- if (y.explicit) {
637
- zoom_y_range = y.range
638
- } else {
639
- const result = expand_range_if_needed(initial_y_range, y.range)
640
- if (result.changed) {
641
- ;[initial_y_range, zoom_y_range] = [result.range, result.range]
563
+ // untrack reads of `ranges`: this effect also writes it, and tracked reads of the
564
+ // deep proxy would re-trigger the effect on every current/initial write
565
+ for (const axis of [`x`, `x2`, `y`] as const) {
566
+ const { explicit, range } = resolved[axis]
567
+ if (explicit) {
568
+ ranges.current[axis] = range
569
+ } else {
570
+ const result = expand_range_if_needed(untrack(() => ranges.initial[axis]), range)
571
+ if (result.changed) {
572
+ ranges.initial[axis] = result.range
573
+ ranges.current[axis] = result.range
574
+ }
642
575
  }
643
576
  }
644
577
 
645
578
  // Y2 axis: explicit → direct, else expand initial range then optionally sync
646
- if (y2.explicit) {
647
- zoom_y2_range = y2.range
579
+ if (resolved.y2.explicit) {
580
+ ranges.current.y2 = resolved.y2.range
648
581
  } else {
649
- const result = expand_range_if_needed(initial_y2_range, y2.range)
650
- if (result.changed) initial_y2_range = result.range
582
+ const result = expand_range_if_needed(untrack(() => ranges.initial.y2), resolved.y2.range)
583
+ if (result.changed) ranges.initial.y2 = result.range
651
584
  // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
652
585
  if (y2_sync_config.mode !== `none`) {
653
- zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
586
+ // Pan/zoom handlers sync y2 themselves.
587
+ ranges.current.y2 = sync_y2_range(
588
+ untrack(() => ranges.current.y),
589
+ untrack(() => ranges.initial.y2),
590
+ y2_sync_config,
591
+ )
654
592
  } else if (result.changed) {
655
- zoom_y2_range = result.range
593
+ ranges.current.y2 = result.range
656
594
  }
657
595
  }
658
596
  })
659
597
 
660
- let [x_min, x_max] = $derived(zoom_x_range)
661
- let [x2_min, x2_max] = $derived(zoom_x2_range)
662
- let [y_min, y_max] = $derived(zoom_y_range)
663
- let [y2_min, y2_max] = $derived(zoom_y2_range)
598
+ let [x_min, x_max] = $derived(ranges.current.x)
599
+ let [x2_min, x2_max] = $derived(ranges.current.x2)
600
+ let [y_min, y_max] = $derived(ranges.current.y)
601
+ let [y2_min, y2_max] = $derived(ranges.current.y2)
664
602
 
665
603
  // Create auto color range
666
604
  let auto_color_range = $derived(
@@ -723,80 +661,29 @@
723
661
 
724
662
  // Filter series data to only include points within bounds and augment with internal data
725
663
  let filtered_series = $derived(
726
- series_with_ids
727
- .map((data_series: DataSeries<Metadata>, series_idx): DataSeries<Metadata> => {
728
- // Handle null/undefined series first
729
- if (!data_series) {
730
- return {
731
- x: [],
732
- y: [],
733
- visible: true,
734
- filtered_data: [],
735
- _id: series_idx,
736
- orig_series_idx: series_idx,
737
- }
738
- }
739
-
740
- // Handle explicitly hidden series
741
- if (!(data_series.visible ?? true)) {
742
- return {
743
- ...data_series,
744
- visible: false,
745
- filtered_data: [],
746
- orig_series_idx: series_idx,
747
- }
748
- }
749
-
750
- const { x: xs, y: ys, color_values, size_values, ...series_rest } = data_series
751
-
752
- // Process points internally, adding properties beyond the base Point type
753
- const processed_points: InternalPoint<Metadata>[] = xs.map(
754
- (x_val: number, point_idx: number) => ({
755
- x: x_val,
756
- y: ys[point_idx],
757
- color_value: color_values?.[point_idx],
758
- metadata: process_prop(series_rest.metadata, point_idx) as Metadata | undefined,
759
- point_style: process_prop(series_rest.point_style, point_idx),
760
- point_hover: process_prop(series_rest.point_hover, point_idx),
761
- point_label: process_prop(series_rest.point_label, point_idx),
762
- point_offset: process_prop(series_rest.point_offset, point_idx),
763
- series_idx,
764
- point_idx,
765
- size_value: size_values?.[point_idx],
766
- }),
767
- )
768
-
769
- // Filter to points within the plot bounds (handles inverted ranges like [3.5, 1.4])
770
- // Determine which ranges to use based on series axis properties
771
- const [series_x_min, series_x_max] = (data_series.x_axis ?? `x1`) === `x2`
772
- ? [x2_min, x2_max]
773
- : [x_min, x_max]
774
- const [series_y_min, series_y_max] = (data_series.y_axis ?? `y1`) === `y2`
775
- ? [y2_min, y2_max]
776
- : [y_min, y_max]
777
-
778
- const filtered_data_with_extras = processed_points.filter(
779
- ({ x, y }) =>
780
- in_range(x, series_x_min, series_x_max) &&
781
- in_range(y, series_y_min, series_y_max),
782
- )
783
-
784
- // Return structure consistent with DataSeries but acknowledge internal data structure (filtered_data)
785
- return {
786
- ...data_series,
787
- visible: true, // Mark series as visible here
788
- filtered_data: filtered_data_with_extras,
789
- orig_series_idx: series_idx, // Store original index for auto-cycling colors/symbols
790
- }
791
- })
792
- // Filter series end up completely empty after point filtering
793
- .filter((
794
- srs,
795
- ): srs is DataSeries<Metadata> & { filtered_data: InternalPoint<Metadata>[] } =>
796
- !!srs.filtered_data && srs.filtered_data.length > 0
797
- ),
664
+ filter_series_to_ranges(series_with_ids, {
665
+ x: [x_min, x_max],
666
+ x2: [x2_min, x2_max],
667
+ y: [y_min, y_max],
668
+ y2: [y2_min, y2_max],
669
+ }),
798
670
  )
799
671
 
672
+ // Tally line series/points to budget path-morph tweens (see resolve_line_tween).
673
+ // Disabling the morph for high-cardinality plots (e.g. phonon bands) keeps them
674
+ // snappy; Line.svelte short-circuits the Tween when duration <= 0.
675
+ let line_tween_load = $derived.by(() => {
676
+ if (!styles.show_lines) return { series: 0, points: 0 }
677
+ let [n_series, n_points] = [0, 0]
678
+ for (const srs of filtered_series ?? []) {
679
+ if (!(srs.markers ?? DEFAULT_MARKERS).includes(`line`)) continue
680
+ n_series += 1
681
+ n_points += srs.x.length
682
+ }
683
+ return { series: n_series, points: n_points }
684
+ })
685
+ let effective_line_tween = $derived(resolve_line_tween(line_tween, line_tween_load))
686
+
800
687
  // Obstacle field for legend/colorbar auto-placement. Sampling only data points lets the
801
688
  // legend land on top of a steep connecting line whose markers are sparse (e.g. y=x^2), so
802
689
  // sample_series_obstacle_points also walks each drawn segment at a fixed pixel cadence.
@@ -824,14 +711,6 @@
824
711
  return points
825
712
  })
826
713
 
827
- // Explicitly define the type for display_style matching PlotLegend expectations
828
- type LegendDisplayStyle = {
829
- symbol_type?: D3SymbolName
830
- symbol_color?: string
831
- line_color?: string
832
- line_dash?: string
833
- }
834
-
835
714
  const fill_hover_key = (
836
715
  source_type: `fill_region` | `error_band`,
837
716
  source_idx: number,
@@ -894,19 +773,22 @@
894
773
  })),
895
774
  ]
896
775
 
897
- // Compute unique x-values once for all fills
898
- // Optimization: deduplicate first (O(n)), then sort only unique values (O(k log k))
899
- // This is faster for datasets with many duplicate x-values across series
900
- const x_set = new SvelteSet<number>()
901
- for (const data_series of series_with_ids) {
902
- if (!data_series) continue
903
- for (const val of data_series.x) {
904
- if (typeof val === `number` && isFinite(val)) x_set.add(val)
905
- }
906
- }
907
- const unique_x = [...x_set].sort((val_a, val_b) => val_a - val_b)
776
+ // On log axes, clamp non-positive coords to the scale's domain floor (x_min/y_min) before
777
+ // scaling. A fixed tiny epsilon can sit far below the domain and map to extreme pixel coords.
778
+ const x_scale_type = final_x_axis.scale_type ?? `linear`
779
+ const y_scale_type = final_y_axis.scale_type ?? `linear`
780
+ const to_px = (pt: Pt): Pt => ({
781
+ x: x_scale_fn(x_scale_type === `log` && pt.x <= 0 ? x_min : pt.x),
782
+ y: y_scale_fn(y_scale_type === `log` && pt.y <= 0 ? y_min : pt.y),
783
+ })
908
784
 
909
- if (unique_x.length === 0) return []
785
+ // Each boundary is traced through its own points with the same curve the series line uses,
786
+ // so fill edges coincide exactly with the lines they border (x_domain anchors flat boundaries).
787
+ const domains = {
788
+ x_domain: [x_min, x_max] as Vec2,
789
+ y_domain: [y_min, y_max] as Vec2,
790
+ y2_domain: [y2_min, y2_max] as Vec2,
791
+ }
910
792
 
911
793
  return all_regions
912
794
  .filter((
@@ -918,71 +800,24 @@
918
800
  hover_key: string
919
801
  } => entry.region !== null)
920
802
  .map(({ region, source_type, source_idx, hover_key }, idx) => {
921
- if (region.visible === false) return null
922
-
923
- // Domain context for boundary resolution
924
- const domains = {
925
- y_domain: [y_min, y_max] as Vec2,
926
- y2_domain: [y2_min, y2_max] as Vec2,
927
- }
928
-
929
- // Resolve upper and lower boundaries
930
- const upper_values = resolve_boundary(
931
- region.upper,
932
- series_with_ids,
933
- unique_x,
934
- domains,
935
- )
936
- const lower_values = resolve_boundary(
937
- region.lower,
938
- series_with_ids,
939
- unique_x,
940
- domains,
941
- )
942
-
943
- if (!upper_values || !lower_values) return null
944
-
945
- // Apply range constraints
946
- const range_filtered = apply_range_constraints(
947
- unique_x,
948
- lower_values,
949
- upper_values,
950
- region,
951
- )
952
-
953
- // Clamp for log scale if needed
954
- const y_scale_type = final_y_axis.scale_type ?? `linear`
955
- const x_scale_type = final_x_axis.scale_type ?? `linear`
956
- const clamped = clamp_for_log_scale(
957
- range_filtered.x,
958
- range_filtered.y1,
959
- range_filtered.y2,
960
- y_scale_type,
961
- x_scale_type,
962
- )
963
-
964
- // Apply where condition (splits into segments)
965
- const conditioned = apply_where_condition(
966
- clamped.x,
967
- clamped.y1,
968
- clamped.y2,
969
- region,
970
- )
971
-
972
- // Generate paths for each segment (convert to pixel coordinates)
973
- const path_segments = conditioned.segments
974
- .filter((segment) => segment.length > 1)
975
- .map((segment) => {
976
- const pixel_data: FillPathPoint[] = segment.map((point) => ({
977
- x: x_scale_fn(point.x),
978
- y1: y_scale_fn(point.y1),
979
- y2: y_scale_fn(point.y2),
980
- }))
981
- return generate_fill_path(pixel_data, region.curve ?? `monotoneX`)
982
- })
983
- .filter((path) => path.length > 0)
803
+ // Hidden fills keep their entry (with empty path_segments -> nothing renders) so the
804
+ // legend item persists greyed-out and can be toggled back on.
805
+ const hidden = region.visible === false
806
+ const path_segments = hidden
807
+ ? []
808
+ : compute_fill_segments(region, series_with_ids, domains)
809
+ .map((seg) =>
810
+ generate_fill_path(
811
+ seg.upper.map(to_px),
812
+ seg.lower.map(to_px),
813
+ seg.upper_curve,
814
+ seg.lower_curve,
815
+ )
816
+ )
817
+ .filter((path) => path.length > 0)
984
818
 
985
- if (path_segments.length === 0) return null
819
+ // Drop only visible fills with no geometry; keep hidden ones for the legend
820
+ if (!hidden && path_segments.length === 0) return null
986
821
 
987
822
  return { ...region, idx, source_type, source_idx, hover_key, path_segments }
988
823
  })
@@ -990,161 +825,9 @@
990
825
  })
991
826
 
992
827
  // Prepare data needed for the legend component
993
- let legend_data = $derived.by(() => {
994
- const items = series_with_ids.map(
995
- (data_series: DataSeries & { _id?: string | number }, series_idx: number) => {
996
- const is_visible = data_series?.visible ?? true
997
- // Prefer top-level label, fallback to metadata label
998
- const explicit_label = data_series?.label ??
999
- (typeof data_series?.metadata === `object` &&
1000
- data_series.metadata !== null &&
1001
- `label` in data_series.metadata &&
1002
- typeof data_series.metadata.label === `string`
1003
- ? data_series.metadata.label
1004
- : null)
1005
- // Use explicit label or generate default
1006
- const label = explicit_label ?? `Series ${series_idx + 1}`
1007
- const has_explicit_label = explicit_label != null
1008
-
1009
- // Use series-specific defaults for auto-differentiation
1010
- const series_default_color = get_series_color(series_idx)
1011
- const series_default_symbol = get_series_symbol(series_idx)
1012
-
1013
- const display_style: LegendDisplayStyle = {
1014
- symbol_type: series_default_symbol,
1015
- symbol_color: series_default_color,
1016
- line_color: series_default_color,
1017
- }
1018
- const series_markers = data_series?.markers ?? DEFAULT_MARKERS
1019
-
1020
- // Check point_style (could be object or array)
1021
- const first_point_style = Array.isArray(data_series?.point_style)
1022
- ? data_series.point_style[0]
1023
- : data_series?.point_style
1024
-
1025
- if (series_markers?.includes(`points`)) {
1026
- if (first_point_style) {
1027
- // Use explicit symbol_type if provided and valid, otherwise keep series default
1028
- if (
1029
- typeof first_point_style.symbol_type === `string` &&
1030
- symbol_names.includes(first_point_style.symbol_type as D3SymbolName)
1031
- ) {
1032
- display_style.symbol_type = first_point_style
1033
- .symbol_type as D3SymbolName
1034
- }
1035
-
1036
- // Use explicit fill color if provided
1037
- if (first_point_style.fill) {
1038
- display_style.symbol_color = first_point_style.fill
1039
- }
1040
- if (first_point_style.stroke) {
1041
- // Use stroke color if fill is none or transparent
1042
- if (
1043
- !display_style.symbol_color ||
1044
- display_style.symbol_color === `none` ||
1045
- display_style.symbol_color.startsWith(`rgba(`, 0) // Check if transparent
1046
- ) display_style.symbol_color = first_point_style.stroke
1047
- }
1048
- }
1049
- // else: keep series-specific defaults for symbol_type and symbol_color
1050
- } else {
1051
- // If no points marker, explicitly remove marker style for legend
1052
- display_style.symbol_type = undefined
1053
- display_style.symbol_color = undefined
1054
- }
1055
-
1056
- // Check line_style
1057
- if (series_markers?.includes(`line`)) {
1058
- // Prefer explicit line stroke, then other explicit colors, then series default
1059
- let legend_line_color = data_series?.line_style?.stroke
1060
- if (!legend_line_color) {
1061
- // Try color scale if available
1062
- const first_cv = Array.isArray(data_series?.color_values)
1063
- ? data_series?.color_values?.find((color_val: number | null) =>
1064
- color_val != null
1065
- )
1066
- : undefined
1067
- legend_line_color =
1068
- (first_cv != null ? color_scale_fn(first_cv) : undefined) ||
1069
- first_point_style?.fill ||
1070
- first_point_style?.stroke ||
1071
- series_default_color
1072
- }
1073
- display_style.line_color = legend_line_color
1074
- display_style.line_dash = data_series?.line_style?.line_dash
1075
- } else {
1076
- // If no line marker, explicitly remove line style for legend
1077
- display_style.line_dash = undefined
1078
- display_style.line_color = undefined
1079
- }
1080
-
1081
- return {
1082
- series_idx,
1083
- label,
1084
- visible: is_visible,
1085
- display_style,
1086
- has_explicit_label,
1087
- legend_group: data_series?.legend_group,
1088
- }
1089
- },
1090
- )
1091
-
1092
- // Deduplicate by label+legend_group - keep first occurrence of each unique combination
1093
- const seen_labels = new SvelteSet<string>()
1094
- const series_items = items.filter(
1095
- (
1096
- legend_item: {
1097
- label: string
1098
- series_idx: number
1099
- visible: boolean
1100
- display_style: LegendDisplayStyle
1101
- has_explicit_label: boolean
1102
- legend_group?: string
1103
- },
1104
- ) => {
1105
- // Use label+group as unique key (group may be undefined)
1106
- const unique_key = `${legend_item.legend_group ?? ``}::${legend_item.label}`
1107
- if (seen_labels.has(unique_key)) return false
1108
- seen_labels.add(unique_key)
1109
- return true
1110
- },
1111
- )
1112
-
1113
- // Add fill region items to legend (deduplicated using same key format as series)
1114
- const fill_items = computed_fills
1115
- .filter((fill) => fill.show_in_legend !== false && fill.label)
1116
- .filter((fill) => {
1117
- // Use same composite key as series: legend_group::label
1118
- const unique_key = `${fill.legend_group ?? ``}::${fill.label ?? ``}`
1119
- if (seen_labels.has(unique_key)) return false
1120
- seen_labels.add(unique_key)
1121
- return true
1122
- })
1123
- .map((fill) => {
1124
- // Pass gradient for swatch rendering, or solid color as fallback
1125
- const fill_gradient = is_fill_gradient(fill.fill) ? fill.fill : undefined
1126
- const fill_color = typeof fill.fill === `string` ? fill.fill : undefined
1127
-
1128
- return {
1129
- series_idx: -1, // Not a series
1130
- fill_idx: fill.idx,
1131
- fill_source_type: fill.source_type,
1132
- fill_source_idx: fill.source_idx,
1133
- item_type: `fill` as const,
1134
- label: fill.label ?? ``,
1135
- visible: fill.visible !== false,
1136
- legend_group: fill.legend_group,
1137
- display_style: {
1138
- fill_color,
1139
- fill_opacity: fill.fill_opacity ?? 0.3,
1140
- edge_color: fill.edge_upper?.color,
1141
- fill_gradient,
1142
- },
1143
- }
1144
- })
1145
-
1146
- return [...series_items, ...fill_items]
1147
- })
828
+ let legend_data = $derived(
829
+ build_legend_data(series_with_ids, computed_fills, color_scale_fn),
830
+ )
1148
831
 
1149
832
  // Group fills by z-index for ordered rendering (single pass instead of 4 filters)
1150
833
  let fills_by_z = $derived.by(() => {
@@ -1171,7 +854,7 @@
1171
854
  // Calculate best legend placement using continuous grid sampling
1172
855
  let legend_placement = $derived.by(() => {
1173
856
  const should_place = legend != null &&
1174
- (legend_data.length > 1 || Object.keys(legend).length > 0)
857
+ (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
1175
858
 
1176
859
  if (!should_place || !width || !height) return null
1177
860
 
@@ -1192,7 +875,7 @@
1192
875
 
1193
876
  // Calculate color bar placement (coordinates with legend to avoid overlap)
1194
877
  let color_bar_placement = $derived.by(() => {
1195
- if (!color_bar || !all_color_values.length || !width || !height) return null
878
+ if (!color_bar || all_color_values.length === 0 || !width || !height) return null
1196
879
 
1197
880
  const plot_width = width - pad.l - pad.r
1198
881
  const plot_height = height - pad.t - pad.b
@@ -1242,60 +925,23 @@
1242
925
  return legend_placement
1243
926
  })
1244
927
 
1245
- // Initialize tweened values for color bar position - create once, update target via effect
1246
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
1247
- const tweened_colorbar_coords = new Tween(
1248
- { x: 0, y: 0 },
1249
- untrack(() => ({ duration: 400, ...color_bar?.tween })),
1250
- )
1251
- // Initialize tweened values for legend position - create once, update target via effect
1252
- const tweened_legend_coords = new Tween(
1253
- { x: 0, y: 0 },
1254
- untrack(() => ({ duration: 400, ...legend?.tween })),
1255
- )
1256
-
1257
- // Update placement positions (with animation and stability checks)
1258
- $effect(() => {
1259
- if (!width || !height) return
1260
-
1261
- // Track dimensions for resize detection
1262
- const dims_changed = dim_tracker.has_changed(width, height)
1263
- if (dims_changed) dim_tracker.update(width, height)
1264
-
1265
- // Update colorbar position (stable after initial placement unless responsive)
1266
- if (color_bar_placement) {
1267
- const is_responsive = color_bar?.responsive ?? false
1268
- const should_update = dims_changed || (!colorbar_hover.is_locked.current &&
1269
- (is_responsive || !has_initial_colorbar_placement))
1270
-
1271
- if (should_update) {
1272
- tweened_colorbar_coords.set(
1273
- { x: color_bar_placement.x, y: color_bar_placement.y },
1274
- has_initial_colorbar_placement ? undefined : { duration: 0 },
1275
- )
1276
- if (colorbar_element && !has_initial_colorbar_placement) {
1277
- has_initial_colorbar_placement = true
1278
- }
1279
- }
1280
- }
1281
-
1282
- // Update legend position (stable after initial placement unless responsive)
1283
- if (legend_manual_position && !legend_is_dragging) {
1284
- // Immediate update (no animation) for manually dragged positions
1285
- tweened_legend_coords.set(legend_manual_position, { duration: 0 })
1286
- } else if (active_legend_placement && !legend_is_dragging) {
1287
- const is_responsive = legend?.responsive ?? false
1288
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
1289
- (is_responsive || !has_initial_legend_placement))
1290
-
1291
- if (should_update) {
1292
- tweened_legend_coords.set(
1293
- { x: active_legend_placement.x, y: active_legend_placement.y },
1294
- has_initial_legend_placement ? undefined : { duration: 0 },
1295
- )
1296
- if (legend_element) has_initial_legend_placement = true
1297
- }
1298
- }
928
+ // Tweened colorbar/legend coordinates with shared placement stability gating
929
+ const colorbar_tween = create_placed_tween({
930
+ placement: () => color_bar_placement,
931
+ dims: () => ({ width, height }),
932
+ responsive: () => color_bar?.responsive ?? false,
933
+ element: () => colorbar_element,
934
+ tween: () => color_bar?.tween,
935
+ })
936
+ const legend_tween = create_placed_tween({
937
+ placement: () => active_legend_placement,
938
+ dims: () => ({ width, height }),
939
+ responsive: () => legend?.responsive ?? false,
940
+ element: () => legend_element,
941
+ tween: () => legend?.tween,
942
+ // Leave coords alone mid-drag; once dragged, the manual position wins permanently
943
+ suspended: () => legend_is_dragging,
944
+ manual_position: () => legend_manual_position,
1299
945
  })
1300
946
 
1301
947
  // Generate axis ticks - consolidated into single derived for efficiency
@@ -1361,346 +1007,76 @@
1361
1007
  y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
1362
1008
  })
1363
1009
 
1364
- // Define global handlers reference for adding/removing listeners
1365
- const on_window_mouse_move = (evt: MouseEvent) => {
1366
- if (!drag_start_coords || !svg_bounding_box) return // Exit if not dragging or no bounds
1367
-
1368
- // Calculate mouse position relative to the stored SVG bounding box
1369
- const current_x = evt.clientX - svg_bounding_box.left
1370
- const current_y = evt.clientY - svg_bounding_box.top
1371
- drag_current_coords = { x: current_x, y: current_y }
1372
-
1373
- // Optional: update tooltip only if inside SVG bounds
1374
- const is_inside_svg = current_x >= 0 &&
1375
- current_x <= svg_bounding_box.width &&
1376
- current_y >= 0 &&
1377
- current_y <= svg_bounding_box.height
1378
-
1379
- if (is_inside_svg) {
1380
- // Use the already calculated relative coordinates
1381
- update_tooltip_point(current_x, current_y)
1382
- } else tooltip_point = null // Clear tooltip if outside
1383
- }
1384
-
1385
- const on_window_mouse_up = (_evt: MouseEvent) => {
1386
- if (drag_start_coords && drag_current_coords) {
1387
- // Use current scales to invert screen coords to data coords
1388
- const start_data_x_val = x_scale_fn.invert(drag_start_coords.x)
1389
- const end_data_x_val = x_scale_fn.invert(drag_current_coords.x)
1390
- const start_data_y_val = y_scale_fn.invert(drag_start_coords.y)
1391
- const end_data_y_val = y_scale_fn.invert(drag_current_coords.y)
1392
-
1393
- // Ensure range is not zero and order is correct
1394
- let x1: number, x2: number
1395
- if (start_data_x_val instanceof Date && end_data_x_val instanceof Date) {
1396
- x1 = start_data_x_val.getTime()
1397
- x2 = end_data_x_val.getTime()
1398
- } else if (
1399
- typeof start_data_x_val === `number` &&
1400
- typeof end_data_x_val === `number`
1401
- ) {
1402
- x1 = start_data_x_val
1403
- x2 = end_data_x_val
1404
- } else {
1405
- console.error(`Mismatched types for x-axis zoom calculation`)
1406
- // Reset states without zooming if types are wrong
1407
- drag_start_coords = null
1408
- drag_current_coords = null
1409
- window.removeEventListener(`mousemove`, on_window_mouse_move)
1410
- window.removeEventListener(`mouseup`, on_window_mouse_up)
1411
- return
1412
- }
1413
-
1414
- const next_x_range: [number, number] = [Math.min(x1, x2), Math.max(x1, x2)]
1415
- // Y axis is always number
1416
- const next_y_range: [number, number] = [
1417
- Math.min(start_data_y_val, end_data_y_val),
1418
- Math.max(start_data_y_val, end_data_y_val),
1419
- ]
1420
-
1421
- // Check for minuscule zoom box (e.g. accidental click)
1422
- const min_zoom_size = 5 // Minimum pixels to trigger zoom
1423
- const dx = Math.abs(drag_start_coords.x - drag_current_coords.x)
1424
- const dy = Math.abs(drag_start_coords.y - drag_current_coords.y)
1425
-
1426
- if (
1427
- dx > min_zoom_size &&
1428
- dy > min_zoom_size &&
1429
- next_x_range[0] !== next_x_range[1] &&
1430
- next_y_range[0] !== next_y_range[1]
1431
- ) {
1432
- // Update axis ranges to trigger reactivity (like BarPlot/Histogram do)
1433
- // Y2 sync is handled by the effect that reacts to y_axis changes
1434
- x_axis = { ...x_axis, range: next_x_range }
1435
- y_axis = { ...y_axis, range: next_y_range }
1436
-
1437
- // X2 axis: invert screen coords using x2 scale
1438
- if (x2_points.length > 0) {
1439
- const start_x2_val = x2_scale_fn.invert(drag_start_coords.x)
1440
- const end_x2_val = x2_scale_fn.invert(drag_current_coords.x)
1441
- const x2_a = start_x2_val instanceof Date
1442
- ? start_x2_val.getTime()
1443
- : start_x2_val as number
1444
- const x2_b = end_x2_val instanceof Date
1445
- ? end_x2_val.getTime()
1446
- : end_x2_val as number
1447
- x2_axis = {
1448
- ...x2_axis,
1449
- range: [Math.min(x2_a, x2_b), Math.max(x2_a, x2_b)],
1450
- }
1451
- }
1452
- }
1453
- }
1454
-
1455
- // Reset states and remove listeners
1456
- drag_start_coords = null
1457
- drag_current_coords = null
1458
- svg_bounding_box = null
1459
- window.removeEventListener(`mousemove`, on_window_mouse_move)
1460
- window.removeEventListener(`mouseup`, on_window_mouse_up)
1461
- document.body.style.cursor = `default`
1462
- }
1463
-
1464
- // Pan drag handlers
1465
- const on_pan_move = (evt: MouseEvent) => {
1466
- if (!pan_drag_state) return
1467
- const dx = evt.clientX - pan_drag_state.start.x
1468
- const dy = evt.clientY - pan_drag_state.start.y
1469
-
1470
- // Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
1010
+ // Shared pan/zoom/touch/drag-rect interaction controller. set_range routes y2
1011
+ // writes through get_synced_y2 (write-order contract: y is written before y2, so
1012
+ // the sync reads the just-updated y range).
1013
+ const pan_zoom = create_pan_zoom({
1014
+ ranges: () => ranges.current,
1015
+ scale_type: (axis) =>
1016
+ ({ x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis })[axis]
1017
+ .scale_type,
1471
1018
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1472
- const plot_width = Math.max(1, width - pad.l - pad.r)
1473
- const plot_height = Math.max(1, height - pad.t - pad.b)
1474
- const sensitivity = pan?.drag_sensitivity ?? 1
1475
-
1476
- const x_delta = pixels_to_data_delta(
1477
- -dx * sensitivity,
1478
- pan_drag_state.initial_x_range,
1479
- plot_width,
1480
- )
1481
- const x2_delta = pixels_to_data_delta(
1482
- -dx * sensitivity,
1483
- pan_drag_state.initial_x2_range,
1484
- plot_width,
1485
- )
1486
- const y_delta = pixels_to_data_delta(
1487
- dy * sensitivity,
1488
- pan_drag_state.initial_y_range,
1489
- plot_height,
1490
- )
1491
- const y2_delta = pixels_to_data_delta(
1492
- dy * sensitivity,
1493
- pan_drag_state.initial_y2_range,
1494
- plot_height,
1495
- )
1496
-
1497
- zoom_x_range = pan_range(pan_drag_state.initial_x_range, x_delta)
1498
- zoom_x2_range = pan_range(pan_drag_state.initial_x2_range, x2_delta)
1499
- zoom_y_range = pan_range(pan_drag_state.initial_y_range, y_delta)
1500
- zoom_y2_range = get_synced_y2(
1501
- zoom_y_range,
1502
- pan_range(pan_drag_state.initial_y2_range, y2_delta),
1503
- )
1504
- }
1505
-
1506
- const on_pan_end = () => {
1507
- pan_drag_state = null
1508
- document.body.style.cursor = ``
1509
- window.removeEventListener(`mousemove`, on_pan_move)
1510
- window.removeEventListener(`mouseup`, on_pan_end)
1511
- }
1512
-
1513
- function handle_mouse_down(evt: MouseEvent) {
1514
- if (!svg_element) return
1515
-
1516
- // Check if pan is enabled and shift is held for pan mode
1517
- const pan_enabled = pan?.enabled !== false
1518
- if (pan_enabled && evt.shiftKey) {
1519
- evt.preventDefault()
1520
- pan_drag_state = {
1521
- start: { x: evt.clientX, y: evt.clientY },
1522
- initial_x_range: [...zoom_x_range] as [number, number],
1523
- initial_x2_range: [...zoom_x2_range] as [number, number],
1524
- initial_y_range: [...zoom_y_range] as [number, number],
1525
- initial_y2_range: [...zoom_y2_range] as [number, number],
1019
+ plot_dims: () => ({
1020
+ width: Math.max(1, width - pad.l - pad.r),
1021
+ height: Math.max(1, height - pad.t - pad.b),
1022
+ }),
1023
+ pan: () => pan,
1024
+ set_range: (axis, range) => {
1025
+ if (axis === `y2`) ranges.current.y2 = get_synced_y2(ranges.current.y, range)
1026
+ else ranges.current[axis] = range
1027
+ },
1028
+ svg: () => svg_element,
1029
+ on_rect_zoom: (start, current) => {
1030
+ // Update axis ranges to trigger reactivity; both x and y must invert to valid
1031
+ // (finite, non-degenerate) ranges or the rect zoom is discarded entirely
1032
+ const next_x = invert_rect_range(x_scale_fn, start.x, current.x)
1033
+ const next_y = invert_rect_range(y_scale_fn, start.y, current.y)
1034
+ if (!next_x || !next_y) return
1035
+ x_axis = { ...x_axis, range: next_x }
1036
+ y_axis = { ...y_axis, range: next_y }
1037
+
1038
+ // X2 axis: invert screen coords using x2 scale
1039
+ const next_x2 = x2_points.length > 0
1040
+ ? invert_rect_range(x2_scale_fn, start.x, current.x)
1041
+ : null
1042
+ if (next_x2) x2_axis = { ...x2_axis, range: next_x2 }
1043
+
1044
+ // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1045
+ // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1046
+ const next_y2 = y2_points.length > 0 && y2_sync_config.mode === `none`
1047
+ ? invert_rect_range(y2_scale_fn, start.y, current.y)
1048
+ : null
1049
+ if (next_y2) y2_axis = { ...y2_axis, range: next_y2 }
1050
+ },
1051
+ on_reset: () => {
1052
+ // Reset to current auto ranges (not stale initial ranges which may have expanded)
1053
+ // This ensures lazy expansion restarts fresh from current data bounds
1054
+ ranges.initial = {
1055
+ x: [...auto_x_range] as Vec2,
1056
+ x2: [...auto_x2_range] as Vec2,
1057
+ y: [...auto_y_range] as Vec2,
1058
+ y2: [...auto_y2_range] as Vec2,
1526
1059
  }
1527
- document.body.style.cursor = `grabbing`
1528
- window.addEventListener(`mousemove`, on_pan_move)
1529
- window.addEventListener(`mouseup`, on_pan_end)
1530
- return
1531
- }
1532
-
1533
- // Store bounding box first, then calculate coords using it
1534
- svg_bounding_box = svg_element.getBoundingClientRect()
1535
-
1536
- // Calculate initial coords using the same bounding box that will be used during drag
1537
- const initial_x = evt.clientX - svg_bounding_box.left
1538
- const initial_y = evt.clientY - svg_bounding_box.top
1539
- const coords = { x: initial_x, y: initial_y }
1540
-
1541
- drag_start_coords = coords
1542
- drag_current_coords = coords
1543
-
1544
- window.addEventListener(`mousemove`, on_window_mouse_move)
1545
- window.addEventListener(`mouseup`, on_window_mouse_up)
1546
- document.body.style.cursor = `crosshair`
1547
- evt.preventDefault()
1548
- }
1549
-
1550
- // Wheel handler for pan (requires focus and shift)
1551
- function handle_wheel(evt: WheelEvent) {
1552
- const pan_enabled = pan?.enabled !== false
1553
- // Only capture wheel when focused AND Shift is held
1554
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
1555
- if (!pan_enabled || !is_focused || !shift_held) return
1556
-
1557
- evt.preventDefault()
1558
-
1559
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1560
- const plot_width = Math.max(1, width - pad.l - pad.r)
1561
- const plot_height = Math.max(1, height - pad.t - pad.b)
1562
- const sensitivity = pan?.wheel_sensitivity ?? 1
1563
-
1564
- // Determine pan direction based on wheel delta
1565
- // deltaX for horizontal scroll (trackpad), deltaY for vertical
1566
- const x_delta = pixels_to_data_delta(
1567
- evt.deltaX * sensitivity,
1568
- zoom_x_range,
1569
- plot_width,
1570
- )
1571
- const x2_delta = pixels_to_data_delta(
1572
- evt.deltaX * sensitivity,
1573
- zoom_x2_range,
1574
- plot_width,
1575
- )
1576
- const y_delta = pixels_to_data_delta(
1577
- evt.deltaY * sensitivity,
1578
- zoom_y_range,
1579
- plot_height,
1580
- )
1581
- const y2_delta = pixels_to_data_delta(
1582
- evt.deltaY * sensitivity,
1583
- zoom_y2_range,
1584
- plot_height,
1585
- )
1586
-
1587
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
1588
- zoom_x_range = pan_range(zoom_x_range, x_delta)
1589
- zoom_x2_range = pan_range(zoom_x2_range, x2_delta)
1590
- } else {
1591
- zoom_y_range = pan_range(zoom_y_range, y_delta)
1592
- zoom_y2_range = get_synced_y2(zoom_y_range, pan_range(zoom_y2_range, y2_delta))
1593
- }
1594
- }
1595
-
1596
- // Touch handlers for pinch-zoom and two-finger pan
1597
- function handle_touch_start(evt: TouchEvent) {
1598
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
1599
- if (!touch_enabled || evt.touches.length !== 2) return
1600
-
1601
- evt.preventDefault()
1602
- const touches = Array.from(evt.touches)
1603
- touch_state = {
1604
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
1605
- initial_x_range: [...zoom_x_range] as [number, number],
1606
- initial_x2_range: [...zoom_x2_range] as [number, number],
1607
- initial_y_range: [...zoom_y_range] as [number, number],
1608
- initial_y2_range: [...zoom_y2_range] as [number, number],
1609
- }
1610
- }
1611
-
1612
- function handle_touch_move(evt: TouchEvent) {
1613
- if (!touch_state || evt.touches.length !== 2) return
1614
- evt.preventDefault()
1615
-
1616
- const [t1, t2] = Array.from(evt.touches)
1617
- const [s1, s2] = touch_state.start_touches
1618
-
1619
- // Calculate center movement for pan
1620
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
1621
- const curr_center = {
1622
- x: (t1.clientX + t2.clientX) / 2,
1623
- y: (t1.clientY + t2.clientY) / 2,
1624
- }
1625
- const dx = curr_center.x - start_center.x
1626
- const dy = curr_center.y - start_center.y
1627
-
1628
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
1629
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
1630
- // Guard against zero-distance pinch to avoid Infinity scale
1631
- if (start_dist < Number.EPSILON) return
1632
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
1633
- const scale = curr_dist / start_dist
1634
-
1635
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1636
- const plot_width = Math.max(1, width - pad.l - pad.r)
1637
- const plot_height = Math.max(1, height - pad.t - pad.b)
1638
-
1639
- // If scale changed significantly, treat as pinch-zoom
1640
- // Also guard against scale being too small to avoid division by zero
1641
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
1642
- // Pinch zoom centered on gesture center
1643
- // Divide by scale so spread (scale > 1) = smaller span (zoom in)
1644
- const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
1645
- const x2_span = touch_state.initial_x2_range[1] -
1646
- touch_state.initial_x2_range[0]
1647
- const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
1648
- const y2_span = touch_state.initial_y2_range[1] -
1649
- touch_state.initial_y2_range[0]
1650
- const x_center =
1651
- (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
1652
- const x2_center =
1653
- (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
1654
- const y_center =
1655
- (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
1656
- const y2_center =
1657
- (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
1658
-
1659
- zoom_x_range = [x_center - x_span / scale / 2, x_center + x_span / scale / 2]
1660
- zoom_x2_range = [
1661
- x2_center - x2_span / scale / 2,
1662
- x2_center + x2_span / scale / 2,
1663
- ]
1664
- zoom_y_range = [y_center - y_span / scale / 2, y_center + y_span / scale / 2]
1665
- zoom_y2_range = get_synced_y2(zoom_y_range, [
1666
- y2_center - y2_span / scale / 2,
1667
- y2_center + y2_span / scale / 2,
1668
- ])
1669
- } else {
1670
- // Pan
1671
- const x_delta = pixels_to_data_delta(
1672
- -dx,
1673
- touch_state.initial_x_range,
1674
- plot_width,
1675
- )
1676
- const x2_delta = pixels_to_data_delta(
1677
- -dx,
1678
- touch_state.initial_x2_range,
1679
- plot_width,
1680
- )
1681
- const y_delta = pixels_to_data_delta(
1682
- dy,
1683
- touch_state.initial_y_range,
1684
- plot_height,
1685
- )
1686
- const y2_delta = pixels_to_data_delta(
1687
- dy,
1688
- touch_state.initial_y2_range,
1689
- plot_height,
1690
- )
1691
- zoom_x_range = pan_range(touch_state.initial_x_range, x_delta)
1692
- zoom_x2_range = pan_range(touch_state.initial_x2_range, x2_delta)
1693
- zoom_y_range = pan_range(touch_state.initial_y_range, y_delta)
1694
- zoom_y2_range = get_synced_y2(
1695
- zoom_y_range,
1696
- pan_range(touch_state.initial_y2_range, y2_delta),
1697
- )
1698
- }
1699
- }
1700
-
1701
- function handle_touch_end() {
1702
- touch_state = null
1703
- }
1060
+ ranges.current = {
1061
+ x: [...auto_x_range] as Vec2,
1062
+ x2: [...auto_x2_range] as Vec2,
1063
+ y: [...auto_y_range] as Vec2,
1064
+ y2: get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2),
1065
+ }
1066
+ // Also reset axis props so future data changes recalculate auto ranges
1067
+ x_axis = { ...x_axis, range: [null, null] }
1068
+ x2_axis = { ...x2_axis, range: [null, null] }
1069
+ y_axis = { ...y_axis, range: [null, null] }
1070
+ y2_axis = { ...y2_axis, range: [null, null] }
1071
+ },
1072
+ // Live tooltip while rect-dragging: update for the closest point inside the
1073
+ // plot bounds, clear when the cursor leaves the svg
1074
+ on_drag_move: (coords, inside_svg) => {
1075
+ if (inside_svg) update_tooltip_point(coords.x, coords.y)
1076
+ else tooltip_point = null
1077
+ },
1078
+ })
1079
+ onDestroy(() => pan_zoom.destroy())
1704
1080
 
1705
1081
  // tooltip logic: find closest point and update tooltip state
1706
1082
  function update_tooltip_point(x_rel: number, y_rel: number, evt?: MouseEvent) {
@@ -1832,7 +1208,7 @@
1832
1208
  legend_manual_position = { x: constrained_x, y: constrained_y }
1833
1209
  }
1834
1210
 
1835
- function get_screen_coords(point: Point, data_series?: DataSeries): [number, number] {
1211
+ function get_screen_coords(point: Point, data_series?: DataSeries): Vec2 {
1836
1212
  // convert data coordinates to potentially non-finite screen coordinates
1837
1213
  const use_x2 = data_series?.x_axis === `x2`
1838
1214
  const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
@@ -1906,7 +1282,6 @@
1906
1282
  return construct_handler_props(tooltip_point)
1907
1283
  })
1908
1284
 
1909
- let using_controls = $derived(controls.show)
1910
1285
  let has_multiple_series = $derived(series_with_ids.filter(Boolean).length > 1)
1911
1286
 
1912
1287
  // Precompute non-click event names from point_events so we don't rebuild
@@ -1943,32 +1318,12 @@
1943
1318
  set_loading: (axis) => (axis_loading = axis),
1944
1319
  }
1945
1320
 
1946
- // Create shared handler bound to this component's state
1947
- // Using $derived so handler updates when callback props change
1948
- const handle_axis_change = $derived(create_axis_change_handler(
1321
+ // Shared handler + one-shot auto-load bound to this component's state
1322
+ const { handle_axis_change, try_auto_load } = create_axis_loader(
1949
1323
  axis_state,
1950
- data_loader,
1951
- on_axis_change,
1952
- on_error,
1953
- ))
1954
-
1955
- let auto_load_attempted = false // prevent infinite retries on failure
1956
-
1957
- // Auto-load data if series is empty but options exist (runs once)
1958
- $effect(() => {
1959
- if (series.length === 0 && data_loader && !auto_load_attempted) {
1960
- // Check x-axis first, then y-axis
1961
- if (x_axis.options?.length) {
1962
- auto_load_attempted = true
1963
- const first_key = x_axis.selected_key ?? x_axis.options[0].key
1964
- handle_axis_change(`x`, first_key).catch(() => {})
1965
- } else if (y_axis.options?.length) {
1966
- auto_load_attempted = true
1967
- const first_key = y_axis.selected_key ?? y_axis.options[0].key
1968
- handle_axis_change(`y`, first_key).catch(() => {})
1969
- }
1970
- }
1971
- })
1324
+ () => ({ data_loader, on_axis_change, on_error }),
1325
+ )
1326
+ $effect(try_auto_load)
1972
1327
  </script>
1973
1328
 
1974
1329
  {#snippet fill_regions_layer(fills: typeof computed_fills)}
@@ -2034,11 +1389,9 @@
2034
1389
  evt.preventDefault()
2035
1390
  fullscreen = false
2036
1391
  }
2037
- if (evt.key === `Shift`) shift_held = true
2038
- }}
2039
- onkeyup={(evt) => {
2040
- if (evt.key === `Shift`) shift_held = false
1392
+ pan_zoom.on_window_key_down(evt)
2041
1393
  }}
1394
+ onkeyup={pan_zoom.on_window_key_up}
2042
1395
  />
2043
1396
 
2044
1397
  <div
@@ -2065,45 +1418,27 @@
2065
1418
  ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
2066
1419
  `Scatter plot`)}
2067
1420
  tabindex="0"
2068
- onfocusin={() => (is_focused = true)}
2069
- onfocusout={() => (is_focused = false)}
1421
+ onfocusin={() => pan_zoom.set_focused(true)}
1422
+ onfocusout={() => pan_zoom.set_focused(false)}
2070
1423
  onmouseenter={() => (hovered = true)}
2071
- onmousedown={handle_mouse_down}
1424
+ onmousedown={pan_zoom.on_mouse_down}
2072
1425
  onmousemove={(evt: MouseEvent) => {
2073
1426
  // Only find closest point if not actively dragging
2074
- if (!drag_start_coords && !pan_drag_state) on_mouse_move(evt)
1427
+ if (!pan_zoom.drag_start && !pan_zoom.is_pan_dragging) on_mouse_move(evt)
2075
1428
  }}
2076
1429
  onmouseleave={() => {
2077
1430
  hovered = false
2078
1431
  tooltip_point = null
2079
1432
  on_point_hover?.(null)
2080
1433
  }}
2081
- ondblclick={() => {
2082
- // Reset to current auto ranges (not stale initial_*_range which may have expanded)
2083
- // This ensures lazy expansion restarts fresh from current data bounds
2084
- initial_x_range = [...auto_x_range] as [number, number]
2085
- initial_x2_range = [...auto_x2_range] as [number, number]
2086
- initial_y_range = [...auto_y_range] as [number, number]
2087
- initial_y2_range = [...auto_y2_range] as [number, number]
2088
- zoom_x_range = [...auto_x_range] as [number, number]
2089
- zoom_x2_range = [...auto_x2_range] as [number, number]
2090
- zoom_y_range = [...auto_y_range] as [number, number]
2091
- zoom_y2_range = get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2)
2092
- // Also reset axis props so future data changes recalculate auto ranges
2093
- x_axis = { ...x_axis, range: [null, null] }
2094
- x2_axis = { ...x2_axis, range: [null, null] }
2095
- y_axis = { ...y_axis, range: [null, null] }
2096
- y2_axis = { ...y2_axis, range: [null, null] }
2097
- }}
2098
- onwheel={handle_wheel}
2099
- ontouchstart={handle_touch_start}
2100
- ontouchmove={handle_touch_move}
2101
- ontouchend={handle_touch_end}
2102
- style:cursor={pan_drag_state
2103
- ? `grabbing`
2104
- : shift_held && pan?.enabled !== false
2105
- ? `grab`
2106
- : `crosshair`}
1434
+ ondblclick={pan_zoom.reset_view}
1435
+ onkeydown={pan_zoom.on_key_down}
1436
+ onwheel={pan_zoom.on_wheel}
1437
+ ontouchstart={pan_zoom.on_touch_start}
1438
+ ontouchmove={pan_zoom.on_touch_move}
1439
+ ontouchend={pan_zoom.on_touch_end}
1440
+ ontouchcancel={pan_zoom.on_touch_end}
1441
+ style:cursor={pan_zoom.cursor}
2107
1442
  >
2108
1443
  {@render user_content?.({
2109
1444
  height,
@@ -2191,9 +1526,6 @@
2191
1526
 
2192
1527
  <!-- Y2-axis (Right) -->
2193
1528
  {#if y2_points.length > 0}
2194
- {@const y2_inside = final_y2_axis.tick?.label?.inside ?? false}
2195
- {@const y2_tick_shift = y2_inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8}
2196
- {@const y2_tick_width = y2_inside ? 0 : tick_label_widths.y2_max}
2197
1529
  <PlotAxis
2198
1530
  side="y2"
2199
1531
  ticks={y2_tick_values}
@@ -2207,8 +1539,7 @@
2207
1539
  domain={[y2_min, y2_max]}
2208
1540
  unit_on_first_tick
2209
1541
  tick_label={(tick) => get_tick_label(tick, final_y2_axis.ticks)}
2210
- label_x={width - pad.r + y2_tick_shift + y2_tick_width + LABEL_GAP_DEFAULT +
2211
- (final_y2_axis.label_shift?.x ?? 0)}
1542
+ label_x={y2_axis_label_x(final_y2_axis, width, pad.r, tick_label_widths.y2_max)}
2212
1543
  label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y2_axis.label_shift?.y ?? 0)}
2213
1544
  axis_loading={axis_loading === `y2`}
2214
1545
  on_axis_change={(key) => handle_axis_change(`y2`, key)}
@@ -2238,7 +1569,7 @@
2238
1569
 
2239
1570
  <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
2240
1571
 
2241
- <ZoomRect start={drag_start_coords} current={drag_current_coords} />
1572
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
2242
1573
 
2243
1574
  <ZeroLines
2244
1575
  display={final_display}
@@ -2246,10 +1577,10 @@
2246
1577
  {x2_scale_fn}
2247
1578
  {y_scale_fn}
2248
1579
  {y2_scale_fn}
2249
- x_range={zoom_x_range}
2250
- x2_range={zoom_x2_range}
2251
- y_range={zoom_y_range}
2252
- y2_range={zoom_y2_range}
1580
+ x_range={ranges.current.x}
1581
+ x2_range={ranges.current.x2}
1582
+ y_range={ranges.current.y}
1583
+ y2_range={ranges.current.y2}
2253
1584
  x_scale_type={final_x_axis.scale_type}
2254
1585
  x2_scale_type={final_x2_axis.scale_type}
2255
1586
  y_scale_type={final_y_axis.scale_type}
@@ -2300,7 +1631,7 @@
2300
1631
  {@const finite_screen_points = all_line_points
2301
1632
  .map((point) => get_screen_coords(point, series_data))
2302
1633
  .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
2303
- {@const apply_line_controls = using_controls &&
1634
+ {@const apply_line_controls = controls.show &&
2304
1635
  (!has_multiple_series ||
2305
1636
  series_data._id === series_with_ids[selected_series_idx]?._id)}
2306
1637
  {@const ls = series_data.line_style}
@@ -2322,7 +1653,7 @@
2322
1653
  line_width={(tc(`line.width`) ? styles.line?.width : null) ?? ls?.stroke_width ?? 2}
2323
1654
  line_dash={(tc(`line.dash`) ? styles.line?.dash : null) ?? ls?.line_dash}
2324
1655
  area_color="transparent"
2325
- {line_tween}
1656
+ line_tween={effective_line_tween}
2326
1657
  />
2327
1658
  {/if}
2328
1659
  </g>
@@ -2371,7 +1702,7 @@
2371
1702
  {@const screen_y = isFinite(raw_screen_y)
2372
1703
  ? raw_screen_y
2373
1704
  : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
2374
- {@const apply_controls = using_controls &&
1705
+ {@const apply_controls = controls.show &&
2375
1706
  (!has_multiple_series ||
2376
1707
  series_data._id === series_with_ids[selected_series_idx]?._id)}
2377
1708
  {@const pt = point.point_style}
@@ -2446,60 +1777,19 @@
2446
1777
 
2447
1778
  <!-- Tooltip overlay above all plot overlays (legend, colorbar) -->
2448
1779
  {#if handler_props && hovered && tooltip_point}
2449
- {@const { color_value, point_label, point_style, series_idx } = tooltip_point}
2450
- {@const hovered_series = series_with_ids[series_idx]}
2451
- {@const series_markers = hovered_series?.markers ?? DEFAULT_MARKERS}
2452
- {@const is_transparent_or_none = (color: string | undefined | null): boolean =>
2453
- !color ||
2454
- color === `none` ||
2455
- color === `transparent` ||
2456
- /rgba\([^)]+[,/]\s*0(\.0*)?\s*\)$/.test(color)}
2457
- {@const tooltip_bg_color = (() => {
2458
- const scale_color = color_value != null
2459
- ? color_scale_fn(color_value)
2460
- : undefined
2461
- if (!is_transparent_or_none(scale_color)) return scale_color
2462
- const fill_color = point_style?.fill
2463
- if (!is_transparent_or_none(fill_color)) return fill_color
2464
- if (series_markers?.includes(`points`)) {
2465
- const stroke_color = point_style?.stroke
2466
- if (!is_transparent_or_none(stroke_color)) return stroke_color
2467
- }
2468
- if (series_markers?.includes(`line`)) {
2469
- const line_style = hovered_series?.line_style ?? {}
2470
- const first_point_style = Array.isArray(hovered_series?.point_style)
2471
- ? hovered_series?.point_style[0]
2472
- : hovered_series?.point_style
2473
- const first_color_value = hovered_series?.color_values?.[0]
2474
- let line_color_candidate = line_style.stroke
2475
- if (is_transparent_or_none(line_color_candidate)) {line_color_candidate =
2476
- first_point_style?.fill}
2477
- if (
2478
- is_transparent_or_none(line_color_candidate) && first_color_value != null
2479
- ) line_color_candidate = color_scale_fn(first_color_value)
2480
- if (
2481
- is_transparent_or_none(line_color_candidate) &&
2482
- series_markers?.includes(`points`)
2483
- ) line_color_candidate = first_point_style?.stroke
2484
- if (!is_transparent_or_none(line_color_candidate)) return line_color_candidate
2485
- }
2486
- return `rgba(0, 0, 0, 0.7)`
2487
- })()}
2488
- {@const tooltip_pos = constrain_tooltip_position(
2489
- handler_props.cx,
2490
- handler_props.cy,
2491
- tooltip_el?.offsetWidth ?? 120,
2492
- tooltip_el?.offsetHeight ?? 50,
2493
- width,
2494
- height,
2495
- { offset_x: 10, offset_y: 5 },
1780
+ {@const { point_label, series_idx } = tooltip_point}
1781
+ {@const tooltip_bg_color = pick_tooltip_bg(
1782
+ tooltip_point,
1783
+ series_with_ids[series_idx],
1784
+ color_scale_fn,
2496
1785
  )}
2497
1786
  <PlotTooltip
2498
- x={tooltip_pos.x}
2499
- y={tooltip_pos.y}
2500
- offset={{ x: 0, y: 0 }}
1787
+ x={handler_props.cx}
1788
+ y={handler_props.cy}
1789
+ offset={{ x: 10, y: 5 }}
1790
+ constrain_to={{ width, height }}
1791
+ fallback_size={{ width: 120, height: 50 }}
2501
1792
  bg_color={tooltip_bg_color}
2502
- bind:wrapper={tooltip_el}
2503
1793
  >
2504
1794
  {#if tooltip}
2505
1795
  {@render tooltip(handler_props)}
@@ -2558,15 +1848,15 @@
2558
1848
  ] as Vec2}
2559
1849
  <div
2560
1850
  bind:this={colorbar_element}
2561
- onmouseenter={() => colorbar_hover.set_locked(true)}
2562
- onmouseleave={() => colorbar_hover.set_locked(false)}
1851
+ onmouseenter={() => colorbar_tween.set_locked(true)}
1852
+ onmouseleave={() => colorbar_tween.set_locked(false)}
2563
1853
  class="colorbar-wrapper"
2564
1854
  role="img"
2565
1855
  aria-label="Color scale legend"
2566
1856
  style={`${
2567
1857
  // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
2568
1858
  effective_cbar_wrapper_style ??
2569
- `position: absolute; left: ${tweened_colorbar_coords.current.x}px; top: ${tweened_colorbar_coords.current.y}px`}; pointer-events: auto;`}
1859
+ `position: absolute; left: ${colorbar_tween.coords.current.x}px; top: ${colorbar_tween.coords.current.y}px`}; pointer-events: auto;`}
2570
1860
  >
2571
1861
  <ColorBar
2572
1862
  tick_labels={4}
@@ -2585,7 +1875,7 @@
2585
1875
  <!-- Legend -->
2586
1876
  <!-- Only render if multiple series or if legend prop was explicitly provided by user (even if empty object) -->
2587
1877
  {#if legend != null && legend_data.length > 0 &&
2588
- (legend_data.length > 1 || Object.keys(legend).length > 0)}
1878
+ (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
2589
1879
  {@const default_x = pad.l + 10}
2590
1880
  {@const default_y = pad.t + 10}
2591
1881
  {@const current_x = legend_is_dragging && legend_manual_position
@@ -2593,14 +1883,14 @@
2593
1883
  : legend_auto_outside
2594
1884
  ? legend_outside_x
2595
1885
  : legend_placement
2596
- ? tweened_legend_coords.current.x
1886
+ ? legend_tween.coords.current.x
2597
1887
  : default_x}
2598
1888
  {@const current_y = legend_is_dragging && legend_manual_position
2599
1889
  ? legend_manual_position.y
2600
1890
  : legend_auto_outside
2601
1891
  ? legend_outside_y
2602
1892
  : legend_placement
2603
- ? tweened_legend_coords.current.y
1893
+ ? legend_tween.coords.current.y
2604
1894
  : default_y}
2605
1895
  <PlotLegend
2606
1896
  bind:root_element={legend_element}
@@ -2608,32 +1898,29 @@
2608
1898
  on_drag_start={handle_legend_drag_start}
2609
1899
  on_drag={handle_legend_drag}
2610
1900
  on_drag_end={() => (legend_is_dragging = false)}
2611
- on_hover_change={legend_hover.set_locked}
2612
- on_item_hover={(series_idx: number | null) =>
2613
- (hovered_legend_series_idx = series_idx != null && series_idx >= 0
2614
- ? series_idx
2615
- : null)}
1901
+ on_hover_change={legend_tween.set_locked}
1902
+ on_item_hover={(item) => {
1903
+ if (item?.item_type === `fill`) {
1904
+ // highlight the matching fill in the plot (same state plot fill-hover uses), but skip
1905
+ // hidden fills since they render nothing and would mark the legend item active for naught
1906
+ const fill = computed_fills.find((entry) => entry.idx === item.fill_idx)
1907
+ hovered_fill_key = fill && fill.visible !== false ? fill.hover_key : null
1908
+ hovered_legend_series_idx = null
1909
+ } else {
1910
+ hovered_legend_series_idx = item != null && item.series_idx >= 0
1911
+ ? item.series_idx
1912
+ : null
1913
+ hovered_fill_key = null
1914
+ }
1915
+ }}
2616
1916
  active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
1917
+ active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)?.idx ??
1918
+ null}
2617
1919
  draggable={legend?.draggable ?? true}
2618
1920
  {...legend}
2619
- on_toggle={legend?.on_toggle ??
2620
- ((series_idx: number) => {
2621
- series = toggle_series_visibility(series, series_idx)
2622
- })}
2623
- on_double_click={legend?.on_double_click ??
2624
- ((double_clicked_idx: number) => {
2625
- const result = handle_legend_double_click(
2626
- series,
2627
- double_clicked_idx,
2628
- prev_series_visibility,
2629
- )
2630
- series = result.series
2631
- prev_series_visibility = result.prev_visibility
2632
- })}
2633
- on_group_toggle={legend?.on_group_toggle ??
2634
- ((_group_name: string, series_indices: number[]) => {
2635
- series = toggle_group_visibility(series, series_indices)
2636
- })}
1921
+ on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1922
+ on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1923
+ on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
2637
1924
  on_fill_toggle={(source_type: `fill_region` | `error_band`, source_idx: number) => {
2638
1925
  // Only fill_regions can be toggled (error_bands are not bindable)
2639
1926
  if (source_type === `fill_region`) {
@@ -2707,8 +1994,10 @@
2707
1994
  background: var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg)));
2708
1995
  max-height: none !important;
2709
1996
  overflow: hidden;
2710
- /* Add padding to prevent titles from being cropped at top */
2711
- padding-top: var(--plot-fullscreen-padding-top, 2em);
1997
+ /* border-top (not padding-top): bind:clientHeight includes padding but excludes
1998
+ borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
1999
+ border-top: var(--plot-fullscreen-padding-top, 2em) solid
2000
+ var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg, transparent)));
2712
2001
  box-sizing: border-box;
2713
2002
  }
2714
2003
  /* Center the colorbar within its wrapper when shorter than it (e.g. capped by --cbar-max-height