matterviz 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { format_value } from '../../labels'
2
+ import { format_value_or_num } from '../../labels'
3
3
  import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
4
4
  import type {
5
5
  AxisLoadError,
@@ -15,29 +15,35 @@
15
15
  HistogramControls,
16
16
  PlotAxis,
17
17
  PlotLegend,
18
+ PlotMarginals,
18
19
  ReferenceLine,
19
20
  } from '..'
20
- import type { AxisChangeState } from '../core/axis-utils'
21
- import { AXIS_DEFAULTS, create_axis_loader } from '../core/axis-utils'
22
- import { extract_series_color, prepare_legend_data } from '../core/data-transform'
21
+ import type { MarginalSeriesInput, MarginalsProp } from '../core/marginals'
22
+ import {
23
+ add_sides,
24
+ marginal_axis,
25
+ marginal_axis_presence,
26
+ normalize_marginals,
27
+ reserve_marginal_pad,
28
+ } from '../core/marginals'
23
29
  import {
24
- create_dimension_tracker,
25
- create_hover_lock,
26
- } from '../core/hover-lock.svelte'
30
+ AXIS_DEFAULTS,
31
+ type AxisChangeState,
32
+ create_axis_loader,
33
+ } from '../core/axis-utils'
34
+ import { extract_series_color, prepare_legend_data } from '../core/data-transform'
35
+ import { create_placed_tween } from '../core/placed-tween.svelte'
36
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
27
37
  import { create_legend_visibility } from '../core/utils/series-visibility'
28
38
  import {
29
- get_relative_coords,
30
- MIN_TOUCH_DISTANCE_PIXELS,
31
- pan_range_by_pixels,
32
- PINCH_ZOOM_THRESHOLD,
33
- remove_drag_listeners,
39
+ invert_rect_range,
34
40
  resolve_axis_ranges,
35
- sorted_range,
36
41
  vec2_equal,
37
- zoom_range_by_factor,
38
42
  } from '../core/interactions'
39
43
  import {
44
+ AXIS_TITLE_OFFSET,
40
45
  calc_auto_padding,
46
+ DEFAULT_PLOT_PADDING,
41
47
  filter_padding,
42
48
  LABEL_GAP_DEFAULT,
43
49
  y2_axis_label_x,
@@ -49,11 +55,12 @@
49
55
  has_explicit_position,
50
56
  measured_footprint,
51
57
  place_decorations,
58
+ placed_coords,
52
59
  } from '../core/auto-place'
53
60
  import type { IndexedRefLine } from '../core/reference-line'
54
61
  import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
55
62
  import {
56
- create_scale,
63
+ create_axis_scales,
57
64
  generate_ticks,
58
65
  get_nice_data_range,
59
66
  get_tick_label,
@@ -61,20 +68,20 @@
61
68
  import type {
62
69
  BasePlotProps,
63
70
  DataSeries,
64
- InitialRanges,
65
71
  LegendConfig,
66
72
  PlotConfig,
67
- ScaleType,
68
73
  } from '../core/types'
69
- import { get_scale_type_name } from '../core/types'
74
+ import {
75
+ compute_count_range,
76
+ compute_histogram_bins,
77
+ log_safe_range,
78
+ } from './histogram'
70
79
  import ZeroLines from '../core/components/ZeroLines.svelte'
71
80
  import ZoomRect from '../core/components/ZoomRect.svelte'
72
81
  import { DEFAULTS } from '../../settings'
73
- import { bin, max } from 'd3-array'
74
82
  import type { Snippet } from 'svelte'
75
83
  import { onDestroy, untrack } from 'svelte'
76
84
  import type { HTMLAttributes } from 'svelte/elements'
77
- import { Tween } from 'svelte/motion'
78
85
  import type { Vec2 } from '../../math'
79
86
  import PlotTooltip from '../core/components/PlotTooltip.svelte'
80
87
  import { bar_path } from '../core/svg'
@@ -87,8 +94,8 @@
87
94
  y_axis: y_axis_init = {},
88
95
  y2_axis: y2_axis_init = {},
89
96
  display: display_init = DEFAULTS.histogram.display,
90
- range_padding = 0.05,
91
- padding = { t: 20, b: 60, l: 60, r: 20 },
97
+ range_padding = 0,
98
+ padding = DEFAULT_PLOT_PADDING,
92
99
  bins = $bindable(100),
93
100
  show_legend = $bindable(true),
94
101
  legend = {},
@@ -117,49 +124,47 @@
117
124
  on_axis_change,
118
125
  on_error,
119
126
  pan = {},
127
+ marginals = false,
120
128
  ...rest
121
- }: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
122
- series: DataSeries[]
123
- // Component-specific props
124
- bins?: number
125
- show_legend?: boolean
126
- legend?: LegendConfig | null
127
- bar?: BarStyle
128
- selected_property?: string
129
- mode?: `single` | `overlay`
130
- tooltip?: Snippet<[HistogramHandlerProps]>
131
- header_controls?: Snippet<
132
- [{ height: number; width: number; fullscreen: boolean }]
133
- >
134
- controls_extra?: Snippet<[Required<PlotConfig>]>
135
- change?: (data: { value: number; count: number; property: string } | null) => void
136
- on_bar_click?: (
137
- data: {
129
+ }: HTMLAttributes<HTMLDivElement> &
130
+ BasePlotProps &
131
+ PlotConfig & {
132
+ series: DataSeries[]
133
+ // Component-specific props
134
+ bins?: number
135
+ show_legend?: boolean
136
+ legend?: LegendConfig | null
137
+ bar?: BarStyle
138
+ selected_property?: string
139
+ mode?: `single` | `overlay`
140
+ tooltip?: Snippet<[HistogramHandlerProps]>
141
+ header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
142
+ controls_extra?: Snippet<[Required<PlotConfig>]>
143
+ change?: (data: { value: number; count: number; property: string } | null) => void
144
+ on_bar_click?: (data: {
138
145
  value: number
139
146
  count: number
140
147
  property: string
141
148
  event: MouseEvent | KeyboardEvent
142
- },
143
- ) => void
144
- on_bar_hover?: (
145
- data:
146
- | { value: number; count: number; property: string; event: MouseEvent }
147
- | null,
148
- ) => void
149
- ref_lines?: RefLine[]
150
- on_ref_line_click?: (event: RefLineEvent) => void
151
- on_ref_line_hover?: (event: RefLineEvent | null) => void
152
- on_series_toggle?: (series_idx: number) => void
153
- // Interactive axis props
154
- data_loader?: DataLoaderFn
155
- on_axis_change?: (
156
- axis: `x` | `x2` | `y` | `y2`,
157
- key: string,
158
- new_series: DataSeries[],
159
- ) => void
160
- on_error?: (error: AxisLoadError) => void
161
- pan?: PanConfig
162
- } = $props()
149
+ }) => void
150
+ on_bar_hover?: (
151
+ data: { value: number; count: number; property: string; event: MouseEvent } | null,
152
+ ) => void
153
+ ref_lines?: RefLine[]
154
+ on_ref_line_click?: (event: RefLineEvent) => void
155
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
156
+ on_series_toggle?: (series_idx: number) => void
157
+ // Interactive axis props
158
+ data_loader?: DataLoaderFn
159
+ on_axis_change?: (
160
+ axis: `x` | `x2` | `y` | `y2`,
161
+ key: string,
162
+ new_series: DataSeries[],
163
+ ) => void
164
+ on_error?: (error: AxisLoadError) => void
165
+ pan?: PanConfig
166
+ marginals?: MarginalsProp
167
+ } = $props()
163
168
 
164
169
  // Local state for controls (initialized from props, owned by this component)
165
170
  // Include key AXIS_DEFAULTS props (range, ticks, scale_type) that PlotControls needs
