matterviz 0.4.0 → 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 (326) hide show
  1. package/dist/brillouin/BrillouinZone.svelte +68 -145
  2. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -14
  3. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  4. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  7. package/dist/brillouin/BrillouinZoneScene.svelte +49 -203
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  9. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  10. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  11. package/dist/brillouin/compute.d.ts +2 -0
  12. package/dist/brillouin/compute.js +80 -77
  13. package/dist/brillouin/geometry.d.ts +8 -0
  14. package/dist/brillouin/geometry.js +57 -0
  15. package/dist/brillouin/index.d.ts +2 -0
  16. package/dist/brillouin/index.js +2 -0
  17. package/dist/brillouin/types.d.ts +2 -2
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +100 -191
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +176 -464
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  23. package/dist/chempot-diagram/color.d.ts +3 -6
  24. package/dist/chempot-diagram/color.js +5 -5
  25. package/dist/chempot-diagram/compute.d.ts +3 -3
  26. package/dist/chempot-diagram/compute.js +3 -1
  27. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  28. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  29. package/dist/chempot-diagram/export.d.ts +47 -0
  30. package/dist/chempot-diagram/export.js +133 -0
  31. package/dist/chempot-diagram/index.d.ts +1 -0
  32. package/dist/chempot-diagram/index.js +1 -0
  33. package/dist/chempot-diagram/pointer.d.ts +0 -10
  34. package/dist/chempot-diagram/pointer.js +4 -4
  35. package/dist/chempot-diagram/types.d.ts +3 -3
  36. package/dist/colors/index.js +2 -2
  37. package/dist/composition/FormulaFilter.svelte +6 -5
  38. package/dist/composition/PieChart.svelte +5 -5
  39. package/dist/composition/chem-sys.js +3 -2
  40. package/dist/composition/format.js +3 -2
  41. package/dist/composition/parse.d.ts +0 -1
  42. package/dist/composition/parse.js +17 -19
  43. package/dist/controls.d.ts +1 -0
  44. package/dist/controls.js +0 -1
  45. package/dist/convex-hull/ConvexHull.svelte +8 -10
  46. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  47. package/dist/convex-hull/ConvexHull2D.svelte +94 -175
  48. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHull3D.svelte +176 -680
  50. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  51. package/dist/convex-hull/ConvexHull4D.svelte +180 -680
  52. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  53. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  54. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  55. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  56. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  57. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  58. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  59. package/dist/convex-hull/ConvexHullStats.svelte +29 -168
  60. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  61. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  62. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  63. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  64. package/dist/convex-hull/barycentric-coords.js +6 -33
  65. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  66. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  67. package/dist/convex-hull/helpers.d.ts +39 -7
  68. package/dist/convex-hull/helpers.js +154 -69
  69. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  70. package/dist/convex-hull/hull-state.svelte.js +124 -0
  71. package/dist/convex-hull/index.d.ts +9 -7
  72. package/dist/convex-hull/index.js +7 -2
  73. package/dist/convex-hull/thermodynamics.js +91 -920
  74. package/dist/convex-hull/types.d.ts +12 -4
  75. package/dist/convex-hull/types.js +12 -0
  76. package/dist/coordination/CoordinationBarPlot.svelte +4 -11
  77. package/dist/element/BohrAtom.svelte +2 -1
  78. package/dist/element/ElementTile.svelte.d.ts +1 -1
  79. package/dist/element/index.d.ts +4 -0
  80. package/dist/element/index.js +18 -0
  81. package/dist/feedback/DragOverlay.svelte +3 -1
  82. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  83. package/dist/feedback/StatusMessage.svelte +13 -3
  84. package/dist/fermi-surface/FermiSurface.svelte +67 -146
  85. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  86. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -224
  88. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  89. package/dist/fermi-surface/compute.js +11 -10
  90. package/dist/fermi-surface/export.js +4 -15
  91. package/dist/fermi-surface/index.d.ts +0 -1
  92. package/dist/fermi-surface/index.js +0 -1
  93. package/dist/fermi-surface/parse.d.ts +1 -1
  94. package/dist/fermi-surface/parse.js +64 -75
  95. package/dist/fermi-surface/types.d.ts +2 -2
  96. package/dist/heatmap-matrix/HeatmapMatrix.svelte +55 -40
  97. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  98. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  99. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  100. package/dist/heatmap-matrix/index.d.ts +3 -2
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/io/ExportPane.svelte +166 -0
  104. package/dist/io/ExportPane.svelte.d.ts +17 -0
  105. package/dist/io/decompress.js +1 -2
  106. package/dist/io/export.d.ts +5 -1
  107. package/dist/io/export.js +32 -28
  108. package/dist/io/fetch.d.ts +2 -1
  109. package/dist/io/file-drop.d.ts +7 -0
  110. package/dist/io/file-drop.js +13 -0
  111. package/dist/io/index.d.ts +2 -0
  112. package/dist/io/index.js +10 -0
  113. package/dist/io/types.d.ts +13 -0
  114. package/dist/isosurface/parse.js +46 -44
  115. package/dist/labels.js +1 -1
  116. package/dist/layout/FullscreenButton.svelte +33 -0
  117. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  118. package/dist/layout/FullscreenToggle.svelte +8 -14
  119. package/dist/layout/ViewerChrome.svelte +116 -0
  120. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  121. package/dist/layout/fullscreen.d.ts +4 -0
  122. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  123. package/dist/layout/fullscreen.svelte.js +37 -0
  124. package/dist/layout/index.d.ts +3 -0
  125. package/dist/layout/index.js +3 -0
  126. package/dist/math.d.ts +7 -3
  127. package/dist/math.js +18 -21
  128. package/dist/overlays/index.d.ts +4 -0
  129. package/dist/periodic-table/PeriodicTable.svelte +9 -8
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  137. package/dist/phase-diagram/colors.js +1 -1
  138. package/dist/phase-diagram/parse.d.ts +2 -1
  139. package/dist/plot/bar/BarPlot.svelte +79 -316
  140. package/dist/plot/bar/BarPlot.svelte.d.ts +7 -15
  141. package/dist/plot/bar/BarPlotControls.svelte.d.ts +1 -1
  142. package/dist/plot/bar/SpacegroupBarPlot.svelte +2 -1
  143. package/dist/plot/box/BoxPlot.svelte +76 -246
  144. package/dist/plot/box/BoxPlot.svelte.d.ts +4 -3
  145. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  146. package/dist/plot/box/Violin.svelte.d.ts +1 -1
  147. package/dist/plot/box/box-plot.d.ts +3 -2
  148. package/dist/plot/box/box-plot.js +5 -2
  149. package/dist/plot/box/kde.d.ts +2 -1
  150. package/dist/plot/box/kde.js +4 -4
  151. package/dist/plot/core/auto-place.d.ts +1 -1
  152. package/dist/plot/core/auto-place.js +4 -1
  153. package/dist/plot/core/components/ColorBar.svelte +5 -5
  154. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  155. package/dist/plot/core/components/Line.svelte +3 -2
  156. package/dist/plot/core/components/Line.svelte.d.ts +3 -2
  157. package/dist/plot/core/components/PlotAxis.svelte +2 -1
  158. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  159. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  160. package/dist/plot/core/components/ReferenceLine3D.svelte +2 -2
  161. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  162. package/dist/plot/core/components/ReferencePlane.svelte +2 -2
  163. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  164. package/dist/plot/core/data-cleaning.js +18 -18
  165. package/dist/plot/core/fill-utils.d.ts +4 -3
  166. package/dist/plot/core/fill-utils.js +6 -3
  167. package/dist/plot/core/interactions.d.ts +5 -1
  168. package/dist/plot/core/interactions.js +14 -0
  169. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  170. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  171. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  172. package/dist/plot/core/placed-tween.svelte.js +68 -0
  173. package/dist/plot/core/reference-line.d.ts +10 -10
  174. package/dist/plot/core/reference-line.js +6 -6
  175. package/dist/plot/core/scales.d.ts +17 -25
  176. package/dist/plot/core/scales.js +10 -8
  177. package/dist/plot/core/svg.d.ts +2 -1
  178. package/dist/plot/core/types.d.ts +18 -7
  179. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  180. package/dist/plot/core/utils/label-placement.js +3 -3
  181. package/dist/plot/core/utils.d.ts +2 -1
  182. package/dist/plot/histogram/Histogram.svelte +77 -314
  183. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  184. package/dist/plot/sankey/Sankey.svelte +2 -5
  185. package/dist/plot/sankey/Sankey.svelte.d.ts +1 -1
  186. package/dist/plot/sankey/sankey.js +3 -1
  187. package/dist/plot/scatter/BinnedScatterPlot.svelte +3 -5
  188. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +4 -4
  189. package/dist/plot/scatter/ScatterPlot.svelte +160 -450
  190. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +7 -15
  191. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  192. package/dist/plot/scatter/binned-scatter-types.d.ts +4 -11
  193. package/dist/plot/scatter/index.d.ts +1 -1
  194. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +15 -26
  195. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  196. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +9 -10
  197. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  198. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +122 -121
  199. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  200. package/dist/plot/scatter-3d/Surface3D.svelte +6 -5
  201. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  202. package/dist/plot/sunburst/Sunburst.svelte +16 -20
  203. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  204. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +1 -1
  205. package/dist/plot/sunburst/sunburst.js +4 -1
  206. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  207. package/dist/sanitize.js +13 -2
  208. package/dist/scene/SceneCamera.svelte +62 -0
  209. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  210. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  211. package/dist/scene/bind-renderer.svelte.js +14 -0
  212. package/dist/scene/index.d.ts +4 -0
  213. package/dist/scene/index.js +5 -0
  214. package/dist/scene/props.js +52 -0
  215. package/dist/scene/types.d.ts +26 -0
  216. package/dist/scene/types.js +1 -0
  217. package/dist/settings.d.ts +14 -2
  218. package/dist/settings.js +59 -1
  219. package/dist/spectral/Bands.svelte +8 -7
  220. package/dist/spectral/Bands.svelte.d.ts +3 -2
  221. package/dist/spectral/BandsAndDos.svelte +22 -24
  222. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  223. package/dist/spectral/Dos.svelte +5 -4
  224. package/dist/spectral/Dos.svelte.d.ts +2 -1
  225. package/dist/spectral/helpers.d.ts +6 -6
  226. package/dist/spectral/helpers.js +43 -37
  227. package/dist/state.svelte.d.ts +0 -7
  228. package/dist/state.svelte.js +0 -6
  229. package/dist/structure/Arrow.svelte +2 -4
  230. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  231. package/dist/structure/CanvasTooltip.svelte +1 -0
  232. package/dist/structure/CellSelect.svelte +11 -3
  233. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  234. package/dist/structure/Lattice.svelte +2 -2
  235. package/dist/structure/Structure.svelte +291 -355
  236. package/dist/structure/Structure.svelte.d.ts +5 -15
  237. package/dist/structure/StructureControls.svelte +217 -2
  238. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  239. package/dist/structure/StructureExportPane.svelte +54 -156
  240. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  241. package/dist/structure/StructureInfoPane.svelte +5 -3
  242. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  243. package/dist/structure/StructureScene.svelte +365 -198
  244. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  245. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  246. package/dist/structure/{label-placement.js → atom-label-placement.js} +12 -2
  247. package/dist/structure/atom-properties.d.ts +1 -1
  248. package/dist/structure/atom-properties.js +11 -16
  249. package/dist/structure/bond-order-perception.js +2 -4
  250. package/dist/structure/bonding.d.ts +3 -0
  251. package/dist/structure/bonding.js +91 -48
  252. package/dist/structure/export.d.ts +24 -4
  253. package/dist/structure/export.js +64 -122
  254. package/dist/structure/index.d.ts +2 -0
  255. package/dist/structure/index.js +2 -0
  256. package/dist/structure/parse.d.ts +3 -2
  257. package/dist/structure/parse.js +333 -370
  258. package/dist/structure/partial-occupancy.d.ts +0 -1
  259. package/dist/structure/partial-occupancy.js +1 -1
  260. package/dist/structure/pbc.d.ts +1 -1
  261. package/dist/structure/pbc.js +186 -13
  262. package/dist/structure/polyhedra.d.ts +41 -0
  263. package/dist/structure/polyhedra.js +602 -0
  264. package/dist/structure/site.d.ts +4 -0
  265. package/dist/structure/site.js +1 -0
  266. package/dist/structure/supercell.js +3 -2
  267. package/dist/structure/validation.js +5 -6
  268. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  269. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  270. package/dist/symmetry/SymmetryElements.svelte +354 -0
  271. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  272. package/dist/symmetry/SymmetryStats.svelte +111 -6
  273. package/dist/symmetry/WyckoffTable.svelte +68 -7
  274. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  275. package/dist/symmetry/cell-transform.js +7 -14
  276. package/dist/symmetry/index.d.ts +14 -4
  277. package/dist/symmetry/index.js +301 -80
  278. package/dist/symmetry/spacegroups.d.ts +5 -1
  279. package/dist/symmetry/spacegroups.js +15 -1
  280. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  281. package/dist/symmetry/symmetry-elements.js +521 -0
  282. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  283. package/dist/symmetry/wyckoff-db.js +87 -0
  284. package/dist/table/HeatmapTable.svelte +4 -15
  285. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  286. package/dist/trajectory/Trajectory.svelte +58 -61
  287. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  288. package/dist/trajectory/TrajectoryExportPane.svelte +15 -24
  289. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  290. package/dist/trajectory/TrajectoryInfoPane.svelte +3 -2
  291. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  292. package/dist/trajectory/constants.js +6 -2
  293. package/dist/trajectory/extract.js +17 -37
  294. package/dist/trajectory/format-detect.d.ts +0 -1
  295. package/dist/trajectory/format-detect.js +3 -9
  296. package/dist/trajectory/frame-reader.d.ts +0 -1
  297. package/dist/trajectory/frame-reader.js +62 -128
  298. package/dist/trajectory/helpers.d.ts +10 -2
  299. package/dist/trajectory/helpers.js +56 -36
  300. package/dist/trajectory/parse/ase.d.ts +9 -1
  301. package/dist/trajectory/parse/ase.js +47 -32
  302. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  303. package/dist/trajectory/parse/diagnostics.js +14 -0
  304. package/dist/trajectory/parse/index.d.ts +1 -1
  305. package/dist/trajectory/parse/index.js +54 -102
  306. package/dist/trajectory/parse/lammps.d.ts +0 -2
  307. package/dist/trajectory/parse/lammps.js +8 -6
  308. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  309. package/dist/trajectory/parse/pymatgen.js +74 -0
  310. package/dist/trajectory/parse/vasp.js +4 -3
  311. package/dist/trajectory/parse/xyz.d.ts +9 -21
  312. package/dist/trajectory/parse/xyz.js +28 -33
  313. package/dist/trajectory/plotting.d.ts +0 -1
  314. package/dist/trajectory/plotting.js +3 -100
  315. package/dist/utils.d.ts +1 -0
  316. package/dist/utils.js +1 -1
  317. package/dist/xrd/XrdPlot.svelte +14 -29
  318. package/dist/xrd/broadening.d.ts +2 -1
  319. package/dist/xrd/calc-xrd.js +6 -11
  320. package/dist/xrd/index.d.ts +2 -2
  321. package/package.json +29 -16
  322. package/dist/element/data.json +0 -11864
  323. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  324. package/dist/fermi-surface/marching-cubes.js +0 -2
  325. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  326. package/dist/plot/core/hover-lock.svelte.js +0 -45
