matterviz 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -2,7 +2,7 @@
2
2
  lang="ts"
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >
5
- import { format_value } from '../../labels'
5
+ import { format_value_or_num } from '../../labels'
6
6
  import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
7
7
  import type { Vec2 } from '../../math'
8
8
  import type {
@@ -28,32 +28,39 @@
28
28
  compute_element_placement,
29
29
  PlotAxis,
30
30
  PlotLegend,
31
+ PlotMarginals,
31
32
  ReferenceLine,
32
33
  } from '..'
34
+ import type { MarginalSeriesInput, MarginalsProp } from '../core/marginals'
35
+ import {
36
+ add_sides,
37
+ marginal_axis,
38
+ marginal_axis_presence,
39
+ normalize_marginals,
40
+ reserve_marginal_pad,
41
+ } from '../core/marginals'
33
42
  import {
34
43
  build_obstacles_norm,
35
44
  clip_bar,
36
45
  has_explicit_position,
37
46
  measured_footprint,
38
47
  place_decorations,
48
+ placed_coords,
39
49
  } from '../core/auto-place'
40
50
  import { compute_box_stats } from './box-plot'
41
51
  import { gaussian_kde, type KdeResult } from './kde'
42
- import { create_dimension_tracker, create_hover_lock } from '../core/hover-lock.svelte'
52
+ import { create_placed_tween } from '../core/placed-tween.svelte'
53
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
43
54
  import { create_legend_visibility } from '../core/utils/series-visibility'
44
55
  import {
45
56
  axis_ranges_equal,
46
- get_relative_coords,
47
- MIN_TOUCH_DISTANCE_PIXELS,
48
- pan_range_by_pixels,
49
- PINCH_ZOOM_THRESHOLD,
50
- remove_drag_listeners,
57
+ invert_rect_range,
51
58
  resolve_axis_ranges,
52
- sorted_range,
53
- zoom_range_by_factor,
54
59
  } from '../core/interactions'
55
60
  import {
61
+ AXIS_TITLE_OFFSET,
56
62
  calc_auto_padding,
63
+ DEFAULT_PLOT_PADDING,
57
64
  filter_padding,
58
65
  LABEL_GAP_DEFAULT,
59
66
  y2_axis_label_x,
@@ -63,19 +70,17 @@
63
70
  import type { IndexedRefLine } from '../core/reference-line'
64
71
  import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
65
72
  import {
66
- create_scale,
73
+ create_axis_scales,
67
74
  generate_ticks,
68
75
  get_nice_data_range,
69
76
  get_tick_label,
70
77
  } from '../core/scales'
71
- import type { InitialRanges } from '../core/types'
72
78
  import { DEFAULT_SERIES_COLORS } from '../core/types'
73
79
  import { unique_id } from '../core/utils'
74
80
  import { DEFAULTS } from '../../settings'
75
81
  import type { Snippet } from 'svelte'
76
82
  import { onDestroy, untrack } from 'svelte'
77
83
  import type { HTMLAttributes } from 'svelte/elements'
78
- import { Tween, type TweenOptions } from 'svelte/motion'
79
84
  import { SvelteMap } from 'svelte/reactivity'
80
85
  import PlotTooltip from '../core/components/PlotTooltip.svelte'
81
86
  import { violin_path } from '../core/svg'
@@ -120,8 +125,8 @@
120
125
  y_axis = $bindable({}),
121
126
  y2_axis: y2_axis_prop = $bindable({}),
122
127
  display = $bindable(DEFAULTS.box.display),
123
- range_padding = 0.05,
124
- padding = { t: 20, b: 60, l: 60, r: 20 },
128
+ range_padding = 0,
129
+ padding = DEFAULT_PLOT_PADDING,
125
130
  legend = {},
126
131
  show_legend,
127
132
  box = {},
@@ -135,7 +140,7 @@
135
140
  show_mean = $bindable(DEFAULTS.box.show_mean),
136
141
  show_value_labels = false,
137
142
  value_label_stat = `median`,
138
- value_label_format = `.3~s`,
143
+ value_label_format = ``,
139
144
  kind = $bindable(DEFAULTS.box.kind),
140
145
  side = $bindable(DEFAULTS.box.side),
141
146
  bandwidth = DEFAULTS.box.bandwidth,
@@ -164,51 +169,55 @@
164
169
  header_controls,
165
170
  controls_extra,
166
171
  pan = {},
172
+ marginals = false,
167
173
  ...rest
168
- }: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
169
- series?: BoxPlotSeries<Metadata>[]
170
- orientation?: Orientation
171
- legend?: LegendConfig | null
172
- show_legend?: boolean
173
- box?: BoxStyle
174
- whisker?: WhiskerStyle
175
- median_style?: BoxLineStyle
176
- outlier_style?: OutlierStyle
177
- whisker_mode?: WhiskerMode
178
- whisker_range?: number
179
- whisker_percentiles?: [number, number]
180
- show_outliers?: boolean
181
- show_mean?: boolean
182
- show_value_labels?: boolean
183
- value_label_stat?: `median` | `mean`
184
- value_label_format?: string
185
- kind?: ViolinKind
186
- side?: ViolinSide
187
- bandwidth?: BandwidthOption
188
- violin_width?: number
189
- violin_style?: ViolinStyle
190
- kde_points?: number
191
- kde_cut?: number
192
- kde_max_samples?: number
193
- kde_clip?: [number | null, number | null]
194
- tooltip?: Snippet<[BoxHandlerProps<Metadata>]>
195
- user_content?: Snippet<[UserContentProps]>
196
- header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
197
- controls_extra?: Snippet<[{ orientation: Orientation } & Required<PlotConfig>]>
198
- change?: (data: BoxHandlerProps<Metadata> | null) => void
199
- on_box_click?: (
200
- data: BoxHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
201
- ) => void
202
- on_box_hover?: (
203
- data:
204
- | (BoxHandlerProps<Metadata> & { event: MouseEvent | FocusEvent | KeyboardEvent })
205
- | null,
206
- ) => void
207
- ref_lines?: RefLine[]
208
- on_ref_line_click?: (event: RefLineEvent) => void
209
- on_ref_line_hover?: (event: RefLineEvent | null) => void
210
- pan?: PanConfig
211
- } = $props()
174
+ }: HTMLAttributes<HTMLDivElement> &
175
+ BasePlotProps &
176
+ PlotConfig & {
177
+ series?: BoxPlotSeries<Metadata>[]
178
+ orientation?: Orientation
179
+ legend?: LegendConfig | null
180
+ show_legend?: boolean
181
+ box?: BoxStyle
182
+ whisker?: WhiskerStyle
183
+ median_style?: BoxLineStyle
184
+ outlier_style?: OutlierStyle
185
+ whisker_mode?: WhiskerMode
186
+ whisker_range?: number
187
+ whisker_percentiles?: Vec2
188
+ show_outliers?: boolean
189
+ show_mean?: boolean
190
+ show_value_labels?: boolean
191
+ value_label_stat?: `median` | `mean`
192
+ value_label_format?: string
193
+ kind?: ViolinKind
194
+ side?: ViolinSide
195
+ bandwidth?: BandwidthOption
196
+ violin_width?: number
197
+ violin_style?: ViolinStyle
198
+ kde_points?: number
199
+ kde_cut?: number
200
+ kde_max_samples?: number
201
+ kde_clip?: [number | null, number | null]
202
+ tooltip?: Snippet<[BoxHandlerProps<Metadata>]>
203
+ user_content?: Snippet<[UserContentProps]>
204
+ header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
205
+ controls_extra?: Snippet<[{ orientation: Orientation } & Required<PlotConfig>]>
206
+ change?: (data: BoxHandlerProps<Metadata> | null) => void
207
+ on_box_click?: (
208
+ data: BoxHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
209
+ ) => void
210
+ on_box_hover?: (
211
+ data:
212
+ | (BoxHandlerProps<Metadata> & { event: MouseEvent | FocusEvent | KeyboardEvent })
213
+ | null,
214
+ ) => void
215
+ ref_lines?: RefLine[]
216
+ on_ref_line_click?: (event: RefLineEvent) => void
217
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
218
+ pan?: PanConfig
219
+ marginals?: MarginalsProp
220
+ } = $props()
212
221
 