@@ -169,25 +174,29 @@
169
174
  let bar = $state(untrack(() => ({ ...DEFAULTS.histogram.bar, ...bar_init })))
170
175
  let x_axis = $state(untrack(() => ({ ...axis_state_defaults, ...x_axis_init })))
171
176
  // x2-axis needs different default label_shift for top-side positioning
172
- let x2_axis = $state(untrack(() => ({
173
- ...axis_state_defaults,
174
- label_shift: { x: 0, y: 40 },
175
- ...x2_axis_init,
176
- })))
177
+ let x2_axis = $state(
178
+ untrack(() => ({
179
+ ...axis_state_defaults,
180
+ label_shift: { x: 0, y: AXIS_TITLE_OFFSET },
181
+ ...x2_axis_init,
182
+ })),
183
+ )
177
184
  let y_axis = $state(untrack(() => ({ ...axis_state_defaults, ...y_axis_init })))
178
185
  // y2 title stays vertically centered; its x position is computed by y2_axis_label_x
179
- let y2_axis = $state(untrack(() => ({
180
- ...axis_state_defaults,
181
- label_shift: { x: 0, y: 0 },
182
- ...y2_axis_init,
183
- })))
184
- let display = $state(
185
- untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })),
186
+ let y2_axis = $state(
187
+ untrack(() => ({
188
+ ...axis_state_defaults,
189
+ label_shift: { x: 0, y: 0 },
190
+ ...y2_axis_init,
191
+ })),
186
192
  )
193
+ let display = $state(untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })))
187
194
 
188
195
  // Merge component-specific defaults with local state (format comes from here, not AXIS_DEFAULTS)
189
- const final_x_axis = $derived({ label: `Value`, format: `.2~s`, ...x_axis })
190
- const final_x2_axis = $derived({ label: `Value`, format: `.2~s`, ...x2_axis })
196
+ // No default tick format: PlotAxis falls back to format_num, which uses SI
197
+ // prefixes above 1 but plain decimals below (0.2 must not render as 200m)
198
+ const final_x_axis = $derived({ label: `Value`, ...x_axis })
199
+ const final_x2_axis = $derived({ label: `Value`, ...x2_axis })
191
200
  const final_y_axis = $derived({ label: `Count`, format: `d`, ...y_axis })
192
201
  const final_bar = $derived({ ...DEFAULTS.histogram.bar, ...bar })
193
202
  const final_y2_axis = $derived({ label: `Count`, format: `d`, ...y2_axis })
@@ -206,33 +215,11 @@
206
215
  let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
207
216
 
208
217
  // Compute ref_lines with index and group by z-index (using shared utilities)
209
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
210
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
211
- let drag_state = $state<{
212
- start: { x: number; y: number } | null
213
- current: { x: number; y: number } | null
214
- bounds: DOMRect | null
215
- }>({ start: null, current: null, bounds: null })
216
-
217
- // Pan state
218
- let is_focused = $state(false)
219
- let shift_held = $state(false)
220
- let pan_drag_state = $state<
221
- InitialRanges & { start: { x: number; y: number } } | null
222
- >(null)
223
- let touch_state = $state<
224
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
225
- >(null)
218
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
226
219
 
227
220
  // Legend placement stability state
228
221
  let legend_element = $state<HTMLDivElement | undefined>()
229
222
  let hovered_legend_series_idx = $state<number | null>(null)
230
- const legend_hover = create_hover_lock()
231
- const dim_tracker = create_dimension_tracker()
232
- let has_initial_legend_placement = $state(false)
233
-
234
- // Clear pending hover lock timeout on unmount
235
- $effect(() => () => legend_hover.cleanup())
236
223
 
237
224
  // Derived data
238
225
  type IndexedSeries = { series_data: DataSeries; series_idx: number }
@@ -250,29 +237,24 @@
250
237
  let selected_series_entries = $derived<IndexedSeries[]>(
251
238
  series
252
239
  .map((series_data: DataSeries, series_idx: number) => ({ series_data, series_idx }))
253
- .filter(({ series_data }) =>
254
- (series_data.visible ?? true) &&
255
- (mode !== `single` || !selected_property || series_data.label === selected_property)
240
+ .filter(
241
+ ({ series_data }) =>
242
+ (series_data.visible ?? true) &&
243
+ (mode !== `single` || !selected_property || series_data.label === selected_property),
256
244
  ),
257
245
  )
258
- let selected_series = $derived(
259
- selected_series_entries.map(({ series_data }) => series_data),
260
- )
246
+ let selected_series = $derived(selected_series_entries.map(({ series_data }) => series_data))
261
247
 
262
248
  // Separate series by y-axis
263
249
  let y1_series = $derived(
264
250
  selected_series.filter((srs: DataSeries) => (srs.y_axis ?? `y1`) === `y1`),
265
251
  )
266
- let y2_series = $derived(
267
- selected_series.filter((srs: DataSeries) => srs.y_axis === `y2`),
268
- )
269
- let x2_series = $derived(
270
- selected_series.filter((srs: DataSeries) => srs.x_axis === `x2`),
271
- )
252
+ let y2_series = $derived(selected_series.filter((srs: DataSeries) => srs.y_axis === `y2`))
253
+ let x2_series = $derived(selected_series.filter((srs: DataSeries) => srs.x_axis === `x2`))
272
254
 
273
255
  let auto_ranges = $derived.by(() => {
274
256
  // Only x1 series contribute to the x1 auto-range (x2 series get their own domain below)
275
- const x1_values = selected_series.flatMap((srs) => srs.x_axis === `x2` ? [] : srs.y)
257
+ const x1_values = selected_series.flatMap((srs) => (srs.x_axis === `x2` ? [] : srs.y))
276
258
  const auto_x = get_nice_data_range(
277
259
  x1_values.map((val) => ({ x: val, y: 0 })),
278
260
  ({ x }) => x,
@@ -283,68 +265,29 @@
283
265
  )
284
266
 
285
267
  const x2_values = x2_series.flatMap((srs: DataSeries) => srs.y)
286
- const auto_x2 = x2_values.length > 0
287
- ? get_nice_data_range(
288
- x2_values.map((val) => ({ x: val, y: 0 })),
289
- ({ x }) => x,
290
- final_x2_axis.range ?? [null, null],
291
- final_x2_axis.scale_type ?? `linear`,
292
- range_padding,
293
- false,
294
- )
295
- : [0, 1] as Vec2
296
-
297
- // Calculate y-range for a specific set of series
298
- const calc_y_range = (
299
- series_list: typeof selected_series,
300
- y_limit: [number | null, number | null],
301
- scale_type: ScaleType,
302
- ): Vec2 => {
303
- const type_name = get_scale_type_name(scale_type)
304
- if (series_list.length === 0) {
305
- const fallback = type_name === `log` ? 1 : 0
306
- return [fallback, 1]
307
- }
308
- // Bin each series over the domain of the x-axis it renders on (d3 bin() drops
309
- // out-of-domain values, so binning x2 series over the x1 domain skews max_count)
310
- const max_count = Math.max(
311
- 0,
312
- ...series_list.map((srs: DataSeries) => {
313
- const hist = bin().domain(srs.x_axis === `x2` ? auto_x2 : auto_x).thresholds(bins)
314
- return max(hist(srs.y), (data) => data.length) || 0
315
- }),
316
- )
317
-
318
- // If there's effectively no data, avoid log-range issues (counts can't be <= 0 on log)
319
- if (max_count <= 0) {
320
- const fallback = type_name === `log` ? 1 : 0
321
- return [fallback, 1]
322
- }
323
-
324
- const [y0, y1] = get_nice_data_range(
325
- [{ x: 0, y: 0 }, { x: max_count, y: 0 }],
326
- ({ x }) => x,
327
- y_limit,
328
- scale_type,
329
- range_padding,
330
- false,
331
- )
332
- // For log scale, minimum must be >= 1 (count can't be 0 on log)
333
- // For linear/arcsinh, start from 0
334
- const y_min = type_name === `log` ? Math.max(1, y0) : Math.max(0, y0)
335
- return [y_min, y1]
336
- }
337
-
338
- const y1_range = calc_y_range(
339
- y1_series,
340
- final_y_axis.range ?? [null, null],
341
- final_y_axis.scale_type ?? `linear`,
342
- )
343
- const y2_auto_range = calc_y_range(
344
- y2_series,
345
- final_y2_axis.range ?? [null, null],
346
- final_y2_axis.scale_type ?? `linear`,
347
- )
268
+ const auto_x2 =
269
+ x2_values.length > 0
270
+ ? get_nice_data_range(
271
+ x2_values.map((val) => ({ x: val, y: 0 })),
272
+ ({ x }) => x,
273
+ final_x2_axis.range ?? [null, null],
274
+ final_x2_axis.scale_type ?? `linear`,
275
+ range_padding,
276
+ false,
277
+ )
278
+ : ([0, 1] as Vec2)
279
+
280
+ const count_cfg = { x_domain: auto_x, x2_domain: auto_x2, bin_count: bins, range_padding }
281
+ const y1_range = compute_count_range(y1_series, {
282
+ ...count_cfg,
283
+ scale_type: final_y_axis.scale_type ?? `linear`,
284
+ y_limit: log_safe_range(final_y_axis),
285
+ })
286
+ const y2_auto_range = compute_count_range(y2_series, {
287
+ ...count_cfg,
288
+ scale_type: final_y2_axis.scale_type ?? `linear`,
289
+ y_limit: log_safe_range(final_y2_axis),
290
+ })
348
291
 
349
292
  return { x: auto_x, x2: auto_x2, y: y1_range, y2: y2_auto_range }
350
293
  })
@@ -366,10 +309,16 @@
366
309
  })