@@ -2,7 +2,7 @@
2
2
  lang="ts"
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >
5
- import type { D3ColorSchemeName, D3InterpolateName } from '../../colors'
5
+ import type { D3InterpolateName } from '../../colors'
6
6
  import { format_value } from '../../labels'
7
7
  import { sanitize_html } from '../../sanitize'
8
8
  import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
@@ -10,6 +10,7 @@
10
10
  import type {
11
11
  AxisLoadError,
12
12
  BasePlotProps,
13
+ ColorScaleConfig,
13
14
  ControlsConfig,
14
15
  DataLoaderFn,
15
16
  DataSeries,
@@ -17,7 +18,6 @@
17
18
  FillHandlerEvent,
18
19
  FillRegion,
19
20
  HoverConfig,
20
- InitialRanges,
21
21
  InternalPoint,
22
22
  LabelPlacementConfig,
23
23
  LegendConfig,
@@ -26,9 +26,9 @@
26
26
  Point,
27
27
  RefLine,
28
28
  RefLineEvent,
29
- ScaleType,
30
29
  ScatterHandlerEvent,
31
30
  ScatterHandlerProps,
31
+ SizeScaleConfig,
32
32
  StyleOverrides,
33
33
  UserContentProps,
34
34
  } from '..'
@@ -56,10 +56,7 @@
56
56
  import type { AxisChangeState } from '../core/axis-utils'