213
222
  let box_state = $derived({ ...DEFAULTS.box.box, ...box })
214
223
  let whisker_state = $derived({ ...DEFAULTS.box.whisker, ...whisker })
@@ -218,24 +227,20 @@
218
227
 
219
228
  // Merge secondary-axis defaults as deriveds instead of assigning back into the
220
229
  // $bindable props (which would push library defaults into the parent's bound state)
221
- let y2_axis = $derived(
222
- {
223
- format: ``,
224
- scale_type: `linear`,
225
- ticks: 5,
226
- range: [null, null],
227
- ...y2_axis_prop,
228
- } as typeof y2_axis_prop,
229
- )
230
- let x2_axis = $derived(
231
- {
232
- format: ``,
233
- scale_type: `linear`,
234
- ticks: 5,
235
- range: [null, null],
236
- ...x2_axis_prop,
237
- } as typeof x2_axis_prop,
238
- )
230
+ let y2_axis = $derived({
231
+ format: ``,
232
+ scale_type: `linear`,
233
+ ticks: 5,
234
+ range: [null, null],
235
+ ...y2_axis_prop,
236
+ } as typeof y2_axis_prop)
237
+ let x2_axis = $derived({
238
+ format: ``,
239
+ scale_type: `linear`,
240
+ ticks: 5,
241
+ range: [null, null],
242
+ ...x2_axis_prop,
243
+ } as typeof x2_axis_prop)
239
244
 
240
245
  let [width, height] = $state([0, 0])
241
246
  let wrapper: HTMLDivElement | undefined = $state()
@@ -262,7 +267,7 @@
262
267
  whisker_range: srs.whisker_range ?? whisker_range,
263
268
  whisker_percentiles: srs.whisker_percentiles ?? whisker_percentiles,
264
269
  collect_outliers: show_outliers && draws_box(srs) && (srs.visible ?? true),
265
- })
270
+ }),
266
271
  ),
267
272
  )
268
273
 
@@ -315,9 +320,8 @@
315
320
  // KDE per visible violin series, keyed by series index (bandwidth from the full sample)