367
310
 
368
311
  $effect(() => {
369
- // Supports one-sided range pinning (null bounds fall back to auto); returns null
370
- // for transient non-finite bounds (skip: writing NaN breaks scales and loops here)
312
+ // Supports one-sided range pinning (null bounds fall back to auto); returns null for transient
313
+ // non-finite bounds (skip: writing NaN breaks scales and loops here). y/y2 ranges are
314
+ // log-sanitized so an invalid (<= 0) log lower falls back to the auto count minimum.
371
315
  const next = resolve_axis_ranges(
372
- { x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis },
316
+ {
317
+ x: final_x_axis,
318
+ x2: final_x2_axis,
319
+ y: { range: log_safe_range(final_y_axis) },
320
+ y2: { range: log_safe_range(final_y2_axis) },
321
+ },
373
322
  auto_ranges,
374
323
  )
375
324
  if (!next) return
@@ -378,15 +327,16 @@
378
327
  // (reading + writing the same state otherwise causes effect_update_depth_exceeded).
379
328
  const init = untrack(() => ranges.initial)
380
329
  if (!vec2_equal(init.x, next.x)) [ranges.initial.x, ranges.current.x] = [next.x, next.x]
381
- if (!vec2_equal(init.x2, next.x2)) [ranges.initial.x2, ranges.current.x2] = [next.x2, next.x2]
330
+ if (!vec2_equal(init.x2, next.x2))
331
+ [ranges.initial.x2, ranges.current.x2] = [next.x2, next.x2]
382
332
  if (!vec2_equal(init.y, next.y)) [ranges.initial.y, ranges.current.y] = [next.y, next.y]
383
- if (!vec2_equal(init.y2, next.y2)) [ranges.initial.y2, ranges.current.y2] = [next.y2, next.y2]
333
+ if (!vec2_equal(init.y2, next.y2))
334
+ [ranges.initial.y2, ranges.current.y2] = [next.y2, next.y2]
384
335
  })
385
336
 
386
337
  // Layout: dynamic padding based on tick label widths
387
- const default_padding = { t: 20, b: 60, l: 60, r: 20 }
388
338
  // base_pad reserves space for tick labels/axis titles; pad (below) adds decoration reservations
389
- let base_pad = $derived(filter_padding(padding, default_padding))
339
+ let base_pad = $derived(filter_padding(padding, DEFAULT_PLOT_PADDING))
390
340
 
391
341
  // Update padding based on tick label widths (untrack breaks circular dependency)
392
342
  $effect(() => {
@@ -394,19 +344,23 @@
394
344
  const current_ticks_y = untrack(() => ticks.y)
395
345
  const current_ticks_y2 = untrack(() => ticks.y2)
396
346
 
397
- const new_pad = width && height && current_ticks_y.length > 0
398
- ? calc_auto_padding({
399
- padding,
400
- default_padding,
401
- x2_axis: { ...final_x2_axis, tick_values: current_ticks_x2 },
402
- y_axis: { ...final_y_axis, tick_values: current_ticks_y },
403
- y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
404
- })
405
- : filter_padding(padding, default_padding)
347
+ const new_pad =
348
+ width && height && current_ticks_y.length > 0
349
+ ? calc_auto_padding({
350
+ padding,
351
+ default_padding: DEFAULT_PLOT_PADDING,
352
+ x2_axis: { ...final_x2_axis, tick_values: current_ticks_x2 },
353
+ y_axis: { ...final_y_axis, tick_values: current_ticks_y },
354
+ y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
355
+ })
356
+ : filter_padding(padding, DEFAULT_PLOT_PADDING)
406
357
 
407
358
  // Add y2 axis label space (calc_auto_padding only accounts for tick labels)
408
359
  if (
409
- width && height && y2_series.length > 0 && current_ticks_y2.length > 0 &&
360
+ width &&
361
+ height &&
362
+ y2_series.length > 0 &&
363
+ current_ticks_y2.length > 0 &&
410
364
  final_y2_axis.label
411
365
  ) {
412
366
  const inside = final_y2_axis.tick?.label?.inside ?? false
@@ -422,28 +376,31 @@
422
376
 
423
377
  // Add x2 axis label space (mirroring y2 logic for top padding)
424
378
  if (
425
- width && height && x2_series.length > 0 && current_ticks_x2.length > 0 &&
379
+ width &&
380
+ height &&
381
+ x2_series.length > 0 &&
382
+ current_ticks_x2.length > 0 &&
426
383
  final_x2_axis.label
427
384
  ) {
428
385
  const inside = final_x2_axis.tick?.label?.inside ?? false
429
- const tick_shift = inside
430
- ? 0
431
- : Math.abs(final_x2_axis.tick?.label?.shift?.y ?? 0) + 8
386
+ const tick_shift = inside ? 0 : Math.abs(final_x2_axis.tick?.label?.shift?.y ?? 0) + 8
432
387
  const label_thickness = Math.round(12 * 1.2)
433
- new_pad.t = Math.max(
434
- new_pad.t,
435
- tick_shift + LABEL_GAP_DEFAULT + label_thickness,
436
- )
388
+ new_pad.t = Math.max(new_pad.t, tick_shift + LABEL_GAP_DEFAULT + label_thickness)
437
389
  }
438
390
 
439
391
  // Only update if padding actually changed
440
392
  if (
441
- base_pad.t !== new_pad.t || base_pad.b !== new_pad.b ||
442
- base_pad.l !== new_pad.l || base_pad.r !== new_pad.r
443
- ) base_pad = new_pad
393
+ base_pad.t !== new_pad.t ||
394
+ base_pad.b !== new_pad.b ||
395
+ base_pad.l !== new_pad.l ||
396
+ base_pad.r !== new_pad.r
397
+ )
398
+ base_pad = new_pad
444
399
  })