57
57
  import { AXIS_DEFAULTS, create_axis_loader } from '../core/axis-utils'
58
58
  import { get_series_color, get_series_symbol } from '../core/data-transform'
59
- import {
60
- create_dimension_tracker,
61
- create_hover_lock,
62
- } from '../core/hover-lock.svelte'
59
+ import { create_placed_tween } from '../core/placed-tween.svelte'
63
60
  import {
64
61
  DEFAULT_MARKERS,
65
62
  get_scale_type_name,
@@ -73,7 +70,7 @@
73
70
  import type { ComponentProps, Snippet } from 'svelte'
74
71
  import { onDestroy, untrack } from 'svelte'
75
72
  import type { HTMLAttributes } from 'svelte/elements'
76
- import { Tween, type TweenOptions } from 'svelte/motion'
73
+ import type { TweenOptions } from 'svelte/motion'
77
74
  import { SvelteSet } from 'svelte/reactivity'
78
75
  import type { Pt } from '../core/fill-utils'
79
76
  import {
@@ -84,16 +81,11 @@
84
81
  import {
85
82
  expand_range_if_needed,
86
83
  get_relative_coords,
87
- MIN_TOUCH_DISTANCE_PIXELS,
84
+ invert_rect_range,
88
85
  normalize_y2_sync,
89
- pan_range_by_pixels,
90
- PINCH_ZOOM_THRESHOLD,
91
- remove_drag_listeners,
92
- sorted_range,
93
86
  sync_y2_range,
94
- to_epoch_num,
95
- zoom_range_by_factor,
96
87
  } from '../core/interactions'
88
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
97
89
  import type { Rect, Sides } from '../core/layout'
98
90
  import {
99
91
  calc_auto_padding,
@@ -193,16 +185,8 @@
193
185
  change?: (
194
186
  data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
195
187
  ) => void
196
- color_scale?: {
197
- type?: ScaleType
198
- scheme?: D3ColorSchemeName | D3InterpolateName
199
- value_range?: [number, number]
200
- } | D3InterpolateName
201
- size_scale?: {
202
- type?: ScaleType
203
- radius_range?: [number, number]
204
- value_range?: [number, number]
205
- }
188
+ color_scale?: ColorScaleConfig | D3InterpolateName
189
+ size_scale?: SizeScaleConfig
206
190
  color_bar?:
207
191
  | (ComponentProps<typeof ColorBar> & {
208
192
  margin?: number | Sides
@@ -277,7 +261,6 @@
277
261
 
278
262
  let [width, height] = $state([0, 0])
279
263
  let svg_element: SVGElement | null = $state(null) // Bind the SVG element
280
- let svg_bounding_box: DOMRect | null = $state(null) // Store SVG bounds during drag
281
264
 
282
265
  // Track which specific control properties user has modified
283
266
  let touched = new SvelteSet<string>()
@@ -296,19 +279,14 @@
296
279
  }),
297
280
  )
298
281
 
299
- // State for rectangle zoom selection
300
- let drag_start_coords = $state<Point2D | null>(null)
301
- let drag_current_coords = $state<Point2D | null>(null)
302
-
303
282
  // Zoom/pan state - track both initial (data-driven) and current (after pan/zoom) ranges
304
- let initial_x_range = $state<[number, number]>([0, 1])
305
- let initial_x2_range = $state<[number, number]>([0, 1])
306
- let initial_y_range = $state<[number, number]>([0, 1])
307
- let initial_y2_range = $state<[number, number]>([0, 1])
308
- let zoom_x_range = $state<[number, number]>([0, 1])
309
- let zoom_x2_range = $state<[number, number]>([0, 1])
310
- let zoom_y_range = $state<[number, number]>([0, 1])
311
- let zoom_y2_range = $state<[number, number]>([0, 1])
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
+ })
312
290
  const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
