matterviz 0.3.7 → 0.4.0

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 (324) hide show
  1. package/dist/Icon.svelte +7 -4
  2. package/dist/MillerIndexInput.svelte +1 -1
  3. package/dist/api/optimade.js +32 -26
  4. package/dist/app.css +0 -3
  5. package/dist/brillouin/BrillouinZone.svelte +8 -3
  6. package/dist/brillouin/BrillouinZone.svelte.d.ts +2 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +52 -6
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -0
  9. package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
  10. package/dist/brillouin/compute.js +10 -14
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
  12. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +12 -15
  13. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +8 -10
  14. package/dist/chempot-diagram/async-compute.svelte.js +3 -1
  15. package/dist/chempot-diagram/chempot-worker.js +2 -1
  16. package/dist/chempot-diagram/compute.d.ts +1 -1
  17. package/dist/chempot-diagram/compute.js +17 -19
  18. package/dist/colors/index.js +6 -5
  19. package/dist/composition/FormulaFilter.svelte +12 -6
  20. package/dist/composition/PieChart.svelte +6 -5
  21. package/dist/composition/chem-sys.d.ts +8 -0
  22. package/dist/composition/chem-sys.js +85 -0
  23. package/dist/composition/format.js +4 -2
  24. package/dist/composition/index.d.ts +1 -0
  25. package/dist/composition/index.js +1 -0
  26. package/dist/composition/parse.js +25 -13
  27. package/dist/convex-hull/ConvexHull2D.svelte +12 -10
  28. package/dist/convex-hull/ConvexHull3D.svelte +5 -5
  29. package/dist/convex-hull/ConvexHull4D.svelte +5 -9
  30. package/dist/convex-hull/ConvexHullStats.svelte +12 -12
  31. package/dist/convex-hull/GasPressureControls.svelte +4 -4
  32. package/dist/convex-hull/TemperatureSlider.svelte +2 -2
  33. package/dist/convex-hull/demo-temperature.d.ts +1 -1
  34. package/dist/convex-hull/demo-temperature.js +20 -22
  35. package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
  36. package/dist/convex-hull/gas-thermodynamics.js +22 -30
  37. package/dist/convex-hull/helpers.d.ts +3 -0
  38. package/dist/convex-hull/helpers.js +17 -9
  39. package/dist/convex-hull/index.d.ts +1 -1
  40. package/dist/convex-hull/thermodynamics.js +83 -78
  41. package/dist/convex-hull/types.d.ts +1 -1
  42. package/dist/coordination/CoordinationBarPlot.svelte +23 -23
  43. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  44. package/dist/element/ElementTile.svelte.d.ts +1 -1
  45. package/dist/fermi-surface/FermiSlice.svelte +13 -5
  46. package/dist/fermi-surface/FermiSurface.svelte +11 -5
  47. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  48. package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
  49. package/dist/fermi-surface/FermiSurfaceScene.svelte +3 -0
  50. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
  51. package/dist/fermi-surface/compute.js +59 -59
  52. package/dist/fermi-surface/export.js +3 -2
  53. package/dist/fermi-surface/parse.js +7 -4
  54. package/dist/fermi-surface/types.d.ts +1 -0
  55. package/dist/heatmap-matrix/HeatmapMatrix.svelte +23 -21
  56. package/dist/heatmap-matrix/index.js +1 -1
  57. package/dist/io/decompress.js +4 -2
  58. package/dist/io/export.d.ts +4 -4
  59. package/dist/io/export.js +47 -25
  60. package/dist/io/fetch.js +5 -1
  61. package/dist/io/file-drop.d.ts +1 -1
  62. package/dist/io/file-drop.js +35 -36
  63. package/dist/io/url-drop.js +64 -33
  64. package/dist/isosurface/parse.js +6 -7
  65. package/dist/isosurface/slice.js +5 -4
  66. package/dist/isosurface/types.js +1 -1
  67. package/dist/keyboard.d.ts +3 -0
  68. package/dist/keyboard.js +23 -0
  69. package/dist/labels.d.ts +1 -1
  70. package/dist/labels.js +8 -7
  71. package/dist/layout/PropertyFilter.svelte +3 -2
  72. package/dist/layout/SettingsSection.svelte +1 -1
  73. package/dist/layout/json-tree/JsonNode.svelte +1 -1
  74. package/dist/layout/json-tree/JsonTree.svelte +2 -2
  75. package/dist/layout/json-tree/utils.js +5 -4
  76. package/dist/marching-cubes.js +8 -13
  77. package/dist/math.d.ts +5 -1
  78. package/dist/math.js +24 -9
  79. package/dist/overlays/DraggablePane.svelte +4 -4
  80. package/dist/periodic-table/PeriodicTable.svelte +20 -9
  81. package/dist/periodic-table/PropertySelect.svelte +1 -0
  82. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
  83. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  84. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  85. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  86. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
  87. package/dist/phase-diagram/build-diagram.js +2 -2
  88. package/dist/phase-diagram/parse.js +6 -5
  89. package/dist/phase-diagram/types.d.ts +1 -1
  90. package/dist/phase-diagram/utils.d.ts +3 -3
  91. package/dist/phase-diagram/utils.js +8 -12
  92. package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +229 -587
  93. package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +5 -5
  94. package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
  95. package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
  96. package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +6 -6
  97. package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
  98. package/dist/plot/bar/data.d.ts +40 -0
  99. package/dist/plot/bar/data.js +154 -0
  100. package/dist/plot/bar/geometry.d.ts +39 -0
  101. package/dist/plot/bar/geometry.js +60 -0
  102. package/dist/plot/bar/index.d.ts +3 -0
  103. package/dist/plot/bar/index.js +3 -0
  104. package/dist/plot/box/BoxPlot.svelte +1462 -0
  105. package/dist/plot/box/BoxPlot.svelte.d.ts +94 -0
  106. package/dist/plot/box/BoxPlotControls.svelte +109 -0
  107. package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
  108. package/dist/plot/box/Violin.svelte +14 -0
  109. package/dist/plot/box/Violin.svelte.d.ts +70 -0
  110. package/dist/plot/box/box-plot.d.ts +55 -0
  111. package/dist/plot/box/box-plot.js +126 -0
  112. package/dist/plot/box/index.d.ts +5 -0
  113. package/dist/plot/box/index.js +5 -0
  114. package/dist/plot/box/kde.d.ts +16 -0
  115. package/dist/plot/box/kde.js +160 -0
  116. package/dist/plot/box/quantile.d.ts +3 -0
  117. package/dist/plot/box/quantile.js +53 -0
  118. package/dist/plot/{auto-place.js → core/auto-place.js} +2 -2
  119. package/dist/plot/core/axis-utils.d.ts +46 -0
  120. package/dist/plot/core/axis-utils.js +110 -0
  121. package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
  122. package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
  123. package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +36 -33
  124. package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +2 -2
  125. package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
  126. package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
  127. package/dist/plot/core/components/ControlPane.svelte +46 -0
  128. package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
  129. package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
  130. package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
  131. package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
  132. package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
  133. package/dist/plot/{Line.svelte → core/components/Line.svelte} +30 -13
  134. package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +7 -5
  135. package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +3 -2
  136. package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
  137. package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
  138. package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
  139. package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
  140. package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
  141. package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
  142. package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
  143. package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
  144. package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
  145. package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +4 -4
  146. package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +2 -2
  147. package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +7 -7
  148. package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +2 -2
  149. package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
  150. package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
  151. package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
  152. package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
  153. package/dist/plot/core/components/index.d.ts +17 -0
  154. package/dist/plot/core/components/index.js +17 -0
  155. package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
  156. package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +3 -5
  157. package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
  158. package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
  159. package/dist/plot/core/fill-utils.d.ts +33 -0
  160. package/dist/plot/core/fill-utils.js +388 -0
  161. package/dist/plot/{hover-lock.svelte.js → core/hover-lock.svelte.js} +5 -6
  162. package/dist/plot/core/index.d.ts +10 -0
  163. package/dist/plot/core/index.js +11 -0
  164. package/dist/plot/core/interactions.d.ts +35 -0
  165. package/dist/plot/core/interactions.js +195 -0
  166. package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
  167. package/dist/plot/{layout.js → core/layout.js} +16 -8
  168. package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +1 -1
  169. package/dist/plot/{reference-line.js → core/reference-line.js} +23 -36
  170. package/dist/plot/{scales.d.ts → core/scales.d.ts} +2 -2
  171. package/dist/plot/{scales.js → core/scales.js} +84 -85
  172. package/dist/plot/core/svg.d.ts +2 -0
  173. package/dist/plot/core/svg.js +41 -0
  174. package/dist/plot/{types.d.ts → core/types.d.ts} +19 -79
  175. package/dist/plot/{types.js → core/types.js} +1 -1
  176. package/dist/plot/{utils → core/utils}/label-placement.d.ts +2 -2
  177. package/dist/plot/core/utils/series-visibility.d.ts +26 -0
  178. package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
  179. package/dist/plot/core/utils.d.ts +11 -0
  180. package/dist/plot/core/utils.js +27 -0
  181. package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +154 -294
  182. package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
  183. package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
  184. package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
  185. package/dist/plot/histogram/index.d.ts +2 -0
  186. package/dist/plot/histogram/index.js +2 -0
  187. package/dist/plot/index.d.ts +8 -41
  188. package/dist/plot/index.js +10 -39
  189. package/dist/plot/sankey/Sankey.svelte +700 -0
  190. package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
  191. package/dist/plot/sankey/SankeyControls.svelte +98 -0
  192. package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
  193. package/dist/plot/sankey/index.d.ts +4 -0
  194. package/dist/plot/sankey/index.js +3 -0
  195. package/dist/plot/sankey/sankey-types.d.ts +42 -0
  196. package/dist/plot/sankey/sankey-types.js +4 -0
  197. package/dist/plot/sankey/sankey.d.ts +52 -0
  198. package/dist/plot/sankey/sankey.js +187 -0
  199. package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +61 -59
  200. package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +4 -4
  201. package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
  202. package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
  203. package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +221 -642
  204. package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +7 -7
  205. package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
  206. package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +1 -1
  207. package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
  208. package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
  209. package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
  210. package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
  211. package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +3 -3
  212. package/dist/plot/scatter/index.d.ts +7 -0
  213. package/dist/plot/scatter/index.js +5 -0
  214. package/dist/plot/scatter/scatter-data.d.ts +19 -0
  215. package/dist/plot/scatter/scatter-data.js +212 -0
  216. package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +12 -10
  217. package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +7 -7
  218. package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +5 -4
  219. package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +2 -2
  220. package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +11 -11
  221. package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +3 -3
  222. package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +1 -1
  223. package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +1 -1
  224. package/dist/plot/scatter-3d/index.d.ts +4 -0
  225. package/dist/plot/scatter-3d/index.js +4 -0
  226. package/dist/plot/sunburst/Sunburst.svelte +1045 -0
  227. package/dist/plot/sunburst/Sunburst.svelte.d.ts +96 -0
  228. package/dist/plot/sunburst/SunburstControls.svelte +200 -0
  229. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
  230. package/dist/plot/sunburst/index.d.ts +4 -0
  231. package/dist/plot/sunburst/index.js +4 -0
  232. package/dist/plot/sunburst/render.d.ts +34 -0
  233. package/dist/plot/sunburst/render.js +122 -0
  234. package/dist/plot/sunburst/sunburst.d.ts +62 -0
  235. package/dist/plot/sunburst/sunburst.js +266 -0
  236. package/dist/rdf/RdfPlot.svelte +2 -1
  237. package/dist/rdf/calc-rdf.js +11 -24
  238. package/dist/sanitize.js +1 -1
  239. package/dist/settings.d.ts +65 -1
  240. package/dist/settings.js +262 -0
  241. package/dist/spectral/Bands.svelte +39 -29
  242. package/dist/spectral/Bands.svelte.d.ts +3 -4
  243. package/dist/spectral/BandsAndDos.svelte +1 -1
  244. package/dist/spectral/BrillouinBandsDos.svelte +39 -27
  245. package/dist/spectral/Dos.svelte +10 -19
  246. package/dist/spectral/Dos.svelte.d.ts +2 -2
  247. package/dist/spectral/helpers.d.ts +3 -1
  248. package/dist/spectral/helpers.js +95 -29
  249. package/dist/structure/AtomLegend.svelte +8 -9
  250. package/dist/structure/CellSelect.svelte +1 -2
  251. package/dist/structure/Cylinder.svelte +12 -8
  252. package/dist/structure/Cylinder.svelte.d.ts +4 -1
  253. package/dist/structure/Structure.svelte +78 -72
  254. package/dist/structure/Structure.svelte.d.ts +1 -1
  255. package/dist/structure/StructureInfoPane.svelte +5 -6
  256. package/dist/structure/StructureScene.svelte +11 -10
  257. package/dist/structure/atom-properties.js +6 -6
  258. package/dist/structure/bond-order-perception.js +1 -1
  259. package/dist/structure/bonding.d.ts +1 -0
  260. package/dist/structure/bonding.js +43 -15
  261. package/dist/structure/export.js +27 -23
  262. package/dist/structure/index.d.ts +2 -4
  263. package/dist/structure/index.js +1 -3
  264. package/dist/structure/label-placement.js +4 -4
  265. package/dist/structure/measure.d.ts +3 -2
  266. package/dist/structure/measure.js +6 -5
  267. package/dist/structure/parse.js +121 -103
  268. package/dist/structure/pbc.js +4 -0
  269. package/dist/symmetry/SymmetryStats.svelte +2 -2
  270. package/dist/symmetry/index.d.ts +1 -1
  271. package/dist/symmetry/index.js +22 -24
  272. package/dist/symmetry/spacegroups.d.ts +7 -0
  273. package/dist/symmetry/spacegroups.js +48 -13
  274. package/dist/table/HeatmapTable.svelte +63 -11
  275. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  276. package/dist/table/index.d.ts +1 -3
  277. package/dist/table/index.js +1 -1
  278. package/dist/theme/index.js +8 -8
  279. package/dist/tooltip/KCoords.svelte +45 -0
  280. package/dist/tooltip/KCoords.svelte.d.ts +8 -0
  281. package/dist/tooltip/index.d.ts +1 -0
  282. package/dist/tooltip/index.js +1 -0
  283. package/dist/trajectory/Trajectory.svelte +66 -40
  284. package/dist/trajectory/Trajectory.svelte.d.ts +2 -1
  285. package/dist/trajectory/TrajectoryExportPane.svelte +2 -1
  286. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -1
  287. package/dist/trajectory/format-detect.d.ts +1 -0
  288. package/dist/trajectory/format-detect.js +25 -11
  289. package/dist/trajectory/frame-reader.js +17 -50
  290. package/dist/trajectory/helpers.js +1 -1
  291. package/dist/trajectory/index.js +1 -1
  292. package/dist/trajectory/parse/hdf5.js +1 -1
  293. package/dist/trajectory/parse/index.js +14 -6
  294. package/dist/trajectory/parse/vasp.js +36 -17
  295. package/dist/trajectory/parse/xyz.d.ts +24 -0
  296. package/dist/trajectory/parse/xyz.js +102 -89
  297. package/dist/trajectory/plotting.d.ts +1 -1
  298. package/dist/trajectory/plotting.js +15 -15
  299. package/dist/utils.d.ts +1 -0
  300. package/dist/utils.js +6 -4
  301. package/dist/xrd/XrdPlot.svelte +2 -1
  302. package/dist/xrd/calc-xrd.js +15 -12
  303. package/dist/xrd/parse.js +2 -2
  304. package/package.json +22 -18
  305. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  306. package/dist/plot/axis-utils.d.ts +0 -19
  307. package/dist/plot/axis-utils.js +0 -78
  308. package/dist/plot/defaults.d.ts +0 -19
  309. package/dist/plot/defaults.js +0 -9
  310. package/dist/plot/fill-utils.d.ts +0 -46
  311. package/dist/plot/fill-utils.js +0 -322
  312. package/dist/plot/interactions.d.ts +0 -12
  313. package/dist/plot/interactions.js +0 -101
  314. package/dist/plot/svg.d.ts +0 -1
  315. package/dist/plot/svg.js +0 -11
  316. package/dist/plot/utils/series-visibility.d.ts +0 -15
  317. package/dist/plot/utils.d.ts +0 -1
  318. package/dist/plot/utils.js +0 -14
  319. /package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +0 -0
  320. /package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +0 -0
  321. /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
  322. /package/dist/plot/{hover-lock.svelte.d.ts → core/hover-lock.svelte.d.ts} +0 -0
  323. /package/dist/plot/{utils → core/utils}/label-placement.js +0 -0
  324. /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