445
400
 
446
- const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 60 }))
401
+ const legend_footprint = $derived(
402
+ measured_footprint(legend_element, { width: 120, height: 60 }),
403
+ )
447
404
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
448
405
 
449
406
  // Obstacle field in normalized [0,1] plot coords (y=0 at top). Each filled bar is modeled as a
@@ -481,66 +438,64 @@
481
438
  height,
482
439
  obstacles_norm,
483
440
  // gate on legend_element (the render signal) not legend_data, whose entries can read pad
484
- legend: show_legend && legend != null && series.length > 1 &&
485
- legend_element != null && !legend_has_explicit_pos
486
- ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
487
- : null,
488
- })
441
+ legend:
442
+ show_legend &&
443
+ legend != null &&
444
+ series.length > 1 &&
445
+ legend_element != null &&
446
+ !legend_has_explicit_pos
447
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
448
+ : null,
449
+ }),
450
+ )
451
+ // Resolve marginals (default: CDF strip on top) and reserve outer-band padding. Pass the
452
+ // histogram's `bins` as the marginal's histogram bin count (NOT `size`/thickness) so a
453
+ // `histogram` marginal inherits the main binning via normalize_marginals' merge; `cdf`
454
+ // ignores `bins`. Samples lie along x and equal series.y, so the adapter maps y -> x.
455
+ const resolved_marginals = $derived(
456
+ normalize_marginals(marginals, { top: { type: `cdf`, bins } }),
457
+ )
458
+ const pad = $derived(add_sides(decor.pad, reserve_marginal_pad(resolved_marginals)))
459
+ // a lone series uses the configured bar color; with multiple, each gets its own
460
+ const series_color = (series_data: DataSeries) =>
461
+ selected_series.length === 1 ? final_bar.color : extract_series_color(series_data)
462
+ const marginal_series = $derived<MarginalSeriesInput[]>(
463
+ selected_series_entries.map(({ series_data }) => ({
464
+ x: series_data.y ?? [],
465
+ color: series_color(series_data),
466
+ label: series_data.label,
467
+ visible: true,
468
+ x_axis: series_data.x_axis,
469
+ y_axis: series_data.y_axis,
470
+ })),
471
+ )
472
+ const marginal_has_axis = $derived(
473
+ marginal_axis_presence(x2_series.length > 0, y2_series.length > 0),
489
474
  )
490
- const pad = $derived(decor.pad)
491
475
  const legend_auto_outside = $derived(decor.legend_outside)
492
476
  const legend_outside_x = $derived(decor.legend_pos.x)
493
477
  const legend_outside_y = $derived(decor.legend_pos.y)
494
478
 
495
- // Scales and data
496
- let scales = $derived({
497
- x: create_scale(
498
- final_x_axis.scale_type ?? `linear`,
499
- ranges.current.x,
500
- [pad.l, width - pad.r],
501
- ),
502
- x2: create_scale(
503
- final_x2_axis.scale_type ?? `linear`,
504
- ranges.current.x2,
505
- [pad.l, width - pad.r],
506
- ),
507
- y: create_scale(
508
- final_y_axis.scale_type ?? `linear`,
509
- ranges.current.y,
510
- [height - pad.b, pad.t],
511
- ),
512
- y2: create_scale(
513
- final_y2_axis.scale_type ?? `linear`,
514
- ranges.current.y2,
515
- [height - pad.b, pad.t],
479
+ // Scales and data (x/x2 share the horizontal pixel span, y/y2 the inverted vertical one)
480
+ let scales = $derived(
481
+ create_axis_scales(
482
+ { x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis },
483
+ ranges.current,
484
+ pad,
485
+ width,
486
+ height,
516
487
  ),
517
- })
488
+ )
518
489
 
519
490
  // Pad-independent binning (no pixel scales) so the auto-place obstacle field can reuse it
520
491
  let histogram_bins = $derived.by(() => {
521
492
  if (selected_series.length === 0 || !width || !height) return []
522
- const hist_generator = bin()
523
- .domain([ranges.current.x[0], ranges.current.x[1]])
524
- .thresholds(bins)
525
- const x2_hist_generator = x2_series.length > 0
526
- ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
527
- : null
528
- return selected_series_entries.map(({ series_data, series_idx }) => {
529
- const use_x2 = series_data.x_axis === `x2`
530
- const active_hist = use_x2 && x2_hist_generator ? x2_hist_generator : hist_generator
531
- const bins_arr = active_hist(series_data.y)
532
- return {
533
- id: series_data.id ?? series_idx,
534
- series_idx,
535
- label: series_data.label || `Series ${series_idx + 1}`,
536
- color: selected_series.length === 1
537
- ? final_bar.color
538
- : extract_series_color(series_data),
539
- bins: bins_arr,
540
- max_count: max(bins_arr, (data) => data.length) || 0,
541
- x_axis: series_data.x_axis,
542
- y_axis: series_data.y_axis,
543
- }
493
+ return compute_histogram_bins(selected_series_entries, {
494
+ x_domain: ranges.current.x,
495
+ x2_domain: ranges.current.x2,
496
+ has_x2: x2_series.length > 0,
497
+ bin_count: bins,
498
+ series_color,
544
499
  })
545
500
  })
546
501
  // Render-time data adds the pixel scales (pad-dependent)
@@ -552,43 +507,25 @@
552
507
  })),
553
508
  )
554
509
 
555
- let ticks = $derived({
556
- x: width && height
557
- ? generate_ticks(
558
- ranges.current.x,
559
- final_x_axis.scale_type ?? `linear`,
560
- final_x_axis.ticks,
561
- scales.x,
562
- { default_count: 8 },
563
- )
564
- : [],
565
- x2: width && height && x2_series.length > 0
566
- ? generate_ticks(
567
- ranges.current.x2,
568
- final_x2_axis.scale_type ?? `linear`,
569
- final_x2_axis.ticks,
570
- scales.x2,
571
- { default_count: 8 },
572
- )
573
- : [],
574
- y: width && height
575
- ? generate_ticks(
576
- ranges.current.y,
577
- final_y_axis.scale_type ?? `linear`,
578
- final_y_axis.ticks,
579
- scales.y,
580
- { default_count: 6 },
581
- )
582
- : [],
583
- y2: width && height && y2_series.length > 0
584
- ? generate_ticks(
585
- ranges.current.y2,
586
- final_y2_axis.scale_type ?? `linear`,
587
- final_y2_axis.ticks,
588
- scales.y2,
589
- { default_count: 6 },
590
- )
591
- : [],
510
+ let ticks = $derived.by(() => {
511
+ // x/y always render; x2/y2 only when their series exist (else their scale is a [0,1] sentinel)
512
+ const axis_ticks = (
513
+ axis: typeof final_x_axis,
514
+ range: Vec2,
515
+ scale: typeof scales.x,
516
+ default_count: number,
517
+ show = true,
518
+ ) =>
519
+ width && height && show
520
+ ? generate_ticks(range, axis.scale_type ?? `linear`, axis.ticks, scale, {
521
+ default_count,
522
+ })
523
+ : []
524
+ const x = axis_ticks(final_x_axis, ranges.current.x, scales.x, 8)
525
+ const x2 = axis_ticks(final_x2_axis, ranges.current.x2, scales.x2, 8, x2_series.length > 0)
526
+ const y = axis_ticks(final_y_axis, ranges.current.y, scales.y, 6)
527
+ const y2 = axis_ticks(final_y2_axis, ranges.current.y2, scales.y2, 6, y2_series.length > 0)
528
+ return { x, x2, y, y2 }
592
529
  })
593
530
 
594
531
  // Cache measured tick-label widths so expensive text measurement only runs
@@ -644,249 +581,61 @@
644
581
  return result
645
582
  })
646
583
 