313
291
 
314
292
  // Y2 axis sync configuration
@@ -319,7 +297,7 @@
319
297
  // Helper to compute synced y2 range or return fallback when sync disabled
320
298
  const get_synced_y2 = (y1_range: Vec2, fallback: Vec2): Vec2 =>
321
299
  y2_sync_config.mode !== `none`
322
- ? sync_y2_range(y1_range, initial_y2_range, y2_sync_config)
300
+ ? sync_y2_range(y1_range, ranges.initial.y2, y2_sync_config)
323
301
  : fallback
324
302
 
325
303
  // Effect to update y2 range when sync mode changes - use $effect.pre to capture
@@ -330,25 +308,15 @@
330
308
  if (mode !== prev_sync_mode) {
331
309
  // When sync mode becomes enabled (or changes), apply sync immediately
332
310
  if (mode !== `none`) {
333
- 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)
334
312
  } else {
335
313
  // When switching to independent mode, reset Y2 to its data range
336
- zoom_y2_range = [...initial_y2_range] as [number, number]
314
+ ranges.current.y2 = [...ranges.initial.y2] as Vec2
337
315
  }
338
316
  prev_sync_mode = mode
339
317
  }
340
318
  })
341
319
 
342
- // Pan state
343
- let is_focused = $state(false)
344
- let shift_held = $state(false)
345
- let pan_drag_state = $state<
346
- InitialRanges & { start: { x: number; y: number } } | null
347
- >(null)
348
- let touch_state = $state<
349
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
350
- >(null)
351
-
352
320
  // Fill region hover state
353
321
  let hovered_fill_key = $state<string | null>(null)
354
322
 
@@ -370,17 +338,6 @@
370
338
  // State for legend/colorbar placement stability
371
339
  let legend_element = $state<HTMLDivElement | undefined>()
372
340
  let colorbar_element = $state<HTMLDivElement | undefined>()
373
- const legend_hover = create_hover_lock()
374
- const colorbar_hover = create_hover_lock()
375
- const dim_tracker = create_dimension_tracker()
376
- let has_initial_legend_placement = $state(false)
377
- let has_initial_colorbar_placement = $state(false)
378
-
379
- // Clear pending hover lock timeouts on unmount
380
- $effect(() => () => {
381
- legend_hover.cleanup()
382
- colorbar_hover.cleanup()
383
- })
384
341
 
385
342
  // Module-level constants to avoid repeated allocations
386
343
  // Create and categorize points in a single pass (instead of 3 separate iterations)
@@ -596,67 +553,52 @@
596
553
  return { explicit, range }
597
554
  }
598
555
 
599
- const x = get_range(final_x_axis, auto_x_range)
600
- const x2 = get_range(final_x2_axis, auto_x2_range)
601
- const y = get_range(final_y_axis, auto_y_range)
602
- const y2 = get_range(final_y2_axis, auto_y2_range)
603
-
604
- // X axis: explicit → direct, auto → lazy expand
605
- if (x.explicit) {
606
- zoom_x_range = x.range
607
- } else {
608
- const result = expand_range_if_needed(initial_x_range, x.range)
609
- if (result.changed) {
610
- ;[initial_x_range, zoom_x_range] = [result.range, result.range]
611
- }
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),
612
561
  }
613
562
 