316
321
  let violin_kdes = $derived.by(() => {
317
322
  const map = new SvelteMap<number, KdeResult>()
318
- const [val_axis, val_axis2] = orientation === `vertical`
319
- ? [y_axis, y2_axis]
320
- : [x_axis, x2_axis]
323
+ const [val_axis, val_axis2] =
324
+ orientation === `vertical` ? [y_axis, y2_axis] : [x_axis, x2_axis]
321
325
  for (const box_item of visible_boxes) {
322
326
  if (!draws_violin(box_item.series)) continue
323
327
  const samples = box_item.series.y ?? []
@@ -325,7 +329,10 @@
325
329
  // On a log value axis the KDE grid tail (data_min - cut*bandwidth) is usually <= 0 →
326
330
  // NaN pixels + LOG_EPS range pollution. Clamp the grid to the smallest positive sample.
327
331
  if ((is_secondary(box_item.series) ? val_axis2 : val_axis).scale_type === `log`) {
328
- const min_pos = samples.reduce((min, val) => (val > 0 && val < min ? val : min), Infinity)
332
+ const min_pos = samples.reduce(
333
+ (min, val) => (val > 0 && val < min ? val : min),
334
+ Infinity,
335
+ )
329
336
  // Guard: no positive samples → min_pos is Infinity; leave clip unchanged so the KDE
330
337
  // never receives a non-finite lower bound
331
338
  if (Number.isFinite(min_pos)) {
@@ -349,7 +356,11 @@
349
356
  // The horizontal category pixel axis is inverted, so flip the half-violin side to keep
350
357
  // `positive` meaning "above the center line" (vertical/`both` pass through unchanged)
351
358
  const to_screen_side = (eff_side: ViolinSide, vertical: boolean): ViolinSide =>
352
- vertical || eff_side === `both` ? eff_side : eff_side === `positive` ? `negative` : `positive`
359
+ vertical || eff_side === `both`
360
+ ? eff_side
361
+ : eff_side === `positive`
362
+ ? `negative`
363
+ : `positive`
353
364
 
354
365
  // Peak density per violin, computed once on data change (avoids spreading kde.density into
355
366
  // Math.max — unsafe for large kde_points — and re-deriving it on every render/hover).
@@ -366,8 +377,15 @@
366
377
  // Which boxes live on the secondary value axis (y2 for vertical, x2 for horizontal)
367
378
  const is_secondary = (srs: BoxPlotSeries<Metadata>): boolean =>
368
379
  orientation === `vertical` ? srs.y_axis === `y2` : srs.x_axis === `x2`
369
- let secondary_boxes = $derived(visible_boxes.filter((box_item) => is_secondary(box_item.series)))
380
+ let secondary_boxes = $derived(
381
+ visible_boxes.filter((box_item) => is_secondary(box_item.series)),
382
+ )
370
383
  let has_secondary = $derived(secondary_boxes.length > 0)
384
+ // The secondary value axis renders transposed by orientation: x2 (top) when horizontal, y2
385
+ // (right) when vertical. Derive once so axis rendering, ticks, range writes, point picking, and
386
+ // marginal placement all stay provably in sync.
387
+ let show_x2 = $derived(has_secondary && orientation === `horizontal`)
388
+ let show_y2 = $derived(has_secondary && orientation === `vertical`)
371
389
 
372
390
  // Collect value-axis points (whiskers, quartiles, outliers, KDE tails) for auto-range
373
391
  const value_points = (boxes: Box[]): { x: number; y: number }[] =>
@@ -413,8 +431,8 @@
413
431
  )
414
432
 
415
433
  return vertical
416
- ? ({ x: cat_range, x2: [0, 1] as Vec2, y: value_primary, y2: value_secondary })
417
- : ({ x: value_primary, x2: value_secondary, y: cat_range, y2: [0, 1] as Vec2 })
434
+ ? { x: cat_range, x2: [0, 1] as Vec2, y: value_primary, y2: value_secondary }
435
+ : { x: value_primary, x2: value_secondary, y: cat_range, y2: [0, 1] as Vec2 }
418
436
  })
419
437
 
420
438
  let ranges = $state<{
@@ -425,10 +443,14 @@
425
443
  current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
426
444
  })
427
445
 
428
- $effect(() => { // sync ranges from axis.range overrides / auto ranges
446
+ $effect(() => {
447
+ // sync ranges from axis.range overrides / auto ranges
429
448
  // resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
430
449
  // NaN breaks scales and, since NaN !== NaN, loops the effect)
431
- const next = resolve_axis_ranges({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis }, auto_ranges)
450
+ const next = resolve_axis_ranges(
451
+ { x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis },
452
+ auto_ranges,
453
+ )
432
454
  if (!next) return
433
455
  // untrack the read of `ranges` so the assignment can't re-trigger this effect
434
456
  // (reading + writing the same state otherwise causes effect_update_depth_exceeded).
@@ -438,34 +460,40 @@
438
460
  }
439
461
  })
440
462
 
441
- const default_padding = { t: 20, b: 60, l: 60, r: 20 }
442
- let base_pad = $derived(filter_padding(padding, default_padding))
443
-
444
- $effect(() => { // dynamic padding from tick label widths
445
- const new_pad = width && height && ticks.y.length > 0
446
- ? calc_auto_padding({
447
- padding,
448
- default_padding,
449
- x2_axis: { ...x2_axis, tick_values: ticks.x2 },
450
- y_axis: { ...y_axis, tick_values: ticks.y },
451
- y2_axis: { ...y2_axis, tick_values: ticks.y2 },
452
- })
453
- : filter_padding(padding, default_padding)
454
- if (
455
- width && height && orientation === `vertical` && has_secondary && ticks.y2.length > 0
456
- ) {
463
+ let base_pad = $derived(filter_padding(padding, DEFAULT_PLOT_PADDING))
464
+
465
+ $effect(() => {
466
+ // dynamic padding from tick label widths
467
+ const new_pad =
468
+ width && height && ticks.y.length > 0
469
+ ? calc_auto_padding({
470
+ padding,
471
+ default_padding: DEFAULT_PLOT_PADDING,
472
+ x2_axis: { ...x2_axis, tick_values: ticks.x2 },
473
+ y_axis: { ...y_axis, tick_values: ticks.y },
474
+ y2_axis: { ...y2_axis, tick_values: ticks.y2 },
475
+ })
476
+ : filter_padding(padding, DEFAULT_PLOT_PADDING)
477
+ if (width && height && show_y2 && ticks.y2.length > 0) {
457
478
  const inside = y2_axis.tick?.label?.inside ?? false
458
479
  const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
459
480
  const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
460
481
  const label_space = y2_axis.label ? 20 : 0
461
482
  new_pad.r = Math.max(new_pad.r, tick_shift + tick_width_contribution + 30 + label_space)
462
483
  }
463
- if (base_pad.t !== new_pad.t || base_pad.b !== new_pad.b ||
464
- base_pad.l !== new_pad.l || base_pad.r !== new_pad.r) base_pad = new_pad
484
+ if (
485
+ base_pad.t !== new_pad.t ||
486
+ base_pad.b !== new_pad.b ||
487
+ base_pad.l !== new_pad.l ||
488
+ base_pad.r !== new_pad.r
489
+ )
490
+ base_pad = new_pad
465
491
  })
466
492
 
467
493
  let legend_element = $state<HTMLDivElement | undefined>()
468
- const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 60 }))
494
+ const legend_footprint = $derived(
495
+ measured_footprint(legend_element, { width: 120, height: 60 }),
496
+ )
469
497
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
470
498
 
471
499
  // Obstacle field in normalized [0,1] coords: each box modeled as a whisker-spanning segment
@@ -482,8 +510,12 @@
482
510
  const secondary = is_secondary(box_item.series)
483
511
  const cat_rng = vertical ? ranges.current.x : ranges.current.y
484
512
  const val_rng = vertical
485
- ? (secondary ? ranges.current.y2 : ranges.current.y)
486
- : (secondary ? ranges.current.x2 : ranges.current.x)
513
+ ? secondary
514
+ ? ranges.current.y2
515
+ : ranges.current.y
516
+ : secondary
517
+ ? ranges.current.x2
518
+ : ranges.current.x
487
519
  const cat_span = cat_rng[1] - cat_rng[0]
488
520
  const val_span = val_rng[1] - val_rng[0]
489
521
  if (cat_span === 0 || val_span === 0) continue
@@ -505,37 +537,70 @@
505
537
  width,
506
538
  height,
507
539
  obstacles_norm,
508
- legend: legend != null && should_show_legend && legend_element != null &&
509
- !legend_has_explicit_pos
510
- ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
511
- : null,
512
- })
540
+ legend:
541
+ legend != null &&
542
+ should_show_legend &&
543
+ legend_element != null &&
544
+ !legend_has_explicit_pos
545
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
546
+ : null,
547
+ }),
548
+ )
549
+ // Marginals are opt-in (default prop `false`) and bind to the VALUE axis, pooling each box's
550
+ // raw samples. The default side follows orientation (value axis = y when vertical, x when
551
+ // horizontal) so the `marginals` boolean / type-string shorthand land on a meaningful side.
552
+ // Each box is tagged with its value axis so a primary-axis marginal ignores secondary boxes.
553
+ const marginal_vertical = $derived(orientation === `vertical`)
554
+ const resolved_marginals = $derived(
555
+ normalize_marginals(marginals, marginal_vertical ? { right: true } : { top: true }),
513
556
  )
514
- const pad = $derived(decor.pad)
557
+ const pad = $derived(add_sides(decor.pad, reserve_marginal_pad(resolved_marginals)))
558
+ const marginal_series = $derived<MarginalSeriesInput[]>(
559
+ visible_boxes.map((box_item) => {
560
+ const secondary = is_secondary(box_item.series)
561
+ return {
562
+ x: marginal_vertical ? undefined : (box_item.series.y ?? []),
563
+ y: marginal_vertical ? (box_item.series.y ?? []) : undefined,
564
+ color: box_color(box_item.idx),
565
+ label: box_item.series.label,
566
+ visible: true,
567
+ x_axis: marginal_vertical ? `x1` : secondary ? `x2` : `x1`,
568
+ y_axis: marginal_vertical ? (secondary ? `y2` : `y1`) : `y1`,
569
+ }
570
+ }),
571
+ )
572
+ const marginal_has_axis = $derived(marginal_axis_presence(show_x2, show_y2))
515
573
  const legend_auto_outside = $derived(decor.legend_outside)
516
574
  const legend_outside_x = $derived(decor.legend_pos.x)
517
575
  const legend_outside_y = $derived(decor.legend_pos.y)
518
576
  const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
519
577
  const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
520
578
 
521
- let scales = $derived({
522
- x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [pad.l, width - pad.r]),
523
- x2: create_scale(x2_axis.scale_type ?? `linear`, ranges.current.x2, [pad.l, width - pad.r]),
524
- y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
525
- y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
526
- })
579
+ let scales = $derived(
580
+ create_axis_scales(
581
+ { x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis },
582
+ ranges.current,
583
+ pad,
584
+ width,
585
+ height,
586
+ ),
587
+ )
527
588
 