647
- // Tweened legend coordinates for smooth animation - create once, update target via effect
648
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
649
- const tweened_legend_coords = new Tween(
650
- { x: 0, y: 0 },
651
- untrack(() => ({ duration: 400, ...legend?.tween })),
652
- )
653
-
654
- // Update legend position with stability checks
655
- $effect(() => {
656
- if (!width || !height || !legend_placement) return
657
-
658
- // Track dimensions for resize detection
659
- const dims_changed = dim_tracker.has_changed(width, height)
660
- if (dims_changed) dim_tracker.update(width, height)
661
-
662
- // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
663
- const is_responsive = legend?.responsive ?? false
664
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
665
- (is_responsive || !has_initial_legend_placement))
666
-
667
- if (should_update) {
668
- tweened_legend_coords.set(
669
- { x: legend_placement.x, y: legend_placement.y },
670
- // Skip animation on initial placement to avoid jump from (0, 0)
671
- has_initial_legend_placement ? undefined : { duration: 0 },
672
- )
673
- // Only lock position after we have actual measured size
674
- if (legend_element) {
675
- has_initial_legend_placement = true
676
- }
677
- }
584
+ // Tweened legend coordinates with shared placement stability gating
585
+ const legend_tween = create_placed_tween({
586
+ placement: () => legend_placement,
587
+ dims: () => ({ width, height }),
588
+ responsive: () => legend?.responsive ?? false,
589
+ element: () => legend_element,
590
+ tween: () => legend?.tween,
678
591
  })
679
592
 
680
- // Event handlers
681
- const handle_zoom = () => {
682
- if (!drag_state.start || !drag_state.current) return
683
- const start_x = scales.x.invert(drag_state.start.x)
684
- const end_x = scales.x.invert(drag_state.current.x)
685
- const start_x2 = scales.x2.invert(drag_state.start.x)
686
- const end_x2 = scales.x2.invert(drag_state.current.x)
687
- const start_y = scales.y.invert(drag_state.start.y)
688
- const end_y = scales.y.invert(drag_state.current.y)
689
- const start_y2 = scales.y2.invert(drag_state.start.y)
690
- const end_y2 = scales.y2.invert(drag_state.current.y)
691
-
692
- if (typeof start_x === `number` && typeof end_x === `number`) {
693
- const dx = Math.abs(drag_state.start.x - drag_state.current.x)
694
- const dy = Math.abs(drag_state.start.y - drag_state.current.y)
695
- if (dx > 5 && dy > 5) {
696
- // Update axis ranges to trigger reactivity and prevent effect from overriding
697
- x_axis = { ...x_axis, range: sorted_range(start_x, end_x) }
698
- if (x2_series.length > 0) {
699
- x2_axis = { ...x2_axis, range: sorted_range(start_x2, end_x2) }
700
- }
701
- y_axis = { ...y_axis, range: sorted_range(start_y, end_y) }
702
- // gate on y2 series presence (like x2): the y2 scale is a [0, 1] sentinel
703
- // otherwise, so inverting would store a phantom range in the bindable prop
704
- if (y2_series.length > 0) {
705
- y2_axis = { ...y2_axis, range: sorted_range(start_y2, end_y2) }
706
- }
593
+ // Shared pan/zoom/touch/drag-rect interaction controller
594
+ const pan_zoom = create_pan_zoom({
595
+ ranges: () => ranges.current,
596
+ scale_type: (axis) =>
597
+ ({ x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis })[axis]
598
+ .scale_type,
599
+ // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
600
+ plot_dims: () => ({
601
+ width: Math.max(1, width - pad.l - pad.r),
602
+ height: Math.max(1, height - pad.t - pad.b),
603
+ }),
604
+ pan: () => pan,
605
+ set_range: (axis, range) => (ranges.current[axis] = range),
606
+ svg: () => svg_element,
607
+ on_rect_zoom: (start, current) => {
608
+ // Update axis ranges to trigger reactivity and prevent effect from overriding
609
+ const next_x = invert_rect_range(scales.x, start.x, current.x)
610
+ if (!next_x) return
611
+ x_axis = { ...x_axis, range: next_x }
612
+ // gate x2/y2 on series presence: their scales are [0, 1] sentinels otherwise,
613
+ // so inverting would store a phantom range in the bindable prop
614
+ const next_x2 =
615
+ x2_series.length > 0 ? invert_rect_range(scales.x2, start.x, current.x) : null
616
+ if (next_x2) x2_axis = { ...x2_axis, range: next_x2 }
617
+ const next_y = invert_rect_range(scales.y, start.y, current.y)
618
+ if (next_y) y_axis = { ...y_axis, range: next_y }
619
+ const next_y2 =
620
+ y2_series.length > 0 ? invert_rect_range(scales.y2, start.y, current.y) : null
621
+ if (next_y2) y2_axis = { ...y2_axis, range: next_y2 }
622
+ },
623
+ on_reset: () => {
624
+ // Reset zoom to initial ranges (undo any pan/zoom)
625
+ ranges.current = {
626
+ x: [...ranges.initial.x] as Vec2,
627
+ x2: [...ranges.initial.x2] as Vec2,
628
+ y: [...ranges.initial.y] as Vec2,
629
+ y2: [...ranges.initial.y2] as Vec2,
707
630
  }
708
- }
709
- }
710
-
711
- const on_window_mouse_move = (evt: MouseEvent) => {
712
- if (!drag_state.start || !drag_state.bounds) return
713
- drag_state.current = {
714
- x: evt.clientX - drag_state.bounds.left,
715
- y: evt.clientY - drag_state.bounds.top,
716
- }
717
- }
718
-
719
- const on_window_mouse_up = () => {
720
- handle_zoom()
721
- drag_state = { start: null, current: null, bounds: null }
722
- window.removeEventListener(`mousemove`, on_window_mouse_move)
723
- window.removeEventListener(`mouseup`, on_window_mouse_up)
724
- document.body.style.cursor = `default`
725
- }
726
-
727
- // Pan/zoom all four axes from an interaction-start snapshot, each in its own
728
- // scale's transform space (log axes pan by a constant factor, linear by a shift).
729
- // Plot dims clamped to 1px so degenerate containers can't produce Infinity deltas.
730
- const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
731
- const plot_width = Math.max(1, width - pad.l - pad.r)
732
- const plot_height = Math.max(1, height - pad.t - pad.b)
733
- ranges.current.x = pan_range_by_pixels(init.initial_x_range, dx_px, plot_width, final_x_axis.scale_type)
734
- ranges.current.x2 = pan_range_by_pixels(init.initial_x2_range, dx_px, plot_width, final_x2_axis.scale_type)
735
- ranges.current.y = pan_range_by_pixels(init.initial_y_range, dy_px, plot_height, final_y_axis.scale_type)
736
- ranges.current.y2 = pan_range_by_pixels(init.initial_y2_range, dy_px, plot_height, final_y2_axis.scale_type)
737
- }
738
- const zoom_all_axes = (init: InitialRanges, factor: number) => {
739
- ranges.current.x = zoom_range_by_factor(init.initial_x_range, factor, final_x_axis.scale_type)
740
- ranges.current.x2 = zoom_range_by_factor(init.initial_x2_range, factor, final_x2_axis.scale_type)
741
- ranges.current.y = zoom_range_by_factor(init.initial_y_range, factor, final_y_axis.scale_type)
742
- ranges.current.y2 = zoom_range_by_factor(init.initial_y2_range, factor, final_y2_axis.scale_type)
743
- }
744
-
745
- // Pan drag handler (drag direction inverted on x for natural pan feel)
746
- const on_pan_move = (evt: MouseEvent) => {
747
- if (!pan_drag_state) return
748
- const sensitivity = pan?.drag_sensitivity ?? 1
749
- pan_all_axes(
750
- pan_drag_state,
751
- -(evt.clientX - pan_drag_state.start.x) * sensitivity,
752
- (evt.clientY - pan_drag_state.start.y) * sensitivity,
753
- )
754
- }
755
-
756
- const on_pan_end = () => {
757
- pan_drag_state = null
758
- document.body.style.cursor = ``
759
- window.removeEventListener(`mousemove`, on_pan_move)
760
- window.removeEventListener(`mouseup`, on_pan_end)
761
- }
762
-
763
- // Tear down any window listeners + cursor override if the component unmounts mid-drag
764
- // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
765
- // onDestroy also runs during SSR teardown, where window/document don't exist.
766
- onDestroy(() => {
767
- remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
768
- drag_state = { start: null, current: null, bounds: null }
769
- pan_drag_state = null
631
+ // Also reset axis props so future data changes recalculate auto ranges
632
+ x_axis = { ...x_axis, range: [null, null] }
633
+ x2_axis = { ...x2_axis, range: [null, null] }
634
+ y_axis = { ...y_axis, range: [null, null] }
635
+ y2_axis = { ...y2_axis, range: [null, null] }
636
+ },
770
637
  })
771
-
772
- function handle_mouse_down(evt: MouseEvent) {
773
- const coords = get_relative_coords(evt)
774
- if (!coords || !svg_element) return
775
-
776
- // Check if pan is enabled and shift is held for pan mode
777
- const pan_enabled = pan?.enabled !== false
778
- if (pan_enabled && evt.shiftKey) {
779
- evt.preventDefault()
780
- pan_drag_state = {
781
- start: { x: evt.clientX, y: evt.clientY },
782
- initial_x_range: [...ranges.current.x] as [number, number],
783
- initial_x2_range: [...ranges.current.x2] as [number, number],
784
- initial_y_range: [...ranges.current.y] as [number, number],
785
- initial_y2_range: [...ranges.current.y2] as [number, number],
786
- }
787
- document.body.style.cursor = `grabbing`
788
- window.addEventListener(`mousemove`, on_pan_move)
789
- window.addEventListener(`mouseup`, on_pan_end)
790
- return
791
- }
792
-
793
- drag_state = {
794
- start: coords,
795
- current: coords,
796
- bounds: svg_element.getBoundingClientRect(),
797
- }
798
- window.addEventListener(`mousemove`, on_window_mouse_move)
799
- window.addEventListener(`mouseup`, on_window_mouse_up)
800
- evt.preventDefault()
801
- }
802
-
803
- // Wheel handler for pan (requires focus and shift)
804
- function handle_wheel(evt: WheelEvent) {
805
- const pan_enabled = pan?.enabled !== false
806
- // Only capture wheel when focused AND Shift is held
807
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
808
- if (!pan_enabled || !is_focused || !shift_held) return
809
-
810
- evt.preventDefault()
811
-
812
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
813
- const plot_width = Math.max(1, width - pad.l - pad.r)
814
- const plot_height = Math.max(1, height - pad.t - pad.b)
815
- const sensitivity = pan?.wheel_sensitivity ?? 1
816
-
817
- // Pan along the dominant wheel direction
818
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
819
- const dx = evt.deltaX * sensitivity
820
- ranges.current.x = pan_range_by_pixels(ranges.current.x, dx, plot_width, final_x_axis.scale_type)
821
- ranges.current.x2 = pan_range_by_pixels(ranges.current.x2, dx, plot_width, final_x2_axis.scale_type)
822
- } else {
823
- const dy = evt.deltaY * sensitivity
824
- ranges.current.y = pan_range_by_pixels(ranges.current.y, dy, plot_height, final_y_axis.scale_type)
825
- ranges.current.y2 = pan_range_by_pixels(ranges.current.y2, dy, plot_height, final_y2_axis.scale_type)
826
- }
827
- }
828
-
829
- // Touch handlers for pinch-zoom and two-finger pan
830
- function handle_touch_start(evt: TouchEvent) {
831
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
832
- if (!touch_enabled || evt.touches.length !== 2) return
833
-
834
- evt.preventDefault()
835
- const touches = Array.from(evt.touches)
836
- touch_state = {
837
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
838
- initial_x_range: [...ranges.current.x] as [number, number],
839
- initial_x2_range: [...ranges.current.x2] as [number, number],
840
- initial_y_range: [...ranges.current.y] as [number, number],
841
- initial_y2_range: [...ranges.current.y2] as [number, number],
842
- }
843
- }
844
-
845
- function handle_touch_move(evt: TouchEvent) {
846
- if (!touch_state || evt.touches.length !== 2) return
847
- evt.preventDefault()
848
-
849
- const [t1, t2] = Array.from(evt.touches)
850
- const [s1, s2] = touch_state.start_touches
851
-
852
- // Calculate center movement for pan
853
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
854
- const curr_center = {
855
- x: (t1.clientX + t2.clientX) / 2,
856
- y: (t1.clientY + t2.clientY) / 2,
857
- }
858
- const dx = curr_center.x - start_center.x
859
- const dy = curr_center.y - start_center.y
860
-
861
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
862
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
863
- // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
864
- if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
865
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
866
- const scale = curr_dist / start_dist
867
-
868
- // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
869
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
870
- zoom_all_axes(touch_state, scale)
871
- } else pan_all_axes(touch_state, -dx, dy)
872
- }
873
-
874
- function handle_touch_end() {
875
- touch_state = null
876
- }
877
-
878
- function handle_double_click() {
879
- // Reset zoom to initial ranges (undo any pan/zoom)
880
- ranges.current.x = [...ranges.initial.x] as [number, number]
881
- ranges.current.x2 = [...ranges.initial.x2] as [number, number]
882
- ranges.current.y = [...ranges.initial.y] as [number, number]
883
- ranges.current.y2 = [...ranges.initial.y2] as [number, number]
884
- // Also reset axis props so future data changes recalculate auto ranges
885
- x_axis = { ...x_axis, range: [null, null] }
886
- x2_axis = { ...x2_axis, range: [null, null] }
887
- y_axis = { ...y_axis, range: [null, null] }
888
- y2_axis = { ...y2_axis, range: [null, null] }
889
- }
638
+ onDestroy(() => pan_zoom.destroy())
890
639
 
891
640
  function handle_mouse_move(
892
641
  evt: MouseEvent,
@@ -918,7 +667,10 @@
918
667
  on_bar_hover?.({ value, count, property, event: evt })
919
668
  }
920
669
 
921
- const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
670
+ const legend_vis = create_legend_visibility(
671
+ () => series,
672
+ (next) => (series = next),
673
+ )
922
674
 
923
675
  // Set theme-aware background when entering fullscreen
924
676
  $effect(() => {
@@ -926,31 +678,24 @@
926
678
  })
927
679
 
928
680
  // State accessors for shared axis change handler
681
+ // Spread into existing state in each setter to preserve merged type structure
929
682
  const axis_state: AxisChangeState<DataSeries> = {
930
- get_axis: (axis) => {
931
- if (axis === `x`) return x_axis
932
- if (axis === `x2`) return x2_axis
933
- if (axis === `y`) return y_axis
934
- return y2_axis
935
- },
936
- set_axis: (axis, config) => {
937
- // Spread into existing state to preserve merged type structure
938
- if (axis === `x`) x_axis = { ...x_axis, ...config }
939
- else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
940
- else if (axis === `y`) y_axis = { ...y_axis, ...config }
941
- else y2_axis = { ...y2_axis, ...config }
683
+ axes: {
684
+ x: { get: () => x_axis, set: (config) => (x_axis = { ...x_axis, ...config }) },
685
+ x2: { get: () => x2_axis, set: (config) => (x2_axis = { ...x2_axis, ...config }) },
686
+ y: { get: () => y_axis, set: (config) => (y_axis = { ...y_axis, ...config }) },
687
+ y2: { get: () => y2_axis, set: (config) => (y2_axis = { ...y2_axis, ...config }) },
942
688
  },
943
- get_series: () => series,
944
- set_series: (new_series) => (series = new_series),
945
- get_loading: () => axis_loading,
946
- set_loading: (axis) => (axis_loading = axis),
689
+ series: { get: () => series, set: (next) => (series = next) },
690
+ loading: { get: () => axis_loading, set: (axis) => (axis_loading = axis) },
947
691
  }
948
692
 
949
693
  // Shared handler + one-shot auto-load bound to this component's state
950
- const { handle_axis_change, try_auto_load } = create_axis_loader(
951
- axis_state,
952
- () => ({ data_loader, on_axis_change, on_error }),
953
- )
694
+ const { handle_axis_change, try_auto_load } = create_axis_loader(axis_state, () => ({
695
+ data_loader,
696
+ on_axis_change,
697
+ on_error,
698
+ }))
954
699
  $effect(try_auto_load)
955
700
  </script>
956
701
 
@@ -988,11 +733,9 @@
988
733
  evt.preventDefault()
989
734
  fullscreen = false
990
735
  }
991
- if (evt.key === `Shift`) shift_held = true
992
- }}
993
- onkeyup={(evt) => {
994
- if (evt.key === `Shift`) shift_held = false
736
+ pan_zoom.on_window_key_down(evt)
995
737
  }}