614
- // X2 axis: explicit direct, auto lazy expand
615
- if (x2.explicit) {
616
- zoom_x2_range = x2.range
617
- } else {
618
- const result = expand_range_if_needed(initial_x2_range, x2.range)
619
- if (result.changed) {
620
- ;[initial_x2_range, zoom_x2_range] = [result.range, result.range]
621
- }
622
- }
623
-
624
- // Y axis: explicit → direct, auto → lazy expand
625
- if (y.explicit) {
626
- zoom_y_range = y.range
627
- } else {
628
- const result = expand_range_if_needed(initial_y_range, y.range)
629
- if (result.changed) {
630
- ;[initial_y_range, zoom_y_range] = [result.range, result.range]
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
+ }
631
575
  }
632
576
  }
633
577
 
634
578
  // Y2 axis: explicit → direct, else expand initial range then optionally sync
635
- if (y2.explicit) {
636
- zoom_y2_range = y2.range
579
+ if (resolved.y2.explicit) {
580
+ ranges.current.y2 = resolved.y2.range
637
581
  } else {
638
- const result = expand_range_if_needed(initial_y2_range, y2.range)
639
- 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
640
584
  // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
641
585
  if (y2_sync_config.mode !== `none`) {
642
- // untrack the read of zoom_y_range: this effect also writes it (fresh array per
643
- // run when y.explicit), so a tracked read would loop until
644
- // effect_update_depth_exceeded. Pan/zoom handlers sync y2 themselves.
645
- zoom_y2_range = sync_y2_range(
646
- untrack(() => zoom_y_range),
647
- initial_y2_range,
586
+ // Pan/zoom handlers sync y2 themselves.
587
+ ranges.current.y2 = sync_y2_range(
588
+ untrack(() => ranges.current.y),
589
+ untrack(() => ranges.initial.y2),
648
590
  y2_sync_config,
649
591
  )
650
592
  } else if (result.changed) {
651
- zoom_y2_range = result.range
593
+ ranges.current.y2 = result.range
652
594
  }
653
595
  }
654
596
  })
655
597
 
656
- let [x_min, x_max] = $derived(zoom_x_range)
657
- let [x2_min, x2_max] = $derived(zoom_x2_range)
658
- let [y_min, y_max] = $derived(zoom_y_range)
659
- let [y2_min, y2_max] = $derived(zoom_y2_range)
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)
660
602
 
661
603
  // Create auto color range
662
604
  let auto_color_range = $derived(
@@ -983,60 +925,23 @@
983
925
  return legend_placement
984
926
  })
985
927
 
986
- // Initialize tweened values for color bar position - create once, update target via effect
987
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
988
- const tweened_colorbar_coords = new Tween(
989
- { x: 0, y: 0 },
990
- untrack(() => ({ duration: 400, ...color_bar?.tween })),
991
- )
992
- // Initialize tweened values for legend position - create once, update target via effect
993
- const tweened_legend_coords = new Tween(
994
- { x: 0, y: 0 },
995
- untrack(() => ({ duration: 400, ...legend?.tween })),
996
- )
997
-
998
- // Update placement positions (with animation and stability checks)
999
- $effect(() => {
1000
- if (!width || !height) return
1001
-
1002
- // Track dimensions for resize detection
1003
- const dims_changed = dim_tracker.has_changed(width, height)
1004
- if (dims_changed) dim_tracker.update(width, height)
1005
-
1006
- // Update colorbar position (stable after initial placement unless responsive)
1007
- if (color_bar_placement) {
1008
- const is_responsive = color_bar?.responsive ?? false
1009
- const should_update = dims_changed || (!colorbar_hover.is_locked.current &&
1010
- (is_responsive || !has_initial_colorbar_placement))
1011
-
1012
- if (should_update) {
1013
- tweened_colorbar_coords.set(
1014
- { x: color_bar_placement.x, y: color_bar_placement.y },
1015
- has_initial_colorbar_placement ? undefined : { duration: 0 },
1016
- )
1017
- if (colorbar_element && !has_initial_colorbar_placement) {
1018
- has_initial_colorbar_placement = true
1019
- }
1020
- }
1021
- }
1022
-
1023
- // Update legend position (stable after initial placement unless responsive)
1024
- if (legend_manual_position && !legend_is_dragging) {
1025
- // Immediate update (no animation) for manually dragged positions
1026
- tweened_legend_coords.set(legend_manual_position, { duration: 0 })
1027
- } else if (active_legend_placement && !legend_is_dragging) {
1028
- const is_responsive = legend?.responsive ?? false
1029
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
1030
- (is_responsive || !has_initial_legend_placement))
1031
-
1032
- if (should_update) {
1033
- tweened_legend_coords.set(
1034
- { x: active_legend_placement.x, y: active_legend_placement.y },
1035
- has_initial_legend_placement ? undefined : { duration: 0 },
1036
- )
1037
- if (legend_element) has_initial_legend_placement = true
1038
- }
1039
- }
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,
1040
945
  })
1041
946
 
1042
947
  // Generate axis ticks - consolidated into single derived for efficiency
@@ -1102,250 +1007,76 @@
1102
1007
  y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
1103
1008
  })
1104
1009
 