528
589
  // Value scale for a box (vertical -> y/y2, horizontal -> x/x2), made log-safe: on a
529
590
  // log value axis, stats at values <= 0 (whisker_low is often exactly 0; negative
530
591
  // outliers) have no finite pixel. Clamp to LOG_EPS so whiskers/boxes/labels draw
531
592
  // toward the plot edge (the clip group crops the overshoot) instead of NaN coords.
532
- const box_val_scale = (srs: BoxPlotSeries<Metadata>): (val: number) => number => {
593
+ const box_val_scale = (srs: BoxPlotSeries<Metadata>): ((val: number) => number) => {
533
594
  const vertical = orientation === `vertical`
534
595
  const secondary = is_secondary(srs)
535
596
  const scale = vertical
536
- ? (secondary ? scales.y2 : scales.y)
537
- : (secondary ? scales.x2 : scales.x)
538
- const axis = vertical ? (secondary ? y2_axis : y_axis) : (secondary ? x2_axis : x_axis)
597
+ ? secondary
598
+ ? scales.y2
599
+ : scales.y
600
+ : secondary
601
+ ? scales.x2
602
+ : scales.x
603
+ const axis = vertical ? (secondary ? y2_axis : y_axis) : secondary ? x2_axis : x_axis
539
604
  return axis.scale_type === `log` ? (val) => scale(Math.max(val, LOG_EPS)) : scale
540
605
  }
541
606
 
@@ -549,35 +614,32 @@
549
614
  return Object.fromEntries(slot_list.map((cat, idx) => [idx, cat]))
550
615
  })
551
616
 