738
+ onkeyup={pan_zoom.on_window_key_up}
996
739
  />
997
740
 
998
741
  <div
@@ -1017,38 +760,25 @@
1017
760
  bind:this={svg_element}
1018
761
  role="application"
1019
762
  aria-label={rest[`aria-label`] ??
1020
- ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1021
- `Histogram`)}
763
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) || `Histogram`)}
1022
764
  tabindex="0"
1023
- onfocusin={() => (is_focused = true)}
1024
- onfocusout={() => (is_focused = false)}
765
+ onfocusin={() => pan_zoom.set_focused(true)}
766
+ onfocusout={() => pan_zoom.set_focused(false)}
1025
767
  onmouseenter={() => (hovered = true)}
1026
- onmousedown={handle_mouse_down}
768
+ onmousedown={pan_zoom.on_mouse_down}
1027
769
  onmouseleave={() => {
1028
770
  hovered = false
1029
771
  hover_info = null
1030
772
  on_bar_hover?.(null)
1031
773
  }}
1032
- ondblclick={handle_double_click}
1033
- onwheel={handle_wheel}
1034
- ontouchstart={handle_touch_start}
1035
- ontouchmove={handle_touch_move}
1036
- ontouchend={handle_touch_end}
1037
- ontouchcancel={handle_touch_end}
1038
- style:cursor={pan_drag_state
1039
- ? `grabbing`
1040
- : shift_held && pan?.enabled !== false
1041
- ? `grab`
1042
- : `crosshair`}
1043
- onkeydown={(event) => {
1044
- if (event.key === `Escape` && drag_state.start) {
1045
- drag_state = { start: null, current: null, bounds: null }
1046
- }
1047
- if ([`Enter`, ` `].includes(event.key)) {
1048
- event.preventDefault()
1049
- handle_double_click()
1050
- }
1051
- }}
774
+ ondblclick={pan_zoom.reset_view}
775
+ onwheel={pan_zoom.on_wheel}
776
+ ontouchstart={pan_zoom.on_touch_start}
777
+ ontouchmove={pan_zoom.on_touch_move}
778
+ ontouchend={pan_zoom.on_touch_end}
779
+ ontouchcancel={pan_zoom.on_touch_end}
780
+ style:cursor={pan_zoom.cursor}
781
+ onkeydown={pan_zoom.on_key_down}
1052
782
  >