@@ -0,0 +1,1462 @@
1
+ <script
2
+ lang="ts"
3
+ generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
+ >
5
+ import { format_value } from '../../labels'
6
+ import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
7
+ import type { Vec2 } from '../../math'
8
+ import type {
9
+ BandwidthOption,
10
+ BasePlotProps,
11
+ BoxHandlerProps,
12
+ BoxPlotSeries,
13
+ LegendConfig,
14
+ LegendItem,
15
+ Orientation,
16
+ PanConfig,
17
+ PlotConfig,
18
+ RefLine,
19
+ RefLineEvent,
20
+ ScaleType,
21
+ UserContentProps,
22
+ ViolinKind,
23
+ ViolinSide,
24
+ WhiskerMode,
25
+ } from '..'
26
+ import {
27
+ BoxPlotControls,
28
+ compute_element_placement,
29
+ PlotAxis,
30
+ PlotLegend,
31
+ ReferenceLine,
32
+ } from '..'
33
+ import {
34
+ build_obstacles_norm,
35
+ clip_bar,
36
+ has_explicit_position,
37
+ measured_footprint,
38
+ place_decorations,
39
+ } from '../core/auto-place'
40
+ import { compute_box_stats } from './box-plot'
41
+ import { gaussian_kde, type KdeResult } from './kde'
42
+ import { create_dimension_tracker, create_hover_lock } from '../core/hover-lock.svelte'
43
+ import { create_legend_visibility } from '../core/utils/series-visibility'
44
+ import {
45
+ 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,
51
+ resolve_axis_ranges,
52
+ sorted_range,
53
+ zoom_range_by_factor,
54
+ } from '../core/interactions'
55
+ import {
56
+ calc_auto_padding,
57
+ filter_padding,
58
+ LABEL_GAP_DEFAULT,
59
+ y2_axis_label_x,
60
+ measure_max_tick_width,
61
+ } from '../core/layout'
62
+ import { LOG_EPS } from '../../math'
63
+ import type { IndexedRefLine } from '../core/reference-line'
64
+ import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
65
+ import {
66
+ create_scale,
67
+ generate_ticks,
68
+ get_nice_data_range,
69
+ get_tick_label,
70
+ } from '../core/scales'
71
+ import type { InitialRanges } from '../core/types'
72
+ import { DEFAULT_SERIES_COLORS } from '../core/types'
73
+ import { unique_id } from '../core/utils'
74
+ import { DEFAULTS } from '../../settings'
75
+ import type { Snippet } from 'svelte'
76
+ import { onDestroy, untrack } from 'svelte'
77
+ import type { HTMLAttributes } from 'svelte/elements'
78
+ import { Tween, type TweenOptions } from 'svelte/motion'
79
+ import { SvelteMap } from 'svelte/reactivity'
80
+ import PlotTooltip from '../core/components/PlotTooltip.svelte'
81
+ import { violin_path } from '../core/svg'
82
+ import ZeroLines from '../core/components/ZeroLines.svelte'
83
+ import ZoomRect from '../core/components/ZoomRect.svelte'
84
+
85
+ // Box style props
86
+ interface BoxStyle {
87
+ color?: string
88
+ opacity?: number
89
+ stroke_width?: number
90
+ stroke_color?: string
91
+ border_radius?: number
92
+ }
93
+ interface WhiskerStyle {
94
+ width?: number
95
+ color?: string
96
+ cap_fraction?: number
97
+ }
98
+ interface BoxLineStyle {
99
+ width?: number
100
+ color?: string
101
+ }
102
+ interface OutlierStyle {
103
+ radius?: number
104
+ opacity?: number
105
+ stroke_width?: number
106
+ }
107
+ interface ViolinStyle {
108
+ opacity?: number
109
+ stroke_width?: number
110
+ }
111
+
112
+ // Hover state carries the box payload plus the pixel anchor for the tooltip
113
+ type BoxHover = BoxHandlerProps<Metadata> & { cx: number; cy: number }
114
+
115
+ let {
116
+ series = $bindable([]),
117
+ orientation = $bindable(`vertical`),
118
+ x_axis = $bindable({}),
119
+ x2_axis: x2_axis_prop = $bindable({}),
120
+ y_axis = $bindable({}),
121
+ y2_axis: y2_axis_prop = $bindable({}),
122
+ display = $bindable(DEFAULTS.box.display),
123
+ range_padding = 0.05,
124
+ padding = { t: 20, b: 60, l: 60, r: 20 },
125
+ legend = {},
126
+ show_legend,
127
+ box = {},
128
+ whisker = {},
129
+ median_style = {},
130
+ outlier_style = {},
131
+ whisker_mode = $bindable(DEFAULTS.box.whisker_mode),
132
+ whisker_range = 1.5,
133
+ whisker_percentiles = [5, 95],
134
+ show_outliers = $bindable(DEFAULTS.box.show_outliers),
135
+ show_mean = $bindable(DEFAULTS.box.show_mean),
136
+ show_value_labels = false,
137
+ value_label_stat = `median`,
138
+ value_label_format = `.3~s`,
139
+ kind = $bindable(DEFAULTS.box.kind),
140
+ side = $bindable(DEFAULTS.box.side),
141
+ bandwidth = DEFAULTS.box.bandwidth,
142
+ violin_width = DEFAULTS.box.violin_width,
143
+ violin_style = {},
144
+ kde_points = 100,
145
+ kde_cut = 2,
146
+ kde_max_samples = 5000,
147
+ kde_clip = undefined,
148
+ tooltip,
149
+ user_content,
150
+ hovered = $bindable(false),
151
+ change = () => {},
152
+ on_box_click,
153
+ on_box_hover,
154
+ ref_lines = $bindable([]),
155
+ on_ref_line_click,
156
+ on_ref_line_hover,
157
+ show_controls = $bindable(true),
158
+ controls_open = $bindable(false),
159
+ controls_toggle_props,
160
+ controls_pane_props,
161
+ fullscreen = $bindable(false),
162
+ fullscreen_toggle = true,
163
+ children,
164
+ header_controls,
165
+ controls_extra,
166
+ pan = {},
167
+ ...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()
212
+
213
+ let box_state = $derived({ ...DEFAULTS.box.box, ...box })
214
+ let whisker_state = $derived({ ...DEFAULTS.box.whisker, ...whisker })
215
+ let median_state = $derived({ ...DEFAULTS.box.median, ...median_style })
216
+ let outlier_state = $derived({ ...DEFAULTS.box.outlier, ...outlier_style })
217
+ let violin_state = $derived({ ...DEFAULTS.box.violin, ...violin_style })
218
+
219
+ // Merge secondary-axis defaults as deriveds instead of assigning back into the
220
+ // $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
+ )
239
+
240
+ let [width, height] = $state([0, 0])
241
+ let wrapper: HTMLDivElement | undefined = $state()
242
+ let svg_element: SVGElement | null = $state(null)
243
+ const clip_path_id = unique_id(`box-clip`) // stable, collision-resistant (see unique_id)
244
+
245
+ let hovered_ref_line_idx = $state<number | null>(null)
246
+
247
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
248
+
249
+ // === Box stats + slot model ===
250
+ const box_color = (idx: number): string =>
251
+ series[idx]?.color ?? DEFAULT_SERIES_COLORS[idx % DEFAULT_SERIES_COLORS.length]
252
+
253
+ // Which glyph(s) a series draws (per-series kind overrides the component default)
254
+ const effective_kind = (srs: BoxPlotSeries<Metadata>): ViolinKind => srs.kind ?? kind
255
+ const draws_violin = (srs: BoxPlotSeries<Metadata>): boolean => effective_kind(srs) !== `box`
256
+ const draws_box = (srs: BoxPlotSeries<Metadata>): boolean => effective_kind(srs) !== `violin`
257
+
258
+ let box_stats = $derived(
259
+ series.map((srs) =>
260
+ compute_box_stats(srs.y ?? [], {
261
+ whisker_mode: srs.whisker_mode ?? whisker_mode,
262
+ whisker_range: srs.whisker_range ?? whisker_range,
263
+ whisker_percentiles: srs.whisker_percentiles ?? whisker_percentiles,
264
+ collect_outliers: show_outliers && draws_box(srs) && (srs.visible ?? true),
265
+ })
266
+ ),
267
+ )
268
+
269
+ // Slots position boxes/violins along the category axis. Series sharing a `category` occupy
270
+ // one slot (split/grouped violins). Without `category`, each series gets its own slot —
271
+ // byte-identical to the original one-box-per-series behavior. Override tick labels via
272
+ // x_axis.ticks (a Record).
273
+ let use_categories = $derived(series.some((srs) => srs.category != null))
274
+ const slot_key = (srs: BoxPlotSeries<Metadata>, idx: number): string =>
275
+ srs.category ?? `${idx}`
276
+ let slot_list = $derived(
277
+ use_categories
278
+ ? [...new Set(series.map(slot_key))]
279
+ : series.map((srs, idx) => srs.label ?? `${idx}`),
280
+ )
281
+ let slot_lookup = $derived(new Map(slot_list.map((slot, idx) => [slot, idx])))
282
+ const slot_of = (idx: number): number =>
283
+ use_categories ? (slot_lookup.get(slot_key(series[idx], idx)) ?? idx) : idx
284
+ let slot_indices = $derived(slot_list.map((_, idx) => idx))
285
+ // A slot's tick label is colored only when a single series occupies it. Precompute
286
+ // slot -> color in one pass so the PlotAxis tick_color callback stays O(1) per tick.
287
+ let slot_colors = $derived.by(() => {
288
+ const by_slot = new SvelteMap<number, number[]>()
289
+ series.forEach((_srs, idx) => {
290
+ const slot = slot_of(idx)
291
+ const idxs = by_slot.get(slot)
292
+ if (idxs) idxs.push(idx)
293
+ else by_slot.set(slot, [idx])
294
+ })
295
+ const colors = new SvelteMap<number, string | undefined>()
296
+ for (const [slot, idxs] of by_slot) {
297
+ colors.set(slot, idxs.length === 1 ? box_color(idxs[0]) : undefined)
298
+ }
299
+ return colors
300
+ })
301
+ let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
302
+
303
+ type Box = {
304
+ series: BoxPlotSeries<Metadata>
305
+ idx: number
306
+ slot: number
307
+ stats: (typeof box_stats)[number]
308
+ }
309
+ let visible_boxes = $derived<Box[]>(
310
+ series
311
+ .map((srs, idx) => ({ series: srs, idx, slot: slot_of(idx), stats: box_stats[idx] }))
312
+ .filter((box_item) => box_item.series.visible ?? true),
313
+ )
314
+
315
+ // KDE per visible violin series, keyed by series index (bandwidth from the full sample)
316
+ let violin_kdes = $derived.by(() => {
317
+ const map = new SvelteMap<number, KdeResult>()
318
+ const [val_axis, val_axis2] = orientation === `vertical`
319
+ ? [y_axis, y2_axis]
320
+ : [x_axis, x2_axis]
321
+ for (const box_item of visible_boxes) {
322
+ if (!draws_violin(box_item.series)) continue
323
+ const samples = box_item.series.y ?? []
324
+ let clip = box_item.series.clip ?? kde_clip
325
+ // On a log value axis the KDE grid tail (data_min - cut*bandwidth) is usually <= 0 →
326
+ // NaN pixels + LOG_EPS range pollution. Clamp the grid to the smallest positive sample.
327
+ 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)
329
+ // Guard: no positive samples → min_pos is Infinity; leave clip unchanged so the KDE
330
+ // never receives a non-finite lower bound
331
+ if (Number.isFinite(min_pos)) {
332
+ clip = [Math.max(clip?.[0] ?? -Infinity, min_pos), clip?.[1] ?? null]
333
+ }
334
+ }
335
+ map.set(
336
+ box_item.idx,
337
+ gaussian_kde(samples, {
338
+ bandwidth: box_item.series.bandwidth ?? bandwidth,
339
+ n_points: kde_points,
340
+ cut: kde_cut,
341
+ clip,
342
+ max_samples: kde_max_samples,
343
+ }),
344
+ )
345
+ }
346
+ return map
347
+ })
348
+
349
+ // The horizontal category pixel axis is inverted, so flip the half-violin side to keep
350
+ // `positive` meaning "above the center line" (vertical/`both` pass through unchanged)
351
+ const to_screen_side = (eff_side: ViolinSide, vertical: boolean): ViolinSide =>
352
+ vertical || eff_side === `both` ? eff_side : eff_side === `positive` ? `negative` : `positive`
353
+
354
+ // Peak density per violin, computed once on data change (avoids spreading kde.density into
355
+ // Math.max — unsafe for large kde_points — and re-deriving it on every render/hover).
356
+ let violin_max_density = $derived.by(() => {
357
+ const map = new SvelteMap<number, number>()
358
+ for (const [idx, kde] of violin_kdes) {
359
+ let max = 0
360
+ for (const den of kde.density) if (den > max) max = den
361
+ map.set(idx, max)
362
+ }
363
+ return map
364
+ })
365
+
366
+ // Which boxes live on the secondary value axis (y2 for vertical, x2 for horizontal)
367
+ const is_secondary = (srs: BoxPlotSeries<Metadata>): boolean =>
368
+ 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)))
370
+ let has_secondary = $derived(secondary_boxes.length > 0)
371
+
372
+ // Collect value-axis points (whiskers, quartiles, outliers, KDE tails) for auto-range
373
+ const value_points = (boxes: Box[]): { x: number; y: number }[] =>
374
+ boxes.flatMap((box_item) => {
375
+ const { whisker_low, whisker_high, q1, q3, median, mean, outliers } = box_item.stats
376
+ const vals = [whisker_low, whisker_high, q1, q3, median]
377
+ // keep the drawn mean line in range even when hidden outliers drag it past the whiskers
378
+ if (show_mean) vals.push(mean)
379
+ // outliers are sorted ascending; auto-range only needs their extremes (avoids
380
+ // spreading a potentially huge array as call args)
381
+ if (show_outliers && outliers.length > 0) {
382
+ vals.push(outliers[0], outliers[outliers.length - 1])
383
+ }
384
+ const kde = violin_kdes.get(box_item.idx)
385
+ if (kde && kde.grid.length > 0) vals.push(kde.grid[0], kde.grid[kde.grid.length - 1])
386
+ return vals.filter(Number.isFinite).map((val) => ({ x: 0, y: val }))
387
+ })
388
+
389
+ let auto_ranges = $derived.by(() => {
390
+ const cat_count = slot_list.length
391
+ const cat_range: Vec2 = cat_count > 0 ? [-0.5, cat_count - 0.5] : [0, 1]
392
+
393
+ const primary_boxes = visible_boxes.filter((box_item) => !is_secondary(box_item.series))
394
+ const calc_value_range = (
395
+ boxes: Box[],
396
+ limit: [number | null, number | null],
397
+ scale_type: ScaleType,
398
+ ): Vec2 => {
399
+ const pts = value_points(boxes)
400
+ if (pts.length === 0) return [0, 1]
401
+ return get_nice_data_range(pts, (pt) => pt.y, limit, scale_type, range_padding, false)
402
+ }
403
+ const vertical = orientation === `vertical`
404
+ const value_primary = calc_value_range(
405
+ primary_boxes,
406
+ (vertical ? y_axis.range : x_axis.range) ?? [null, null],
407
+ (vertical ? y_axis.scale_type : x_axis.scale_type) ?? `linear`,
408
+ )
409
+ const value_secondary = calc_value_range(
410
+ secondary_boxes,
411
+ (vertical ? y2_axis.range : x2_axis.range) ?? [null, null],
412
+ (vertical ? y2_axis.scale_type : x2_axis.scale_type) ?? `linear`,
413
+ )
414
+
415
+ 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 })
418
+ })
419
+
420
+ let ranges = $state<{
421
+ initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
422
+ current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
423
+ }>({
424
+ initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
425
+ current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
426
+ })
427
+
428
+ $effect(() => { // sync ranges from axis.range overrides / auto ranges
429
+ // resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
430
+ // 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)
432
+ if (!next) return
433
+ // untrack the read of `ranges` so the assignment can't re-trigger this effect
434
+ // (reading + writing the same state otherwise causes effect_update_depth_exceeded).
435
+ const init = untrack(() => ranges.initial)
436
+ if (!axis_ranges_equal(init, next)) {
437
+ ranges = { initial: { ...next }, current: { ...next } }
438
+ }
439
+ })
440
+
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
+ ) {
457
+ const inside = y2_axis.tick?.label?.inside ?? false
458
+ const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
459
+ const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
460
+ const label_space = y2_axis.label ? 20 : 0
461
+ new_pad.r = Math.max(new_pad.r, tick_shift + tick_width_contribution + 30 + label_space)
462
+ }
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
465
+ })
466
+
467
+ let legend_element = $state<HTMLDivElement | undefined>()
468
+ const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 60 }))
469
+ const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
470
+
471
+ // Obstacle field in normalized [0,1] coords: each box modeled as a whisker-spanning segment
472
+ const obstacles_norm = $derived.by(() => {
473
+ if (!width || !height || visible_boxes.length === 0) return []
474
+ const base_w = width - base_pad.l - base_pad.r
475
+ const base_h = height - base_pad.t - base_pad.b
476
+ if (base_w <= 0 || base_h <= 0) return []
477
+ const vertical = orientation === `vertical`
478
+ const segs: { points: { x: number; y: number }[]; draws_line: boolean }[] = []
479
+ for (const box_item of visible_boxes) {
480
+ const { whisker_low, whisker_high, median } = box_item.stats
481
+ if (!Number.isFinite(median)) continue
482
+ const secondary = is_secondary(box_item.series)
483
+ const cat_rng = vertical ? ranges.current.x : ranges.current.y
484
+ const val_rng = vertical
485
+ ? (secondary ? ranges.current.y2 : ranges.current.y)
486
+ : (secondary ? ranges.current.x2 : ranges.current.x)
487
+ const cat_span = cat_rng[1] - cat_rng[0]
488
+ const val_span = val_rng[1] - val_rng[0]
489
+ if (cat_span === 0 || val_span === 0) continue
490
+ const cross = (box_item.slot - cat_rng[0]) / cat_span
491
+ const lo = (whisker_low - val_rng[0]) / val_span
492
+ const hi = (whisker_high - val_rng[0]) / val_span
493
+ const seg = vertical
494
+ ? clip_bar(true, cross, 1 - hi, 1 - lo)
495
+ : clip_bar(false, 1 - cross, lo, hi)
496
+ if (seg) segs.push(seg)
497
+ }
498
+ return build_obstacles_norm(segs, base_w, base_h)
499
+ })
500
+
501
+ const should_show_legend = $derived(show_legend ?? false)
502
+ const decor = $derived.by(() =>
503
+ place_decorations({
504
+ base_pad,
505
+ width,
506
+ height,
507
+ 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
+ })
513
+ )
514
+ const pad = $derived(decor.pad)
515
+ const legend_auto_outside = $derived(decor.legend_outside)
516
+ const legend_outside_x = $derived(decor.legend_pos.x)
517
+ const legend_outside_y = $derived(decor.legend_pos.y)
518
+ const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
519
+ const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
520
+
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
+ })
527
+
528
+ // Value scale for a box (vertical -> y/y2, horizontal -> x/x2), made log-safe: on a
529
+ // log value axis, stats at values <= 0 (whisker_low is often exactly 0; negative
530
+ // outliers) have no finite pixel. Clamp to LOG_EPS so whiskers/boxes/labels draw
531
+ // 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 => {
533
+ const vertical = orientation === `vertical`
534
+ const secondary = is_secondary(srs)
535
+ 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)
539
+ return axis.scale_type === `log` ? (val) => scale(Math.max(val, LOG_EPS)) : scale
540
+ }
541
+
542
+ // Categorical tick labels (slot index -> category name) unless user provides a label mapping
543
+ let effective_cat_ticks = $derived.by(() => {
544
+ if (slot_list.length === 0) return undefined
545
+ const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
546
+ if (user_ticks != null && typeof user_ticks === `object` && !Array.isArray(user_ticks)) {
547
+ return user_ticks
548
+ }
549
+ return Object.fromEntries(slot_list.map((cat, idx) => [idx, cat]))
550
+ })
551
+
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
+ : [],
581
+ })
582
+
583
+ let tick_label_widths = $derived({
584
+ y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
585
+ y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
586
+ x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
587
+ })
588
+
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
+ }
639
+ }
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
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)
761
+
762
+ // === Legend ===
763
+ let legend_data = $derived<LegendItem[]>(
764
+ series.map((srs, idx) => ({
765
+ series_idx: idx,
766
+ label: srs.label ?? `Box ${idx + 1}`,
767
+ visible: srs.visible ?? true,
768
+ legend_group: srs.legend_group,
769
+ display_style: { symbol_type: `Square` as const, symbol_color: box_color(idx) },
770
+ })),
771
+ )
772
+
773
+ const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
774
+
775
+ let box_points_for_placement = $derived.by(() => {
776
+ if (!width || !height || visible_boxes.length === 0) return []
777
+ const vertical = orientation === `vertical`
778
+ return visible_boxes
779
+ .map((box_item) => {
780
+ const val_scale = box_val_scale(box_item.series)
781
+ const cat_scale = vertical ? scales.x : scales.y
782
+ const cc = cat_scale(box_item.slot)
783
+ const vc = val_scale(box_item.stats.median)
784
+ return vertical ? { x: cc, y: vc } : { x: vc, y: cc }
785
+ })
786
+ .filter(({ x, y }) => isFinite(x) && isFinite(y))
787
+ })
788
+
789
+ 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
+
795
+ let legend_placement = $derived.by(() => {
796
+ if (!should_show_legend || !width || !height) return null
797
+ return compute_element_placement({
798
+ plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
799
+ element: legend_element,
800
+ element_size: { width: 120, height: 60 },
801
+ axis_clearance: legend?.axis_clearance,
802
+ exclude_rects: [],
803
+ points: box_points_for_placement,
804
+ })
805
+ })
806
+
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
+ }
825
+ })
826
+
827
+ // === Tooltip / hover ===
828
+ let hover_info = $state<BoxHover | null>(null)
829
+
830
+ function get_box_data(box_item: Box, color: string): BoxHover {
831
+ const vertical = orientation === `vertical`
832
+ const val_scale = box_val_scale(box_item.series)
833
+ const cat_scale = vertical ? scales.x : scales.y
834
+ const cc = cat_scale(box_item.slot)
835
+ const v_hi = val_scale(box_item.stats.whisker_high)
836
+ const v_lo = val_scale(box_item.stats.whisker_low)
837
+ const [cx, cy] = vertical ? [cc, Math.min(v_hi, v_lo)] : [Math.max(v_hi, v_lo), cc]
838
+ const active_y_axis = (vertical ? (box_item.series.y_axis ?? `y1`) : `y1`) as `y1` | `y2`
839
+ const active_x_axis = (vertical ? `x1` : (box_item.series.x_axis ?? `x1`)) as `x1` | `x2`
840
+ return {
841
+ x: vertical ? box_item.slot : box_item.stats.median,
842
+ y: vertical ? box_item.stats.median : box_item.slot,
843
+ stats: box_item.stats,
844
+ color,
845
+ label: box_item.series.label ?? null,
846
+ category_label: slot_list[box_item.slot],
847
+ metadata: box_item.series.metadata,
848
+ series_idx: box_item.idx,
849
+ box_idx: box_item.idx,
850
+ active_x_axis,
851
+ active_y_axis,
852
+ x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
853
+ x2_axis,
854
+ y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
855
+ y2_axis,
856
+ cx,
857
+ cy,
858
+ }
859
+ }
860
+
861
+ const handle_box_hover = (box_item: Box, color: string) => (event: MouseEvent) => {
862
+ hovered = true
863
+ const data = get_box_data(box_item, color)
864
+ // Anchor the tooltip at the cursor (cx/cy default to the box center) so it follows the
865
+ // mouse — boxes/violins are wide, and a center anchor lands far from the pointer.
866
+ const rect = svg_element?.getBoundingClientRect()
867
+ if (rect) {
868
+ data.cx = event.clientX - rect.left
869
+ data.cy = event.clientY - rect.top
870
+ }
871
+ hover_info = data
872
+ change(hover_info)
873
+ on_box_hover?.({ ...hover_info, event })
874
+ }
875
+
876
+ // Set theme-aware background when entering fullscreen
877
+ $effect(() => set_fullscreen_bg(wrapper, fullscreen, `--boxplot-fullscreen-bg`))
878
+
879
+ // Value label helper
880
+ const value_label_for = (stats: Box[`stats`]): string =>
881
+ format_value(value_label_stat === `mean` ? stats.mean : stats.median, value_label_format)
882
+ </script>
883
+
884
+ {#snippet seg(
885
+ p1: [number, number],
886
+ p2: [number, number],
887
+ stroke: string,
888
+ sw: number,
889
+ dash?: string,
890
+ )}
891
+ <line
892
+ x1={p1[0]}
893
+ y1={p1[1]}
894
+ x2={p2[0]}
895
+ y2={p2[1]}
896
+ {stroke}
897
+ stroke-width={sw}
898
+ stroke-dasharray={dash}
899
+ />
900
+ {/snippet}
901
+
902
+ {#snippet ref_lines_layer(lines: IndexedRefLine[])}
903
+ {#each lines as line (line.id ?? line.idx)}
904
+ <ReferenceLine
905
+ ref_line={line}
906
+ line_idx={line.idx}
907
+ x_min={line.x_axis === `x2` ? ranges.current.x2[0] : ranges.current.x[0]}
908
+ x_max={line.x_axis === `x2` ? ranges.current.x2[1] : ranges.current.x[1]}
909
+ y_min={line.y_axis === `y2` ? ranges.current.y2[0] : ranges.current.y[0]}
910
+ y_max={line.y_axis === `y2` ? ranges.current.y2[1] : ranges.current.y[1]}
911
+ x_scale={scales.x}
912
+ x2_scale={scales.x2}
913
+ y_scale={scales.y}
914
+ y2_scale={scales.y2}
915
+ {clip_path_id}
916
+ hovered_line_idx={hovered_ref_line_idx}
917
+ on_click={(event: RefLineEvent) => {
918
+ line.on_click?.(event)
919
+ on_ref_line_click?.(event)
920
+ }}
921
+ on_hover={(event: RefLineEvent | null) => {
922
+ hovered_ref_line_idx = event?.line_idx ?? null
923
+ line.on_hover?.(event)
924
+ on_ref_line_hover?.(event)
925
+ }}
926
+ />
927
+ {/each}
928
+ {/snippet}
929
+
930
+ <svelte:window
931
+ onkeydown={(evt) => {
932
+ if (evt.key === `Escape` && fullscreen) {
933
+ evt.preventDefault()
934
+ fullscreen = false
935
+ }
936
+ if (evt.key === `Shift`) shift_held = true
937
+ }}
938
+ onkeyup={(evt) => {
939
+ if (evt.key === `Shift`) shift_held = false
940
+ }}
941
+ />
942
+
943
+ <div
944
+ bind:this={wrapper}
945
+ bind:clientWidth={width}
946
+ bind:clientHeight={height}
947
+ {...rest}
948
+ class="box-plot {rest.class ?? ``}"
949
+ class:fullscreen
950
+ >
951
+ {#if width && height}
952
+ <div class="header-controls">
953
+ {@render header_controls?.({ height, width, fullscreen })}
954
+ {#if fullscreen_toggle}
955
+ <FullscreenToggle bind:fullscreen />
956
+ {/if}
957
+ </div>
958
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
959
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
960
+ <svg
961
+ bind:this={svg_element}
962
+ role="application"
963
+ aria-label={rest[`aria-label`] ??
964
+ ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Box plot`)}
965
+ 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
+ }}
979
+ onmouseleave={() => {
980
+ hovered = false
981
+ hover_info = null
982
+ change(null)
983
+ on_box_hover?.(null)
984
+ }}
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`}
995
+ >
996
+ <ZoomRect start={drag_state.start} current={drag_state.current} />
997
+
998
+ {@render user_content?.({
999
+ height,
1000
+ width,
1001
+ x_scale_fn: scales.x,
1002
+ x2_scale_fn: scales.x2,
1003
+ y_scale_fn: scales.y,
1004
+ y2_scale_fn: scales.y2,
1005
+ pad,
1006
+ x_range: ranges.current.x,
1007
+ x2_range: ranges.current.x2,
1008
+ y_range: ranges.current.y,
1009
+ y2_range: ranges.current.y2,
1010
+ fullscreen,
1011
+ })}
1012
+
1013
+ {@render ref_lines_layer(ref_lines_by_z.below_grid)}
1014
+
1015
+ <PlotAxis
1016
+ side="x"
1017
+ ticks={ticks.x as number[]}
1018
+ place={scales.x}
1019
+ axis={x_axis}
1020
+ domain={ranges.current.x as Vec2}
1021
+ {pad}
1022
+ {width}
1023
+ {height}
1024
+ show_grid={display.x_grid}
1025
+ tick_label={(tick) =>
1026
+ get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
1027
+ tick_color={cat_axis === `x` ? (tick) => slot_colors.get(tick) : undefined}
1028
+ 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)}
1030
+ />
1031
+
1032
+ {#if has_secondary && orientation === `horizontal`}
1033
+ <PlotAxis
1034
+ side="x2"
1035
+ ticks={ticks.x2 as number[]}
1036
+ place={scales.x2}
1037
+ axis={x2_axis}
1038
+ domain={ranges.current.x2 as Vec2}
1039
+ {pad}
1040
+ {width}
1041
+ {height}
1042
+ show_grid={display.x2_grid}
1043
+ tick_label={(tick) => get_tick_label(tick, x2_axis.ticks)}
1044
+ 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))}
1046
+ />
1047
+ {/if}
1048
+
1049
+ <PlotAxis
1050
+ side="y"
1051
+ ticks={ticks.y as number[]}
1052
+ place={scales.y}
1053
+ axis={y_axis}
1054
+ domain={ranges.current.y as Vec2}
1055
+ {pad}
1056
+ {width}
1057
+ {height}
1058
+ show_grid={display.y_grid}
1059
+ tick_label={(tick) =>
1060
+ get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
1061
+ tick_color={cat_axis === `y` ? (tick) => slot_colors.get(tick) : undefined}
1062
+ label_x={Math.max(
1063
+ 12,
1064
+ pad.l - (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
1065
+ ) + (y_axis.label_shift?.x ?? 0)}
1066
+ label_y={pad.t + chart_height / 2 + (y_axis.label_shift?.y ?? 0)}
1067
+ />
1068
+
1069
+ {#if has_secondary && orientation === `vertical`}
1070
+ <PlotAxis
1071
+ side="y2"
1072
+ ticks={ticks.y2 as number[]}
1073
+ place={scales.y2}
1074
+ axis={y2_axis}
1075
+ domain={ranges.current.y2 as Vec2}
1076
+ {pad}
1077
+ {width}
1078
+ {height}
1079
+ show_grid={display.y2_grid}
1080
+ tick_label={(tick) => get_tick_label(tick, y2_axis.ticks)}
1081
+ label_x={y2_axis_label_x(y2_axis, width, pad.r, tick_label_widths.y2_max)}
1082
+ label_y={pad.t + chart_height / 2 + (y2_axis.label_shift?.y ?? 0)}
1083
+ />
1084
+ {/if}
1085
+
1086
+ <defs>
1087
+ <clipPath id={clip_path_id}>
1088
+ <rect x={pad.l} y={pad.t} width={chart_width} height={chart_height} />
1089
+ </clipPath>
1090
+ </defs>
1091
+
1092
+ <!-- Chart content is clipped in two groups so reference lines can interleave
1093
+ at their z positions while staying outside the chart clip: each line still
1094
+ self-clips to the plot area inside ReferenceLine, only its annotation text
1095
+ is allowed to overflow the plot edges. -->
1096
+ <g clip-path="url(#{clip_path_id})">
1097
+ <ZeroLines
1098
+ {display}
1099
+ x_scale_fn={scales.x}
1100
+ x2_scale_fn={scales.x2}
1101
+ y_scale_fn={scales.y}
1102
+ y2_scale_fn={scales.y2}
1103
+ x_range={ranges.current.x}
1104
+ x2_range={ranges.current.x2}
1105
+ y_range={ranges.current.y}
1106
+ y2_range={ranges.current.y2}
1107
+ x_scale_type={x_axis.scale_type}
1108
+ x2_scale_type={x2_axis.scale_type}
1109
+ y_scale_type={y_axis.scale_type}
1110
+ y2_scale_type={y2_axis.scale_type}
1111
+ has_x2={has_secondary && orientation === `horizontal`}
1112
+ has_y2={has_secondary && orientation === `vertical`}
1113
+ {width}
1114
+ {height}
1115
+ {pad}
1116
+ />
1117
+ </g>
1118
+
1119
+ {@render ref_lines_layer(ref_lines_by_z.below_lines)}
1120
+
1121
+ <!-- Boxes -->
1122
+ <g clip-path="url(#{clip_path_id})">
1123
+ {#each visible_boxes as box_item (box_item.series.id ?? box_item.idx)}
1124
+ {@const stats = box_item.stats}
1125
+ {#if Number.isFinite(stats.median)}
1126
+ {@const vertical = orientation === `vertical`}
1127
+ {@const cat_scale = vertical ? scales.x : scales.y}
1128
+ {@const val_scale = box_val_scale(box_item.series)}
1129
+ {@const color = box_color(box_item.idx)}
1130
+ {@const draw_box = draws_box(box_item.series)}
1131
+ {@const kde = violin_kdes.get(box_item.idx)}
1132
+ {@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)}
1135
+ {@const c_lo = cat_scale(box_item.slot - bw / 2)}
1136
+ {@const c_hi = cat_scale(box_item.slot + bw / 2)}
1137
+ {@const c_center = cat_scale(box_item.slot)}
1138
+ {@const cap = Math.abs(c_hi - c_lo) * (whisker_state.cap_fraction ?? 0.5) / 2}
1139
+ {@const cap_lo = c_center - cap}
1140
+ {@const cap_hi = c_center + cap}
1141
+ {@const v_q1 = val_scale(stats.q1)}
1142
+ {@const v_q3 = val_scale(stats.q3)}
1143
+ {@const v_med = val_scale(stats.median)}
1144
+ {@const v_wl = val_scale(stats.whisker_low)}
1145
+ {@const v_wh = val_scale(stats.whisker_high)}
1146
+ {@const v_mean = val_scale(stats.mean)}
1147
+ {@const pt = (cross: number, val: number): [number, number] =>
1148
+ vertical ? [cross, val] : [val, cross]}
1149
+ {@const [q1x, q1y] = pt(c_lo, v_q1)}
1150
+ {@const [q3x, q3y] = pt(c_hi, v_q3)}
1151
+ {@const [wlx, wly] = pt(c_lo, v_wl)}
1152
+ {@const [whx, why] = pt(c_hi, v_wh)}
1153
+ {@const box_x = Math.min(q1x, q3x)}
1154
+ {@const box_y = Math.min(q1y, q3y)}
1155
+ {@const box_w = Math.abs(q3x - q1x)}
1156
+ {@const box_h = Math.abs(q3y - q1y)}
1157
+ {@const hit_x = Math.min(wlx, whx)}
1158
+ {@const hit_y = Math.min(wly, why)}
1159
+ {@const hit_w = Math.abs(whx - wlx)}
1160
+ {@const hit_h = Math.abs(why - wly)}
1161
+ {@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]}
1164
+ {@const violin_half = Math.abs(
1165
+ cat_scale(box_item.slot + (box_item.series.violin_width ?? violin_width) / 2) -
1166
+ c_center,
1167
+ )}
1168
+ {@const max_density = kde ? (violin_max_density.get(box_item.idx) ?? 0) : 0}
1169
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
1170
+ <g
1171
+ class="box-series"
1172
+ data-box-idx={box_item.idx}
1173
+ role="button"
1174
+ tabindex="0"
1175
+ aria-label={`box ${box_item.idx + 1}: ${box_item.series.label ?? ``}`}
1176
+ style:cursor={on_box_click ? `pointer` : undefined}
1177
+ opacity={hovered_legend_series_idx !== null &&
1178
+ hovered_legend_series_idx !== box_item.idx
1179
+ ? 0.25
1180
+ : 1}
1181
+ onmousemove={handle_box_hover(box_item, color)}
1182
+ onmouseleave={() => {
1183
+ hover_info = null
1184
+ change(null)
1185
+ on_box_hover?.(null)
1186
+ }}
1187
+ onclick={(evt) => on_box_click?.({ ...get_box_data(box_item, color), event: evt })}
1188
+ onkeydown={(evt) => {
1189
+ if (evt.key === `Enter` || evt.key === ` `) {
1190
+ evt.preventDefault()
1191
+ on_box_click?.({ ...get_box_data(box_item, color), event: evt })
1192
+ }
1193
+ }}
1194
+ >
1195
+ <!-- violin (KDE density) -->
1196
+ {#if kde && max_density > 0}
1197
+ {@const grid_px = kde.grid.map((g_val) => val_scale(g_val))}
1198
+ {@const offsets = kde.density.map((den) => (den / max_density) * violin_half)}
1199
+ {@const screen_side = to_screen_side(eff_side, vertical)}
1200
+ <path
1201
+ class="violin-area"
1202
+ d={violin_path(grid_px, offsets, c_center, screen_side, pt)}
1203
+ fill={color}
1204
+ fill-opacity={violin_state.opacity}
1205
+ stroke={color}
1206
+ stroke-width={violin_state.stroke_width}
1207
+ />
1208
+ {/if}
1209
+ {#if draw_box}
1210
+ {@const wc = whisker_state.color}
1211
+ {@const ww = whisker_state.width}
1212
+ <!-- whiskers + caps -->
1213
+ {@render seg(pt(c_center, v_q1), pt(c_center, v_wl), wc, ww)}
1214
+ {@render seg(pt(c_center, v_q3), pt(c_center, v_wh), wc, ww)}
1215
+ {#if cap > 0}
1216
+ {@render seg(pt(cap_lo, v_wl), pt(cap_hi, v_wl), wc, ww)}
1217
+ {@render seg(pt(cap_lo, v_wh), pt(cap_hi, v_wh), wc, ww)}
1218
+ {/if}
1219
+ <!-- IQR box -->
1220
+ <rect
1221
+ class="iqr-box"
1222
+ x={box_x}
1223
+ y={box_y}
1224
+ width={Math.max(1, box_w)}
1225
+ height={Math.max(1, box_h)}
1226
+ rx={box_state.border_radius}
1227
+ ry={box_state.border_radius}
1228
+ fill={color}
1229
+ fill-opacity={box_state.opacity}
1230
+ stroke={box_state.stroke_color}
1231
+ stroke-width={box_state.stroke_width}
1232
+ />
1233
+ <!-- median (solid) and mean (dashed) -->
1234
+ {@render seg(pt(c_lo, v_med), pt(c_hi, v_med), median_state.color, median_state.width)}
1235
+ {#if show_mean}
1236
+ {@render seg(
1237
+ pt(c_lo, v_mean),
1238
+ pt(c_hi, v_mean),
1239
+ median_state.color,
1240
+ median_state.width,
1241
+ `3 2`,
1242
+ )}
1243
+ {/if}
1244
+ <!-- outliers -->
1245
+ {#if show_outliers}
1246
+ {#each stats.outliers as outlier, out_idx (out_idx)}
1247
+ {@const [ox, oy] = pt(c_center, val_scale(outlier))}
1248
+ <circle
1249
+ cx={ox}
1250
+ cy={oy}
1251
+ r={outlier_state.radius}
1252
+ fill={color}
1253
+ fill-opacity={outlier_state.opacity}
1254
+ stroke={box_state.stroke_color}
1255
+ stroke-width={outlier_state.stroke_width}
1256
+ />
1257
+ {/each}
1258
+ {/if}
1259
+ {/if}
1260
+ <!-- value label -->
1261
+ {#if show_value_labels}
1262
+ <text
1263
+ x={label_x}
1264
+ y={label_y}
1265
+ text-anchor={vertical ? `middle` : `start`}
1266
+ dominant-baseline={vertical ? `auto` : `central`}
1267
+ class="value-label"
1268
+ fill={color}
1269
+ >
1270
+ {value_label_for(stats)}
1271
+ </text>
1272
+ {/if}
1273
+ <!-- transparent backing so the box/whisker region is hoverable (the violin
1274
+ path is a painted child and bubbles to the group's pointer handlers too) -->
1275
+ <rect
1276
+ class="hover-target"
1277
+ x={hit_x}
1278
+ y={hit_y}
1279
+ width={Math.max(1, hit_w)}
1280
+ height={Math.max(1, hit_h)}
1281
+ fill="transparent"
1282
+ />
1283
+ </g>
1284
+ {/if}
1285
+ {/each}
1286
+ </g>
1287
+
1288
+ {@render ref_lines_layer(ref_lines_by_z.below_points)}
1289
+ {@render ref_lines_layer(ref_lines_by_z.above_all)}
1290
+ </svg>
1291
+
1292
+ {#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}
1303
+ <PlotLegend
1304
+ bind:root_element={legend_element}
1305
+ {...legend}
1306
+ series_data={legend_data}
1307
+ on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1308
+ on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
1309
+ on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1310
+ on_hover_change={legend_hover.set_locked}
1311
+ on_item_hover={(item) =>
1312
+ (hovered_legend_series_idx = item != null && item.series_idx >= 0
1313
+ ? item.series_idx
1314
+ : null)}
1315
+ 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; ${
1317
+ legend?.style || ``
1318
+ }`}
1319
+ />
1320
+ {/if}
1321
+
1322
+ {#if hover_info && hovered}
1323
+ <PlotTooltip
1324
+ x={hover_info.cx}
1325
+ y={hover_info.cy}
1326
+ offset={{ x: 10, y: 5 }}
1327
+ constrain_to={{ width, height }}
1328
+ fallback_size={{ width: 140, height: 50 }}
1329
+ bg_color={hover_info.color}
1330
+ >
1331
+ {#if tooltip}
1332
+ {@render tooltip({ ...hover_info, fullscreen })}
1333
+ {:else}
1334
+ {@const fmt = (orientation === `vertical` ? y_axis.format : x_axis.format) || `.3~s`}
1335
+ {@const stat = hover_info.stats}
1336
+ {@const rows = [
1337
+ [`max`, stat.whisker_high],
1338
+ [`q3`, stat.q3],
1339
+ [`median`, stat.median],
1340
+ [`q1`, stat.q1],
1341
+ [`min`, stat.whisker_low],
1342
+ ...(show_mean ? [[`mean`, stat.mean] as const] : []),
1343
+ ] as const}
1344
+ {#if hover_info.category_label}
1345
+ <div><strong>{hover_info.category_label}</strong></div>
1346
+ {/if}
1347
+ {#each rows as [label, value] (label)}
1348
+ <div>{label}: {format_value(value, fmt)}</div>
1349
+ {/each}
1350
+ {#if show_outliers && stat.outliers.length > 0}
1351
+ <div>outliers: {stat.outliers.length}</div>
1352
+ {/if}
1353
+ {/if}
1354
+ </PlotTooltip>
1355
+ {/if}
1356
+
1357
+ {#if show_controls}
1358
+ <BoxPlotControls
1359
+ toggle_props={{
1360
+ ...controls_toggle_props,
1361
+ style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); ${
1362
+ controls_toggle_props?.style ?? ``
1363
+ }`,
1364
+ }}
1365
+ pane_props={controls_pane_props}
1366
+ bind:show_controls
1367
+ bind:controls_open
1368
+ bind:orientation
1369
+ bind:whisker_mode
1370
+ bind:show_outliers
1371
+ bind:show_mean
1372
+ bind:kind
1373
+ bind:side
1374
+ bind:x_axis
1375
+ bind:x2_axis={x2_axis_prop}
1376
+ bind:y_axis
1377
+ bind:y2_axis={y2_axis_prop}
1378
+ 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`}
1385
+ children={controls_extra}
1386
+ />
1387
+ {/if}
1388
+ {/if}
1389
+
1390
+ {@render children?.({ height, width, fullscreen })}
1391
+ </div>
1392
+
1393
+ <style>
1394
+ .box-plot {
1395
+ position: relative;
1396
+ width: 100%;
1397
+ height: var(--boxplot-height, auto);
1398
+ min-height: var(--boxplot-min-height, 300px);
1399
+ container-type: size;
1400
+ z-index: var(--boxplot-z-index, auto);
1401
+ border-radius: var(--boxplot-border-radius, var(--border-radius, 3pt));
1402
+ flex: var(--boxplot-flex, 1);
1403
+ display: var(--boxplot-display, flex);
1404
+ flex-direction: column;
1405
+ background: var(--boxplot-bg, var(--plot-bg));
1406
+ }
1407
+ .box-plot.fullscreen {
1408
+ position: fixed;
1409
+ top: 0;
1410
+ left: 0;
1411
+ width: 100vw !important;
1412
+ height: 100vh !important;
1413
+ z-index: var(--boxplot-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
1414
+ margin: 0;
1415
+ border-radius: 0;
1416
+ background: var(--boxplot-fullscreen-bg, var(--boxplot-bg, var(--plot-bg)));
1417
+ max-height: none !important;
1418
+ overflow: hidden;
1419
+ /* border-top (not padding-top): bind:clientHeight includes padding but excludes
1420
+ borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
1421
+ border-top: var(--plot-fullscreen-padding-top, 2em) solid
1422
+ var(--boxplot-fullscreen-bg, var(--boxplot-bg, var(--plot-bg, transparent)));
1423
+ box-sizing: border-box;
1424
+ }
1425
+ .header-controls {
1426
+ position: absolute;
1427
+ top: var(--ctrl-btn-top, 5pt);
1428
+ right: var(--fullscreen-btn-right, 4px);
1429
+ z-index: var(--fullscreen-btn-z-index, 10);
1430
+ display: flex;
1431
+ align-items: center;
1432
+ gap: 8px;
1433
+ }
1434
+ .header-controls :global(.fullscreen-toggle) {
1435
+ position: static;
1436
+ opacity: 1;
1437
+ }
1438
+ .box-plot :global(.pane-toggle),
1439
+ .box-plot .header-controls {
1440
+ opacity: 0;
1441
+ transition: opacity 0.2s, background-color 0.2s;
1442
+ }
1443
+ .box-plot:hover :global(.pane-toggle),
1444
+ .box-plot:hover .header-controls,
1445
+ .box-plot :global(.pane-toggle:focus-visible),
1446
+ .box-plot :global(.pane-toggle[aria-expanded='true']),
1447
+ .box-plot .header-controls:focus-within {
1448
+ opacity: 1;
1449
+ }
1450
+ svg {
1451
+ width: var(--boxplot-svg-width, 100%);
1452
+ height: var(--boxplot-svg-height, 100%);
1453
+ flex: var(--boxplot-svg-flex, 1);
1454
+ overflow: var(--boxplot-svg-overflow, visible);
1455
+ fill: var(--text-color);
1456
+ font-weight: var(--scatter-font-weight);
1457
+ font-size: var(--scatter-font-size);
1458
+ }
1459
+ .value-label {
1460
+ font-size: 11px;
1461
+ }
1462
+ </style>