matterviz 0.3.6 → 0.3.7

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 (863) hide show
  1. package/dist/EmptyState.svelte.d.ts +9 -0
  2. package/dist/FilePicker.svelte +360 -0
  3. package/dist/FilePicker.svelte.d.ts +17 -0
  4. package/dist/Icon.svelte.d.ts +13 -0
  5. package/dist/MillerIndexInput.svelte +66 -0
  6. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  7. package/dist/api/mp.d.ts +6 -0
  8. package/dist/api/mp.js +22 -0
  9. package/dist/api/optimade.d.ts +45 -0
  10. package/dist/api/optimade.js +135 -0
  11. package/dist/brillouin/BrillouinZone.svelte +549 -0
  12. package/dist/brillouin/BrillouinZone.svelte.d.ts +83 -0
  13. package/dist/brillouin/BrillouinZoneControls.svelte +144 -0
  14. package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +17 -0
  15. package/dist/brillouin/BrillouinZoneExportPane.svelte +146 -0
  16. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +15 -0
  17. package/dist/brillouin/BrillouinZoneInfoPane.svelte +146 -0
  18. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +13 -0
  19. package/dist/brillouin/BrillouinZoneScene.svelte +476 -0
  20. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +48 -0
  21. package/dist/brillouin/BrillouinZoneTooltip.svelte +92 -0
  22. package/dist/brillouin/BrillouinZoneTooltip.svelte.d.ts +8 -0
  23. package/dist/brillouin/compute.d.ts +17 -0
  24. package/dist/brillouin/compute.js +426 -0
  25. package/dist/brillouin/index.d.ts +8 -0
  26. package/dist/brillouin/index.js +7 -0
  27. package/dist/brillouin/types.d.ts +43 -0
  28. package/dist/brillouin/types.js +1 -0
  29. package/dist/chempot-diagram/ChemPotDiagram.svelte +327 -0
  30. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  31. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +846 -0
  32. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  33. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3193 -0
  34. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  35. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  36. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  37. package/dist/chempot-diagram/async-compute.svelte.js +78 -0
  38. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  39. package/dist/chempot-diagram/chempot-worker.js +11 -0
  40. package/dist/chempot-diagram/color.d.ts +10 -0
  41. package/dist/chempot-diagram/color.js +32 -0
  42. package/dist/chempot-diagram/compute.d.ts +48 -0
  43. package/dist/chempot-diagram/compute.js +806 -0
  44. package/dist/chempot-diagram/index.d.ts +6 -0
  45. package/dist/chempot-diagram/index.js +6 -0
  46. package/dist/chempot-diagram/pointer.d.ts +16 -0
  47. package/dist/chempot-diagram/pointer.js +40 -0
  48. package/dist/chempot-diagram/temperature.d.ts +15 -0
  49. package/dist/chempot-diagram/temperature.js +34 -0
  50. package/dist/chempot-diagram/types.d.ts +81 -0
  51. package/dist/chempot-diagram/types.js +28 -0
  52. package/dist/colors/index.d.ts +47 -0
  53. package/dist/colors/index.js +203 -0
  54. package/dist/composition/BarChart.svelte +297 -0
  55. package/dist/composition/BarChart.svelte.d.ts +39 -0
  56. package/dist/composition/BubbleChart.svelte +218 -0
  57. package/dist/composition/BubbleChart.svelte.d.ts +28 -0
  58. package/dist/composition/Composition.svelte +165 -0
  59. package/dist/composition/Composition.svelte.d.ts +15 -0
  60. package/dist/composition/Formula.svelte +268 -0
  61. package/dist/composition/Formula.svelte.d.ts +19 -0
  62. package/dist/composition/FormulaFilter.svelte +1257 -0
  63. package/dist/composition/FormulaFilter.svelte.d.ts +51 -0
  64. package/dist/composition/PieChart.svelte +323 -0
  65. package/dist/composition/PieChart.svelte.d.ts +37 -0
  66. package/dist/composition/format.d.ts +15 -0
  67. package/dist/composition/format.js +109 -0
  68. package/dist/composition/index.d.ts +20 -0
  69. package/dist/composition/index.js +14 -0
  70. package/dist/composition/parse.d.ts +56 -0
  71. package/dist/composition/parse.js +474 -0
  72. package/dist/constants.d.ts +29 -0
  73. package/dist/constants.js +99 -0
  74. package/dist/controls.d.ts +14 -0
  75. package/dist/controls.js +30 -0
  76. package/dist/convex-hull/ConvexHull.svelte +157 -0
  77. package/dist/convex-hull/ConvexHull.svelte.d.ts +13 -0
  78. package/dist/convex-hull/ConvexHull2D.svelte +825 -0
  79. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +11 -0
  80. package/dist/convex-hull/ConvexHull3D.svelte +1801 -0
  81. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +8 -0
  82. package/dist/convex-hull/ConvexHull4D.svelte +1398 -0
  83. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +8 -0
  84. package/dist/convex-hull/ConvexHullControls.svelte +535 -0
  85. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +48 -0
  86. package/dist/convex-hull/ConvexHullInfoPane.svelte +125 -0
  87. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +20 -0
  88. package/dist/convex-hull/ConvexHullStats.svelte +929 -0
  89. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +17 -0
  90. package/dist/convex-hull/ConvexHullTooltip.svelte +131 -0
  91. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +33 -0
  92. package/dist/convex-hull/GasPressureControls.svelte +247 -0
  93. package/dist/convex-hull/GasPressureControls.svelte.d.ts +11 -0
  94. package/dist/convex-hull/StructurePopup.svelte +151 -0
  95. package/dist/convex-hull/StructurePopup.svelte.d.ts +18 -0
  96. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +8 -0
  97. package/dist/convex-hull/barycentric-coords.d.ts +18 -0
  98. package/dist/convex-hull/barycentric-coords.js +182 -0
  99. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  100. package/dist/convex-hull/demo-temperature.js +40 -0
  101. package/dist/convex-hull/gas-thermodynamics.d.ts +16 -0
  102. package/dist/convex-hull/gas-thermodynamics.js +314 -0
  103. package/dist/convex-hull/helpers.d.ts +114 -0
  104. package/dist/convex-hull/helpers.js +710 -0
  105. package/dist/convex-hull/index.d.ts +119 -0
  106. package/dist/convex-hull/index.js +58 -0
  107. package/dist/convex-hull/thermodynamics.d.ts +67 -0
  108. package/dist/convex-hull/thermodynamics.js +1752 -0
  109. package/dist/convex-hull/types.d.ts +162 -0
  110. package/dist/convex-hull/types.js +36 -0
  111. package/dist/coordination/CoordinationBarPlot.svelte +311 -0
  112. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +30 -0
  113. package/dist/coordination/calc-coordination.d.ts +15 -0
  114. package/dist/coordination/calc-coordination.js +63 -0
  115. package/dist/coordination/index.d.ts +8 -0
  116. package/dist/coordination/index.js +7 -0
  117. package/dist/effects.svelte.d.ts +12 -0
  118. package/dist/effects.svelte.js +37 -0
  119. package/dist/element/BohrAtom.svelte.d.ts +20 -0
  120. package/dist/element/ElementHeading.svelte +26 -0
  121. package/dist/element/ElementHeading.svelte.d.ts +8 -0
  122. package/dist/element/ElementPhoto.svelte +57 -0
  123. package/dist/element/ElementPhoto.svelte.d.ts +9 -0
  124. package/dist/element/ElementStats.svelte +80 -0
  125. package/dist/element/ElementStats.svelte.d.ts +8 -0
  126. package/dist/element/ElementTile.svelte +484 -0
  127. package/dist/element/ElementTile.svelte.d.ts +29 -0
  128. package/dist/element/Nucleus.svelte.d.ts +17 -0
  129. package/dist/element/data.d.ts +2 -0
  130. package/dist/element/data.js +2 -0
  131. package/dist/element/index.d.ts +8 -0
  132. package/dist/element/index.js +7 -0
  133. package/dist/element/types.d.ts +57 -0
  134. package/dist/element/types.js +1 -0
  135. package/dist/feedback/ClickFeedback.svelte +58 -0
  136. package/dist/feedback/ClickFeedback.svelte.d.ts +12 -0
  137. package/dist/feedback/DragOverlay.svelte +42 -0
  138. package/dist/feedback/DragOverlay.svelte.d.ts +7 -0
  139. package/dist/feedback/Spinner.svelte.d.ts +7 -0
  140. package/dist/feedback/StatusMessage.svelte.d.ts +9 -0
  141. package/dist/feedback/index.d.ts +4 -0
  142. package/dist/feedback/index.js +4 -0
  143. package/dist/fermi-surface/FermiSlice.svelte +189 -0
  144. package/dist/fermi-surface/FermiSlice.svelte.d.ts +24 -0
  145. package/dist/fermi-surface/FermiSurface.svelte +600 -0
  146. package/dist/fermi-surface/FermiSurface.svelte.d.ts +83 -0
  147. package/dist/fermi-surface/FermiSurfaceControls.svelte +448 -0
  148. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +35 -0
  149. package/dist/fermi-surface/FermiSurfaceScene.svelte +794 -0
  150. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +50 -0
  151. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +111 -0
  152. package/dist/fermi-surface/FermiSurfaceTooltip.svelte.d.ts +8 -0
  153. package/dist/fermi-surface/compute.d.ts +5 -0
  154. package/dist/fermi-surface/compute.js +538 -0
  155. package/dist/fermi-surface/constants.d.ts +9 -0
  156. package/dist/fermi-surface/constants.js +27 -0
  157. package/dist/fermi-surface/export.d.ts +5 -0
  158. package/dist/fermi-surface/export.js +50 -0
  159. package/dist/fermi-surface/index.d.ts +12 -0
  160. package/dist/fermi-surface/index.js +13 -0
  161. package/dist/fermi-surface/marching-cubes.d.ts +2 -0
  162. package/dist/fermi-surface/marching-cubes.js +2 -0
  163. package/dist/fermi-surface/parse.d.ts +2 -0
  164. package/dist/fermi-surface/parse.js +491 -0
  165. package/dist/fermi-surface/symmetry.d.ts +3 -0
  166. package/dist/fermi-surface/symmetry.js +46 -0
  167. package/dist/fermi-surface/types.d.ts +110 -0
  168. package/dist/fermi-surface/types.js +4 -0
  169. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1545 -0
  170. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  171. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  172. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
  173. package/dist/heatmap-matrix/index.d.ts +53 -0
  174. package/dist/heatmap-matrix/index.js +100 -0
  175. package/dist/heatmap-matrix/shared.d.ts +2 -0
  176. package/dist/heatmap-matrix/shared.js +4 -0
  177. package/dist/icons.d.ts +569 -0
  178. package/dist/icons.js +648 -0
  179. package/dist/index.d.ts +39 -0
  180. package/dist/index.js +39 -0
  181. package/dist/io/decompress.d.ts +11 -0
  182. package/dist/io/decompress.js +74 -0
  183. package/dist/io/export.d.ts +16 -0
  184. package/dist/io/export.js +316 -0
  185. package/dist/io/fetch.d.ts +5 -0
  186. package/dist/io/fetch.js +39 -0
  187. package/dist/io/file-drop.d.ts +7 -0
  188. package/dist/io/file-drop.js +43 -0
  189. package/dist/io/index.d.ts +7 -0
  190. package/dist/io/index.js +6 -0
  191. package/dist/io/is-binary.d.ts +1 -0
  192. package/dist/io/is-binary.js +20 -0
  193. package/dist/io/types.d.ts +8 -0
  194. package/dist/io/types.js +1 -0
  195. package/dist/io/url-drop.d.ts +2 -0
  196. package/dist/io/url-drop.js +123 -0
  197. package/dist/isosurface/Isosurface.svelte +285 -0
  198. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  199. package/dist/isosurface/IsosurfaceControls.svelte +277 -0
  200. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  201. package/dist/isosurface/index.d.ts +5 -0
  202. package/dist/isosurface/index.js +6 -0
  203. package/dist/isosurface/parse.d.ts +6 -0
  204. package/dist/isosurface/parse.js +553 -0
  205. package/dist/isosurface/slice.d.ts +11 -0
  206. package/dist/isosurface/slice.js +140 -0
  207. package/dist/isosurface/types.d.ts +56 -0
  208. package/dist/isosurface/types.js +227 -0
  209. package/dist/labels.d.ts +53 -0
  210. package/dist/labels.js +277 -0
  211. package/dist/layout/FullscreenToggle.svelte +50 -0
  212. package/dist/layout/FullscreenToggle.svelte.d.ts +7 -0
  213. package/dist/layout/InfoCard.svelte +120 -0
  214. package/dist/layout/InfoCard.svelte.d.ts +21 -0
  215. package/dist/layout/InfoTag.svelte +185 -0
  216. package/dist/layout/InfoTag.svelte.d.ts +19 -0
  217. package/dist/layout/PropertyFilter.svelte +246 -0
  218. package/dist/layout/PropertyFilter.svelte.d.ts +24 -0
  219. package/dist/layout/SettingsSection.svelte +148 -0
  220. package/dist/layout/SettingsSection.svelte.d.ts +17 -0
  221. package/dist/layout/SubpageGrid.svelte +82 -0
  222. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  223. package/dist/layout/fullscreen.d.ts +9 -0
  224. package/dist/layout/fullscreen.js +53 -0
  225. package/dist/layout/index.d.ts +10 -0
  226. package/dist/layout/index.js +8 -0
  227. package/dist/layout/json-tree/JsonNode.svelte +548 -0
  228. package/dist/layout/json-tree/JsonNode.svelte.d.ts +11 -0
  229. package/dist/layout/json-tree/JsonTree.svelte +1230 -0
  230. package/dist/layout/json-tree/JsonTree.svelte.d.ts +6 -0
  231. package/dist/layout/json-tree/JsonValue.svelte.d.ts +9 -0
  232. package/dist/layout/json-tree/index.d.ts +3 -0
  233. package/dist/layout/json-tree/index.js +3 -0
  234. package/dist/layout/json-tree/types.d.ts +74 -0
  235. package/dist/layout/json-tree/types.js +2 -0
  236. package/dist/layout/json-tree/utils.d.ts +29 -0
  237. package/dist/layout/json-tree/utils.js +641 -0
  238. package/dist/marching-cubes.d.ts +14 -0
  239. package/dist/marching-cubes.js +540 -0
  240. package/dist/math.d.ts +101 -0
  241. package/dist/math.js +905 -0
  242. package/dist/overlays/ContextMenu.svelte +162 -0
  243. package/dist/overlays/ContextMenu.svelte.d.ts +25 -0
  244. package/dist/overlays/CopyButton.svelte +45 -0
  245. package/dist/overlays/CopyButton.svelte.d.ts +8 -0
  246. package/dist/overlays/DragControlTab.svelte +98 -0
  247. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  248. package/dist/overlays/DraggablePane.svelte +487 -0
  249. package/dist/overlays/DraggablePane.svelte.d.ts +36 -0
  250. package/dist/overlays/InfoPaneCards.svelte +149 -0
  251. package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
  252. package/dist/overlays/index.d.ts +3 -0
  253. package/dist/overlays/index.js +3 -0
  254. package/dist/periodic-table/PeriodicTable.svelte +469 -0
  255. package/dist/periodic-table/PeriodicTable.svelte.d.ts +55 -0
  256. package/dist/periodic-table/PeriodicTableControls.svelte +557 -0
  257. package/dist/periodic-table/PeriodicTableControls.svelte.d.ts +24 -0
  258. package/dist/periodic-table/PropertySelect.svelte +37 -0
  259. package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
  260. package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
  261. package/dist/periodic-table/index.d.ts +10 -0
  262. package/dist/periodic-table/index.js +4 -0
  263. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +1086 -0
  264. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +44 -0
  265. package/dist/phase-diagram/PhaseDiagramControls.svelte +444 -0
  266. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +30 -0
  267. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  268. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  269. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +184 -0
  270. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +19 -0
  271. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +391 -0
  272. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +16 -0
  273. package/dist/phase-diagram/TdbInfoPanel.svelte +203 -0
  274. package/dist/phase-diagram/TdbInfoPanel.svelte.d.ts +12 -0
  275. package/dist/phase-diagram/build-diagram.d.ts +11 -0
  276. package/dist/phase-diagram/build-diagram.js +160 -0
  277. package/dist/phase-diagram/colors.d.ts +35 -0
  278. package/dist/phase-diagram/colors.js +51 -0
  279. package/dist/phase-diagram/diagram-input.d.ts +29 -0
  280. package/dist/phase-diagram/diagram-input.js +3 -0
  281. package/dist/phase-diagram/index.d.ts +13 -0
  282. package/dist/phase-diagram/index.js +11 -0
  283. package/dist/phase-diagram/parse.d.ts +55 -0
  284. package/dist/phase-diagram/parse.js +272 -0
  285. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  286. package/dist/phase-diagram/svg-to-diagram.js +867 -0
  287. package/dist/phase-diagram/types.d.ts +93 -0
  288. package/dist/phase-diagram/types.js +1 -0
  289. package/dist/phase-diagram/utils.d.ts +118 -0
  290. package/dist/phase-diagram/utils.js +604 -0
  291. package/dist/plot/AxisLabel.svelte +51 -0
  292. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  293. package/dist/plot/BarPlot.svelte +2113 -0
  294. package/dist/plot/BarPlot.svelte.d.ts +84 -0
  295. package/dist/plot/BarPlotControls.svelte +66 -0
  296. package/dist/plot/BarPlotControls.svelte.d.ts +18 -0
  297. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  298. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  299. package/dist/plot/ColorBar.svelte +721 -0
  300. package/dist/plot/ColorBar.svelte.d.ts +31 -0
  301. package/dist/plot/ColorScaleSelect.svelte +54 -0
  302. package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
  303. package/dist/plot/ElementScatter.svelte +63 -0
  304. package/dist/plot/ElementScatter.svelte.d.ts +14 -0
  305. package/dist/plot/FillArea.svelte.d.ts +21 -0
  306. package/dist/plot/Histogram.svelte +1558 -0
  307. package/dist/plot/Histogram.svelte.d.ts +50 -0
  308. package/dist/plot/HistogramControls.svelte +212 -0
  309. package/dist/plot/HistogramControls.svelte.d.ts +22 -0
  310. package/dist/plot/InteractiveAxisLabel.svelte +96 -0
  311. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +14 -0
  312. package/dist/plot/Line.svelte +84 -0
  313. package/dist/plot/Line.svelte.d.ts +15 -0
  314. package/dist/plot/PlotAxis.svelte +169 -0
  315. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  316. package/dist/plot/PlotControls.svelte +537 -0
  317. package/dist/plot/PlotControls.svelte.d.ts +4 -0
  318. package/dist/plot/PlotLegend.svelte +569 -0
  319. package/dist/plot/PlotLegend.svelte.d.ts +29 -0
  320. package/dist/plot/PlotTooltip.svelte +67 -0
  321. package/dist/plot/PlotTooltip.svelte.d.ts +17 -0
  322. package/dist/plot/PortalSelect.svelte +253 -0
  323. package/dist/plot/PortalSelect.svelte.d.ts +16 -0
  324. package/dist/plot/ReferenceLine.svelte.d.ts +20 -0
  325. package/dist/plot/ReferenceLine3D.svelte +156 -0
  326. package/dist/plot/ReferenceLine3D.svelte.d.ts +14 -0
  327. package/dist/plot/ReferencePlane.svelte +175 -0
  328. package/dist/plot/ReferencePlane.svelte.d.ts +14 -0
  329. package/dist/plot/ScatterPlot.svelte +2778 -0
  330. package/dist/plot/ScatterPlot.svelte.d.ts +96 -0
  331. package/dist/plot/ScatterPlot3D.svelte +529 -0
  332. package/dist/plot/ScatterPlot3D.svelte.d.ts +95 -0
  333. package/dist/plot/ScatterPlot3DControls.svelte +437 -0
  334. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +20 -0
  335. package/dist/plot/ScatterPlot3DScene.svelte +912 -0
  336. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +74 -0
  337. package/dist/plot/ScatterPlotControls.svelte +306 -0
  338. package/dist/plot/ScatterPlotControls.svelte.d.ts +17 -0
  339. package/dist/plot/ScatterPoint.svelte +182 -0
  340. package/dist/plot/ScatterPoint.svelte.d.ts +22 -0
  341. package/dist/plot/SpacegroupBarPlot.svelte +293 -0
  342. package/dist/plot/SpacegroupBarPlot.svelte.d.ts +9 -0
  343. package/dist/plot/Surface3D.svelte +197 -0
  344. package/dist/plot/Surface3D.svelte.d.ts +13 -0
  345. package/dist/plot/ZeroLines.svelte +97 -0
  346. package/dist/plot/ZeroLines.svelte.d.ts +33 -0
  347. package/dist/plot/ZoomRect.svelte +23 -0
  348. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  349. package/dist/plot/adaptive-density.d.ts +69 -0
  350. package/dist/plot/adaptive-density.js +191 -0
  351. package/dist/plot/auto-place.d.ts +43 -0
  352. package/dist/plot/auto-place.js +122 -0
  353. package/dist/plot/axis-utils.d.ts +19 -0
  354. package/dist/plot/axis-utils.js +78 -0
  355. package/dist/plot/binned-scatter-types.d.ts +59 -0
  356. package/dist/plot/binned-scatter-types.js +1 -0
  357. package/dist/plot/data-cleaning.d.ts +37 -0
  358. package/dist/plot/data-cleaning.js +855 -0
  359. package/dist/plot/data-transform.d.ts +16 -0
  360. package/dist/plot/data-transform.js +45 -0
  361. package/dist/plot/defaults.d.ts +19 -0
  362. package/dist/plot/defaults.js +9 -0
  363. package/dist/plot/fill-utils.d.ts +46 -0
  364. package/dist/plot/fill-utils.js +322 -0
  365. package/dist/plot/hover-lock.svelte.d.ts +14 -0
  366. package/dist/plot/hover-lock.svelte.js +46 -0
  367. package/dist/plot/index.d.ts +41 -0
  368. package/dist/plot/index.js +39 -0
  369. package/dist/plot/interactions.d.ts +12 -0
  370. package/dist/plot/interactions.js +101 -0
  371. package/dist/plot/layout.d.ts +78 -0
  372. package/dist/plot/layout.js +273 -0
  373. package/dist/plot/reference-line.d.ts +60 -0
  374. package/dist/plot/reference-line.js +314 -0
  375. package/dist/plot/scales.d.ts +48 -0
  376. package/dist/plot/scales.js +481 -0
  377. package/dist/plot/svg.d.ts +1 -0
  378. package/dist/plot/svg.js +11 -0
  379. package/dist/plot/types.d.ts +831 -0
  380. package/dist/plot/types.js +99 -0
  381. package/dist/plot/utils/label-placement.d.ts +68 -0
  382. package/dist/plot/utils/label-placement.js +326 -0
  383. package/dist/plot/utils/series-visibility.d.ts +15 -0
  384. package/dist/plot/utils/series-visibility.js +85 -0
  385. package/dist/plot/utils.d.ts +1 -0
  386. package/dist/plot/utils.js +14 -0
  387. package/dist/rdf/RdfPlot.svelte +247 -0
  388. package/dist/rdf/RdfPlot.svelte.d.ts +27 -0
  389. package/dist/rdf/calc-rdf.d.ts +4 -0
  390. package/dist/rdf/calc-rdf.js +111 -0
  391. package/dist/rdf/index.d.ts +23 -0
  392. package/dist/rdf/index.js +2 -0
  393. package/dist/sanitize.d.ts +6 -0
  394. package/dist/sanitize.js +116 -0
  395. package/dist/settings.d.ts +255 -0
  396. package/dist/settings.js +1132 -0
  397. package/dist/spectral/Bands.svelte +1040 -0
  398. package/dist/spectral/Bands.svelte.d.ts +40 -0
  399. package/dist/spectral/BandsAndDos.svelte +134 -0
  400. package/dist/spectral/BandsAndDos.svelte.d.ts +18 -0
  401. package/dist/spectral/BrillouinBandsDos.svelte +252 -0
  402. package/dist/spectral/BrillouinBandsDos.svelte.d.ts +20 -0
  403. package/dist/spectral/Dos.svelte +697 -0
  404. package/dist/spectral/Dos.svelte.d.ts +29 -0
  405. package/dist/spectral/helpers.d.ts +119 -0
  406. package/dist/spectral/helpers.js +1032 -0
  407. package/dist/spectral/index.d.ts +6 -0
  408. package/dist/spectral/index.js +6 -0
  409. package/dist/spectral/types.d.ts +84 -0
  410. package/dist/spectral/types.js +2 -0
  411. package/dist/state.svelte.d.ts +25 -0
  412. package/dist/state.svelte.js +45 -0
  413. package/dist/structure/Arrow.svelte +72 -0
  414. package/dist/structure/Arrow.svelte.d.ts +15 -0
  415. package/dist/structure/AtomLegend.svelte +815 -0
  416. package/dist/structure/AtomLegend.svelte.d.ts +35 -0
  417. package/dist/structure/Bond.svelte +140 -0
  418. package/dist/structure/Bond.svelte.d.ts +9 -0
  419. package/dist/structure/CanvasTooltip.svelte +33 -0
  420. package/dist/structure/CanvasTooltip.svelte.d.ts +12 -0
  421. package/dist/structure/CellSelect.svelte +349 -0
  422. package/dist/structure/CellSelect.svelte.d.ts +13 -0
  423. package/dist/structure/Cylinder.svelte +45 -0
  424. package/dist/structure/Cylinder.svelte.d.ts +10 -0
  425. package/dist/structure/Lattice.svelte +196 -0
  426. package/dist/structure/Lattice.svelte.d.ts +17 -0
  427. package/dist/structure/Structure.svelte +2248 -0
  428. package/dist/structure/Structure.svelte.d.ts +89 -0
  429. package/dist/structure/StructureControls.svelte +1273 -0
  430. package/dist/structure/StructureControls.svelte.d.ts +31 -0
  431. package/dist/structure/StructureExportPane.svelte +252 -0
  432. package/dist/structure/StructureExportPane.svelte.d.ts +17 -0
  433. package/dist/structure/StructureInfoPane.svelte +737 -0
  434. package/dist/structure/StructureInfoPane.svelte.d.ts +19 -0
  435. package/dist/structure/StructureScene.svelte +2255 -0
  436. package/dist/structure/StructureScene.svelte.d.ts +111 -0
  437. package/dist/structure/atom-properties.d.ts +37 -0
  438. package/dist/structure/atom-properties.js +200 -0
  439. package/dist/structure/bond-order-perception.d.ts +13 -0
  440. package/dist/structure/bond-order-perception.js +384 -0
  441. package/dist/structure/bonding.d.ts +68 -0
  442. package/dist/structure/bonding.js +696 -0
  443. package/dist/structure/export.d.ts +20 -0
  444. package/dist/structure/export.js +727 -0
  445. package/dist/structure/index.d.ts +126 -0
  446. package/dist/structure/index.js +169 -0
  447. package/dist/structure/label-placement.d.ts +14 -0
  448. package/dist/structure/label-placement.js +72 -0
  449. package/dist/structure/measure.d.ts +6 -0
  450. package/dist/structure/measure.js +29 -0
  451. package/dist/structure/parse.d.ts +66 -0
  452. package/dist/structure/parse.js +1392 -0
  453. package/dist/structure/partial-occupancy.d.ts +25 -0
  454. package/dist/structure/partial-occupancy.js +99 -0
  455. package/dist/structure/pbc.d.ts +9 -0
  456. package/dist/structure/pbc.js +123 -0
  457. package/dist/structure/supercell.d.ts +8 -0
  458. package/dist/structure/supercell.js +170 -0
  459. package/dist/structure/validation.d.ts +2 -0
  460. package/dist/structure/validation.js +10 -0
  461. package/dist/symmetry/SymmetryStats.svelte +226 -0
  462. package/dist/symmetry/SymmetryStats.svelte.d.ts +21 -0
  463. package/dist/symmetry/WyckoffTable.svelte +120 -0
  464. package/dist/symmetry/WyckoffTable.svelte.d.ts +11 -0
  465. package/dist/symmetry/cell-transform.d.ts +12 -0
  466. package/dist/symmetry/cell-transform.js +91 -0
  467. package/dist/symmetry/index.d.ts +43 -0
  468. package/dist/symmetry/index.js +228 -0
  469. package/dist/symmetry/spacegroups.d.ts +9 -0
  470. package/dist/symmetry/spacegroups.js +394 -0
  471. package/dist/table/HeatmapTable.svelte +1833 -0
  472. package/dist/table/HeatmapTable.svelte.d.ts +49 -0
  473. package/dist/table/ToggleMenu.svelte +385 -0
  474. package/dist/table/ToggleMenu.svelte.d.ts +11 -0
  475. package/dist/table/index.d.ts +74 -0
  476. package/dist/table/index.js +38 -0
  477. package/dist/theme/ThemeControl.svelte +53 -0
  478. package/dist/theme/ThemeControl.svelte.d.ts +9 -0
  479. package/dist/theme/index.d.ts +29 -0
  480. package/dist/theme/index.js +79 -0
  481. package/dist/time.d.ts +4 -0
  482. package/dist/time.js +70 -0
  483. package/dist/tooltip/TooltipContent.svelte +58 -0
  484. package/dist/tooltip/TooltipContent.svelte.d.ts +31 -0
  485. package/dist/tooltip/index.d.ts +2 -0
  486. package/dist/tooltip/index.js +1 -0
  487. package/dist/tooltip/types.d.ts +8 -0
  488. package/dist/tooltip/types.js +1 -0
  489. package/dist/trajectory/Trajectory.svelte +1545 -0
  490. package/dist/trajectory/Trajectory.svelte.d.ts +77 -0
  491. package/dist/trajectory/TrajectoryError.svelte +128 -0
  492. package/dist/trajectory/TrajectoryError.svelte.d.ts +13 -0
  493. package/dist/trajectory/TrajectoryExportPane.svelte +357 -0
  494. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +17 -0
  495. package/dist/trajectory/TrajectoryInfoPane.svelte +313 -0
  496. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +17 -0
  497. package/dist/trajectory/constants.d.ts +6 -0
  498. package/dist/trajectory/constants.js +7 -0
  499. package/dist/trajectory/extract.d.ts +5 -0
  500. package/dist/trajectory/extract.js +162 -0
  501. package/dist/trajectory/format-detect.d.ts +9 -0
  502. package/dist/trajectory/format-detect.js +76 -0
  503. package/dist/trajectory/frame-reader.d.ts +17 -0
  504. package/dist/trajectory/frame-reader.js +332 -0
  505. package/dist/trajectory/helpers.d.ts +15 -0
  506. package/dist/trajectory/helpers.js +164 -0
  507. package/dist/trajectory/index.d.ts +63 -0
  508. package/dist/trajectory/index.js +126 -0
  509. package/dist/trajectory/parse/ase.d.ts +2 -0
  510. package/dist/trajectory/parse/ase.js +73 -0
  511. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  512. package/dist/trajectory/parse/hdf5.js +127 -0
  513. package/dist/trajectory/parse/index.d.ts +12 -0
  514. package/dist/trajectory/parse/index.js +298 -0
  515. package/dist/trajectory/parse/lammps.d.ts +5 -0
  516. package/dist/trajectory/parse/lammps.js +179 -0
  517. package/dist/trajectory/parse/vasp.d.ts +2 -0
  518. package/dist/trajectory/parse/vasp.js +68 -0
  519. package/dist/trajectory/parse/xyz.d.ts +2 -0
  520. package/dist/trajectory/parse/xyz.js +110 -0
  521. package/dist/trajectory/plotting.d.ts +28 -0
  522. package/dist/trajectory/plotting.js +423 -0
  523. package/dist/trajectory/types.d.ts +11 -0
  524. package/dist/trajectory/types.js +1 -0
  525. package/dist/utils.d.ts +6 -0
  526. package/dist/utils.js +45 -0
  527. package/dist/xrd/XrdPlot.svelte +615 -0
  528. package/dist/xrd/XrdPlot.svelte.d.ts +28 -0
  529. package/dist/xrd/broadening.d.ts +20 -0
  530. package/dist/xrd/broadening.js +97 -0
  531. package/dist/xrd/calc-xrd.d.ts +37 -0
  532. package/dist/xrd/calc-xrd.js +336 -0
  533. package/dist/xrd/index.d.ts +37 -0
  534. package/dist/xrd/index.js +4 -0
  535. package/dist/xrd/parse.d.ts +13 -0
  536. package/dist/xrd/parse.js +749 -0
  537. package/license +1 -1
  538. package/package.json +232 -1457
  539. package/readme.md +98 -171
  540. package/.vscode/launch.json +0 -13
  541. package/.vscodeignore +0 -7
  542. package/dist/assets/STLExporter-BpTH3YHE.js +0 -8
  543. package/dist/assets/browser-DdDecX_W.js +0 -1
  544. package/dist/assets/export-qgn-H9y6.js +0 -2
  545. package/dist/assets/main-DiKYzti2.css +0 -1
  546. package/dist/assets/moyo_wasm_bg-0ocwg7xY.wasm +0 -0
  547. package/dist/extension.js +0 -31293
  548. package/dist/src/lib/FilePicker.svelte +0 -360
  549. package/dist/src/lib/MillerIndexInput.svelte +0 -66
  550. package/dist/src/lib/api/mp.ts +0 -26
  551. package/dist/src/lib/api/optimade.ts +0 -204
  552. package/dist/src/lib/brillouin/BrillouinZone.svelte +0 -549
  553. package/dist/src/lib/brillouin/BrillouinZoneControls.svelte +0 -144
  554. package/dist/src/lib/brillouin/BrillouinZoneExportPane.svelte +0 -146
  555. package/dist/src/lib/brillouin/BrillouinZoneInfoPane.svelte +0 -146
  556. package/dist/src/lib/brillouin/BrillouinZoneScene.svelte +0 -476
  557. package/dist/src/lib/brillouin/BrillouinZoneTooltip.svelte +0 -92
  558. package/dist/src/lib/brillouin/compute.ts +0 -529
  559. package/dist/src/lib/brillouin/index.ts +0 -8
  560. package/dist/src/lib/brillouin/types.ts +0 -51
  561. package/dist/src/lib/chempot-diagram/ChemPotDiagram.svelte +0 -327
  562. package/dist/src/lib/chempot-diagram/ChemPotDiagram2D.svelte +0 -846
  563. package/dist/src/lib/chempot-diagram/ChemPotDiagram3D.svelte +0 -3193
  564. package/dist/src/lib/chempot-diagram/async-compute.svelte.ts +0 -94
  565. package/dist/src/lib/chempot-diagram/chempot-worker.ts +0 -11
  566. package/dist/src/lib/chempot-diagram/color.ts +0 -42
  567. package/dist/src/lib/chempot-diagram/compute.ts +0 -1014
  568. package/dist/src/lib/chempot-diagram/index.ts +0 -6
  569. package/dist/src/lib/chempot-diagram/pointer.ts +0 -56
  570. package/dist/src/lib/chempot-diagram/temperature.ts +0 -77
  571. package/dist/src/lib/chempot-diagram/types.ts +0 -130
  572. package/dist/src/lib/colors/index.ts +0 -249
  573. package/dist/src/lib/composition/BarChart.svelte +0 -297
  574. package/dist/src/lib/composition/BubbleChart.svelte +0 -218
  575. package/dist/src/lib/composition/Composition.svelte +0 -165
  576. package/dist/src/lib/composition/Formula.svelte +0 -268
  577. package/dist/src/lib/composition/FormulaFilter.svelte +0 -1257
  578. package/dist/src/lib/composition/PieChart.svelte +0 -323
  579. package/dist/src/lib/composition/format.ts +0 -155
  580. package/dist/src/lib/composition/index.ts +0 -37
  581. package/dist/src/lib/composition/parse.ts +0 -605
  582. package/dist/src/lib/constants.ts +0 -134
  583. package/dist/src/lib/controls.ts +0 -42
  584. package/dist/src/lib/convex-hull/ConvexHull.svelte +0 -157
  585. package/dist/src/lib/convex-hull/ConvexHull2D.svelte +0 -825
  586. package/dist/src/lib/convex-hull/ConvexHull3D.svelte +0 -1801
  587. package/dist/src/lib/convex-hull/ConvexHull4D.svelte +0 -1398
  588. package/dist/src/lib/convex-hull/ConvexHullControls.svelte +0 -535
  589. package/dist/src/lib/convex-hull/ConvexHullInfoPane.svelte +0 -125
  590. package/dist/src/lib/convex-hull/ConvexHullStats.svelte +0 -929
  591. package/dist/src/lib/convex-hull/ConvexHullTooltip.svelte +0 -131
  592. package/dist/src/lib/convex-hull/GasPressureControls.svelte +0 -247
  593. package/dist/src/lib/convex-hull/StructurePopup.svelte +0 -151
  594. package/dist/src/lib/convex-hull/barycentric-coords.ts +0 -246
  595. package/dist/src/lib/convex-hull/demo-temperature.ts +0 -63
  596. package/dist/src/lib/convex-hull/gas-thermodynamics.ts +0 -405
  597. package/dist/src/lib/convex-hull/helpers.ts +0 -932
  598. package/dist/src/lib/convex-hull/index.ts +0 -202
  599. package/dist/src/lib/convex-hull/thermodynamics.ts +0 -2192
  600. package/dist/src/lib/convex-hull/types.ts +0 -267
  601. package/dist/src/lib/coordination/CoordinationBarPlot.svelte +0 -311
  602. package/dist/src/lib/coordination/calc-coordination.ts +0 -93
  603. package/dist/src/lib/coordination/index.ts +0 -9
  604. package/dist/src/lib/effects.svelte.ts +0 -48
  605. package/dist/src/lib/element/ElementHeading.svelte +0 -26
  606. package/dist/src/lib/element/ElementPhoto.svelte +0 -57
  607. package/dist/src/lib/element/ElementStats.svelte +0 -80
  608. package/dist/src/lib/element/ElementTile.svelte +0 -484
  609. package/dist/src/lib/element/data.ts +0 -14
  610. package/dist/src/lib/element/index.ts +0 -8
  611. package/dist/src/lib/element/types.ts +0 -62
  612. package/dist/src/lib/feedback/ClickFeedback.svelte +0 -58
  613. package/dist/src/lib/feedback/DragOverlay.svelte +0 -42
  614. package/dist/src/lib/feedback/index.ts +0 -4
  615. package/dist/src/lib/fermi-surface/FermiSlice.svelte +0 -189
  616. package/dist/src/lib/fermi-surface/FermiSurface.svelte +0 -600
  617. package/dist/src/lib/fermi-surface/FermiSurfaceControls.svelte +0 -448
  618. package/dist/src/lib/fermi-surface/FermiSurfaceScene.svelte +0 -794
  619. package/dist/src/lib/fermi-surface/FermiSurfaceTooltip.svelte +0 -111
  620. package/dist/src/lib/fermi-surface/compute.ts +0 -728
  621. package/dist/src/lib/fermi-surface/constants.ts +0 -32
  622. package/dist/src/lib/fermi-surface/export.ts +0 -64
  623. package/dist/src/lib/fermi-surface/index.ts +0 -14
  624. package/dist/src/lib/fermi-surface/marching-cubes.ts +0 -3
  625. package/dist/src/lib/fermi-surface/parse.ts +0 -574
  626. package/dist/src/lib/fermi-surface/symmetry.ts +0 -56
  627. package/dist/src/lib/fermi-surface/types.ts +0 -159
  628. package/dist/src/lib/heatmap-matrix/HeatmapMatrix.svelte +0 -1545
  629. package/dist/src/lib/heatmap-matrix/HeatmapMatrixControls.svelte +0 -225
  630. package/dist/src/lib/heatmap-matrix/index.ts +0 -167
  631. package/dist/src/lib/heatmap-matrix/shared.ts +0 -7
  632. package/dist/src/lib/icons.ts +0 -650
  633. package/dist/src/lib/index.ts +0 -61
  634. package/dist/src/lib/io/decompress.ts +0 -92
  635. package/dist/src/lib/io/export.ts +0 -385
  636. package/dist/src/lib/io/fetch.ts +0 -46
  637. package/dist/src/lib/io/file-drop.ts +0 -51
  638. package/dist/src/lib/io/index.ts +0 -7
  639. package/dist/src/lib/io/is-binary.ts +0 -24
  640. package/dist/src/lib/io/types.ts +0 -8
  641. package/dist/src/lib/io/url-drop.ts +0 -141
  642. package/dist/src/lib/isosurface/Isosurface.svelte +0 -285
  643. package/dist/src/lib/isosurface/IsosurfaceControls.svelte +0 -277
  644. package/dist/src/lib/isosurface/index.ts +0 -7
  645. package/dist/src/lib/isosurface/parse.ts +0 -656
  646. package/dist/src/lib/isosurface/slice.ts +0 -175
  647. package/dist/src/lib/isosurface/types.ts +0 -309
  648. package/dist/src/lib/labels.ts +0 -320
  649. package/dist/src/lib/layout/FullscreenToggle.svelte +0 -50
  650. package/dist/src/lib/layout/InfoCard.svelte +0 -120
  651. package/dist/src/lib/layout/InfoTag.svelte +0 -185
  652. package/dist/src/lib/layout/PropertyFilter.svelte +0 -246
  653. package/dist/src/lib/layout/SettingsSection.svelte +0 -148
  654. package/dist/src/lib/layout/SubpageGrid.svelte +0 -82
  655. package/dist/src/lib/layout/fullscreen.ts +0 -65
  656. package/dist/src/lib/layout/index.ts +0 -11
  657. package/dist/src/lib/layout/json-tree/JsonNode.svelte +0 -548
  658. package/dist/src/lib/layout/json-tree/JsonTree.svelte +0 -1230
  659. package/dist/src/lib/layout/json-tree/index.ts +0 -3
  660. package/dist/src/lib/layout/json-tree/types.ts +0 -126
  661. package/dist/src/lib/layout/json-tree/utils.ts +0 -682
  662. package/dist/src/lib/marching-cubes.ts +0 -614
  663. package/dist/src/lib/math.ts +0 -1081
  664. package/dist/src/lib/overlays/ContextMenu.svelte +0 -162
  665. package/dist/src/lib/overlays/CopyButton.svelte +0 -45
  666. package/dist/src/lib/overlays/DragControlTab.svelte +0 -98
  667. package/dist/src/lib/overlays/DraggablePane.svelte +0 -487
  668. package/dist/src/lib/overlays/InfoPaneCards.svelte +0 -149
  669. package/dist/src/lib/overlays/index.ts +0 -3
  670. package/dist/src/lib/periodic-table/PeriodicTable.svelte +0 -469
  671. package/dist/src/lib/periodic-table/PeriodicTableControls.svelte +0 -557
  672. package/dist/src/lib/periodic-table/PropertySelect.svelte +0 -37
  673. package/dist/src/lib/periodic-table/index.ts +0 -12
  674. package/dist/src/lib/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +0 -1086
  675. package/dist/src/lib/phase-diagram/PhaseDiagramControls.svelte +0 -444
  676. package/dist/src/lib/phase-diagram/PhaseDiagramEditorPane.svelte +0 -126
  677. package/dist/src/lib/phase-diagram/PhaseDiagramExportPane.svelte +0 -184
  678. package/dist/src/lib/phase-diagram/PhaseDiagramTooltip.svelte +0 -391
  679. package/dist/src/lib/phase-diagram/TdbInfoPanel.svelte +0 -203
  680. package/dist/src/lib/phase-diagram/build-diagram.ts +0 -186
  681. package/dist/src/lib/phase-diagram/colors.ts +0 -58
  682. package/dist/src/lib/phase-diagram/diagram-input.ts +0 -40
  683. package/dist/src/lib/phase-diagram/index.ts +0 -13
  684. package/dist/src/lib/phase-diagram/parse.ts +0 -348
  685. package/dist/src/lib/phase-diagram/svg-to-diagram.ts +0 -1023
  686. package/dist/src/lib/phase-diagram/types.ts +0 -144
  687. package/dist/src/lib/phase-diagram/utils.ts +0 -775
  688. package/dist/src/lib/plot/AxisLabel.svelte +0 -51
  689. package/dist/src/lib/plot/BarPlot.svelte +0 -2113
  690. package/dist/src/lib/plot/BarPlotControls.svelte +0 -66
  691. package/dist/src/lib/plot/BinnedScatterPlot.svelte +0 -1114
  692. package/dist/src/lib/plot/ColorBar.svelte +0 -721
  693. package/dist/src/lib/plot/ColorScaleSelect.svelte +0 -54
  694. package/dist/src/lib/plot/ElementScatter.svelte +0 -63
  695. package/dist/src/lib/plot/Histogram.svelte +0 -1558
  696. package/dist/src/lib/plot/HistogramControls.svelte +0 -212
  697. package/dist/src/lib/plot/InteractiveAxisLabel.svelte +0 -96
  698. package/dist/src/lib/plot/Line.svelte +0 -84
  699. package/dist/src/lib/plot/PlotAxis.svelte +0 -169
  700. package/dist/src/lib/plot/PlotControls.svelte +0 -537
  701. package/dist/src/lib/plot/PlotLegend.svelte +0 -569
  702. package/dist/src/lib/plot/PlotTooltip.svelte +0 -67
  703. package/dist/src/lib/plot/PortalSelect.svelte +0 -253
  704. package/dist/src/lib/plot/ReferenceLine3D.svelte +0 -156
  705. package/dist/src/lib/plot/ReferencePlane.svelte +0 -175
  706. package/dist/src/lib/plot/ScatterPlot.svelte +0 -2778
  707. package/dist/src/lib/plot/ScatterPlot3D.svelte +0 -529
  708. package/dist/src/lib/plot/ScatterPlot3DControls.svelte +0 -437
  709. package/dist/src/lib/plot/ScatterPlot3DScene.svelte +0 -912
  710. package/dist/src/lib/plot/ScatterPlotControls.svelte +0 -306
  711. package/dist/src/lib/plot/ScatterPoint.svelte +0 -182
  712. package/dist/src/lib/plot/SpacegroupBarPlot.svelte +0 -293
  713. package/dist/src/lib/plot/Surface3D.svelte +0 -197
  714. package/dist/src/lib/plot/ZeroLines.svelte +0 -97
  715. package/dist/src/lib/plot/ZoomRect.svelte +0 -23
  716. package/dist/src/lib/plot/adaptive-density.ts +0 -316
  717. package/dist/src/lib/plot/auto-place.ts +0 -184
  718. package/dist/src/lib/plot/axis-utils.ts +0 -122
  719. package/dist/src/lib/plot/binned-scatter-types.ts +0 -83
  720. package/dist/src/lib/plot/data-cleaning.ts +0 -1069
  721. package/dist/src/lib/plot/data-transform.ts +0 -69
  722. package/dist/src/lib/plot/defaults.ts +0 -9
  723. package/dist/src/lib/plot/fill-utils.ts +0 -494
  724. package/dist/src/lib/plot/hover-lock.svelte.ts +0 -60
  725. package/dist/src/lib/plot/index.ts +0 -53
  726. package/dist/src/lib/plot/interactions.ts +0 -119
  727. package/dist/src/lib/plot/layout.ts +0 -425
  728. package/dist/src/lib/plot/reference-line.ts +0 -426
  729. package/dist/src/lib/plot/scales.ts +0 -654
  730. package/dist/src/lib/plot/svg.ts +0 -23
  731. package/dist/src/lib/plot/types.ts +0 -1144
  732. package/dist/src/lib/plot/utils/label-placement.ts +0 -541
  733. package/dist/src/lib/plot/utils/series-visibility.ts +0 -140
  734. package/dist/src/lib/plot/utils.ts +0 -11
  735. package/dist/src/lib/rdf/RdfPlot.svelte +0 -247
  736. package/dist/src/lib/rdf/calc-rdf.ts +0 -167
  737. package/dist/src/lib/rdf/index.ts +0 -27
  738. package/dist/src/lib/sanitize.ts +0 -126
  739. package/dist/src/lib/settings.ts +0 -1479
  740. package/dist/src/lib/spectral/Bands.svelte +0 -1040
  741. package/dist/src/lib/spectral/BandsAndDos.svelte +0 -134
  742. package/dist/src/lib/spectral/BrillouinBandsDos.svelte +0 -252
  743. package/dist/src/lib/spectral/Dos.svelte +0 -697
  744. package/dist/src/lib/spectral/helpers.ts +0 -1381
  745. package/dist/src/lib/spectral/index.ts +0 -8
  746. package/dist/src/lib/spectral/types.ts +0 -112
  747. package/dist/src/lib/state.svelte.ts +0 -64
  748. package/dist/src/lib/structure/Arrow.svelte +0 -72
  749. package/dist/src/lib/structure/AtomLegend.svelte +0 -815
  750. package/dist/src/lib/structure/Bond.svelte +0 -140
  751. package/dist/src/lib/structure/CanvasTooltip.svelte +0 -33
  752. package/dist/src/lib/structure/CellSelect.svelte +0 -349
  753. package/dist/src/lib/structure/Cylinder.svelte +0 -45
  754. package/dist/src/lib/structure/Lattice.svelte +0 -196
  755. package/dist/src/lib/structure/Structure.svelte +0 -2248
  756. package/dist/src/lib/structure/StructureControls.svelte +0 -1273
  757. package/dist/src/lib/structure/StructureExportPane.svelte +0 -252
  758. package/dist/src/lib/structure/StructureInfoPane.svelte +0 -737
  759. package/dist/src/lib/structure/StructureScene.svelte +0 -2255
  760. package/dist/src/lib/structure/atom-properties.ts +0 -316
  761. package/dist/src/lib/structure/bond-order-perception.ts +0 -447
  762. package/dist/src/lib/structure/bonding.ts +0 -944
  763. package/dist/src/lib/structure/export.ts +0 -861
  764. package/dist/src/lib/structure/index.ts +0 -291
  765. package/dist/src/lib/structure/label-placement.ts +0 -130
  766. package/dist/src/lib/structure/measure.ts +0 -45
  767. package/dist/src/lib/structure/parse.ts +0 -1705
  768. package/dist/src/lib/structure/partial-occupancy.ts +0 -183
  769. package/dist/src/lib/structure/pbc.ts +0 -164
  770. package/dist/src/lib/structure/supercell.ts +0 -226
  771. package/dist/src/lib/structure/validation.ts +0 -11
  772. package/dist/src/lib/symmetry/SymmetryStats.svelte +0 -226
  773. package/dist/src/lib/symmetry/WyckoffTable.svelte +0 -120
  774. package/dist/src/lib/symmetry/cell-transform.ts +0 -118
  775. package/dist/src/lib/symmetry/index.ts +0 -348
  776. package/dist/src/lib/symmetry/spacegroups.ts +0 -404
  777. package/dist/src/lib/table/HeatmapTable.svelte +0 -1833
  778. package/dist/src/lib/table/ToggleMenu.svelte +0 -385
  779. package/dist/src/lib/table/index.ts +0 -139
  780. package/dist/src/lib/theme/ThemeControl.svelte +0 -53
  781. package/dist/src/lib/theme/index.ts +0 -107
  782. package/dist/src/lib/time.ts +0 -71
  783. package/dist/src/lib/tooltip/TooltipContent.svelte +0 -58
  784. package/dist/src/lib/tooltip/index.ts +0 -2
  785. package/dist/src/lib/tooltip/types.ts +0 -13
  786. package/dist/src/lib/trajectory/Trajectory.svelte +0 -1545
  787. package/dist/src/lib/trajectory/TrajectoryError.svelte +0 -128
  788. package/dist/src/lib/trajectory/TrajectoryExportPane.svelte +0 -357
  789. package/dist/src/lib/trajectory/TrajectoryInfoPane.svelte +0 -313
  790. package/dist/src/lib/trajectory/constants.ts +0 -7
  791. package/dist/src/lib/trajectory/extract.ts +0 -196
  792. package/dist/src/lib/trajectory/format-detect.ts +0 -96
  793. package/dist/src/lib/trajectory/frame-reader.ts +0 -456
  794. package/dist/src/lib/trajectory/helpers.ts +0 -217
  795. package/dist/src/lib/trajectory/index.ts +0 -218
  796. package/dist/src/lib/trajectory/parse/ase.ts +0 -109
  797. package/dist/src/lib/trajectory/parse/hdf5.ts +0 -173
  798. package/dist/src/lib/trajectory/parse/index.ts +0 -411
  799. package/dist/src/lib/trajectory/parse/lammps.ts +0 -215
  800. package/dist/src/lib/trajectory/parse/vasp.ts +0 -102
  801. package/dist/src/lib/trajectory/parse/xyz.ts +0 -143
  802. package/dist/src/lib/trajectory/plotting.ts +0 -599
  803. package/dist/src/lib/trajectory/types.ts +0 -13
  804. package/dist/src/lib/utils.ts +0 -56
  805. package/dist/src/lib/xrd/XrdPlot.svelte +0 -615
  806. package/dist/src/lib/xrd/broadening.ts +0 -130
  807. package/dist/src/lib/xrd/calc-xrd.ts +0 -397
  808. package/dist/src/lib/xrd/index.ts +0 -38
  809. package/dist/src/lib/xrd/parse.ts +0 -858
  810. package/dist/webview.js +0 -29421
  811. package/icon.png +0 -0
  812. package/matterviz-0.3.2.vsix +0 -0
  813. package/matterviz-0.3.4.vsix +0 -0
  814. package/matterviz-0.3.5.vsix +0 -0
  815. package/scripts/sync-config.ts +0 -101
  816. package/src/declarations.d.ts +0 -2
  817. package/src/extension.ts +0 -972
  818. package/src/node-io.ts +0 -65
  819. package/src/types.ts +0 -17
  820. package/src/webview/JsonBrowser.svelte +0 -1079
  821. package/src/webview/PlotPanel.svelte +0 -346
  822. package/src/webview/detect.ts +0 -444
  823. package/src/webview/main.ts +0 -764
  824. package/src/webview/plot-utils.ts +0 -250
  825. package/test-fixtures/all-viz-types.json.gz +0 -0
  826. package/test-fixtures/plot-demo-data.json.gz +0 -0
  827. package/tests/detect.test.ts +0 -604
  828. package/tests/extension.test.ts +0 -2041
  829. package/tests/node-io.test.ts +0 -39
  830. package/tests/plot-utils.test.ts +0 -302
  831. package/tests/vite-plugin-json-gz.test.ts +0 -114
  832. package/tests/vscode-mock.ts +0 -18
  833. package/tests/webview.test.ts +0 -231
  834. package/tsconfig.json +0 -20
  835. package/vite-plugin-json-gz.ts +0 -29
  836. package/vite.config.ts +0 -34
  837. package/vite.extension.config.ts +0 -34
  838. /package/dist/{src/lib/EmptyState.svelte → EmptyState.svelte} +0 -0
  839. /package/dist/{src/lib/Icon.svelte → Icon.svelte} +0 -0
  840. /package/dist/{src/lib/app.css → app.css} +0 -0
  841. /package/dist/{src/lib/chempot-diagram → chempot-diagram}/ChemPotScene3D.svelte +0 -0
  842. /package/dist/{src/lib/colors → colors}/alloy-colors.json +0 -0
  843. /package/dist/{src/lib/colors → colors}/dark-mode-colors.json +0 -0
  844. /package/dist/{src/lib/colors → colors}/jmol-colors.json +0 -0
  845. /package/dist/{src/lib/colors → colors}/muted-colors.json +0 -0
  846. /package/dist/{src/lib/colors → colors}/pastel-colors.json +0 -0
  847. /package/dist/{src/lib/colors → colors}/vesta-colors.json +0 -0
  848. /package/dist/{src/lib/convex-hull → convex-hull}/TemperatureSlider.svelte +0 -0
  849. /package/dist/{src/lib/element → element}/BohrAtom.svelte +0 -0
  850. /package/dist/{src/lib/element → element}/Nucleus.svelte +0 -0
  851. /package/dist/{src/lib/element → element}/data.json +0 -0
  852. /package/dist/{src/lib/element → element}/data.json.gz +0 -0
  853. /package/dist/{src/lib/element → element}/data.json.gz.d.ts +0 -0
  854. /package/dist/{src/lib/element → element}/data.schema.json +0 -0
  855. /package/dist/{src/lib/element-image-urls.json → element-image-urls.json} +0 -0
  856. /package/dist/{src/lib/feedback → feedback}/Spinner.svelte +0 -0
  857. /package/dist/{src/lib/feedback → feedback}/StatusMessage.svelte +0 -0
  858. /package/dist/{src/lib/layout → layout}/json-tree/JsonValue.svelte +0 -0
  859. /package/dist/{src/lib/periodic-table → periodic-table}/TableInset.svelte +0 -0
  860. /package/dist/{src/lib/plot → plot}/FillArea.svelte +0 -0
  861. /package/dist/{src/lib/plot → plot}/ReferenceLine.svelte +0 -0
  862. /package/dist/{src/lib/theme → theme}/themes.mjs +0 -0
  863. /package/dist/{src/lib/xrd → xrd}/atomic_scattering_params.json +0 -0