552
- let ticks = $derived({
553
- x: width && height
554
- ? (cat_axis === `x` ? slot_indices : generate_ticks(
555
- ranges.current.x,
556
- x_axis.scale_type ?? `linear`,
557
- x_axis.ticks,
558
- scales.x,
559
- { default_count: 8 },
560
- ))
561
- : [],
562
- y: width && height
563
- ? (cat_axis === `y` ? slot_indices : generate_ticks(
564
- ranges.current.y,
565
- y_axis.scale_type ?? `linear`,
566
- y_axis.ticks,
567
- scales.y,
568
- { default_count: 6 },
569
- ))
570
- : [],
571
- y2: width && height && has_secondary && orientation === `vertical`
572
- ? generate_ticks(ranges.current.y2, y2_axis.scale_type ?? `linear`, y2_axis.ticks, scales.y2, {
573
- default_count: 6,
574
- })
575
- : [],
576
- x2: width && height && has_secondary && orientation === `horizontal`
577
- ? generate_ticks(ranges.current.x2, x2_axis.scale_type ?? `linear`, x2_axis.ticks, scales.x2, {
578
- default_count: 8,
579
- })
580
- : [],
617
+ let ticks = $derived.by(() => {
618
+ const axis_ticks = (
619
+ axis: typeof x_axis,
620
+ range: Vec2,
621
+ scale: typeof scales.x,
622
+ default_count: number,
623
+ show = true,
624
+ ) =>
625
+ width && height && show
626
+ ? generate_ticks(range, axis.scale_type ?? `linear`, axis.ticks, scale, {
627
+ default_count,
628
+ })
629
+ : []
630
+ // categorical axes show one tick per slot instead of generated numeric ticks
631
+ return {
632
+ x:
633
+ cat_axis === `x` && width && height
634
+ ? slot_indices
635
+ : axis_ticks(x_axis, ranges.current.x, scales.x, 8),
636
+ y:
637
+ cat_axis === `y` && width && height
638
+ ? slot_indices
639
+ : axis_ticks(y_axis, ranges.current.y, scales.y, 6),
640
+ y2: axis_ticks(y2_axis, ranges.current.y2, scales.y2, 6, show_y2),
641
+ x2: axis_ticks(x2_axis, ranges.current.x2, scales.x2, 8, show_x2),
642
+ }
581
643
  })
582
644
 
583
645
  let tick_label_widths = $derived({
@@ -586,178 +648,43 @@
586
648
  x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
587
649
  })
588
650
 
589
- // === Interaction state (pan / zoom / touch) ===
590
- let drag_state = $state<{
591
- start: { x: number; y: number } | null
592
- current: { x: number; y: number } | null
593
- bounds: DOMRect | null
594
- }>({ start: null, current: null, bounds: null })
595
- let is_focused = $state(false)
596
- let shift_held = $state(false)
597
- let pan_drag_state = $state<InitialRanges & { start: { x: number; y: number } } | null>(null)
598
- let touch_state = $state<InitialRanges & { start_touches: { x: number; y: number }[] } | null>(
599
- null,
600
- )
601
-
602
- const on_window_mouse_move = (evt: MouseEvent) => {
603
- if (!drag_state.start || !drag_state.bounds) return
604
- drag_state.current = {
605
- x: evt.clientX - drag_state.bounds.left,
606
- y: evt.clientY - drag_state.bounds.top,
607
- }
608
- }
609
- const on_window_mouse_up = () => {
610
- if (drag_state.start && drag_state.current) {
611
- const x1 = scales.x.invert(drag_state.start.x) as number
612
- const x2 = scales.x.invert(drag_state.current.x) as number
613
- const y1 = scales.y.invert(drag_state.start.y)
614
- const y2 = scales.y.invert(drag_state.current.y)
615
- const y2_1 = scales.y2.invert(drag_state.start.y)
616
- const y2_2 = scales.y2.invert(drag_state.current.y)
617
- const x2_1 = scales.x2.invert(drag_state.start.x) as number
618
- const x2_2 = scales.x2.invert(drag_state.current.x) as number
619
- const dx = Math.abs(drag_state.start.x - drag_state.current.x)
620
- const dy = Math.abs(drag_state.start.y - drag_state.current.y)
621
- if (dx > 5 && dy > 5 && Number.isFinite(x1) && Number.isFinite(x2)) {
622
- x_axis = { ...x_axis, range: sorted_range(x1, x2) }
623
- // the secondary value axis is x2 only in horizontal mode, y2 only in vertical
624
- // (is_secondary keys off orientation); writing the off-orientation axis would
625
- // store a phantom range from its [0, 1] sentinel scale into the bound prop
626
- if (
627
- has_secondary && orientation === `horizontal` &&
628
- Number.isFinite(x2_1) && Number.isFinite(x2_2)
629
- ) {
630
- x2_axis_prop = { ...x2_axis_prop, range: sorted_range(x2_1, x2_2) }
631
- }
632
- y_axis = { ...y_axis, range: sorted_range(y1, y2) }
633
- if (
634
- has_secondary && orientation === `vertical` &&
635
- Number.isFinite(y2_1) && Number.isFinite(y2_2)
636
- ) {
637
- y2_axis_prop = { ...y2_axis_prop, range: sorted_range(y2_1, y2_2) }
638
- }
651
+ // Shared pan/zoom/touch/drag-rect interaction controller
652
+ const pan_zoom = create_pan_zoom({
653
+ ranges: () => ranges.current,
654
+ scale_type: (axis) =>
655
+ ({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis })[axis].scale_type,
656
+ plot_dims: () => ({ width: chart_width, height: chart_height }),
657
+ pan: () => pan,
658
+ set_range: (axis, range) => (ranges.current[axis] = range),
659
+ svg: () => svg_element,
660
+ on_rect_zoom: (start, current) => {
661
+ const next_x = invert_rect_range(scales.x, start.x, current.x)
662
+ if (!next_x) return
663
+ x_axis = { ...x_axis, range: next_x }
664
+ // the secondary value axis is x2 only in horizontal mode, y2 only in vertical
665
+ // (is_secondary keys off orientation); writing the off-orientation axis would
666
+ // store a phantom range from its [0, 1] sentinel scale into the bound prop
667
+ const next_x2 = show_x2 ? invert_rect_range(scales.x2, start.x, current.x) : null
668
+ if (next_x2) x2_axis_prop = { ...x2_axis_prop, range: next_x2 }
669
+ const next_y = invert_rect_range(scales.y, start.y, current.y)
670
+ if (next_y) y_axis = { ...y_axis, range: next_y }
671
+ const next_y2 = show_y2 ? invert_rect_range(scales.y2, start.y, current.y) : null
672
+ if (next_y2) y2_axis_prop = { ...y2_axis_prop, range: next_y2 }
673
+ },
674
+ on_reset: () => {
675
+ ranges.current = {
676
+ x: [...ranges.initial.x] as Vec2,
677
+ x2: [...ranges.initial.x2] as Vec2,
678
+ y: [...ranges.initial.y] as Vec2,
679
+ y2: [...ranges.initial.y2] as Vec2,
639
680
  }
640
- }
641
- drag_state = { start: null, current: null, bounds: null }
642
- window.removeEventListener(`mousemove`, on_window_mouse_move)
643
- window.removeEventListener(`mouseup`, on_window_mouse_up)
644
- document.body.style.cursor = `default`
645
- }
646
-
647
- // Pan/zoom all four axes from an interaction-start snapshot, each in its own
648
- // scale's transform space (log axes pan by a constant factor, linear by a shift)
649
- const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
650
- ranges.current.x = pan_range_by_pixels(init.initial_x_range, dx_px, chart_width, x_axis.scale_type)
651
- ranges.current.x2 = pan_range_by_pixels(init.initial_x2_range, dx_px, chart_width, x2_axis.scale_type)
652
- ranges.current.y = pan_range_by_pixels(init.initial_y_range, dy_px, chart_height, y_axis.scale_type)
653
- ranges.current.y2 = pan_range_by_pixels(init.initial_y2_range, dy_px, chart_height, y2_axis.scale_type)
654
- }
655
- const zoom_all_axes = (init: InitialRanges, factor: number) => {
656
- ranges.current.x = zoom_range_by_factor(init.initial_x_range, factor, x_axis.scale_type)
657
- ranges.current.x2 = zoom_range_by_factor(init.initial_x2_range, factor, x2_axis.scale_type)
658
- ranges.current.y = zoom_range_by_factor(init.initial_y_range, factor, y_axis.scale_type)
659
- ranges.current.y2 = zoom_range_by_factor(init.initial_y2_range, factor, y2_axis.scale_type)
660
- }
661
-
662
- // Pan drag handler (drag direction inverted on x for natural pan feel)
663
- const on_pan_move = (evt: MouseEvent) => {
664
- if (!pan_drag_state) return
665
- const sensitivity = pan?.drag_sensitivity ?? 1
666
- pan_all_axes(
667
- pan_drag_state,
668
- -(evt.clientX - pan_drag_state.start.x) * sensitivity,
669
- (evt.clientY - pan_drag_state.start.y) * sensitivity,
670
- )
671
- }
672
- const on_pan_end = () => {
673
- pan_drag_state = null
674
- document.body.style.cursor = ``
675
- window.removeEventListener(`mousemove`, on_pan_move)
676
- window.removeEventListener(`mouseup`, on_pan_end)
677
- }
678
-
679
- // Tear down any window listeners + cursor override if the component unmounts mid-drag
680
- // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
681
- // onDestroy also runs during SSR teardown, where window/document don't exist.
682
- onDestroy(() => {
683
- remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
684
- drag_state = { start: null, current: null, bounds: null }
685
- pan_drag_state = null
681
+ x_axis = { ...x_axis, range: [null, null] }
682
+ x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
683
+ y_axis = { ...y_axis, range: [null, null] }
684
+ y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
685
+ },
686
686
  })
687
-
688
- function handle_mouse_down(evt: MouseEvent) {
689
- const coords = get_relative_coords(evt)
690
- if (!coords || !svg_element) return
691
- const pan_enabled = pan?.enabled !== false
692
- if (pan_enabled && evt.shiftKey) {
693
- evt.preventDefault()
694
- pan_drag_state = {
695
- start: { x: evt.clientX, y: evt.clientY },
696
- initial_x_range: [...ranges.current.x] as Vec2,
697
- initial_x2_range: [...ranges.current.x2] as Vec2,
698
- initial_y_range: [...ranges.current.y] as Vec2,
699
- initial_y2_range: [...ranges.current.y2] as Vec2,
700
- }
701
- document.body.style.cursor = `grabbing`
702
- window.addEventListener(`mousemove`, on_pan_move)
703
- window.addEventListener(`mouseup`, on_pan_end)
704
- return
705
- }
706
- drag_state = { start: coords, current: coords, bounds: svg_element.getBoundingClientRect() }
707
- window.addEventListener(`mousemove`, on_window_mouse_move)
708
- window.addEventListener(`mouseup`, on_window_mouse_up)
709
- evt.preventDefault()
710
- }
711
-
712
- function handle_wheel(evt: WheelEvent) {
713
- const pan_enabled = pan?.enabled !== false
714
- if (!pan_enabled || !is_focused || !shift_held) return
715
- evt.preventDefault()
716
- const sensitivity = pan?.wheel_sensitivity ?? 1
717
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
718
- const dx = evt.deltaX * sensitivity
719
- ranges.current.x = pan_range_by_pixels(ranges.current.x, dx, chart_width, x_axis.scale_type)
720
- ranges.current.x2 = pan_range_by_pixels(ranges.current.x2, dx, chart_width, x2_axis.scale_type)
721
- } else {
722
- const dy = evt.deltaY * sensitivity
723
- ranges.current.y = pan_range_by_pixels(ranges.current.y, dy, chart_height, y_axis.scale_type)
724
- ranges.current.y2 = pan_range_by_pixels(ranges.current.y2, dy, chart_height, y2_axis.scale_type)
725
- }
726
- }
727
-
728
- function handle_touch_start(evt: TouchEvent) {
729
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
730
- if (!touch_enabled || evt.touches.length !== 2) return
731
- evt.preventDefault()
732
- const touches = Array.from(evt.touches)
733
- touch_state = {
734
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
735
- initial_x_range: [...ranges.current.x] as Vec2,
736
- initial_x2_range: [...ranges.current.x2] as Vec2,
737
- initial_y_range: [...ranges.current.y] as Vec2,
738
- initial_y2_range: [...ranges.current.y2] as Vec2,
739
- }
740
- }
741
- function handle_touch_move(evt: TouchEvent) {
742
- if (!touch_state || evt.touches.length !== 2) return
743
- evt.preventDefault()
744
- const [t1, t2] = Array.from(evt.touches)
745
- const [s1, s2] = touch_state.start_touches
746
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
747
- const curr_center = { x: (t1.clientX + t2.clientX) / 2, y: (t1.clientY + t2.clientY) / 2 }
748
- const dx = curr_center.x - start_center.x
749
- const dy = curr_center.y - start_center.y
750
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
751
- // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
752
- if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
753
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
754
- const scale = curr_dist / start_dist
755
- // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
756
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
757
- zoom_all_axes(touch_state, scale)
758
- } else pan_all_axes(touch_state, -dx, dy)
759
- }
760
- const handle_touch_end = () => (touch_state = null)
687
+ onDestroy(() => pan_zoom.destroy())
761
688
 
762
689
  // === Legend ===
763
690
  let legend_data = $derived<LegendItem[]>(
@@ -770,7 +697,10 @@
770
697
  })),