1053
783
  <!-- Define clip path for chart area -->
1054
784
  <defs>
@@ -1065,7 +795,7 @@
1065
795
  <!-- Reference lines: below grid (must render first to appear behind grid) -->
1066
796
  {@render ref_lines_layer(ref_lines_by_z.below_grid)}
1067
797
 
1068
- <ZoomRect start={drag_state.start} current={drag_state.current} />
798
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
1069
799
 
1070
800
  <ZeroLines
1071
801
  {display}
@@ -1097,17 +827,17 @@
1097
827
  <!-- X-axis -->
1098
828
  <PlotAxis
1099
829
  side="x"
1100
- ticks={ticks.x as number[]}
830
+ ticks={ticks.x}
1101
831
  place={scales.x}
1102
832
  axis={final_x_axis}
1103
- domain={ranges.current.x as Vec2}
833
+ domain={ranges.current.x}
1104
834
  {pad}
1105
835
  {width}
1106
836
  {height}
1107
837
  show_grid={display.x_grid}
1108
838
  tick_label={(tick) => get_tick_label(tick, final_x_axis.ticks)}
1109
839
  label_x={(pad.l + width - pad.r) / 2 + (final_x_axis.label_shift?.x ?? 0)}
1110
- label_y={height - 10 + (final_x_axis.label_shift?.y ?? 0)}
840
+ label_y={height - pad.b + AXIS_TITLE_OFFSET + (final_x_axis.label_shift?.y ?? 0)}
1111
841
  axis_loading={axis_loading === `x`}
1112
842
  on_axis_change={(key) => handle_axis_change(`x`, key)}
1113
843
  />
@@ -1116,17 +846,17 @@
1116
846
  {#if x2_series.length > 0}
1117
847
  <PlotAxis
1118
848
  side="x2"
1119
- ticks={ticks.x2 as number[]}
849
+ ticks={ticks.x2}
1120
850
  place={scales.x2}
1121
851
  axis={final_x2_axis}
1122
- domain={ranges.current.x2 as Vec2}
852
+ domain={ranges.current.x2}
1123
853
  {pad}
1124
854
  {width}
1125
855
  {height}
1126
856
  show_grid={display.x2_grid}
1127
857
  tick_label={(tick) => get_tick_label(tick, final_x2_axis.ticks)}
1128
858
  label_x={(pad.l + width - pad.r) / 2 + (final_x2_axis.label_shift?.x ?? 0)}
1129
- label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? 40))}
859
+ label_y={Math.max(12, pad.t - (final_x2_axis.label_shift?.y ?? AXIS_TITLE_OFFSET))}
1130
860
  axis_loading={axis_loading === `x2`}
1131
861
  on_axis_change={(key) => handle_axis_change(`x2`, key)}
1132
862
  />
@@ -1135,10 +865,10 @@
1135
865
  <!-- Y-axis -->
1136
866
  <PlotAxis
1137
867
  side="y"
1138
- ticks={ticks.y as number[]}
868
+ ticks={ticks.y}
1139
869
  place={scales.y}
1140
870
  axis={final_y_axis}
1141
- domain={ranges.current.y as Vec2}
871
+ domain={ranges.current.y}
1142
872
  {pad}
1143
873
  {width}
1144
874
  {height}
@@ -1146,7 +876,8 @@
1146
876
  tick_label={(tick) => get_tick_label(tick, final_y_axis.ticks)}
1147
877
  label_x={Math.max(
1148
878
  12,
1149
- pad.l - (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
879
+ pad.l -
880
+ (final_y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1150
881
  LABEL_GAP_DEFAULT,
1151
882
  ) + (final_y_axis.label_shift?.x ?? 0)}
1152
883
  label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y_axis.label_shift?.y ?? 0)}