@@ -0,0 +1,2778 @@
1
+ <script
2
+ lang="ts"
3
+ generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
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'
11
+ import type {
12
+ AxisLoadError,
13
+ BasePlotProps,
14
+ ControlsConfig,
15
+ DataLoaderFn,
16
+ DataSeries,
17
+ ErrorBand,
18
+ FillHandlerEvent,
19
+ FillRegion,
20
+ HoverConfig,
21
+ InitialRanges,
22
+ InternalPoint,
23
+ LabelPlacementConfig,
24
+ LegendConfig,
25
+ PanConfig,
26
+ PlotConfig,
27
+ Point,
28
+ RefLine,
29
+ RefLineEvent,
30
+ ScaleType,
31
+ ScatterHandlerEvent,
32
+ ScatterHandlerProps,
33
+ StyleOverrides,
34
+ UserContentProps,
35
+ } from './'
36
+ import {
37
+ ColorBar,
38
+ compute_element_placement,
39
+ FillArea,
40
+ get_tick_label,
41
+ Line,
42
+ PlotAxis,
43
+ PlotLegend,
44
+ PlotTooltip,
45
+ ReferenceLine,
46
+ ScatterPlotControls,
47
+ ScatterPoint,
48
+ ZeroLines,
49
+ ZoomRect,
50
+ } from './'
51
+ import {
52
+ build_obstacles_norm,
53
+ has_explicit_position,
54
+ measured_footprint,
55
+ 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'
69
+ import {
70
+ DEFAULT_MARKERS,
71
+ get_scale_type_name,
72
+ 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'
82
+ import { extent } from 'd3-array'
83
+ import { scaleTime } from 'd3-scale'
84
+ import type { ComponentProps, Snippet } from 'svelte'
85
+ import { untrack } from 'svelte'
86
+ import type { HTMLAttributes } from 'svelte/elements'
87
+ import { Tween, type TweenOptions } from 'svelte/motion'
88
+ import { SvelteSet } from 'svelte/reactivity'
89
+ import type { FillPathPoint } from './fill-utils'
90
+ import {
91
+ apply_range_constraints,
92
+ apply_where_condition,
93
+ clamp_for_log_scale,
94
+ convert_error_band_to_fill_region,
95
+ generate_fill_path,
96
+ is_fill_gradient,
97
+ resolve_boundary,
98
+ } from './fill-utils'
99
+ import {
100
+ expand_range_if_needed,
101
+ get_relative_coords,
102
+ normalize_y2_sync,
103
+ pan_range,
104
+ PINCH_ZOOM_THRESHOLD,
105
+ pixels_to_data_delta,
106
+ sync_y2_range,
107
+ } from './interactions'
108
+ import type { Rect, Sides } from './layout'
109
+ import {
110
+ calc_auto_padding,
111
+ constrain_tooltip_position,
112
+ filter_padding,
113
+ LABEL_GAP_DEFAULT,
114
+ measure_full_footprint,
115
+ measure_max_tick_width,
116
+ 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'
120
+ import {
121
+ create_color_scale,
122
+ create_scale,
123
+ create_size_scale,
124
+ generate_ticks,
125
+ 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)
130
+
131
+ let {
132
+ series = $bindable([]),
133
+ x_axis = $bindable({}),
134
+ x2_axis = $bindable({}),
135
+ y_axis = $bindable({}),
136
+ y2_axis = $bindable({}),
137
+ display = $bindable(DEFAULTS.scatter.display),
138
+ styles: styles_init = {},
139
+ controls: controls_init = {},
140
+ padding = {},
141
+ range_padding = 0.05,
142
+ current_x_value = null,
143
+ tooltip_point = $bindable(null),
144
+ selected_point = null,
145
+ hovered = $bindable(false),
146
+ tooltip,
147
+ user_content,
148
+ change = () => {},
149
+ color_scale = {
150
+ type: `linear`,
151
+ scheme: `interpolateViridis`,
152
+ value_range: undefined,
153
+ },
154
+ color_bar = {},
155
+ size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
156
+ label_placement_config = {},
157
+ hover_config = {},
158
+ legend = {},
159
+ point_tween,
160
+ line_tween,
161
+ point_events,
162
+ on_point_click,
163
+ on_point_hover,
164
+ fill_regions = $bindable([]),
165
+ error_bands = [],
166
+ on_fill_click,
167
+ on_fill_hover,
168
+ ref_lines = $bindable([]),
169
+ on_ref_line_click,
170
+ on_ref_line_hover,
171
+ selected_series_idx = $bindable(0),
172
+ wrapper = $bindable(),
173
+ fullscreen = $bindable(false),
174
+ fullscreen_toggle = true,
175
+ children,
176
+ header_controls,
177
+ controls_extra,
178
+ data_loader,
179
+ on_axis_change,
180
+ on_error,
181
+ pan = {},
182
+ ...rest
183
+ }: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, `change`> & PlotConfig & {
184
+ series?: DataSeries<Metadata>[]
185
+ styles?: StyleOverrides
186
+ controls?: ControlsConfig
187
+ current_x_value?: number | null
188
+ tooltip_point?: InternalPoint<Metadata> | null
189
+ selected_point?: { series_idx: number; point_idx: number } | null
190
+ tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
191
+ user_content?: Snippet<[UserContentProps]>
192
+ header_controls?: Snippet<
193
+ [{ height: number; width: number; fullscreen: boolean }]
194
+ >
195
+ controls_extra?: Snippet<
196
+ [
197
+ & { styles: StyleOverrides; selected_series_idx: number }
198
+ & Required<PlotConfig>,
199
+ ]
200
+ >
201
+ change?: (
202
+ data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
203
+ ) => 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
+ }
214
+ color_bar?:
215
+ | (ComponentProps<typeof ColorBar> & {
216
+ margin?: number | Sides
217
+ tween?: TweenOptions<Point2D>
218
+ responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
219
+ axis_clearance?: number // Min distance kept from plot edges/axes (default: 15)
220
+ })
221
+ | null
222
+ label_placement_config?: Partial<LabelPlacementConfig>
223
+ hover_config?: Partial<HoverConfig>
224
+ legend?: LegendConfig | null
225
+ point_tween?: TweenOptions<Point2D>
226
+ line_tween?: TweenOptions<string>
227
+ point_events?: Record<
228
+ string,
229
+ (payload: { point: InternalPoint<Metadata>; event: Event }) => void
230
+ >
231
+ on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
232
+ on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
233
+ fill_regions?: FillRegion[] // Bindable for legend toggle support
234
+ error_bands?: ErrorBand[]
235
+ on_fill_click?: (event: FillHandlerEvent) => void
236
+ on_fill_hover?: (event: FillHandlerEvent | null) => void
237
+ ref_lines?: RefLine[] // Bindable for legend toggle support
238
+ on_ref_line_click?: (event: RefLineEvent) => void
239
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
240
+ selected_series_idx?: number
241
+ wrapper?: HTMLDivElement
242
+ // Interactive axis props
243
+ data_loader?: DataLoaderFn<Metadata>
244
+ on_axis_change?: (
245
+ axis: `x` | `x2` | `y` | `y2`,
246
+ key: string,
247
+ new_series: DataSeries<Metadata>[],
248
+ ) => void
249
+ on_error?: (error: AxisLoadError) => void
250
+ pan?: PanConfig
251
+ } = $props()
252
+
253
+ // Merged axis/display values with defaults (use $derived to avoid breaking $bindable)
254
+ const final_x_axis = $derived({
255
+ ...AXIS_DEFAULTS,
256
+ label_shift: { x: 0, y: -40 }, // x-axis needs different label position
257
+ ...x_axis,
258
+ })
259
+ const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...y_axis })
260
+ const final_x2_axis = $derived({
261
+ ...AXIS_DEFAULTS,
262
+ label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
263
+ ...x2_axis,
264
+ })
265
+ const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...y2_axis })
266
+ // Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
267
+ let is_time_x = $derived(
268
+ is_time_scale(final_x_axis.scale_type, final_x_axis.format),
269
+ )
270
+ let is_time_x2 = $derived(
271
+ is_time_scale(final_x2_axis.scale_type, final_x2_axis.format),
272
+ )
273
+ const final_display = $derived({ ...DEFAULTS.scatter.display, ...display })
274
+ // Local state for styles (initialized from prop, owned by this component for controls)
275
+ // Using $state because styles has bindings in ScatterPlotControls
276
+ // untrack() explicitly captures initial prop value (intentional - props provide initial config)
277
+ let styles = $state(untrack(() => ({
278
+ show_points: DEFAULTS.scatter.show_points,
279
+ show_lines: DEFAULTS.scatter.show_lines,
280
+ point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
281
+ line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
282
+ ...styles_init,
283
+ })))
284
+ let controls = $derived({ show: true, open: false, ...controls_init })
285
+
286
+ let [width, height] = $state([0, 0])
287
+ 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
+
290
+ // Track which specific control properties user has modified
291
+ let touched = new SvelteSet<string>()
292
+
293
+ // Unique component ID to avoid clipPath conflicts between multiple instances
294
+ let component_id = $state(`scatter-${crypto.randomUUID()}`)
295
+ let clip_path_id = $derived(`plot-area-clip-${component_id}`)
296
+
297
+ // Assign stable IDs to series for keying
298
+ let series_with_ids = $derived(
299
+ series.map((srs: DataSeries<Metadata>, idx: number) => {
300
+ if (!srs || typeof srs !== `object`) return srs
301
+ // Use series.id if provided, otherwise fall back to index
302
+ // prevents re-mounts when series are reordered if stable IDs are provided
303
+ return { ...srs, _id: srs.id ?? idx }
304
+ }),
305
+ )
306
+
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
+ // 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)
321
+
322
+ // Y2 axis sync configuration
323
+ let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
324
+ // Track previous sync mode to detect changes (updated in $effect.pre to avoid race conditions)
325
+ let prev_sync_mode = $state<string>(`none`)
326
+
327
+ // Helper to compute synced y2 range or return fallback when sync disabled
328
+ const get_synced_y2 = (y1_range: Vec2, fallback: Vec2): Vec2 =>
329
+ y2_sync_config.mode !== `none`
330
+ ? sync_y2_range(y1_range, initial_y2_range, y2_sync_config)
331
+ : fallback
332
+
333
+ // Effect to update y2 range when sync mode changes - use $effect.pre to capture
334
+ // mode change before the main range-update effect runs, ensuring sync is applied
335
+ // immediately when toggled (not delayed until next data change)
336
+ $effect.pre(() => {
337
+ const mode = y2_sync_config.mode
338
+ if (mode !== prev_sync_mode) {
339
+ // When sync mode becomes enabled (or changes), apply sync immediately
340
+ if (mode !== `none`) {
341
+ zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
342
+ } else {
343
+ // When switching to independent mode, reset Y2 to its data range
344
+ zoom_y2_range = [...initial_y2_range] as [number, number]
345
+ }
346
+ prev_sync_mode = mode
347
+ }
348
+ })
349
+
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
+ // Fill region hover state
361
+ let hovered_fill_key = $state<string | null>(null)
362
+
363
+ // Reference line hover state
364
+ let hovered_ref_line_idx = $state<number | null>(null)
365
+
366
+ // Interactive axis loading state
367
+ let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
368
+
369
+ // State to hold the calculated label positions after simulation
370
+ let label_positions = $state<Record<string, Point2D>>({})
371
+
372
+ // State for legend dragging
373
+ let legend_is_dragging = $state(false)
374
+ let legend_drag_offset = $state<{ x: number; y: number }>({ x: 0, y: 0 })
375
+ let legend_manual_position = $state<{ x: number; y: number } | null>(null)
376
+ let hovered_legend_series_idx = $state<number | null>(null)
377
+
378
+ // State for legend/colorbar placement stability
379
+ let legend_element = $state<HTMLDivElement | undefined>()
380
+ 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
+
396
+ // Module-level constants to avoid repeated allocations
397
+ // Create and categorize points in a single pass (instead of 3 separate iterations)
398
+ type SimplePoint = { x: number; y: number }
399
+ let points_by_axis = $derived.by(() => {
400
+ const all: SimplePoint[] = []
401
+ const y1: SimplePoint[] = []
402
+ const y2: SimplePoint[] = []
403
+ const x2: SimplePoint[] = []
404
+
405
+ for (const srs of series_with_ids) {
406
+ if (!srs) continue
407
+ const { x: xs, y: ys, visible = true, y_axis: series_y_axis = `y1`, x_axis: x_ax = `x1` } =
408
+ srs as DataSeries
409
+ for (let idx = 0; idx < xs.length; idx++) {
410
+ const point = { x: xs[idx], y: ys[idx] }
411
+ all.push(point)
412
+ if (visible) {
413
+ if (series_y_axis === `y2`) y2.push(point)
414
+ else y1.push(point)
415
+ if (x_ax === `x2`) x2.push(point)
416
+ }
417
+ }
418
+ }
419
+ return { all, y1, y2, x2 }
420
+ })
421
+
422
+ let all_points = $derived(points_by_axis.all)
423
+ let y1_points = $derived(points_by_axis.y1)
424
+ let y2_points = $derived(points_by_axis.y2)
425
+ let x2_points = $derived(points_by_axis.x2)
426
+
427
+ // Layout: tick-label padding (decoration reservations are added in `pad` below)
428
+ const default_padding = { t: 5, b: 50, l: 50, r: 20 }
429
+ let base_pad = $state(untrack(() => filter_padding(padding, default_padding)))
430
+
431
+ // Update padding when format or ticks change
432
+ $effect(() => {
433
+ const new_pad = width && height &&
434
+ (y_tick_values.length || y2_tick_values.length || x2_tick_values.length)
435
+ ? calc_auto_padding({
436
+ padding,
437
+ default_padding,
438
+ x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
439
+ y_axis: { ...final_y_axis, tick_values: y_tick_values },
440
+ y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
441
+ })
442
+ : filter_padding(padding, default_padding)
443
+
444
+ if (
445
+ base_pad.t !== new_pad.t ||
446
+ base_pad.b !== new_pad.b ||
447
+ base_pad.l !== new_pad.l ||
448
+ base_pad.r !== new_pad.r
449
+ ) base_pad = new_pad
450
+ })
451
+
452
+ // === Auto-move legend/colorbar outside the plot when interior overlap is unavoidable ===
453
+ // (shared logic lives in auto-place.ts so every 2D plot reuses it)
454
+ // ColorBar's orientation prop defaults to horizontal, so treat unset as horizontal too
455
+ const colorbar_is_horizontal = $derived((color_bar?.orientation ?? `horizontal`) === `horizontal`)
456
+ const colorbar_footprint = $derived(
457
+ colorbar_element?.offsetWidth && colorbar_element?.offsetHeight
458
+ ? measure_full_footprint(colorbar_element)
459
+ : colorbar_is_horizontal
460
+ ? { width: 220, height: 56 }
461
+ : { width: 56, height: 100 },
462
+ )
463
+ const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 80 }))
464
+ const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
465
+
466
+ // Plot-specific obstacle field: series points/lines normalized to [0,1] (y=0 at top)
467
+ const obstacles_norm = $derived.by(() => {
468
+ if (!width || !height || !filtered_series) return []
469
+ const base_w = width - base_pad.l - base_pad.r
470
+ const base_h = height - base_pad.t - base_pad.b
471
+ if (base_w <= 0 || base_h <= 0) return []
472
+ const norm_x = is_time_x
473
+ ? scaleTime().domain([new Date(x_min), new Date(x_max)]).range([0, 1])
474
+ : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1])
475
+ const norm_y = create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [0, 1])
476
+ return build_obstacles_norm(
477
+ filtered_series
478
+ .filter((srs) => srs?.filtered_data)
479
+ .map((srs) => ({
480
+ points: srs.filtered_data.map((pt) => ({
481
+ x: is_time_x ? norm_x(new Date(pt.x)) : norm_x(pt.x),
482
+ y: 1 - norm_y(pt.y), // norm_y is 0 at bottom; invert so 0 = top
483
+ })),
484
+ draws_line: styles.show_lines && (srs.markers ?? DEFAULT_MARKERS).includes(`line`),
485
+ })),
486
+ base_w,
487
+ base_h,
488
+ )
489
+ })
490
+
491
+ const decor = $derived.by(() =>
492
+ place_decorations({
493
+ base_pad,
494
+ width,
495
+ height,
496
+ obstacles_norm,
497
+ // gate on legend_element (the actual render signal) not legend_data, whose fill entries read
498
+ // computed_fills -> pad and would make this derived reference itself
499
+ legend: legend != null && legend_element != null &&
500
+ !legend_has_explicit_pos && !legend_is_dragging && !legend_manual_position
501
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
502
+ : null,
503
+ // gate on a measured colorbar: its outside style stretches it to full width, so deciding from
504
+ // the (wide) pre-measure fallback would flip-flop placement between interior and outside
505
+ colorbar: Boolean(color_bar) && all_color_values.length > 0 && !color_bar?.wrapper_style &&
506
+ (colorbar_element?.offsetWidth ?? 0) > 0 && (colorbar_element?.offsetHeight ?? 0) > 0
507
+ ? {
508
+ footprint: colorbar_footprint,
509
+ horizontal: colorbar_is_horizontal,
510
+ clearance: color_bar?.axis_clearance,
511
+ }
512
+ : null,
513
+ })
514
+ )
515
+ const pad = $derived(decor.pad)
516
+ const legend_auto_outside = $derived(decor.legend_outside)
517
+ const legend_outside_x = $derived(decor.legend_pos.x)
518
+ const legend_outside_y = $derived(decor.legend_pos.y)
519
+ const effective_cbar_wrapper_style = $derived(
520
+ color_bar?.wrapper_style ?? (decor.colorbar_outside ? decor.colorbar_style : undefined),
521
+ )
522
+
523
+ // Reactive clip area dimensions to ensure proper responsiveness
524
+ let clip_area = $derived({
525
+ x: pad.l || 0,
526
+ y: pad.t || 0,
527
+ width: isFinite(width - pad.l - pad.r) ? Math.max(1, width - pad.l - pad.r) : 1,
528
+ height: isFinite(height - pad.t - pad.b)
529
+ ? Math.max(1, height - pad.t - pad.b)
530
+ : 1,
531
+ })
532
+
533
+ // Calculate plot area center coordinates
534
+ let plot_center_x = $derived(pad.l + (width - pad.r - pad.l) / 2)
535
+ let plot_center_y = $derived(pad.t + (height - pad.b - pad.t) / 2)
536
+
537
+ // Extract color and size values in single pass (used for scale computations)
538
+ let series_value_arrays = $derived.by(() => {
539
+ const color_values: number[] = []
540
+ const size_values: number[] = []
541
+ for (const srs of series_with_ids) {
542
+ if (!srs) continue
543
+ const { color_values: cvs, size_values: svs } = srs as DataSeries
544
+ if (cvs) { for (const val of cvs) if (val != null) color_values.push(val) }
545
+ if (svs) { for (const val of svs) if (val != null) size_values.push(val) }
546
+ }
547
+ return { color_values, size_values }
548
+ })
549
+ let all_color_values = $derived(series_value_arrays.color_values)
550
+
551
+ // Compute auto ranges based on data and limits
552
+ let auto_x_range = $derived(
553
+ get_nice_data_range(
554
+ all_points,
555
+ ({ x }) => x,
556
+ final_x_axis.range ?? [null, null],
557
+ final_x_axis.scale_type ?? `linear`,
558
+ range_padding,
559
+ is_time_x,
560
+ ),
561
+ )
562
+
563
+ let auto_y_range = $derived(
564
+ get_nice_data_range(
565
+ y1_points,
566
+ ({ y }) => y,
567
+ final_y_axis.range ?? [null, null],
568
+ final_y_axis.scale_type ?? `linear`,
569
+ range_padding,
570
+ false,
571
+ ),
572
+ )
573
+
574
+ let auto_x2_range = $derived(
575
+ get_nice_data_range(
576
+ x2_points,
577
+ ({ x }) => x,
578
+ final_x2_axis.range ?? [null, null],
579
+ final_x2_axis.scale_type ?? `linear`,
580
+ range_padding,
581
+ is_time_x2,
582
+ ),
583
+ )
584
+
585
+ let auto_y2_range = $derived(
586
+ get_nice_data_range(
587
+ y2_points,
588
+ ({ y }) => y,
589
+ final_y2_axis.range ?? [null, null],
590
+ final_y2_axis.scale_type ?? `linear`,
591
+ range_padding,
592
+ false,
593
+ ),
594
+ )
595
+
596
+ // Update zoom ranges when auto ranges or explicit ranges change
597
+ // - Explicit ranges (from zoom/pan): apply directly
598
+ // - Auto ranges (from data changes): use lazy expansion to preserve view context
599
+ $effect(() => {
600
+ // Helper to get effective range (explicit ?? auto) and check if explicit
601
+ const get_range = (
602
+ axis: { range?: [number | null, number | null] },
603
+ auto: Vec2,
604
+ ): { explicit: boolean; range: Vec2 } => {
605
+ const explicit = axis.range?.[0] != null && axis.range?.[1] != null
606
+ const range = [axis.range?.[0] ?? auto[0], axis.range?.[1] ?? auto[1]] as Vec2
607
+ return { explicit, range }
608
+ }
609
+
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
+ }
633
+ }
634
+
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]
642
+ }
643
+ }
644
+
645
+ // Y2 axis: explicit → direct, else expand initial range then optionally sync
646
+ if (y2.explicit) {
647
+ zoom_y2_range = y2.range
648
+ } else {
649
+ const result = expand_range_if_needed(initial_y2_range, y2.range)
650
+ if (result.changed) initial_y2_range = result.range
651
+ // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
652
+ if (y2_sync_config.mode !== `none`) {
653
+ zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
654
+ } else if (result.changed) {
655
+ zoom_y2_range = result.range
656
+ }
657
+ }
658
+ })
659
+
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)
664
+
665
+ // Create auto color range
666
+ let auto_color_range = $derived(
667
+ // Ensure we only calculate extent on actual numbers, filtering out nulls/undefined
668
+ all_color_values.length > 0
669
+ ? extent(
670
+ all_color_values.filter((color_val: number | null): color_val is number =>
671
+ typeof color_val === `number`
672
+ ),
673
+ )
674
+ : [0, 1],
675
+ ) as Vec2
676
+
677
+ // Create scale functions
678
+ // For time scales, use scaleTime directly; otherwise use create_scale (supports linear/log/arcsinh)
679
+ let x_scale_fn = $derived(
680
+ is_time_x
681
+ ? scaleTime()
682
+ .domain([new Date(x_min), new Date(x_max)])
683
+ .range([pad.l, width - pad.r])
684
+ : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [
685
+ pad.l,
686
+ width - pad.r,
687
+ ]),
688
+ )
689
+
690
+ let x2_scale_fn = $derived(
691
+ is_time_x2
692
+ ? scaleTime()
693
+ .domain([new Date(x2_min), new Date(x2_max)])
694
+ .range([pad.l, width - pad.r])
695
+ : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [
696
+ pad.l,
697
+ width - pad.r,
698
+ ]),
699
+ )
700
+
701
+ let y_scale_fn = $derived(
702
+ create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [
703
+ height - pad.b,
704
+ pad.t,
705
+ ]),
706
+ )
707
+
708
+ let y2_scale_fn = $derived(
709
+ create_scale(final_y2_axis.scale_type ?? `linear`, [y2_min, y2_max], [
710
+ height - pad.b,
711
+ pad.t,
712
+ ]),
713
+ )
714
+
715
+ // All size values from series (for size scale) - extracted in series_value_arrays
716
+ let all_size_values = $derived(series_value_arrays.size_values)
717
+
718
+ // Size scale function (using shared utility)
719
+ let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
720
+
721
+ // Color scale function (using shared utility)
722
+ let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range))
723
+
724
+ // Filter series data to only include points within bounds and augment with internal data
725
+ 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
+ ),
798
+ )
799
+
800
+ // Obstacle field for legend/colorbar auto-placement. Sampling only data points lets the
801
+ // legend land on top of a steep connecting line whose markers are sparse (e.g. y=x^2), so
802
+ // sample_series_obstacle_points also walks each drawn segment at a fixed pixel cadence.
803
+ const SEGMENT_SAMPLE_STEP = 12 // px between samples taken along a connecting line
804
+ let plot_points_for_placement = $derived.by(() => {
805
+ if (!width || !height || !filtered_series) return []
806
+
807
+ const points: { x: number; y: number }[] = []
808
+
809
+ for (const series_data of filtered_series) {
810
+ if (!series_data?.filtered_data) continue
811
+ const use_x2_scale = series_data.x_axis === `x2`
812
+ const active_x_scale = use_x2_scale ? x2_scale_fn : x_scale_fn
813
+ const active_is_time_x = use_x2_scale ? is_time_x2 : is_time_x
814
+ const active_y_scale = series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn
815
+ const draws_line = styles.show_lines &&
816
+ (series_data.markers ?? DEFAULT_MARKERS).includes(`line`)
817
+
818
+ const pixel_points = series_data.filtered_data.map((point) => ({
819
+ x: active_is_time_x ? active_x_scale(new Date(point.x)) : active_x_scale(point.x),
820
+ y: active_y_scale(point.y),
821
+ }))
822
+ points.push(...sample_series_obstacle_points(pixel_points, draws_line, SEGMENT_SAMPLE_STEP))
823
+ }
824
+ return points
825
+ })
826
+
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
+ const fill_hover_key = (
836
+ source_type: `fill_region` | `error_band`,
837
+ source_idx: number,
838
+ id?: string | number,
839
+ is_duplicate_id = false,
840
+ ): string => {
841
+ if (id == null) return `${source_type}:idx:${source_idx}`
842
+ if (is_duplicate_id) return `${source_type}:id:${id}:idx:${source_idx}`
843
+ return `${source_type}:id:${id}`
844
+ }
845
+ const has_duplicate_id = <T extends { id?: string | number }>(
846
+ items: readonly T[] | undefined,
847
+ source_idx: number,
848
+ id?: string | number,
849
+ ): boolean =>
850
+ id != null && (items?.some((item, idx) => idx !== source_idx && item.id === id) ?? false)
851
+
852
+ // Computed fill regions: merge fill_regions and converted error_bands, resolve boundaries
853
+ type ComputedFill = FillRegion & {
854
+ idx: number
855
+ source_type: `fill_region` | `error_band`
856
+ source_idx: number
857
+ hover_key: string
858
+ path_segments: string[]
859
+ }
860
+ let computed_fills = $derived.by((): ComputedFill[] => {
861
+ // Early exit: skip expensive computation if no fills to render
862
+ const has_fill_regions = fill_regions && fill_regions.length > 0
863
+ const has_error_bands = error_bands && error_bands.length > 0
864
+ if (!has_fill_regions && !has_error_bands) return []
865
+
866
+ // Merge fill_regions and converted error_bands, tracking source
867
+ const all_regions: {
868
+ region: FillRegion | null
869
+ source_type: `fill_region` | `error_band`
870
+ source_idx: number
871
+ hover_key: string
872
+ }[] = [
873
+ ...(fill_regions ?? []).map((region, source_idx) => ({
874
+ region,
875
+ source_type: `fill_region` as const,
876
+ source_idx,
877
+ hover_key: fill_hover_key(
878
+ `fill_region`,
879
+ source_idx,
880
+ region.id,
881
+ has_duplicate_id(fill_regions, source_idx, region.id),
882
+ ),
883
+ })),
884
+ ...(error_bands ?? []).map((band, source_idx) => ({
885
+ region: convert_error_band_to_fill_region(band, series_with_ids),
886
+ source_type: `error_band` as const,
887
+ source_idx,
888
+ hover_key: fill_hover_key(
889
+ `error_band`,
890
+ source_idx,
891
+ band.id,
892
+ has_duplicate_id(error_bands, source_idx, band.id),
893
+ ),
894
+ })),
895
+ ]
896
+
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)
908
+
909
+ if (unique_x.length === 0) return []
910
+
911
+ return all_regions
912
+ .filter((
913
+ entry,
914
+ ): entry is {
915
+ region: FillRegion
916
+ source_type: `fill_region` | `error_band`
917
+ source_idx: number
918
+ hover_key: string
919
+ } => entry.region !== null)
920
+ .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)
984
+
985
+ if (path_segments.length === 0) return null
986
+
987
+ return { ...region, idx, source_type, source_idx, hover_key, path_segments }
988
+ })
989
+ .filter((fill): fill is ComputedFill => fill !== null)
990
+ })
991
+
992
+ // 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
+ })
1148
+
1149
+ // Group fills by z-index for ordered rendering (single pass instead of 4 filters)
1150
+ let fills_by_z = $derived.by(() => {
1151
+ const groups: {
1152
+ below_grid: typeof computed_fills
1153
+ below_lines: typeof computed_fills
1154
+ below_points: typeof computed_fills
1155
+ above_all: typeof computed_fills
1156
+ } = { below_grid: [], below_lines: [], below_points: [], above_all: [] }
1157
+
1158
+ for (const fill of computed_fills) {
1159
+ if (fill.z_index === `below-grid`) groups.below_grid.push(fill)
1160
+ else if (fill.z_index === `below-points`) groups.below_points.push(fill)
1161
+ else if (fill.z_index === `above-all`) groups.above_all.push(fill)
1162
+ else groups.below_lines.push(fill) // default: no z_index or 'below-lines'
1163
+ }
1164
+ return groups
1165
+ })
1166
+
1167
+ // Compute ref_lines with index and group by z-index (using shared utilities)
1168
+ let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
1169
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
1170
+
1171
+ // Calculate best legend placement using continuous grid sampling
1172
+ let legend_placement = $derived.by(() => {
1173
+ const should_place = legend != null &&
1174
+ (legend_data.length > 1 || Object.keys(legend).length > 0)
1175
+
1176
+ if (!should_place || !width || !height) return null
1177
+
1178
+ const plot_width = width - pad.l - pad.r
1179
+ const plot_height = height - pad.t - pad.b
1180
+
1181
+ const placement_config = {
1182
+ plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
1183
+ element: legend_element,
1184
+ element_size: { width: 120, height: 80 }, // fallback before first render
1185
+ axis_clearance: legend?.axis_clearance,
1186
+ exclude_rects: [],
1187
+ points: plot_points_for_placement,
1188
+ }
1189
+
1190
+ return compute_element_placement(placement_config)
1191
+ })
1192
+
1193
+ // Calculate color bar placement (coordinates with legend to avoid overlap)
1194
+ let color_bar_placement = $derived.by(() => {
1195
+ if (!color_bar || !all_color_values.length || !width || !height) return null
1196
+
1197
+ const plot_width = width - pad.l - pad.r
1198
+ const plot_height = height - pad.t - pad.b
1199
+
1200
+ // Fallback estimate (with room for tick labels) used before the colorbar first
1201
+ // renders; compute_element_placement measures the real footprint once it's laid out
1202
+ const is_horizontal = (color_bar.orientation ?? `horizontal`) === `horizontal`
1203
+ const colorbar_size = is_horizontal
1204
+ ? { width: 220, height: 56 }
1205
+ : { width: 56, height: 100 }
1206
+
1207
+ // Build exclusion rects (avoid legend if it's placed)
1208
+ const exclude_rects: Rect[] = []
1209
+ if (legend_element && legend_placement) {
1210
+ exclude_rects.push({
1211
+ x: legend_placement.x,
1212
+ y: legend_placement.y,
1213
+ width: legend_element.offsetWidth || 120,
1214
+ height: legend_element.offsetHeight || 80,
1215
+ })
1216
+ }
1217
+
1218
+ return compute_element_placement({
1219
+ plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
1220
+ element: colorbar_element,
1221
+ element_size: colorbar_size,
1222
+ // Small gap from the corner; the full-footprint measurement reserves the tick
1223
+ // labels, so this alone keeps the colorbar off the axes
1224
+ axis_clearance: color_bar?.axis_clearance ?? 15,
1225
+ exclude_rects,
1226
+ points: plot_points_for_placement,
1227
+ })
1228
+ })
1229
+
1230
+ // Active legend placement (null if user set explicit position)
1231
+ let active_legend_placement = $derived.by(() => {
1232
+ if (!legend_placement) return null
1233
+
1234
+ // Skip auto-placement if user set explicit position in style
1235
+ const legend_style = legend?.style ?? ``
1236
+ if (
1237
+ /(^|[;{]\s*)(top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(
1238
+ legend_style,
1239
+ )
1240
+ ) return null
1241
+
1242
+ return legend_placement
1243
+ })
1244
+
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
+ }
1299
+ })
1300
+
1301
+ // Generate axis ticks - consolidated into single derived for efficiency
1302
+ let axis_ticks = $derived.by(() => {
1303
+ if (!width || !height) return { x: [], x2: [], y: [], y2: [] }
1304
+
1305
+ // X-axis ticks: choose appropriate scale for tick generation
1306
+ // Time scales (format starts with %) use scaleTime for better tick placement
1307
+ const x_scale_for_ticks = is_time_x
1308
+ ? scaleTime().domain([new Date(x_min), new Date(x_max)])
1309
+ : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1])
1310
+
1311
+ const x2_scale_for_ticks = is_time_x2
1312
+ ? scaleTime().domain([new Date(x2_min), new Date(x2_max)])
1313
+ : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [0, 1])
1314
+
1315
+ return {
1316
+ x: generate_ticks(
1317
+ [x_min, x_max],
1318
+ final_x_axis.scale_type ?? `linear`,
1319
+ final_x_axis.ticks,
1320
+ x_scale_for_ticks,
1321
+ { format: final_x_axis.format },
1322
+ ),
1323
+ x2: x2_points.length > 0
1324
+ ? generate_ticks(
1325
+ [x2_min, x2_max],
1326
+ final_x2_axis.scale_type ?? `linear`,
1327
+ final_x2_axis.ticks,
1328
+ x2_scale_for_ticks,
1329
+ { format: final_x2_axis.format },
1330
+ )
1331
+ : [],
1332
+ y: generate_ticks(
1333
+ [y_min, y_max],
1334
+ final_y_axis.scale_type ?? `linear`,
1335
+ final_y_axis.ticks,
1336
+ y_scale_fn,
1337
+ { default_count: 5 },
1338
+ ),
1339
+ y2: y2_points.length > 0
1340
+ ? generate_ticks(
1341
+ [y2_min, y2_max],
1342
+ final_y2_axis.scale_type ?? `linear`,
1343
+ final_y2_axis.ticks,
1344
+ y2_scale_fn,
1345
+ { default_count: 5 },
1346
+ )
1347
+ : [],
1348
+ }
1349
+ })
1350
+
1351
+ let x_tick_values = $derived(axis_ticks.x)
1352
+ let x2_tick_values = $derived(axis_ticks.x2)
1353
+ let y_tick_values = $derived(axis_ticks.y)
1354
+ let y2_tick_values = $derived(axis_ticks.y2)
1355
+
1356
+ // Cache measured tick-label widths so expensive text measurement only runs
1357
+ // when tick values/format change, not on every template rerender.
1358
+ let tick_label_widths = $derived({
1359
+ x2_max: measure_max_tick_width(x2_tick_values, final_x2_axis.format ?? ``),
1360
+ y_max: measure_max_tick_width(y_tick_values, final_y_axis.format ?? ``),
1361
+ y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
1362
+ })
1363
+
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)
1471
+ // 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],
1526
+ }
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
+ }
1704
+
1705
+ // tooltip logic: find closest point and update tooltip state
1706
+ function update_tooltip_point(x_rel: number, y_rel: number, evt?: MouseEvent) {
1707
+ if (!width || !height) return
1708
+
1709
+ let closest_point: InternalPoint<Metadata> | null = null
1710
+ let closest_series: DataSeries<Metadata> | null = null
1711
+ let min_screen_dist_sq = Infinity
1712
+ const { threshold_px = 20 } = hover_config // Use configured threshold
1713
+ const hover_threshold_px_sq = threshold_px * threshold_px
1714
+
1715
+ // Iterate through points to find the closest one in screen coordinates
1716
+ for (const series_data of filtered_series) {
1717
+ if (!series_data?.filtered_data) continue
1718
+
1719
+ const tooltip_use_x2 = series_data.x_axis === `x2`
1720
+ const tooltip_x_scale = tooltip_use_x2 ? x2_scale_fn : x_scale_fn
1721
+ const tooltip_is_time_x = tooltip_use_x2 ? is_time_x2 : is_time_x
1722
+ for (const point of series_data.filtered_data) {
1723
+ // Calculate screen coordinates of the point
1724
+ const point_cx = tooltip_is_time_x
1725
+ ? tooltip_x_scale(new Date(point.x))
1726
+ : tooltip_x_scale(point.x)
1727
+ const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(
1728
+ point.y,
1729
+ )
1730
+
1731
+ // Calculate squared screen distance between mouse and point
1732
+ const screen_dx = x_rel - point_cx
1733
+ const screen_dy = y_rel - point_cy
1734
+ const screen_distance_sq = screen_dx * screen_dx + screen_dy * screen_dy
1735
+
1736
+ // Update if this point is closer
1737
+ if (screen_distance_sq < min_screen_dist_sq) {
1738
+ min_screen_dist_sq = screen_distance_sq
1739
+ closest_point = point
1740
+ closest_series = series_data
1741
+ }
1742
+ }
1743
+ }
1744
+
1745
+ // Check if the closest point is within the hover threshold
1746
+ if (
1747
+ closest_point &&
1748
+ closest_series &&
1749
+ min_screen_dist_sq <= hover_threshold_px_sq
1750
+ ) {
1751
+ // Construct handler props synchronously to avoid stale derived reads
1752
+ const props = construct_handler_props(closest_point)
1753
+ tooltip_point = closest_point
1754
+ // Construct object matching change signature
1755
+ const { x, y, metadata } = closest_point
1756
+ change({ x, y, metadata, series: closest_series })
1757
+ // Call hover handler with synchronously constructed props
1758
+ if (evt && props) {
1759
+ on_point_hover?.({ ...props, event: evt, point: closest_point })
1760
+ }
1761
+ } else {
1762
+ tooltip_point = null
1763
+ change(null)
1764
+ on_point_hover?.(null)
1765
+ }
1766
+ }
1767
+
1768
+ function on_mouse_move(evt: MouseEvent) {
1769
+ hovered = true
1770
+
1771
+ const coords = get_relative_coords(evt)
1772
+ if (!coords) return
1773
+
1774
+ update_tooltip_point(coords.x, coords.y, evt)
1775
+ }
1776
+
1777
+ // Merge user config with defaults before the effect that uses it
1778
+ let actual_label_config = $derived({
1779
+ sa_iterations: 2000,
1780
+ max_labels: 300,
1781
+ leader_line_threshold: 15,
1782
+ ...label_placement_config,
1783
+ })
1784
+
1785
+ $effect(() => {
1786
+ if (!width || !height) {
1787
+ label_positions = {}
1788
+ return
1789
+ }
1790
+
1791
+ label_positions = compute_label_positions(
1792
+ filtered_series,
1793
+ actual_label_config,
1794
+ { x_scale_fn, y_scale_fn, y2_scale_fn, x_axis: final_x_axis },
1795
+ { width, height, pad },
1796
+ )
1797
+ })
1798
+
1799
+ // Legend drag handlers
1800
+ function handle_legend_drag_start(event: MouseEvent) {
1801
+ if (!svg_element) return
1802
+
1803
+ legend_is_dragging = true
1804
+
1805
+ // Get the actual rendered position of the legend element (accounts for transforms)
1806
+ const legend_el = event.currentTarget
1807
+ if (!(legend_el instanceof HTMLElement)) return
1808
+ const legend_rect = legend_el.getBoundingClientRect()
1809
+
1810
+ // Calculate offset from mouse to legend's actual rendered position relative to SVG
1811
+ const [x, y] = [event.clientX - legend_rect.left, event.clientY - legend_rect.top]
1812
+ legend_drag_offset = { x, y }
1813
+ }
1814
+
1815
+ function handle_legend_drag(event: MouseEvent) {
1816
+ if (!legend_is_dragging || !svg_element || !legend_element) return
1817
+
1818
+ const svg_rect = svg_element.getBoundingClientRect()
1819
+
1820
+ // Calculate new position: mouse position relative to SVG, minus the offset within the legend
1821
+ const new_x = event.clientX - svg_rect.left - legend_drag_offset.x
1822
+ const new_y = event.clientY - svg_rect.top - legend_drag_offset.y
1823
+
1824
+ // Get actual legend dimensions for accurate bounds checking using the bound element reference
1825
+ const { width: legend_width, height: legend_height } = legend_element
1826
+ .getBoundingClientRect()
1827
+
1828
+ // Constrain to plot bounds using measured legend size
1829
+ const constrained_x = Math.max(0, Math.min(width - legend_width, new_x))
1830
+ const constrained_y = Math.max(0, Math.min(height - legend_height, new_y))
1831
+
1832
+ legend_manual_position = { x: constrained_x, y: constrained_y }
1833
+ }
1834
+
1835
+ function get_screen_coords(point: Point, data_series?: DataSeries): [number, number] {
1836
+ // convert data coordinates to potentially non-finite screen coordinates
1837
+ const use_x2 = data_series?.x_axis === `x2`
1838
+ const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
1839
+ const active_is_time_x = use_x2 ? is_time_x2 : is_time_x
1840
+ const screen_x = active_is_time_x
1841
+ ? active_x_scale(new Date(point.x))
1842
+ : active_x_scale(point.x)
1843
+
1844
+ const y_val = point.y
1845
+ // Determine which y-scale to use based on series y_axis property
1846
+ const use_y2 = data_series?.y_axis === `y2`
1847
+ const y_scale = use_y2 ? y2_scale_fn : y_scale_fn
1848
+ const y_scale_type = use_y2
1849
+ ? get_scale_type_name(final_y2_axis.scale_type)
1850
+ : get_scale_type_name(final_y_axis.scale_type)
1851
+ // Only log scale needs domain clamping; linear and arcsinh can handle any value
1852
+ const min_domain_y = y_scale_type === `log` ? y_scale.domain()[0] : -Infinity
1853
+ const safe_y_val = y_scale_type === `log` ? Math.max(y_val, min_domain_y) : y_val
1854
+ const screen_y = y_scale(safe_y_val) // This might be non-finite
1855
+
1856
+ return [screen_x, screen_y]
1857
+ }
1858
+
1859
+ // Helper function to construct ScatterHandlerProps synchronously from InternalPoint
1860
+ function construct_handler_props(
1861
+ point: InternalPoint<Metadata>,
1862
+ ): ScatterHandlerProps<Metadata> | null {
1863
+ const hovered_series = series_with_ids[point.series_idx]
1864
+ if (!hovered_series) return null
1865
+ const { x, y, color_value, metadata, series_idx } = point
1866
+ const handler_use_x2 = hovered_series.x_axis === `x2`
1867
+ const handler_x_scale = handler_use_x2 ? x2_scale_fn : x_scale_fn
1868
+ const handler_is_time_x = handler_use_x2 ? is_time_x2 : is_time_x
1869
+ const cx = handler_is_time_x ? handler_x_scale(new Date(x)) : handler_x_scale(x)
1870
+ const cy = (hovered_series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(y)
1871
+ const active_x_config = handler_use_x2 ? final_x2_axis : final_x_axis
1872
+ const active_y_config = hovered_series.y_axis === `y2`
1873
+ ? final_y2_axis
1874
+ : final_y_axis
1875
+ const coords = {
1876
+ x,
1877
+ y,
1878
+ cx,
1879
+ cy,
1880
+ x_axis: active_x_config,
1881
+ x2_axis: final_x2_axis,
1882
+ y_axis: active_y_config,
1883
+ y2_axis: final_y2_axis,
1884
+ }
1885
+ return {
1886
+ ...coords,
1887
+ fullscreen,
1888
+ metadata,
1889
+ label: hovered_series.label ?? null,
1890
+ series_idx,
1891
+ x_formatted: format_value(x, active_x_config.format || `.3~s`),
1892
+ y_formatted: format_value(y, active_y_config.format || `.3~s`),
1893
+ color_value: color_value ?? null,
1894
+ colorbar: {
1895
+ value: color_value ?? null,
1896
+ title: color_bar?.title ?? null,
1897
+ scale: color_scale,
1898
+ tick_format: color_bar?.tick_format ?? null,
1899
+ },
1900
+ }
1901
+ }
1902
+
1903
+ // Derive handler props from hovered point for both tooltip and event handlers
1904
+ let handler_props = $derived.by((): ScatterHandlerProps<Metadata> | null => {
1905
+ if (!tooltip_point) return null
1906
+ return construct_handler_props(tooltip_point)
1907
+ })
1908
+
1909
+ let using_controls = $derived(controls.show)
1910
+ let has_multiple_series = $derived(series_with_ids.filter(Boolean).length > 1)
1911
+
1912
+ // Precompute non-click event names from point_events so we don't rebuild
1913
+ // the entries array on every point render.
1914
+ let point_event_names = $derived(
1915
+ point_events
1916
+ ? Object.keys(point_events).filter((name) => name !== `onclick`)
1917
+ : [],
1918
+ )
1919
+
1920
+ // Set theme-aware background when entering fullscreen
1921
+ $effect(() => {
1922
+ set_fullscreen_bg(wrapper, fullscreen, `--scatter-fullscreen-bg`)
1923
+ })
1924
+
1925
+ // State accessors for shared axis change handler
1926
+ const axis_state: AxisChangeState<DataSeries<Metadata>> = {
1927
+ get_axis: (axis) => {
1928
+ if (axis === `x`) return x_axis
1929
+ if (axis === `x2`) return x2_axis
1930
+ if (axis === `y`) return y_axis
1931
+ return y2_axis
1932
+ },
1933
+ set_axis: (axis, config) => {
1934
+ // Spread into existing state to preserve merged type structure
1935
+ if (axis === `x`) x_axis = { ...x_axis, ...config }
1936
+ else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
1937
+ else if (axis === `y`) y_axis = { ...y_axis, ...config }
1938
+ else y2_axis = { ...y2_axis, ...config }
1939
+ },
1940
+ get_series: () => series,
1941
+ set_series: (new_series) => (series = new_series),
1942
+ get_loading: () => axis_loading,
1943
+ set_loading: (axis) => (axis_loading = axis),
1944
+ }
1945
+
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(
1949
+ 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
+ })
1972
+ </script>
1973
+
1974
+ {#snippet fill_regions_layer(fills: typeof computed_fills)}
1975
+ {#each fills as fill (fill.hover_key)}
1976
+ {#each fill.path_segments as
1977
+ path_d,
1978
+ segment_idx
1979
+ (`${fill.id ?? fill.idx}-${segment_idx}`)
1980
+ }
1981
+ <FillArea
1982
+ region={fill}
1983
+ region_idx={fill.idx}
1984
+ path={path_d}
1985
+ {clip_path_id}
1986
+ {x_scale_fn}
1987
+ {y_scale_fn}
1988
+ is_hovered={hovered_fill_key === fill.hover_key}
1989
+ on_click={(event: FillHandlerEvent) => {
1990
+ fill.on_click?.(event)
1991
+ on_fill_click?.(event)
1992
+ }}
1993
+ on_hover={(event: FillHandlerEvent | null) => {
1994
+ hovered_fill_key = event ? fill.hover_key : null
1995
+ fill.on_hover?.(event)
1996
+ on_fill_hover?.(event)
1997
+ }}
1998
+ />
1999
+ {/each}
2000
+ {/each}
2001
+ {/snippet}
2002
+
2003
+ {#snippet ref_lines_layer(lines: IndexedRefLine[])}
2004
+ {#each lines as line (line.id ?? line.idx)}
2005
+ <ReferenceLine
2006
+ ref_line={line}
2007
+ line_idx={line.idx}
2008
+ x_min={line.x_axis === `x2` ? x2_min : x_min}
2009
+ x_max={line.x_axis === `x2` ? x2_max : x_max}
2010
+ y_min={line.y_axis === `y2` ? y2_min : y_min}
2011
+ y_max={line.y_axis === `y2` ? y2_max : y_max}
2012
+ x_scale={x_scale_fn}
2013
+ x2_scale={x2_scale_fn}
2014
+ y_scale={y_scale_fn}
2015
+ y2_scale={y2_scale_fn}
2016
+ {clip_path_id}
2017
+ hovered_line_idx={hovered_ref_line_idx}
2018
+ on_click={(event: RefLineEvent) => {
2019
+ line.on_click?.(event)
2020
+ on_ref_line_click?.(event)
2021
+ }}
2022
+ on_hover={(event: RefLineEvent | null) => {
2023
+ hovered_ref_line_idx = event?.line_idx ?? null
2024
+ line.on_hover?.(event)
2025
+ on_ref_line_hover?.(event)
2026
+ }}
2027
+ />
2028
+ {/each}
2029
+ {/snippet}
2030
+
2031
+ <svelte:window
2032
+ onkeydown={(evt) => {
2033
+ if (evt.key === `Escape` && fullscreen) {
2034
+ evt.preventDefault()
2035
+ fullscreen = false
2036
+ }
2037
+ if (evt.key === `Shift`) shift_held = true
2038
+ }}
2039
+ onkeyup={(evt) => {
2040
+ if (evt.key === `Shift`) shift_held = false
2041
+ }}
2042
+ />
2043
+
2044
+ <div
2045
+ bind:this={wrapper}
2046
+ bind:clientWidth={width}
2047
+ bind:clientHeight={height}
2048
+ {...rest}
2049
+ class="scatter {rest.class ?? ``}"
2050
+ class:fullscreen
2051
+ >
2052
+ {#if width && height}
2053
+ <div class="header-controls">
2054
+ {@render header_controls?.({ height, width, fullscreen })}
2055
+ {#if fullscreen_toggle}
2056
+ <FullscreenToggle bind:fullscreen />
2057
+ {/if}
2058
+ </div>
2059
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
2060
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
2061
+ <svg
2062
+ bind:this={svg_element}
2063
+ role="application"
2064
+ aria-label={rest[`aria-label`] ??
2065
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
2066
+ `Scatter plot`)}
2067
+ tabindex="0"
2068
+ onfocusin={() => (is_focused = true)}
2069
+ onfocusout={() => (is_focused = false)}
2070
+ onmouseenter={() => (hovered = true)}
2071
+ onmousedown={handle_mouse_down}
2072
+ onmousemove={(evt: MouseEvent) => {
2073
+ // Only find closest point if not actively dragging
2074
+ if (!drag_start_coords && !pan_drag_state) on_mouse_move(evt)
2075
+ }}
2076
+ onmouseleave={() => {
2077
+ hovered = false
2078
+ tooltip_point = null
2079
+ on_point_hover?.(null)
2080
+ }}
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`}
2107
+ >
2108
+ {@render user_content?.({
2109
+ height,
2110
+ width,
2111
+ x_scale_fn,
2112
+ x2_scale_fn,
2113
+ y_scale_fn,
2114
+ y2_scale_fn,
2115
+ pad,
2116
+ x_range: [x_min, x_max],
2117
+ x2_range: [x2_min, x2_max],
2118
+ y_range: [y_min, y_max],
2119
+ y2_range: [y2_min, y2_max],
2120
+ fullscreen,
2121
+ })}
2122
+
2123
+ <!-- Fill regions: below grid -->
2124
+ {@render fill_regions_layer(fills_by_z.below_grid)}
2125
+ <!-- Reference lines: below grid -->
2126
+ {@render ref_lines_layer(ref_lines_by_z.below_grid)}
2127
+
2128
+ <PlotAxis
2129
+ side="x"
2130
+ ticks={x_tick_values}
2131
+ place={(tick) => (is_time_x ? x_scale_fn(new Date(tick)) : x_scale_fn(tick))}
2132
+ axis={final_x_axis}
2133
+ {pad}
2134
+ {width}
2135
+ {height}
2136
+ show_grid={final_display.x_grid}
2137
+ show_baseline={false}
2138
+ domain={[x_min, x_max]}
2139
+ tick_label={(tick) => get_tick_label(tick, final_x_axis.ticks)}
2140
+ label_x={width / 2 + (final_x_axis.label_shift?.x ?? 0)}
2141
+ label_y={height - pad.b - (final_x_axis.label_shift?.y ?? -40)}
2142
+ axis_loading={axis_loading === `x`}
2143
+ on_axis_change={(key) => handle_axis_change(`x`, key)}
2144
+ />
2145
+
2146
+ <!-- Current frame indicator -->
2147
+ {#if current_x_value != null}
2148
+ {@const current_pos_raw = is_time_x
2149
+ ? x_scale_fn(new Date(current_x_value))
2150
+ : x_scale_fn(current_x_value)}
2151
+ {#if isFinite(current_pos_raw)}
2152
+ {@const current_pos = current_pos_raw}
2153
+ {#if current_pos >= pad.l && current_pos <= width - pad.r}
2154
+ {@const active_tick_height = 7}
2155
+ <rect
2156
+ x={current_pos - 1.5}
2157
+ y={height - pad.b - active_tick_height / 2}
2158
+ width="3"
2159
+ height={active_tick_height}
2160
+ fill="var(--scatter-current-frame-color, #ff6b35)"
2161
+ stroke="white"
2162
+ stroke-width="1"
2163
+ class="current-frame-indicator"
2164
+ />
2165
+ {/if}
2166
+ {/if}
2167
+ {/if}
2168
+
2169
+ <PlotAxis
2170
+ side="y"
2171
+ ticks={y_tick_values}
2172
+ place={y_scale_fn}
2173
+ axis={final_y_axis}
2174
+ {pad}
2175
+ {width}
2176
+ {height}
2177
+ show_grid={final_display.y_grid}
2178
+ show_baseline={false}
2179
+ domain={[y_min, y_max]}
2180
+ unit_on_first_tick
2181
+ tick_label={(tick) => get_tick_label(tick, final_y_axis.ticks)}
2182
+ label_x={Math.max(
2183
+ 12,
2184
+ pad.l - (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
2185
+ LABEL_GAP_DEFAULT,
2186
+ ) + (final_y_axis.label_shift?.x ?? 0)}
2187
+ label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y_axis.label_shift?.y ?? 0)}
2188
+ axis_loading={axis_loading === `y`}
2189
+ on_axis_change={(key) => handle_axis_change(`y`, key)}
2190
+ />
2191
+
2192
+ <!-- Y2-axis (Right) -->
2193
+ {#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
+ <PlotAxis
2198
+ side="y2"
2199
+ ticks={y2_tick_values}
2200
+ place={y2_scale_fn}
2201
+ axis={final_y2_axis}
2202
+ {pad}
2203
+ {width}
2204
+ {height}
2205
+ show_grid={final_display.y2_grid}
2206
+ show_baseline={false}
2207
+ domain={[y2_min, y2_max]}
2208
+ unit_on_first_tick
2209
+ 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)}
2212
+ label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y2_axis.label_shift?.y ?? 0)}
2213
+ axis_loading={axis_loading === `y2`}
2214
+ on_axis_change={(key) => handle_axis_change(`y2`, key)}
2215
+ />
2216
+ {/if}
2217
+
2218
+ <!-- X2-axis (Top) -->
2219
+ {#if x2_points.length > 0}
2220
+ <PlotAxis
2221
+ side="x2"
2222
+ ticks={x2_tick_values}
2223
+ place={(tick) => (is_time_x2 ? x2_scale_fn(new Date(tick)) : x2_scale_fn(tick))}
2224
+ axis={final_x2_axis}
2225
+ {pad}
2226
+ {width}
2227
+ {height}
2228
+ show_grid={final_display.x2_grid}
2229
+ show_baseline={false}
2230
+ domain={[x2_min, x2_max]}
2231
+ tick_label={(tick) => get_tick_label(tick, final_x2_axis.ticks)}
2232
+ label_x={width / 2 + (final_x2_axis.label_shift?.x ?? 0)}
2233
+ label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? 40))}
2234
+ axis_loading={axis_loading === `x2`}
2235
+ on_axis_change={(key) => handle_axis_change(`x2`, key)}
2236
+ />
2237
+ {/if}
2238
+
2239
+ <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
2240
+
2241
+ <ZoomRect start={drag_start_coords} current={drag_current_coords} />
2242
+
2243
+ <ZeroLines
2244
+ display={final_display}
2245
+ {x_scale_fn}
2246
+ {x2_scale_fn}
2247
+ {y_scale_fn}
2248
+ {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}
2253
+ x_scale_type={final_x_axis.scale_type}
2254
+ x2_scale_type={final_x2_axis.scale_type}
2255
+ y_scale_type={final_y_axis.scale_type}
2256
+ y2_scale_type={final_y2_axis.scale_type}
2257
+ x_is_time={is_time_x}
2258
+ x2_is_time={is_time_x2}
2259
+ has_x2={x2_points.length > 0}
2260
+ has_y2={y2_points.length > 0}
2261
+ {width}
2262
+ {height}
2263
+ {pad}
2264
+ />
2265
+
2266
+ <defs>
2267
+ <clipPath id={clip_path_id}>
2268
+ <rect
2269
+ x={clip_area.x}
2270
+ y={clip_area.y}
2271
+ width={clip_area.width}
2272
+ height={clip_area.height}
2273
+ />
2274
+ </clipPath>
2275
+ </defs>
2276
+
2277
+ <!-- Fill regions: below lines (default z-index) -->
2278
+ {@render fill_regions_layer(fills_by_z.below_lines)}
2279
+ <!-- Reference lines: below lines (default z-index) -->
2280
+ {@render ref_lines_layer(ref_lines_by_z.below_lines)}
2281
+
2282
+ <!-- Lines -->
2283
+ {#if styles.show_lines}
2284
+ {#each filtered_series ?? [] as series_data (series_data._id)}
2285
+ {@const series_markers = series_data.markers ?? DEFAULT_MARKERS}
2286
+ {@const series_default_color = get_series_color(series_data.orig_series_idx ?? 0)}
2287
+ <g
2288
+ data-series-id={series_data._id}
2289
+ clip-path="url(#{clip_path_id})"
2290
+ opacity={hovered_legend_series_idx !== null &&
2291
+ hovered_legend_series_idx !== series_data.orig_series_idx
2292
+ ? 0.25
2293
+ : 1}
2294
+ >
2295
+ {#if series_markers?.includes(`line`)}
2296
+ {@const all_line_points = series_data.x.map((x, idx) => ({
2297
+ x,
2298
+ y: series_data.y[idx],
2299
+ }))}
2300
+ {@const finite_screen_points = all_line_points
2301
+ .map((point) => get_screen_coords(point, series_data))
2302
+ .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
2303
+ {@const apply_line_controls = using_controls &&
2304
+ (!has_multiple_series ||
2305
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
2306
+ {@const ls = series_data.line_style}
2307
+ {@const tc = (key: string) => apply_line_controls && touched.has(key)}
2308
+ {@const color_fallback = ls?.stroke ??
2309
+ (Array.isArray(series_data.point_style)
2310
+ ? series_data.point_style[0]?.fill
2311
+ : series_data.point_style?.fill) ??
2312
+ (series_data.color_values?.[0] != null
2313
+ ? color_scale_fn(series_data.color_values[0])
2314
+ : series_default_color)}
2315
+ <Line
2316
+ points={finite_screen_points}
2317
+ origin={[
2318
+ is_time_x ? x_scale_fn(new Date(x_min)) : x_scale_fn(x_min),
2319
+ series_data.y_axis === `y2` ? y2_scale_fn(y2_min) : y_scale_fn(y_min),
2320
+ ]}
2321
+ line_color={(tc(`line.color`) ? styles.line?.color : null) ?? color_fallback}
2322
+ line_width={(tc(`line.width`) ? styles.line?.width : null) ?? ls?.stroke_width ?? 2}
2323
+ line_dash={(tc(`line.dash`) ? styles.line?.dash : null) ?? ls?.line_dash}
2324
+ area_color="transparent"
2325
+ {line_tween}
2326
+ />
2327
+ {/if}
2328
+ </g>
2329
+ {/each}
2330
+ {/if}
2331
+
2332
+ <!-- Fill regions: below points -->
2333
+ {@render fill_regions_layer(fills_by_z.below_points)}
2334
+ <!-- Reference lines: below points -->
2335
+ {@render ref_lines_layer(ref_lines_by_z.below_points)}
2336
+
2337
+ <!-- Points -->
2338
+ {#if styles.show_points}
2339
+ {#each filtered_series ?? [] as series_data (series_data._id)}
2340
+ {@const series_markers = series_data.markers ?? DEFAULT_MARKERS}
2341
+ {@const series_default_color = get_series_color(series_data.orig_series_idx ?? 0)}
2342
+ {@const series_default_symbol = get_series_symbol(series_data.orig_series_idx ?? 0)}
2343
+ <g data-series-id={series_data._id}>
2344
+ {#if series_markers?.includes(`points`)}
2345
+ {#each series_data.filtered_data as
2346
+ point
2347
+ (`${point.series_idx}-${point.point_idx}`)
2348
+ }
2349
+ {@const label_id = `${point.series_idx}-${point.point_idx}`}
2350
+ {@const calculated_label_pos = label_positions[label_id]}
2351
+ {@const point_label = point.point_label ?? {}}
2352
+ {@const label_style = point_label.auto_placement &&
2353
+ actual_label_config.max_neighbors &&
2354
+ !calculated_label_pos
2355
+ ? {}
2356
+ : point_label}
2357
+ {@const final_label = calculated_label_pos
2358
+ ? {
2359
+ ...label_style,
2360
+ offset: {
2361
+ x: calculated_label_pos.x -
2362
+ (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
2363
+ y: calculated_label_pos.y - (series_data.y_axis === `y2`
2364
+ ? y2_scale_fn(point.y)
2365
+ : y_scale_fn(point.y)),
2366
+ },
2367
+ }
2368
+ : label_style}
2369
+ {@const [raw_screen_x, raw_screen_y] = get_screen_coords(point, series_data)}
2370
+ {@const screen_x = isFinite(raw_screen_x) ? raw_screen_x : x_scale_fn.range()[0]}
2371
+ {@const screen_y = isFinite(raw_screen_y)
2372
+ ? raw_screen_y
2373
+ : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
2374
+ {@const apply_controls = using_controls &&
2375
+ (!has_multiple_series ||
2376
+ series_data._id === series_with_ids[selected_series_idx]?._id)}
2377
+ {@const pt = point.point_style}
2378
+ {@const tc = (key: string) => apply_controls && touched.has(key)}
2379
+ {@const computed_radius = point.size_value != null
2380
+ ? size_scale_fn(point.size_value)
2381
+ : (tc(`point.size`) ? styles.point?.size : null) ?? pt?.radius ?? 4}
2382
+ <ScatterPoint
2383
+ x={screen_x}
2384
+ y={screen_y}
2385
+ is_dimmed={hovered_legend_series_idx !== null &&
2386
+ hovered_legend_series_idx !== point.series_idx}
2387
+ is_hovered={tooltip_point?.series_idx === point.series_idx &&
2388
+ tooltip_point?.point_idx === point.point_idx}
2389
+ is_selected={selected_point?.series_idx === point.series_idx &&
2390
+ selected_point?.point_idx === point.point_idx}
2391
+ leader_line_threshold={actual_label_config.leader_line_threshold}
2392
+ style={{
2393
+ symbol_type: pt?.symbol_type ?? series_default_symbol,
2394
+ ...pt,
2395
+ radius: computed_radius,
2396
+ stroke_width:
2397
+ (tc(`point.stroke_width`) ? styles.point?.stroke_width : null) ??
2398
+ pt?.stroke_width ?? 1,
2399
+ stroke:
2400
+ (tc(`point.stroke_color`) ? styles.point?.stroke_color : null) ??
2401
+ pt?.stroke ?? `#000`,
2402
+ stroke_opacity:
2403
+ (tc(`point.stroke_opacity`) ? styles.point?.stroke_opacity : null) ??
2404
+ pt?.stroke_opacity ?? 1,
2405
+ fill_opacity: (tc(`point.opacity`) ? styles.point?.opacity : null) ??
2406
+ pt?.fill_opacity ?? 1,
2407
+ cursor: (on_point_click || point_events?.onclick)
2408
+ ? `pointer`
2409
+ : undefined,
2410
+ }}
2411
+ hover={point.point_hover ?? {}}
2412
+ label={final_label}
2413
+ offset={point.point_offset ?? { x: 0, y: 0 }}
2414
+ {point_tween}
2415
+ origin={{ x: plot_center_x, y: plot_center_y }}
2416
+ --point-fill-color={point.color_value != null
2417
+ ? color_scale_fn(point.color_value)
2418
+ : (tc(`point.color`) ? styles.point?.color : null) ?? pt?.fill ??
2419
+ series_default_color}
2420
+ {...point_events &&
2421
+ Object.fromEntries(
2422
+ point_event_names.map((name) => [name, (event: Event) =>
2423
+ point_events?.[name]?.({ point, event })]
2424
+ ),
2425
+ )}
2426
+ onclick={(event: MouseEvent) => {
2427
+ // Call user-provided onclick handler first if it exists
2428
+ point_events?.onclick?.({ point, event })
2429
+ // then handle internal logic
2430
+ const props = construct_handler_props(point)
2431
+ tooltip_point = point
2432
+ if (props) on_point_click?.({ ...props, event, point })
2433
+ }}
2434
+ />
2435
+ {/each}
2436
+ {/if}
2437
+ </g>
2438
+ {/each}
2439
+ {/if}
2440
+
2441
+ <!-- Fill regions: above all -->
2442
+ {@render fill_regions_layer(fills_by_z.above_all)}
2443
+ <!-- Reference lines: above all -->
2444
+ {@render ref_lines_layer(ref_lines_by_z.above_all)}
2445
+ </svg>
2446
+
2447
+ <!-- Tooltip overlay above all plot overlays (legend, colorbar) -->
2448
+ {#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 },
2496
+ )}
2497
+ <PlotTooltip
2498
+ x={tooltip_pos.x}
2499
+ y={tooltip_pos.y}
2500
+ offset={{ x: 0, y: 0 }}
2501
+ bg_color={tooltip_bg_color}
2502
+ bind:wrapper={tooltip_el}
2503
+ >
2504
+ {#if tooltip}
2505
+ {@render tooltip(handler_props)}
2506
+ {:else}
2507
+ {@const hp = handler_props}
2508
+ {#if has_multiple_series && hp.label}<strong>{hp.label}</strong><br />{/if}
2509
+ {@html sanitize_html(point_label?.text ? `${point_label.text}<br />` : ``)}
2510
+ {@html sanitize_html(hp.x_axis.label || `x`)}: {hp.x_formatted}<br />
2511
+ {@html sanitize_html(hp.y_axis.label || `y`)}: {hp.y_formatted}
2512
+ {#if hp.colorbar?.value != null}
2513
+ <br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {
2514
+ format_value(hp.colorbar.value, hp.colorbar.tick_format || `.3~g`)
2515
+ }
2516
+ {/if}
2517
+ {/if}
2518
+ </PlotTooltip>
2519
+ {/if}
2520
+
2521
+ <!-- Control Pane -->
2522
+ {#if controls.show}
2523
+ <ScatterPlotControls
2524
+ toggle_props={{
2525
+ ...controls.toggle_props,
2526
+ style:
2527
+ `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
2528
+ controls.toggle_props?.style ?? ``
2529
+ }`,
2530
+ }}
2531
+ pane_props={controls.pane_props}
2532
+ bind:x_axis
2533
+ bind:x2_axis
2534
+ bind:y_axis
2535
+ bind:y2_axis
2536
+ bind:display
2537
+ bind:styles
2538
+ {auto_x_range}
2539
+ {auto_x2_range}
2540
+ {auto_y_range}
2541
+ {auto_y2_range}
2542
+ bind:selected_series_idx
2543
+ series={series_with_ids}
2544
+ has_x2_points={x2_points.length > 0}
2545
+ has_y2_points={y2_points.length > 0}
2546
+ children={controls_extra}
2547
+ on_touch={(key: string) => touched.add(key)}
2548
+ />
2549
+ {/if}
2550
+
2551
+ <!-- Color Bar -->
2552
+ {#if color_bar && all_color_values.length > 0 && color_bar_placement}
2553
+ {@const color_domain = [
2554
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[0] ??
2555
+ auto_color_range[0],
2556
+ (typeof color_scale === `string` ? undefined : color_scale.value_range)?.[1] ??
2557
+ auto_color_range[1],
2558
+ ] as Vec2}
2559
+ <div
2560
+ bind:this={colorbar_element}
2561
+ onmouseenter={() => colorbar_hover.set_locked(true)}
2562
+ onmouseleave={() => colorbar_hover.set_locked(false)}
2563
+ class="colorbar-wrapper"
2564
+ role="img"
2565
+ aria-label="Color scale legend"
2566
+ style={`${
2567
+ // explicit wrapper_style or auto-outside places the colorbar; else auto-placement coords
2568
+ effective_cbar_wrapper_style ??
2569
+ `position: absolute; left: ${tweened_colorbar_coords.current.x}px; top: ${tweened_colorbar_coords.current.y}px`}; pointer-events: auto;`}
2570
+ >
2571
+ <ColorBar
2572
+ tick_labels={4}
2573
+ tick_side="primary"
2574
+ {color_scale_fn}
2575
+ color_scale_domain={color_domain}
2576
+ scale_type={typeof color_scale === `string` ? undefined : color_scale.type}
2577
+ range={color_domain?.every((val) => val != null) ? color_domain : undefined}
2578
+ bar_style="width: 220px; height: 16px; {color_bar?.style ?? ``}"
2579
+ {...color_bar}
2580
+ wrapper_style={effective_cbar_wrapper_style ? `height: 100%; width: 100%;` : ``}
2581
+ />
2582
+ </div>
2583
+ {/if}
2584
+
2585
+ <!-- Legend -->
2586
+ <!-- Only render if multiple series or if legend prop was explicitly provided by user (even if empty object) -->
2587
+ {#if legend != null && legend_data.length > 0 &&
2588
+ (legend_data.length > 1 || Object.keys(legend).length > 0)}
2589
+ {@const default_x = pad.l + 10}
2590
+ {@const default_y = pad.t + 10}
2591
+ {@const current_x = legend_is_dragging && legend_manual_position
2592
+ ? legend_manual_position.x
2593
+ : legend_auto_outside
2594
+ ? legend_outside_x
2595
+ : legend_placement
2596
+ ? tweened_legend_coords.current.x
2597
+ : default_x}
2598
+ {@const current_y = legend_is_dragging && legend_manual_position
2599
+ ? legend_manual_position.y
2600
+ : legend_auto_outside
2601
+ ? legend_outside_y
2602
+ : legend_placement
2603
+ ? tweened_legend_coords.current.y
2604
+ : default_y}
2605
+ <PlotLegend
2606
+ bind:root_element={legend_element}
2607
+ series_data={legend_data}
2608
+ on_drag_start={handle_legend_drag_start}
2609
+ on_drag={handle_legend_drag}
2610
+ 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)}
2616
+ active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
2617
+ draggable={legend?.draggable ?? true}
2618
+ {...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
+ })}
2637
+ on_fill_toggle={(source_type: `fill_region` | `error_band`, source_idx: number) => {
2638
+ // Only fill_regions can be toggled (error_bands are not bindable)
2639
+ if (source_type === `fill_region`) {
2640
+ fill_regions = fill_regions.map((region, idx) =>
2641
+ idx === source_idx
2642
+ ? { ...region, visible: !(region.visible !== false) }
2643
+ : region
2644
+ )
2645
+ }
2646
+ }}
2647
+ on_fill_double_click={(
2648
+ source_type: `fill_region` | `error_band`,
2649
+ source_idx: number,
2650
+ ) => {
2651
+ // Only fill_regions can be toggled (error_bands are not bindable)
2652
+ if (source_type !== `fill_region`) return
2653
+ // Toggle: if only this fill is visible, show all; otherwise show only this one
2654
+ const visible_count = fill_regions.filter((region) => region.visible !== false).length
2655
+ const this_visible = fill_regions[source_idx]?.visible !== false
2656
+ if (visible_count === 1 && this_visible) {
2657
+ // Show all fills
2658
+ fill_regions = fill_regions.map((region) => ({ ...region, visible: true }))
2659
+ } else {
2660
+ // Show only this fill
2661
+ fill_regions = fill_regions.map((region, idx) => ({
2662
+ ...region,
2663
+ visible: idx === source_idx,
2664
+ }))
2665
+ }
2666
+ }}
2667
+ style={`
2668
+ position: absolute;
2669
+ left: ${current_x}px;
2670
+ top: ${current_y}px;
2671
+ pointer-events: auto;
2672
+ ${legend?.style ?? ``}
2673
+ `}
2674
+ />
2675
+ {/if}
2676
+ {/if}
2677
+
2678
+ <!-- User-provided children (e.g. for custom absolutely-positioned overlays) -->
2679
+ {@render children?.({ height, width, fullscreen })}
2680
+ </div>
2681
+
2682
+ <style>
2683
+ div.scatter {
2684
+ position: relative; /* Needed for absolute positioning of children like ColorBar */
2685
+ width: var(--scatter-width, 100%);
2686
+ height: var(--scatter-height, auto);
2687
+ min-height: var(--scatter-min-height, 350px);
2688
+ container-type: size; /* enable cqh for panes */
2689
+ container-name: scatter-plot;
2690
+ z-index: var(--scatter-z-index);
2691
+ flex: var(--scatter-flex, 1); /* Allow filling available space in flex containers */
2692
+ display: var(--scatter-display, flex);
2693
+ flex-direction: column;
2694
+ background: var(--scatter-bg, var(--plot-bg));
2695
+ border-radius: var(--scatter-border-radius, var(--border-radius, 3pt));
2696
+ }
2697
+ div.scatter.fullscreen {
2698
+ position: fixed;
2699
+ top: 0;
2700
+ left: 0;
2701
+ width: 100vw !important;
2702
+ height: 100vh !important;
2703
+ /* Must be higher than Structure.svelte's --struct-buttons-z-index. */
2704
+ z-index: var(--scatter-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
2705
+ margin: 0;
2706
+ border-radius: 0;
2707
+ background: var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg)));
2708
+ max-height: none !important;
2709
+ overflow: hidden;
2710
+ /* Add padding to prevent titles from being cropped at top */
2711
+ padding-top: var(--plot-fullscreen-padding-top, 2em);
2712
+ box-sizing: border-box;
2713
+ }
2714
+ /* Center the colorbar within its wrapper when shorter than it (e.g. capped by --cbar-max-height
2715
+ in fullscreen). Users can override via wrapper_style (inline wins). */
2716
+ .colorbar-wrapper {
2717
+ display: flex;
2718
+ align-items: center;
2719
+ justify-content: center;
2720
+ }
2721
+ .header-controls {
2722
+ position: absolute;
2723
+ top: var(--ctrl-btn-top, 5pt);
2724
+ right: var(--fullscreen-btn-right, 4px);
2725
+ z-index: var(--fullscreen-btn-z-index, 10);
2726
+ display: flex;
2727
+ align-items: center;
2728
+ gap: 8px;
2729
+ }
2730
+ .header-controls :global(.fullscreen-toggle) {
2731
+ position: static; /* Override absolute positioning since container handles it */
2732
+ opacity: 1; /* Always visible when inside header-controls, container controls visibility */
2733
+ }
2734
+ /* Hide controls and fullscreen toggles by default, show on hover */
2735
+ div.scatter :global(.pane-toggle),
2736
+ div.scatter .header-controls {
2737
+ opacity: 0;
2738
+ transition: opacity 0.2s, background-color 0.2s;
2739
+ }
2740
+ div.scatter:hover :global(.pane-toggle),
2741
+ div.scatter:hover .header-controls,
2742
+ div.scatter :global(.pane-toggle:focus-visible),
2743
+ div.scatter :global(.pane-toggle[aria-expanded='true']),
2744
+ div.scatter .header-controls:focus-within {
2745
+ opacity: 1;
2746
+ }
2747
+ svg {
2748
+ width: var(--scatter-svg-width, 100%);
2749
+ height: var(--scatter-svg-height, 100%);
2750
+ flex: var(--scatter-svg-flex, 1);
2751
+ overflow: var(--scatter-svg-overflow, visible);
2752
+ fill: var(--text-color);
2753
+ font-weight: var(--scatter-font-weight);
2754
+ font-size: var(--scatter-font-size);
2755
+ }
2756
+ .scatter :global(.axis-label) {
2757
+ text-align: center;
2758
+ width: 100%;
2759
+ height: 100%;
2760
+ font-size: var(--scatter-font-size, inherit);
2761
+ font-weight: var(--scatter-font-weight, normal);
2762
+ color: var(--text-color);
2763
+ white-space: nowrap;
2764
+ /* Use line-height to center text vertically without flexbox */
2765
+ line-height: var(
2766
+ --scatter-axis-label-line-height,
2767
+ 20px
2768
+ ); /* Match foreignObject height */
2769
+ display: block;
2770
+ }
2771
+ .current-frame-indicator {
2772
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
2773
+ transition: opacity 0.2s ease;
2774
+ }
2775
+ .current-frame-indicator:hover {
2776
+ opacity: 0.8;
2777
+ }
2778
+ </style>