771
698
  )
772
699
 
773
- const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
700
+ const legend_vis = create_legend_visibility(
701
+ () => series,
702
+ (next) => (series = next),
703
+ )
774
704
 
775
705
  let box_points_for_placement = $derived.by(() => {
776
706
  if (!width || !height || visible_boxes.length === 0) return []
@@ -787,10 +717,6 @@
787
717
  })
788
718
 
789
719
  let hovered_legend_series_idx = $state<number | null>(null)
790
- const legend_hover = create_hover_lock()
791
- const dim_tracker = create_dimension_tracker()
792
- let has_initial_legend_placement = $state(false)
793
- $effect(() => () => legend_hover.cleanup())
794
720
 
795
721
  let legend_placement = $derived.by(() => {
796
722
  if (!should_show_legend || !width || !height) return null
@@ -804,24 +730,13 @@
804
730
  })
805
731
  })
806
732
 
807
- const tweened_legend_coords = new Tween(
808
- { x: 0, y: 0 },
809
- untrack(() => ({ duration: 400, ...legend?.tween })),
810
- )
811
- $effect(() => {
812
- if (!width || !height || !legend_placement) return
813
- const dims_changed = dim_tracker.has_changed(width, height)
814
- if (dims_changed) dim_tracker.update(width, height)
815
- const is_responsive = legend?.responsive ?? false
816
- const should_update = dims_changed ||
817
- (!legend_hover.is_locked.current && (is_responsive || !has_initial_legend_placement))
818
- if (should_update) {
819
- tweened_legend_coords.set(
820
- { x: legend_placement.x, y: legend_placement.y },
821
- has_initial_legend_placement ? undefined : { duration: 0 },
822
- )
823
- if (legend_element) has_initial_legend_placement = true
824
- }
733
+ // Tweened legend coordinates with shared placement stability gating
734
+ const legend_tween = create_placed_tween({
735
+ placement: () => legend_placement,
736
+ dims: () => ({ width, height }),
737
+ responsive: () => legend?.responsive ?? false,
738
+ element: () => legend_element,
739
+ tween: () => legend?.tween,
825
740
  })
826
741
 
827
742
  // === Tooltip / hover ===
@@ -878,16 +793,13 @@
878
793
 
879
794
  // Value label helper
880
795
  const value_label_for = (stats: Box[`stats`]): string =>
881
- format_value(value_label_stat === `mean` ? stats.mean : stats.median, value_label_format)
796
+ format_value_or_num(
797
+ value_label_stat === `mean` ? stats.mean : stats.median,
798
+ value_label_format,
799
+ )
882
800
  </script>
883
801
 
884
- {#snippet seg(
885
- p1: [number, number],
886
- p2: [number, number],
887
- stroke: string,
888
- sw: number,
889
- dash?: string,
890
- )}
802
+ {#snippet seg(p1: Vec2, p2: Vec2, stroke: string, sw: number, dash?: string)}
891
803
  <line
892
804
  x1={p1[0]}
893
805
  y1={p1[1]}
@@ -933,11 +845,9 @@
933
845
  evt.preventDefault()
934
846
  fullscreen = false
935
847
  }
936
- if (evt.key === `Shift`) shift_held = true
937
- }}
938
- onkeyup={(evt) => {
939
- if (evt.key === `Shift`) shift_held = false
848
+ pan_zoom.on_window_key_down(evt)
940
849
  }}
850
+ onkeyup={pan_zoom.on_window_key_up}
941
851
  />
942
852
 
943
853
  <div
@@ -945,7 +855,7 @@
945
855
  bind:clientWidth={width}
946
856
  bind:clientHeight={height}
947
857
  {...rest}
948
- class="box-plot {rest.class ?? ``}"
858
+ class={[`box-plot`, rest.class]}
949
859
  class:fullscreen
950
860
  >
951
861
  {#if width && height}
@@ -961,39 +871,27 @@
961
871
  bind:this={svg_element}
962
872
  role="application"
963
873
  aria-label={rest[`aria-label`] ??
964
- ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Box plot`)}
874
+ ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Box plot`)}
965
875
  tabindex="0"
966
- onfocusin={() => (is_focused = true)}
967
- onfocusout={() => (is_focused = false)}
968
- onmousedown={handle_mouse_down}
969
- ondblclick={() => {
970
- ranges.current.x = [...ranges.initial.x] as Vec2
971
- ranges.current.x2 = [...ranges.initial.x2] as Vec2
972
- ranges.current.y = [...ranges.initial.y] as Vec2
973
- ranges.current.y2 = [...ranges.initial.y2] as Vec2
974
- x_axis = { ...x_axis, range: [null, null] }
975
- x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
976
- y_axis = { ...y_axis, range: [null, null] }
977
- y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
978
- }}
876
+ onfocusin={() => pan_zoom.set_focused(true)}
877
+ onfocusout={() => pan_zoom.set_focused(false)}
878
+ onmousedown={pan_zoom.on_mouse_down}
879
+ ondblclick={pan_zoom.reset_view}
880
+ onkeydown={pan_zoom.on_key_down}
979
881
  onmouseleave={() => {
980
882
  hovered = false
981
883
  hover_info = null
982
884
  change(null)
983
885
  on_box_hover?.(null)
984
886
  }}
985
- onwheel={handle_wheel}
986
- ontouchstart={handle_touch_start}
987
- ontouchmove={handle_touch_move}
988
- ontouchend={handle_touch_end}
989
- ontouchcancel={handle_touch_end}
990
- style:cursor={pan_drag_state
991
- ? `grabbing`
992
- : shift_held && pan?.enabled !== false
993
- ? `grab`
994
- : `crosshair`}
887
+ onwheel={pan_zoom.on_wheel}
888
+ ontouchstart={pan_zoom.on_touch_start}
889
+ ontouchmove={pan_zoom.on_touch_move}
890
+ ontouchend={pan_zoom.on_touch_end}
891
+ ontouchcancel={pan_zoom.on_touch_end}
892
+ style:cursor={pan_zoom.cursor}
995
893
  >
996
- <ZoomRect start={drag_state.start} current={drag_state.current} />
894
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
997
895
 