@@ -1158,10 +889,10 @@
1158
889
  {#if y2_series.length > 0}
1159
890
  <PlotAxis
1160
891
  side="y2"
1161
- ticks={ticks.y2 as number[]}
892
+ ticks={ticks.y2}
1162
893
  place={scales.y2}
1163
894
  axis={final_y2_axis}
1164
- domain={ranges.current.y2 as Vec2}
895
+ domain={ranges.current.y2}
1165
896
  {pad}
1166
897
  {width}
1167
898
  {height}
@@ -1175,24 +906,19 @@
1175
906
  {/if}
1176
907
 
1177
908
  <!-- Histogram bars (rendered after axes so bars appear above grid lines) -->
1178
- {#each histogram_data as
1179
- { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis, series_idx },
1180
- idx
1181
- (id ?? idx)
1182
- }
909
+ {#each histogram_data as { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis, series_idx }, idx (id ?? idx)}
1183
910
  <g
1184
911
  class="histogram-series"
1185
912
  data-series-idx={series_idx}
1186
913
  clip-path="url(#{clip_path_id})"
1187
- opacity={hovered_legend_series_idx !== null &&
1188
- hovered_legend_series_idx !== series_idx
914
+ opacity={hovered_legend_series_idx !== null && hovered_legend_series_idx !== series_idx
1189
915
  ? 0.25
1190
916
  : 1}
1191
917
  >
1192
918
  {#each bins as bin, bin_idx (bin_idx)}
1193
919
  {@const bar_x = x_scale(bin.x0!)}
1194
920
  {@const bar_width = Math.max(1, Math.abs(x_scale(bin.x1!) - bar_x))}
1195
- {@const bar_height = Math.max(0, (height - pad.b) - y_scale(bin.length))}
921
+ {@const bar_height = Math.max(0, height - pad.b - y_scale(bin.length))}
1196
922
  {@const bar_y = y_scale(bin.length)}
1197
923
  {@const value = (bin.x0! + bin.x1!) / 2}
1198
924
  {#if bar_height > 0}
@@ -1212,22 +938,22 @@
1212
938
  role="button"
1213
939
  tabindex="0"
1214
940
  onmousemove={(evt) =>
1215
- handle_mouse_move(
1216
- evt,
1217
- value,
1218
- bin.length,
1219
- label,
1220
- (y_axis ?? `y1`) as `y1` | `y2`,
1221
- series_idx,
1222
- (srs_x_axis ?? `x1`) as `x1` | `x2`,
1223
- )}
941
+ handle_mouse_move(
942
+ evt,
943
+ value,
944
+ bin.length,
945
+ label,
946
+ (y_axis ?? `y1`) as `y1` | `y2`,
947
+ series_idx,
948
+ (srs_x_axis ?? `x1`) as `x1` | `x2`,
949
+ )}
1224
950
  onmouseleave={() => {
1225
951
  hover_info = null
1226
952
  change(null)
1227
953
  on_bar_hover?.(null)
1228
954
  }}
1229
955
  onclick={(event) =>
1230
- on_bar_click?.({ value, count: bin.length, property: label, event })}
956
+ on_bar_click?.({ value, count: bin.length, property: label, event })}
1231
957
  onkeydown={(event: KeyboardEvent) => {
1232
958
  if ([`Enter`, ` `].includes(event.key)) {
1233
959
  event.preventDefault()
@@ -1243,6 +969,23 @@
1243
969
 
1244
970
  <!-- Reference lines: above all -->
1245
971
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
972
+
973
+ <!-- Marginal distribution strips -->
974
+ <PlotMarginals
975
+ marginals={resolved_marginals}
976
+ series={marginal_series}
977
+ {width}
978
+ {height}
979
+ {pad}
980
+ has_axis={marginal_has_axis}
981
+ axes={{
982
+ x1: marginal_axis(scales.x, ranges.current.x, final_x_axis),
983
+ x2: marginal_axis(scales.x2, ranges.current.x2, final_x2_axis),
984
+ y1: marginal_axis(scales.y, ranges.current.y, final_y_axis),
985
+ y2: marginal_axis(scales.y2, ranges.current.y2, final_y2_axis),
986
+ }}
987
+ id={clip_path_id}
988
+ />
1246
989
  </svg>
1247
990
 
1248
991
  <!-- Tooltip (outside SVG for proper HTML rendering) -->
@@ -1260,8 +1003,8 @@
1260
1003
  {#if tooltip}
1261
1004
  {@render tooltip({ ...hover_info, fullscreen })}
1262
1005
  {:else}
1263
- <div>Value: {format_value(value, hover_info.x_axis.format || `.3~s`)}</div>
1264
- <div>Count: {format_value(count, hover_info.y_axis.format || `.3~s`)}</div>
1006
+ <div>Value: {format_value_or_num(value, hover_info.x_axis.format)}</div>
1007
+ <div>Count: {format_value_or_num(count, hover_info.y_axis.format)}</div>
1265
1008
  {#if mode === `overlay`}<div>{property}</div>{/if}
1266
1009
  {/if}
1267
1010
  </PlotTooltip>
@@ -1300,36 +1043,33 @@
1300
1043
  {/if}
1301
1044
 
1302
1045
  {#if show_legend && legend != null && series.length > 1}
1303
- {@const legend_left = legend_auto_outside
1304
- ? legend_outside_x
1305
- : legend_placement
1306
- ? tweened_legend_coords.current.x
1307
- : pad.l + 10}
1308
- {@const legend_top = legend_auto_outside
1309
- ? legend_outside_y
1310
- : legend_placement
1311
- ? tweened_legend_coords.current.y
1312
- : pad.t + 10}
1046
+ {@const legend_pos = placed_coords(
1047
+ legend_auto_outside,
1048
+ { x: legend_outside_x, y: legend_outside_y },
1049
+ legend_placement,
1050
+ legend_tween.coords.current,
1051
+ { x: pad.l + 10, y: pad.t + 10 },
1052
+ )}
1313
1053
  <PlotLegend
1314
1054
  bind:root_element={legend_element}
1315
1055
  {...legend}
1316
1056
  series_data={legend_data}
1317
- on_toggle={legend?.on_toggle ?? ((series_idx: number) => {
1318
- if (series_idx < 0 || series_idx >= series.length) return
1319
- legend_vis.on_toggle(series_idx)
1320
- on_series_toggle(series_idx)
1321
- })}
1057
+ on_toggle={legend?.on_toggle ??
1058
+ ((series_idx: number) => {
1059
+ if (series_idx < 0 || series_idx >= series.length) return
1060
+ legend_vis.on_toggle(series_idx)
1061
+ on_series_toggle(series_idx)
1062
+ })}
1322
1063
  on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1323
- on_hover_change={legend_hover.set_locked}
1064
+ on_hover_change={legend_tween.set_locked}
1324
1065
  on_item_hover={(item) =>
1325
- (hovered_legend_series_idx = item != null && item.series_idx >= 0
1326
- ? item.series_idx
1327
- : null)}
1066
+ (hovered_legend_series_idx =
1067
+ item != null && item.series_idx >= 0 ? item.series_idx : null)}
1328
1068
  active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1329
1069
  style={`
1330
1070
  position: absolute;
1331
- left: ${legend_left}px;
1332
- top: ${legend_top}px;
1071
+ left: ${legend_pos.x}px;
1072
+ top: ${legend_pos.y}px;
1333
1073
  pointer-events: auto;
1334
1074
  ${legend?.style || ``}
1335
1075
  `}
@@ -1390,7 +1130,9 @@
1390
1130
  .histogram :global(.pane-toggle),
1391
1131
  .histogram .header-controls {
1392
1132
  opacity: 0;
1393
- transition: opacity 0.2s, background-color 0.2s;
1133
+ transition:
1134
+ opacity 0.2s,
1135
+ background-color 0.2s;
1394
1136
  }
1395
1137
  .histogram:hover :global(.pane-toggle),
1396
1138
  .histogram:hover .header-controls,