1105
- // Define global handlers reference for adding/removing listeners
1106
- const on_window_mouse_move = (evt: MouseEvent) => {
1107
- if (!drag_start_coords || !svg_bounding_box) return // Exit if not dragging or no bounds
1108
-
1109
- // Calculate mouse position relative to the stored SVG bounding box
1110
- const current_x = evt.clientX - svg_bounding_box.left
1111
- const current_y = evt.clientY - svg_bounding_box.top
1112
- drag_current_coords = { x: current_x, y: current_y }
1113
-
1114
- // Optional: update tooltip only if inside SVG bounds
1115
- const is_inside_svg = current_x >= 0 &&
1116
- current_x <= svg_bounding_box.width &&
1117
- current_y >= 0 &&
1118
- current_y <= svg_bounding_box.height
1119
-
1120
- if (is_inside_svg) {
1121
- // Use the already calculated relative coordinates
1122
- update_tooltip_point(current_x, current_y)
1123
- } else tooltip_point = null // Clear tooltip if outside
1124
- }
1125
-
1126
- const on_window_mouse_up = (_evt: MouseEvent) => {
1127
- if (drag_start_coords && drag_current_coords) {
1128
- // Use current scales to invert screen coords to data coords
1129
- const start_data_x_val = x_scale_fn.invert(drag_start_coords.x)
1130
- const end_data_x_val = x_scale_fn.invert(drag_current_coords.x)
1131
- const start_data_y_val = y_scale_fn.invert(drag_start_coords.y)
1132
- const end_data_y_val = y_scale_fn.invert(drag_current_coords.y)
1133
-
1134
- // Same scale inverts both coords, so both are numbers or both are Dates
1135
- const [x1, x2] = [to_epoch_num(start_data_x_val), to_epoch_num(end_data_x_val)]
1136
- const next_x_range = sorted_range(x1, x2)
1137
- // Y axis is always number
1138
- const next_y_range = sorted_range(start_data_y_val, end_data_y_val)
1139
-
1140
- // Check for minuscule zoom box (e.g. accidental click)
1141
- const min_zoom_size = 5 // Minimum pixels to trigger zoom
1142
- const dx = Math.abs(drag_start_coords.x - drag_current_coords.x)
1143
- const dy = Math.abs(drag_start_coords.y - drag_current_coords.y)
1144
-
1145
- if (
1146
- dx > min_zoom_size &&
1147
- dy > min_zoom_size &&
1148
- next_x_range[0] !== next_x_range[1] &&
1149
- next_y_range[0] !== next_y_range[1]
1150
- ) {
1151
- // Update axis ranges to trigger reactivity (like BarPlot/Histogram do)
1152
- x_axis = { ...x_axis, range: next_x_range }
1153
- y_axis = { ...y_axis, range: next_y_range }
1154
-
1155
- // X2 axis: invert screen coords using x2 scale
1156
- if (x2_points.length > 0) {
1157
- const x2_a = to_epoch_num(x2_scale_fn.invert(drag_start_coords.x))
1158
- const x2_b = to_epoch_num(x2_scale_fn.invert(drag_current_coords.x))
1159
- x2_axis = { ...x2_axis, range: sorted_range(x2_a, x2_b) }
1160
- }
1161
-
1162
- // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1163
- // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1164
- if (y2_points.length > 0 && y2_sync_config.mode === `none`) {
1165
- const y2_a = y2_scale_fn.invert(drag_start_coords.y)
1166
- const y2_b = y2_scale_fn.invert(drag_current_coords.y)
1167
- y2_axis = { ...y2_axis, range: sorted_range(y2_a, y2_b) }
1168
- }
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,
1018
+ // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
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,
1169
1059
  }
1170
- }
1171
-
1172
- // Reset states and remove listeners
1173
- drag_start_coords = null
1174
- drag_current_coords = null
1175
- svg_bounding_box = null
1176
- window.removeEventListener(`mousemove`, on_window_mouse_move)
1177
- window.removeEventListener(`mouseup`, on_window_mouse_up)
1178
- document.body.style.cursor = `default`
1179
- }
1180
-
1181
- // Pan/zoom all four axes from an interaction-start snapshot, each in its own
1182
- // scale's transform space (log axes pan by a constant factor, linear by a shift).
1183
- // Plot dims clamped to 1px so degenerate containers can't produce Infinity deltas.
1184
- const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
1185
- const plot_width = Math.max(1, width - pad.l - pad.r)
1186
- const plot_height = Math.max(1, height - pad.t - pad.b)
1187
- zoom_x_range = pan_range_by_pixels(init.initial_x_range, dx_px, plot_width, final_x_axis.scale_type)
1188
- zoom_x2_range = pan_range_by_pixels(init.initial_x2_range, dx_px, plot_width, final_x2_axis.scale_type)
1189
- zoom_y_range = pan_range_by_pixels(init.initial_y_range, dy_px, plot_height, final_y_axis.scale_type)
1190
- zoom_y2_range = get_synced_y2(
1191
- zoom_y_range,
1192
- pan_range_by_pixels(init.initial_y2_range, dy_px, plot_height, final_y2_axis.scale_type),
1193
- )
1194
- }
1195
- const zoom_all_axes = (init: InitialRanges, factor: number) => {
1196
- zoom_x_range = zoom_range_by_factor(init.initial_x_range, factor, final_x_axis.scale_type)
1197
- zoom_x2_range = zoom_range_by_factor(init.initial_x2_range, factor, final_x2_axis.scale_type)
1198
- zoom_y_range = zoom_range_by_factor(init.initial_y_range, factor, final_y_axis.scale_type)
1199
- zoom_y2_range = get_synced_y2(
1200
- zoom_y_range,
1201
- zoom_range_by_factor(init.initial_y2_range, factor, final_y2_axis.scale_type),
1202
- )
1203
- }
1204
-
1205
- // Pan drag handler (drag direction inverted on x for natural pan feel)
1206
- const on_pan_move = (evt: MouseEvent) => {
1207
- if (!pan_drag_state) return
1208
- const sensitivity = pan?.drag_sensitivity ?? 1
1209
- pan_all_axes(
1210
- pan_drag_state,
1211
- -(evt.clientX - pan_drag_state.start.x) * sensitivity,
1212
- (evt.clientY - pan_drag_state.start.y) * sensitivity,
1213
- )
1214
- }
1215
-
1216
- const on_pan_end = () => {
1217
- pan_drag_state = null
1218
- document.body.style.cursor = ``
1219
- window.removeEventListener(`mousemove`, on_pan_move)
1220
- window.removeEventListener(`mouseup`, on_pan_end)
1221
- }
1222
-
1223
- // Tear down any window listeners + cursor override if the component unmounts mid-drag
1224
- // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
1225
- // onDestroy also runs during SSR teardown, where window/document don't exist.
1226
- onDestroy(() => {
1227
- remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
1228
- drag_start_coords = null
1229
- drag_current_coords = null
1230
- svg_bounding_box = null
1231
- pan_drag_state = null
1232
- })
1233
-
1234
- function handle_mouse_down(evt: MouseEvent) {
1235
- if (!svg_element) return
1236
-
1237
- // Check if pan is enabled and shift is held for pan mode
1238
- const pan_enabled = pan?.enabled !== false
1239
- if (pan_enabled && evt.shiftKey) {
1240
- evt.preventDefault()
1241
- pan_drag_state = {
1242
- start: { x: evt.clientX, y: evt.clientY },
1243
- initial_x_range: [...zoom_x_range] as [number, number],
1244
- initial_x2_range: [...zoom_x2_range] as [number, number],
1245
- initial_y_range: [...zoom_y_range] as [number, number],
1246
- initial_y2_range: [...zoom_y2_range] as [number, number],
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),
1247
1065
  }
1248
- document.body.style.cursor = `grabbing`
1249
- window.addEventListener(`mousemove`, on_pan_move)
1250
- window.addEventListener(`mouseup`, on_pan_end)
1251
- return
1252
- }
1253
-
1254
- // Store bounding box first, then calculate coords using it
1255
- svg_bounding_box = svg_element.getBoundingClientRect()
1256
-
1257
- // Calculate initial coords using the same bounding box that will be used during drag
1258
- const initial_x = evt.clientX - svg_bounding_box.left
1259
- const initial_y = evt.clientY - svg_bounding_box.top
1260
- const coords = { x: initial_x, y: initial_y }
1261
-
1262
- drag_start_coords = coords
1263
- drag_current_coords = coords
1264
-
1265
- window.addEventListener(`mousemove`, on_window_mouse_move)
1266
- window.addEventListener(`mouseup`, on_window_mouse_up)
1267
- document.body.style.cursor = `crosshair`
1268
- evt.preventDefault()
1269
- }
1270
-
1271
- // Wheel handler for pan (requires focus and shift)
1272
- function handle_wheel(evt: WheelEvent) {
1273
- const pan_enabled = pan?.enabled !== false
1274
- // Only capture wheel when focused AND Shift is held
1275
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
1276
- if (!pan_enabled || !is_focused || !shift_held) return
1277
-
1278
- evt.preventDefault()
1279
-
1280
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1281
- const plot_width = Math.max(1, width - pad.l - pad.r)
1282
- const plot_height = Math.max(1, height - pad.t - pad.b)
1283
- const sensitivity = pan?.wheel_sensitivity ?? 1
1284
-
1285
- // Pan along the dominant wheel direction
1286
- // (deltaX for horizontal scroll on trackpads, deltaY for vertical)
1287
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
1288
- const dx = evt.deltaX * sensitivity
1289
- zoom_x_range = pan_range_by_pixels(zoom_x_range, dx, plot_width, final_x_axis.scale_type)
1290
- zoom_x2_range = pan_range_by_pixels(zoom_x2_range, dx, plot_width, final_x2_axis.scale_type)
1291
- } else {
1292
- const dy = evt.deltaY * sensitivity
1293
- zoom_y_range = pan_range_by_pixels(zoom_y_range, dy, plot_height, final_y_axis.scale_type)
1294
- zoom_y2_range = get_synced_y2(
1295
- zoom_y_range,
1296
- pan_range_by_pixels(zoom_y2_range, dy, plot_height, final_y2_axis.scale_type),
1297
- )
1298
- }
1299
- }
1300
-
1301
- // Touch handlers for pinch-zoom and two-finger pan
1302
- function handle_touch_start(evt: TouchEvent) {
1303
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
1304
- if (!touch_enabled || evt.touches.length !== 2) return
1305
-
1306
- evt.preventDefault()
1307
- const touches = Array.from(evt.touches)
1308
- touch_state = {
1309
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
1310
- initial_x_range: [...zoom_x_range] as [number, number],
1311
- initial_x2_range: [...zoom_x2_range] as [number, number],
1312
- initial_y_range: [...zoom_y_range] as [number, number],
1313
- initial_y2_range: [...zoom_y2_range] as [number, number],
1314
- }
1315
- }
1316
-
1317
- function handle_touch_move(evt: TouchEvent) {
1318
- if (!touch_state || evt.touches.length !== 2) return
1319
- evt.preventDefault()
1320
-
1321
- const [t1, t2] = Array.from(evt.touches)
1322
- const [s1, s2] = touch_state.start_touches
1323
-
1324
- // Calculate center movement for pan
1325
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
1326
- const curr_center = {
1327
- x: (t1.clientX + t2.clientX) / 2,
1328
- y: (t1.clientY + t2.clientY) / 2,
1329
- }
1330
- const dx = curr_center.x - start_center.x
1331
- const dy = curr_center.y - start_center.y
1332
-
1333
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
1334
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
1335
- // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
1336
- if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
1337
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
1338
- const scale = curr_dist / start_dist
1339
-
1340
- // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
1341
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
1342
- zoom_all_axes(touch_state, scale)
1343
- } else pan_all_axes(touch_state, -dx, dy)
1344
- }
1345
-
1346
- function handle_touch_end() {
1347
- touch_state = null
1348
- }
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())
1349
1080
 