998
896
  {@render user_content?.({
999
897
  height,
@@ -1014,65 +912,67 @@
1014
912
 
1015
913
  <PlotAxis
1016
914
  side="x"
1017
- ticks={ticks.x as number[]}
915
+ ticks={ticks.x}
1018
916
  place={scales.x}
1019
917
  axis={x_axis}
1020
- domain={ranges.current.x as Vec2}
918
+ domain={ranges.current.x}
1021
919
  {pad}
1022
920
  {width}
1023
921
  {height}
1024
922
  show_grid={display.x_grid}
1025
923
  tick_label={(tick) =>
1026
- get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
924
+ get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
1027
925
  tick_color={cat_axis === `x` ? (tick) => slot_colors.get(tick) : undefined}
1028
926
  label_x={pad.l + chart_width / 2 + (x_axis.label_shift?.x ?? 0)}
1029
- label_y={height - pad.b / 3 + (x_axis.label_shift?.y ?? 0)}
927
+ label_y={height - pad.b + AXIS_TITLE_OFFSET + (x_axis.label_shift?.y ?? 0)}
1030
928
  />
1031
929
 
1032
- {#if has_secondary && orientation === `horizontal`}
930
+ {#if show_x2}
1033
931
  <PlotAxis
1034
932
  side="x2"
1035
- ticks={ticks.x2 as number[]}
933
+ ticks={ticks.x2}
1036
934
  place={scales.x2}
1037
935
  axis={x2_axis}
1038
- domain={ranges.current.x2 as Vec2}
936
+ domain={ranges.current.x2}
1039
937
  {pad}
1040
938
  {width}
1041
939
  {height}
1042
940
  show_grid={display.x2_grid}
1043
941
  tick_label={(tick) => get_tick_label(tick, x2_axis.ticks)}
1044
942
  label_x={pad.l + chart_width / 2 + (x2_axis.label_shift?.x ?? 0)}
1045
- label_y={Math.max(12, pad.t - (x2_axis.label_shift?.y ?? 40))}
943
+ label_y={Math.max(12, pad.t - (x2_axis.label_shift?.y ?? AXIS_TITLE_OFFSET))}
1046
944
  />
1047
945
  {/if}
1048
946
 
1049
947
  <PlotAxis
1050
948
  side="y"
1051
- ticks={ticks.y as number[]}
949
+ ticks={ticks.y}
1052
950
  place={scales.y}
1053
951
  axis={y_axis}
1054
- domain={ranges.current.y as Vec2}
952
+ domain={ranges.current.y}
1055
953
  {pad}
1056
954
  {width}
1057
955
  {height}
1058
956
  show_grid={display.y_grid}
1059
957
  tick_label={(tick) =>
1060
- get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
958
+ get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
1061
959
  tick_color={cat_axis === `y` ? (tick) => slot_colors.get(tick) : undefined}
1062
960
  label_x={Math.max(
1063
961
  12,
1064
- pad.l - (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
962
+ pad.l -
963
+ (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
964
+ LABEL_GAP_DEFAULT,
1065
965
  ) + (y_axis.label_shift?.x ?? 0)}
1066
966
  label_y={pad.t + chart_height / 2 + (y_axis.label_shift?.y ?? 0)}
1067
967
  />
1068
968
 
1069
- {#if has_secondary && orientation === `vertical`}
969
+ {#if show_y2}
1070
970
  <PlotAxis
1071
971
  side="y2"
1072
- ticks={ticks.y2 as number[]}
972
+ ticks={ticks.y2}
1073
973
  place={scales.y2}
1074
974
  axis={y2_axis}
1075
- domain={ranges.current.y2 as Vec2}
975
+ domain={ranges.current.y2}
1076
976
  {pad}
1077
977
  {width}
1078
978
  {height}
@@ -1108,8 +1008,8 @@
1108
1008
  x2_scale_type={x2_axis.scale_type}
1109
1009
  y_scale_type={y_axis.scale_type}
1110
1010
  y2_scale_type={y2_axis.scale_type}
1111
- has_x2={has_secondary && orientation === `horizontal`}
1112
- has_y2={has_secondary && orientation === `vertical`}
1011
+ has_x2={show_x2}
1012
+ has_y2={show_y2}
1113
1013
  {width}
1114
1014
  {height}
1115
1015
  {pad}
@@ -1130,12 +1030,13 @@
1130
1030
  {@const draw_box = draws_box(box_item.series)}
1131
1031
  {@const kde = violin_kdes.get(box_item.idx)}
1132
1032
  {@const eff_side = box_item.series.side ?? side}
1133
- {@const bw = box_item.series.box_width ??
1134
- (kde ? DEFAULTS.box.violin_box_width : DEFAULTS.box.box_width)}
1033
+ {@const bw =
1034
+ box_item.series.box_width ??
1035
+ (kde ? DEFAULTS.box.violin_box_width : DEFAULTS.box.box_width)}
1135
1036
  {@const c_lo = cat_scale(box_item.slot - bw / 2)}
1136
1037
  {@const c_hi = cat_scale(box_item.slot + bw / 2)}
1137
1038
  {@const c_center = cat_scale(box_item.slot)}
1138
- {@const cap = Math.abs(c_hi - c_lo) * (whisker_state.cap_fraction ?? 0.5) / 2}
1039
+ {@const cap = (Math.abs(c_hi - c_lo) * (whisker_state.cap_fraction ?? 0.5)) / 2}
1139
1040
  {@const cap_lo = c_center - cap}
1140
1041
  {@const cap_hi = c_center + cap}
1141
1042
  {@const v_q1 = val_scale(stats.q1)}
@@ -1144,8 +1045,8 @@
1144
1045
  {@const v_wl = val_scale(stats.whisker_low)}
1145
1046
  {@const v_wh = val_scale(stats.whisker_high)}
1146
1047
  {@const v_mean = val_scale(stats.mean)}
1147
- {@const pt = (cross: number, val: number): [number, number] =>
1148
- vertical ? [cross, val] : [val, cross]}
1048
+ {@const pt = (cross: number, val: number): Vec2 =>
1049
+ vertical ? [cross, val] : [val, cross]}
1149
1050
  {@const [q1x, q1y] = pt(c_lo, v_q1)}
1150
1051
  {@const [q3x, q3y] = pt(c_hi, v_q3)}
1151
1052
  {@const [wlx, wly] = pt(c_lo, v_wl)}
@@ -1159,12 +1060,12 @@
1159
1060
  {@const hit_w = Math.abs(whx - wlx)}
1160
1061
  {@const hit_h = Math.abs(why - wly)}
1161
1062
  {@const [label_x, label_y] = vertical
1162
- ? [c_center, Math.min(v_wh, v_wl) - 6]
1163
- : [Math.max(v_wh, v_wl) + 6, c_center]}
1063
+ ? [c_center, Math.min(v_wh, v_wl) - 6]
1064
+ : [Math.max(v_wh, v_wl) + 6, c_center]}
1164
1065
  {@const violin_half = Math.abs(
1165
- cat_scale(box_item.slot + (box_item.series.violin_width ?? violin_width) / 2) -
1166
- c_center,
1167
- )}
1066
+ cat_scale(box_item.slot + (box_item.series.violin_width ?? violin_width) / 2) -
1067
+ c_center,
1068
+ )}
1168
1069
  {@const max_density = kde ? (violin_max_density.get(box_item.idx) ?? 0) : 0}
1169
1070
  <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
1170
1071
  <g
@@ -1175,7 +1076,7 @@
1175
1076
  aria-label={`box ${box_item.idx + 1}: ${box_item.series.label ?? ``}`}
1176
1077
  style:cursor={on_box_click ? `pointer` : undefined}
1177
1078
  opacity={hovered_legend_series_idx !== null &&
1178
- hovered_legend_series_idx !== box_item.idx
1079
+ hovered_legend_series_idx !== box_item.idx
1179
1080
  ? 0.25
1180
1081
  : 1}
1181
1082
  onmousemove={handle_box_hover(box_item, color)}
@@ -1184,7 +1085,8 @@
1184
1085
  change(null)
1185
1086
  on_box_hover?.(null)
1186
1087
  }}
1187
- onclick={(evt) => on_box_click?.({ ...get_box_data(box_item, color), event: evt })}
1088
+ onclick={(evt) =>
1089
+ on_box_click?.({ ...get_box_data(box_item, color), event: evt })}
1188
1090
  onkeydown={(evt) => {
1189
1091
  if (evt.key === `Enter` || evt.key === ` `) {
1190
1092
  evt.preventDefault()
@@ -1231,7 +1133,12 @@
1231
1133
  stroke-width={box_state.stroke_width}
1232
1134
  />
1233
1135
  <!-- median (solid) and mean (dashed) -->
1234
- {@render seg(pt(c_lo, v_med), pt(c_hi, v_med), median_state.color, median_state.width)}
1136
+ {@render seg(
1137
+ pt(c_lo, v_med),
1138
+ pt(c_hi, v_med),
1139
+ median_state.color,
1140
+ median_state.width,
1141
+ )}
1235
1142
  {#if show_mean}
1236
1143
  {@render seg(
1237
1144
  pt(c_lo, v_mean),
@@ -1287,19 +1194,33 @@
1287
1194
 
1288
1195
  {@render ref_lines_layer(ref_lines_by_z.below_points)}
1289
1196
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
1197
+
1198
+ <!-- Marginal distribution strips -->
1199
+ <PlotMarginals
1200
+ marginals={resolved_marginals}
1201
+ series={marginal_series}
1202
+ {width}
1203
+ {height}
1204
+ {pad}
1205
+ has_axis={marginal_has_axis}
1206
+ axes={{
1207
+ x1: marginal_axis(scales.x, ranges.current.x, x_axis),
1208
+ x2: marginal_axis(scales.x2, ranges.current.x2, x2_axis),
1209
+ y1: marginal_axis(scales.y, ranges.current.y, y_axis),
1210
+ y2: marginal_axis(scales.y2, ranges.current.y2, y2_axis),
1211
+ }}
1212
+ id={clip_path_id}
1213
+ />
1290
1214
  </svg>
1291
1215
 
1292
1216
  {#if legend && should_show_legend}
1293
- {@const legend_left = legend_auto_outside
1294
- ? legend_outside_x
1295
- : legend_placement
1296
- ? tweened_legend_coords.current.x
1297
- : pad.l + 10}
1298
- {@const legend_top = legend_auto_outside
1299
- ? legend_outside_y
1300
- : legend_placement
1301
- ? tweened_legend_coords.current.y
1302
- : pad.t + 10}
1217
+ {@const legend_pos = placed_coords(
1218
+ legend_auto_outside,
1219
+ { x: legend_outside_x, y: legend_outside_y },
1220
+ legend_placement,
1221
+ legend_tween.coords.current,
1222
+ { x: pad.l + 10, y: pad.t + 10 },
1223
+ )}
1303
1224
  <PlotLegend
1304
1225
  bind:root_element={legend_element}
1305
1226
  {...legend}
@@ -1307,13 +1228,12 @@
1307
1228
  on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1308
1229
  on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
1309
1230
  on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1310
- on_hover_change={legend_hover.set_locked}
1231
+ on_hover_change={legend_tween.set_locked}
1311
1232
  on_item_hover={(item) =>
1312
- (hovered_legend_series_idx = item != null && item.series_idx >= 0
1313
- ? item.series_idx
1314
- : null)}
1233
+ (hovered_legend_series_idx =
1234
+ item != null && item.series_idx >= 0 ? item.series_idx : null)}
1315
1235
  active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1316
- style={`position: absolute; left: ${legend_left}px; top: ${legend_top}px; pointer-events: auto; ${
1236
+ style={`position: absolute; left: ${legend_pos.x}px; top: ${legend_pos.y}px; pointer-events: auto; ${
1317
1237
  legend?.style || ``
1318
1238
  }`}
1319
1239
  />
@@ -1331,7 +1251,7 @@
1331
1251
  {#if tooltip}
1332
1252
  {@render tooltip({ ...hover_info, fullscreen })}
1333
1253
  {:else}
1334
- {@const fmt = (orientation === `vertical` ? y_axis.format : x_axis.format) || `.3~s`}
1254
+ {@const fmt = orientation === `vertical` ? y_axis.format : x_axis.format}
1335
1255
  {@const stat = hover_info.stats}
1336
1256
  {@const rows = [
1337
1257
  [`max`, stat.whisker_high],
@@ -1345,7 +1265,7 @@
1345
1265
  <div><strong>{hover_info.category_label}</strong></div>
1346
1266
  {/if}
1347
1267
  {#each rows as [label, value] (label)}
1348
- <div>{label}: {format_value(value, fmt)}</div>
1268
+ <div>{label}: {format_value_or_num(value, fmt)}</div>
1349
1269
  {/each}
1350
1270
  {#if show_outliers && stat.outliers.length > 0}
1351
1271
  <div>outliers: {stat.outliers.length}</div>
@@ -1376,12 +1296,12 @@
1376
1296
  bind:y_axis
1377
1297
  bind:y2_axis={y2_axis_prop}
1378
1298
  bind:display
1379
- auto_x_range={auto_ranges.x as Vec2}
1380
- auto_x2_range={auto_ranges.x2 as Vec2}
1381
- auto_y_range={auto_ranges.y as Vec2}
1382
- auto_y2_range={auto_ranges.y2 as Vec2}
1383
- has_x2_points={has_secondary && orientation === `horizontal`}
1384
- has_y2_points={has_secondary && orientation === `vertical`}
1299
+ auto_x_range={auto_ranges.x}
1300
+ auto_x2_range={auto_ranges.x2}
1301
+ auto_y_range={auto_ranges.y}
1302
+ auto_y2_range={auto_ranges.y2}
1303
+ has_x2_points={show_x2}
1304
+ has_y2_points={show_y2}
1385
1305
  children={controls_extra}
1386
1306
  />
1387
1307
  {/if}
@@ -1438,7 +1358,9 @@
1438
1358
  .box-plot :global(.pane-toggle),
1439
1359
  .box-plot .header-controls {
1440
1360
  opacity: 0;
1441
- transition: opacity 0.2s, background-color 0.2s;
1361
+ transition:
1362
+ opacity 0.2s,
1363
+ background-color 0.2s;
1442
1364
  }
1443
1365
  .box-plot:hover :global(.pane-toggle),
1444
1366
  .box-plot:hover .header-controls,