1350
1081
  // tooltip logic: find closest point and update tooltip state
1351
1082
  function update_tooltip_point(x_rel: number, y_rel: number, evt?: MouseEvent) {
@@ -1477,7 +1208,7 @@
1477
1208
  legend_manual_position = { x: constrained_x, y: constrained_y }
1478
1209
  }
1479
1210
 
1480
- function get_screen_coords(point: Point, data_series?: DataSeries): [number, number] {
1211
+ function get_screen_coords(point: Point, data_series?: DataSeries): Vec2 {
1481
1212
  // convert data coordinates to potentially non-finite screen coordinates
1482
1213
  const use_x2 = data_series?.x_axis === `x2`
1483
1214
  const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
@@ -1658,11 +1389,9 @@
1658
1389
  evt.preventDefault()
1659
1390
  fullscreen = false
1660
1391
  }
1661
- if (evt.key === `Shift`) shift_held = true
1662
- }}
1663
- onkeyup={(evt) => {
1664
- if (evt.key === `Shift`) shift_held = false
1392
+ pan_zoom.on_window_key_down(evt)
1665
1393
  }}
1394
+ onkeyup={pan_zoom.on_window_key_up}
1666
1395
  />
1667
1396
 
1668
1397
  <div
@@ -1689,46 +1418,27 @@
1689
1418
  ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1690
1419
  `Scatter plot`)}
1691
1420
  tabindex="0"
1692
- onfocusin={() => (is_focused = true)}
1693
- onfocusout={() => (is_focused = false)}
1421
+ onfocusin={() => pan_zoom.set_focused(true)}
1422
+ onfocusout={() => pan_zoom.set_focused(false)}
1694
1423
  onmouseenter={() => (hovered = true)}
1695
- onmousedown={handle_mouse_down}
1424
+ onmousedown={pan_zoom.on_mouse_down}
1696
1425
  onmousemove={(evt: MouseEvent) => {
1697
1426
  // Only find closest point if not actively dragging
1698
- 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)
1699
1428
  }}
1700
1429
  onmouseleave={() => {
1701
1430
  hovered = false
1702
1431
  tooltip_point = null
1703
1432
  on_point_hover?.(null)
1704
1433
  }}
1705
- ondblclick={() => {
1706
- // Reset to current auto ranges (not stale initial_*_range which may have expanded)
1707
- // This ensures lazy expansion restarts fresh from current data bounds
1708
- initial_x_range = [...auto_x_range] as [number, number]
1709
- initial_x2_range = [...auto_x2_range] as [number, number]
1710
- initial_y_range = [...auto_y_range] as [number, number]
1711
- initial_y2_range = [...auto_y2_range] as [number, number]
1712
- zoom_x_range = [...auto_x_range] as [number, number]
1713
- zoom_x2_range = [...auto_x2_range] as [number, number]
1714
- zoom_y_range = [...auto_y_range] as [number, number]
1715
- zoom_y2_range = get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2)
1716
- // Also reset axis props so future data changes recalculate auto ranges
1717
- x_axis = { ...x_axis, range: [null, null] }
1718
- x2_axis = { ...x2_axis, range: [null, null] }
1719
- y_axis = { ...y_axis, range: [null, null] }
1720
- y2_axis = { ...y2_axis, range: [null, null] }
1721
- }}
1722
- onwheel={handle_wheel}
1723
- ontouchstart={handle_touch_start}
1724
- ontouchmove={handle_touch_move}
1725
- ontouchend={handle_touch_end}
1726
- ontouchcancel={handle_touch_end}
1727
- style:cursor={pan_drag_state
1728
- ? `grabbing`
1729
- : shift_held && pan?.enabled !== false
1730
- ? `grab`
1731
- : `crosshair`}
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}
1732
1442
  >
1733
1443
  {@render user_content?.({
1734
1444
  height,
@@ -1859,7 +1569,7 @@
1859
1569
 
1860
1570
  <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
1861
1571
 
1862
- <ZoomRect start={drag_start_coords} current={drag_current_coords} />
1572
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
1863
1573
 
1864
1574
  <ZeroLines
1865
1575
  display={final_display}
@@ -1867,10 +1577,10 @@
1867
1577
  {x2_scale_fn}
1868
1578
  {y_scale_fn}
1869
1579
  {y2_scale_fn}
1870
- x_range={zoom_x_range}
1871
- x2_range={zoom_x2_range}
1872
- y_range={zoom_y_range}
1873
- y2_range={zoom_y2_range}
1580
+ x_range={ranges.current.x}
1581
+ x2_range={ranges.current.x2}
1582
+ y_range={ranges.current.y}
1583
+ y2_range={ranges.current.y2}
1874
1584
  x_scale_type={final_x_axis.scale_type}
1875
1585
  x2_scale_type={final_x2_axis.scale_type}
1876
1586
  y_scale_type={final_y_axis.scale_type}
@@ -2138,15 +1848,15 @@
2138
1848
  ] as Vec2}
2139
1849
  <div
2140
1850
  bind:this={colorbar_element}
2141
- onmouseenter={() => colorbar_hover.set_locked(true)}
2142
- onmouseleave={() => colorbar_hover.set_locked(false)}
1851
+ onmouseenter={() => colorbar_tween.set_locked(true)}
1852
+ onmouseleave={() => colorbar_tween.set_locked(false)}
2143
1853
  class="colorbar-wrapper"
2144
1854
  role="img"
2145
1855
  aria-label="Color scale legend"
2146
1856
  style={`${
2147
1857
  // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
2148
1858
  effective_cbar_wrapper_style ??
2149
- `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;`}
2150
1860
  >
2151
1861
  <ColorBar
2152
1862
  tick_labels={4}
@@ -2173,14 +1883,14 @@
2173
1883
  : legend_auto_outside
2174
1884
  ? legend_outside_x
2175
1885
  : legend_placement
2176
- ? tweened_legend_coords.current.x
1886
+ ? legend_tween.coords.current.x
2177
1887
  : default_x}
2178
1888
  {@const current_y = legend_is_dragging && legend_manual_position
2179
1889
  ? legend_manual_position.y
2180
1890
  : legend_auto_outside
2181
1891
  ? legend_outside_y
2182
1892
  : legend_placement
2183
- ? tweened_legend_coords.current.y
1893
+ ? legend_tween.coords.current.y
2184
1894
  : default_y}
2185
1895
  <PlotLegend
2186
1896
  bind:root_element={legend_element}
@@ -2188,7 +1898,7 @@
2188
1898
  on_drag_start={handle_legend_drag_start}
2189
1899
  on_drag={handle_legend_drag}
2190
1900
  on_drag_end={() => (legend_is_dragging = false)}
2191
- on_hover_change={legend_hover.set_locked}
1901
+ on_hover_change={legend_tween.set_locked}
2192
1902
  on_item_hover={(item) => {
2193
1903
  if (item?.item_type === `fill`) {
2194
1904
  // highlight the matching fill in the plot (same state plot fill-hover uses), but skip