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,8 +2,8 @@
2
2
  lang="ts"
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >
5
- import type { D3ColorSchemeName, D3InterpolateName } from '../../colors'
6
- import { format_value } from '../../labels'
5
+ import type { D3InterpolateName } from '../../colors'
6
+ import { format_value_or_num } from '../../labels'
7
7
  import { sanitize_html } from '../../sanitize'
8
8
  import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
9
9
  import type { Point2D, Vec2 } from '../../math'
@@ -14,8 +14,8 @@
14
14
  BarSeries,
15
15
  BarStyle,
16
16
  BasePlotProps,
17
+ ColorScaleConfig,
17
18
  DataLoaderFn,
18
- InitialRanges,
19
19
  InternalPoint,
20
20
  LegendConfig,
21
21
  LegendItem,
@@ -25,7 +25,7 @@
25
25
  PlotConfig,
26
26
  RefLine,
27
27
  RefLineEvent,
28
- ScaleType,
28
+ SizeScaleConfig,
29
29
  UserContentProps,
30
30
  } from '..'
31
31
  import {
@@ -33,53 +33,55 @@
33
33
  compute_element_placement,
34
34
  PlotAxis,
35
35
  PlotLegend,
36
+ PlotMarginals,
36
37
  ReferenceLine,
37
38
  ScatterPoint,
38
39
  } from '..'
39
- import type { AxisChangeState } from '../core/axis-utils'
40
- import { create_axis_loader } from '../core/axis-utils'
40
+ import type { MarginalSeriesInput, MarginalsProp } from '../core/marginals'
41
41
  import {
42
- create_dimension_tracker,
43
- create_hover_lock,
44
- } from '../core/hover-lock.svelte'
42
+ add_sides,
43
+ marginal_axis,
44
+ marginal_axis_presence,
45
+ normalize_marginals,
46
+ reserve_marginal_pad,
47
+ } from '../core/marginals'
48
+ import { type AxisChangeState, create_axis_loader } from '../core/axis-utils'
49
+ import { create_placed_tween } from '../core/placed-tween.svelte'
50
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
45
51
  import { create_legend_visibility } from '../core/utils/series-visibility'
46
52
  import {
47
53
  axis_ranges_equal,
48
- get_relative_coords,
49
- MIN_TOUCH_DISTANCE_PIXELS,
50
- pan_range_by_pixels,
51
- PINCH_ZOOM_THRESHOLD,
52
- remove_drag_listeners,
54
+ invert_rect_range,
53
55
  resolve_axis_ranges,
54
- sorted_range,
55
- to_epoch_num,
56
- zoom_range_by_factor,
57
56
  } from '../core/interactions'
58
57
  import type { IndexedRefLine } from '../core/reference-line'
59
58
  import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
60
59
  import {
60
+ create_axis_scales,
61
61
  create_color_scale,
62
- create_scale,
63
62
  create_size_scale,
64
63
  generate_ticks,
65
64
  get_tick_label,
66
65
  } from '../core/scales'
67
- import { DEFAULT_MARKERS } from '../core/types'
66
+ import { DEFAULT_MARKERS, SCALE_DEFAULTS } from '../core/types'
68
67
  import { DEFAULTS } from '../../settings'
69
68
  import { extent } from 'd3-array'
70
69
  import type { Snippet } from 'svelte'
71
70
  import { onDestroy, untrack } from 'svelte'
72
71
  import type { HTMLAttributes } from 'svelte/elements'
73
- import { Tween, type TweenOptions } from 'svelte/motion'
72
+ import type { TweenOptions } from 'svelte/motion'
74
73
  import {
75
74
  build_obstacles_norm,
76
75
  clip_bar,
77
76
  has_explicit_position,
78
77
  measured_footprint,
79
78
  place_decorations,
79
+ placed_coords,
80
80
  } from '../core/auto-place'
81
81
  import {
82
+ AXIS_TITLE_OFFSET,
82
83
  calc_auto_padding,
84
+ DEFAULT_PLOT_PADDING,
83
85
  filter_padding,
84
86
  LABEL_GAP_DEFAULT,
85
87
  y2_axis_label_x,
@@ -116,8 +118,8 @@
116
118
  y_axis = $bindable({}),
117
119
  y2_axis: y2_axis_prop = $bindable({}),
118
120
  display = $bindable(DEFAULTS.bar.display),
119
- range_padding = 0.05,
120
- padding = { t: 20, b: 60, l: 60, r: 20 },
121
+ range_padding = 0,
122
+ padding = DEFAULT_PLOT_PADDING,
121
123
  legend = {},
122
124
  show_legend,
123
125
  bar = {},
@@ -129,12 +131,8 @@
129
131
  on_bar_click,
130
132
  on_bar_hover,
131
133
  // Line marker props (matching ScatterPlot)
132
- color_scale = {
133
- type: `linear`,
134
- scheme: `interpolateViridis`,
135
- value_range: undefined,
136
- },
137
- size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
134
+ color_scale = SCALE_DEFAULTS.color,
135
+ size_scale = SCALE_DEFAULTS.size,
138
136
  point_tween,
139
137
  on_point_click,
140
138
  on_point_hover,
@@ -154,73 +152,65 @@
154
152
  on_axis_change,
155
153
  on_error,
156
154
  pan = {},
155
+ marginals = false,
157
156
  ...rest
158
- }: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
159
- series?: BarSeries<Metadata>[]
160
- // Component-specific props
161
- orientation?: Orientation
162
- mode?: BarMode
163
- legend?: LegendConfig | null
164
- show_legend?: boolean
165
- bar?: BarStyle
166
- line?: LineStyle
167
- tooltip?: Snippet<[BarHandlerProps<Metadata>]>
168
- user_content?: Snippet<[UserContentProps]>
169
- header_controls?: Snippet<
170
- [{ height: number; width: number; fullscreen: boolean }]
171
- >
172
- controls_extra?: Snippet<
173
- [{ orientation: Orientation; mode: BarMode } & Required<PlotConfig>]
174
- >
175
- change?: (data: BarHandlerProps<Metadata> | null) => void
176
- on_bar_click?: (
177
- data: BarHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
178
- ) => void
179
- on_bar_hover?: (
180
- data:
181
- | (BarHandlerProps<Metadata> & {
182
- event: MouseEvent | FocusEvent | KeyboardEvent
183
- })
184
- | null,
185
- ) => void
186
- // Line marker props (matching ScatterPlot)
187
- // Note: For line series with markers, BOTH on_bar_* AND on_point_* events fire.
188
- // Use on_point_* for marker-specific data (includes `point` with InternalPoint details)
189
- // or on_bar_* for backward compatibility with bar-style event handling.
190
- color_scale?: {
191
- type?: ScaleType
192
- scheme?: D3ColorSchemeName | D3InterpolateName
193
- value_range?: [number, number]
194
- } | D3InterpolateName
195
- size_scale?: {
196
- type?: ScaleType
197
- radius_range?: [number, number]
198
- value_range?: [number, number]
199
- }
200
- point_tween?: TweenOptions<Point2D>
201
- on_point_click?: (
202
- data: LineMarkerHandlerProps & { event: MouseEvent | KeyboardEvent },
203
- ) => void
204
- on_point_hover?: (
205
- data:
206
- | (LineMarkerHandlerProps & {
207
- event: MouseEvent | FocusEvent | KeyboardEvent
208
- })
209
- | null,
210
- ) => void
211
- ref_lines?: RefLine[]
212
- on_ref_line_click?: (event: RefLineEvent) => void
213
- on_ref_line_hover?: (event: RefLineEvent | null) => void
214
- // Interactive axis props
215
- data_loader?: DataLoaderFn<Metadata, BarSeries<Metadata>>
216
- on_axis_change?: (
217
- axis: `x` | `x2` | `y` | `y2`,
218
- key: string,
219
- new_series: BarSeries<Metadata>[],
220
- ) => void
221
- on_error?: (error: AxisLoadError) => void
222
- pan?: PanConfig
223
- } = $props()
157
+ }: HTMLAttributes<HTMLDivElement> &
158
+ BasePlotProps &
159
+ PlotConfig & {
160
+ series?: BarSeries<Metadata>[]
161
+ // Component-specific props
162
+ orientation?: Orientation
163
+ mode?: BarMode
164
+ legend?: LegendConfig | null
165
+ show_legend?: boolean
166
+ bar?: BarStyle
167
+ line?: LineStyle
168
+ tooltip?: Snippet<[BarHandlerProps<Metadata>]>
169
+ user_content?: Snippet<[UserContentProps]>
170
+ header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
171
+ controls_extra?: Snippet<
172
+ [{ orientation: Orientation; mode: BarMode } & Required<PlotConfig>]
173
+ >
174
+ change?: (data: BarHandlerProps<Metadata> | null) => void
175
+ on_bar_click?: (
176
+ data: BarHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
177
+ ) => void
178
+ on_bar_hover?: (
179
+ data:
180
+ | (BarHandlerProps<Metadata> & { event: MouseEvent | FocusEvent | KeyboardEvent })
181
+ | null,
182
+ ) => void
183
+ // Line marker props (matching ScatterPlot)
184
+ // Note: For line series with markers, BOTH on_bar_* AND on_point_* events fire.
185
+ // Use on_point_* for marker-specific data (includes `point` with InternalPoint details)
186
+ // or on_bar_* for backward compatibility with bar-style event handling.
187
+ color_scale?: ColorScaleConfig | D3InterpolateName
188
+ size_scale?: SizeScaleConfig
189
+ point_tween?: TweenOptions<Point2D>
190
+ on_point_click?: (
191
+ data: LineMarkerHandlerProps & { event: MouseEvent | KeyboardEvent },
192
+ ) => void
193
+ on_point_hover?: (
194
+ data:
195
+ | (LineMarkerHandlerProps & {
196
+ event: MouseEvent | FocusEvent | KeyboardEvent
197
+ })
198
+ | null,
199
+ ) => void
200
+ ref_lines?: RefLine[]
201
+ on_ref_line_click?: (event: RefLineEvent) => void
202
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
203
+ // Interactive axis props
204
+ data_loader?: DataLoaderFn<Metadata, BarSeries<Metadata>>
205
+ on_axis_change?: (
206
+ axis: `x` | `x2` | `y` | `y2`,
207
+ key: string,
208
+ new_series: BarSeries<Metadata>[],
209
+ ) => void
210
+ on_error?: (error: AxisLoadError) => void
211
+ pan?: PanConfig
212
+ marginals?: MarginalsProp
213
+ } = $props()
224
214
 
225
215
  // Initialize bar, line, y2_axis with defaults - using $derived for reactivity
226
216
  let bar_state = $derived({ ...DEFAULTS.bar.bar, ...bar })
@@ -240,7 +230,7 @@
240
230
  format: ``,
241
231
  scale_type: `linear`,
242
232
  ticks: 5,
243
- label_shift: { x: 0, y: 40 },
233
+ label_shift: { x: 0, y: AXIS_TITLE_OFFSET },
244
234
  tick: { label: { shift: { x: 0, y: 0 } } },
245
235
  range: [null, null],
246
236
  ...x2_axis_prop,
@@ -258,8 +248,7 @@
258
248
  let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
259
249
 
260
250
  // Compute ref_lines with index and group by z-index (using shared utilities)
261
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
262
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
251
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
263
252
 
264
253
  // === Categorical Normalization (string x values -> integer indices, see ./data) ===
265
254
  let cat_norm = $derived(normalize_categorical(series, x_axis.categories))
@@ -277,47 +266,44 @@
277
266
  const axis_px = (orientation === `horizontal` ? height : width) || 0
278
267
  const max_ticks = Math.max(1, Math.floor(axis_px / 28)) // ~28px per category label
279
268
  const step = Math.ceil(category_indices.length / max_ticks)
280
- return step <= 1
281
- ? category_indices
282
- : category_indices.filter((_, idx) => idx % step === 0)
269
+ return step <= 1 ? category_indices : category_indices.filter((_, idx) => idx % step === 0)
283
270
  })
284
271
 
285
272
  // Compute auto ranges from visible series
286
- let visible_series = $derived(
287
- internal_series.filter((srs) => srs?.visible ?? true),
288
- )
273
+ let visible_series = $derived(internal_series.filter((srs) => srs?.visible ?? true))
289
274
 
290
275
  // Separate series by y-axis
291
- let y1_series = $derived(
292
- visible_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`),
293
- )
294
- let y2_series = $derived(
295
- visible_series.filter((srs) => srs.y_axis === `y2`),
276
+ let y1_series = $derived(visible_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`))
277
+ let y2_series = $derived(visible_series.filter((srs) => srs.y_axis === `y2`))
278
+ let x2_series = $derived(visible_series.filter((srs) => srs.x_axis === `x2`))
279
+ // Whether the secondary x2 (top) / y2 (right) axis actually renders: BarPlot only supports
280
+ // them in vertical orientation. Derive once so ticks, padding, axis rendering, and marginal
281
+ // placement stay in sync. (Data-existence checks below use the bare `*_series.length` instead.)
282
+ let show_x2 = $derived(x2_series.length > 0 && orientation === `vertical`)
283
+ let show_y2 = $derived(y2_series.length > 0 && orientation === `vertical`)
284
+
285
+ let auto_ranges = $derived(
286
+ compute_bar_auto_ranges({
287
+ visible_series,
288
+ y1_series,
289
+ y2_series,
290
+ x2_series,
291
+ mode,
292
+ orientation,
293
+ range_padding,
294
+ category_count: category_list.length,
295
+ x_range: x_axis.range ?? [null, null],
296
+ x_scale_type: x_axis.scale_type ?? `linear`,
297
+ x_is_time: x_axis.format?.startsWith(`%`) || false,
298
+ x2_range: x2_axis.range ?? [null, null],
299
+ x2_scale_type: x2_axis.scale_type ?? `linear`,
300
+ x2_is_time: x2_axis.format?.startsWith(`%`) || false,
301
+ y_range: y_axis.range ?? [null, null],
302
+ y_scale_type: y_axis.scale_type ?? `linear`,
303
+ y2_range: y2_axis.range ?? [null, null],
304
+ y2_scale_type: y2_axis.scale_type ?? `linear`,
305
+ }),
296
306
  )
297
- let x2_series = $derived(
298
- visible_series.filter((srs) => srs.x_axis === `x2`),
299
- )
300
-
301
- let auto_ranges = $derived(compute_bar_auto_ranges({
302
- visible_series,
303
- y1_series,
304
- y2_series,
305
- x2_series,
306
- mode,
307
- orientation,
308
- range_padding,
309
- category_count: category_list.length,
310
- x_range: x_axis.range ?? [null, null],
311
- x_scale_type: x_axis.scale_type ?? `linear`,
312
- x_is_time: x_axis.format?.startsWith(`%`) || false,
313
- x2_range: x2_axis.range ?? [null, null],
314
- x2_scale_type: x2_axis.scale_type ?? `linear`,
315
- x2_is_time: x2_axis.format?.startsWith(`%`) || false,
316
- y_range: y_axis.range ?? [null, null],
317
- y_scale_type: y_axis.scale_type ?? `linear`,
318
- y2_range: y2_axis.range ?? [null, null],
319
- y2_scale_type: y2_axis.scale_type ?? `linear`,
320
- }))
321
307
 
322
308
  // Initialize and current ranges
323
309
  let ranges = $state<{
@@ -328,10 +314,14 @@
328
314
  current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
329
315
  })
330
316
 
331
- $effect(() => { // handle x_axis.range / x2_axis.range / y_axis.range / y2_axis.range changes
317
+ $effect(() => {
318
+ // handle x_axis.range / x2_axis.range / y_axis.range / y2_axis.range changes
332
319
  // resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
333
320
  // NaN breaks scales and, since NaN !== NaN, loops the effect)
334
- const next = resolve_axis_ranges({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis }, auto_ranges)
321
+ const next = resolve_axis_ranges(
322
+ { x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis },
323
+ auto_ranges,
324
+ )
335
325
  if (!next) return
336
326
  // Only update if the initial (data-driven) ranges changed, not when user pans.
337
327
  // untrack the read of `ranges` so the assignment below can't re-trigger this effect
@@ -343,42 +333,33 @@
343
333
  })
344
334
 
345
335
  // Layout: dynamic padding based on tick label widths
346
- const default_padding = { t: 20, b: 60, l: 60, r: 20 }
347
336
  // base_pad reserves space for tick labels/axis titles; pad (below) adds decoration reservations
348
- let base_pad = $derived(filter_padding(padding, default_padding))
337
+ let base_pad = $derived(filter_padding(padding, DEFAULT_PLOT_PADDING))
349
338
 
350
339
  // Update padding when format or ticks change
351
340
  $effect(() => {
352
- const new_pad = width && height && ticks.y.length > 0
353
- ? calc_auto_padding({
354
- padding,
355
- default_padding,
356
- x2_axis: { ...x2_axis, tick_values: ticks.x2 },
357
- y_axis: { ...y_axis, tick_values: ticks.y },
358
- y2_axis: { ...y2_axis, tick_values: ticks.y2 },
359
- })
360
- : filter_padding(padding, default_padding)
341
+ const new_pad =
342
+ width && height && ticks.y.length > 0
343
+ ? calc_auto_padding({
344
+ padding,
345
+ default_padding: DEFAULT_PLOT_PADDING,
346
+ x2_axis: { ...x2_axis, tick_values: ticks.x2 },
347
+ y_axis: { ...y_axis, tick_values: ticks.y },
348
+ y2_axis: { ...y2_axis, tick_values: ticks.y2 },
349
+ })
350
+ : filter_padding(padding, DEFAULT_PLOT_PADDING)
361
351
  // Expand right padding if y2 ticks are shown (only for vertical orientation)
362
- if (
363
- width && height && y2_series.length > 0 && ticks.y2.length > 0 &&
364
- orientation === `vertical`
365
- ) {
352
+ if (width && height && show_y2 && ticks.y2.length > 0) {
366
353
  // Need space for: tick shift + tick width + gap (30px) + label space (20px if present)
367
354
  // When ticks are inside, they don't contribute to padding
368
355
  const inside = y2_axis.tick?.label?.inside ?? false
369
356
  const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
370
357
  const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
371
358
  const label_space = y2_axis.label ? 20 : 0
372
- new_pad.r = Math.max(
373
- new_pad.r,
374
- tick_shift + tick_width_contribution + 30 + label_space,
375
- )
359
+ new_pad.r = Math.max(new_pad.r, tick_shift + tick_width_contribution + 30 + label_space)
376
360
  }
377
361
  // Expand top padding if x2 ticks are shown (only for vertical orientation)
378
- if (
379
- width && height && x2_series.length > 0 && ticks.x2.length > 0 &&
380
- orientation === `vertical`
381
- ) {
362
+ if (width && height && show_x2 && ticks.x2.length > 0) {
382
363
  const inside = x2_axis.tick?.label?.inside ?? false
383
364
  const tick_shift = inside ? 0 : Math.abs(x2_axis.tick?.label?.shift?.y ?? 0) + 5
384
365
  const tick_height = inside ? 0 : 16
@@ -388,13 +369,18 @@
388
369
 
389
370
  // Only update if padding actually changed (prevents infinite loop)
390
371
  if (
391
- base_pad.t !== new_pad.t || base_pad.b !== new_pad.b ||
392
- base_pad.l !== new_pad.l || base_pad.r !== new_pad.r
393
- ) base_pad = new_pad
372
+ base_pad.t !== new_pad.t ||
373
+ base_pad.b !== new_pad.b ||
374
+ base_pad.l !== new_pad.l ||
375
+ base_pad.r !== new_pad.r
376
+ )
377
+ base_pad = new_pad
394
378
  })
395
379
 
396
380
  let legend_element = $state<HTMLDivElement | undefined>()
397
- const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 60 }))
381
+ const legend_footprint = $derived(
382
+ measured_footprint(legend_element, { width: 120, height: 60 }),
383
+ )
398
384
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
399
385
 
400
386
  // Obstacle field in normalized [0,1] plot coords (y=0 at top). Each bar is modeled as a segment
@@ -423,8 +409,18 @@
423
409
  const value = base + srs.y[bar_idx]
424
410
  // vertical: category on x, value rises on y (inverted). horizontal: category on y, value on x
425
411
  const seg = vertical
426
- ? clip_bar(true, (x_val - ax0) / x_span, 1 - (value - vy0) / y_span, 1 - (base - vy0) / y_span)
427
- : clip_bar(false, 1 - (x_val - cy0) / cy_span, (value - ax0) / x_span, (base - ax0) / x_span)
412
+ ? clip_bar(
413
+ true,
414
+ (x_val - ax0) / x_span,
415
+ 1 - (value - vy0) / y_span,
416
+ 1 - (base - vy0) / y_span,
417
+ )
418
+ : clip_bar(
419
+ false,
420
+ 1 - (x_val - cy0) / cy_span,
421
+ (value - ax0) / x_span,
422
+ (base - ax0) / x_span,
423
+ )
428
424
  if (seg) bars.push(seg)
429
425
  })
430
426
  })
@@ -439,39 +435,58 @@
439
435
  height,
440
436
  obstacles_norm,
441
437
  // gate on legend_element (the render signal) not legend_data, whose entries can read pad
442
- legend: legend != null &&
443
- (show_legend !== undefined ? show_legend : series.length > 1) &&
444
- legend_element != null && !legend_has_explicit_pos
445
- ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
446
- : null,
447
- })
438
+ legend:
439
+ legend != null &&
440
+ (show_legend !== undefined ? show_legend : series.length > 1) &&
441
+ legend_element != null &&
442
+ !legend_has_explicit_pos
443
+ ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
444
+ : null,
445
+ }),
448
446
  )
449
- const pad = $derived(decor.pad)
447
+ // Resolve marginals: a cumulative/Pareto CDF over the CATEGORY axis weighted by bar height.
448
+ // Categories sit on x (vertical) or y (horizontal), so the default side and the value array
449
+ // flip with orientation. The value axis carries no marginal (a bar's height isn't a sample).
450
+ const marginal_is_vertical = $derived(orientation === `vertical`)
451
+ const resolved_marginals = $derived(
452
+ normalize_marginals(
453
+ marginals,
454
+ marginal_is_vertical ? { top: { type: `cdf` } } : { right: { type: `cdf` } },
455
+ ),
456
+ )
457
+ const pad = $derived(add_sides(decor.pad, reserve_marginal_pad(resolved_marginals)))
458
+ const marginal_series = $derived<MarginalSeriesInput[]>(
459
+ internal_series.map((srs) => ({
460
+ x: marginal_is_vertical ? (srs?.x ?? []) : undefined,
461
+ y: marginal_is_vertical ? undefined : (srs?.x ?? []),
462
+ // magnitude weights so negative bars still yield a monotonic cumulative (CDF) marginal
463
+ weight: srs?.y?.map((value) => Math.abs(value)) ?? [],
464
+ color:
465
+ srs?.color ??
466
+ (srs?.render_mode === `line` ? line_state.color : bar_state.color) ??
467
+ `steelblue`,
468
+ label: srs?.label,
469
+ visible: srs?.visible ?? true,
470
+ x_axis: srs?.x_axis,
471
+ y_axis: srs?.y_axis,
472
+ })),
473
+ )
474
+ const marginal_has_axis = $derived(marginal_axis_presence(show_x2, show_y2))
450
475
  const legend_auto_outside = $derived(decor.legend_outside)
451
476
  const legend_outside_x = $derived(decor.legend_pos.x)
452
477
  const legend_outside_y = $derived(decor.legend_pos.y)
453
478
  const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
454
479
  const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
455
480
 
456
- // Scales
457
- let scales = $derived({
458
- x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [
459
- pad.l,
460
- width - pad.r,
461
- ]),
462
- x2: create_scale(x2_axis.scale_type ?? `linear`, ranges.current.x2, [
463
- pad.l,
464
- width - pad.r,
465
- ]),
466
- y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [
467
- height - pad.b,
468
- pad.t,
469
- ]),
470
- y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [
471
- height - pad.b,
472
- pad.t,
473
- ]),
474
- })
481
+ let scales = $derived(
482
+ create_axis_scales(
483
+ { x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis },
484
+ ranges.current,
485
+ pad,
486
+ width,
487
+ height,
488
+ ),
489
+ )
475
490
 
476
491
  // Compute plot center for point tweening origin
477
492
  let plot_center_x = $derived(pad.l + (width - pad.r - pad.l) / 2)
@@ -482,14 +497,12 @@
482
497
  visible_series
483
498
  .filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
484
499
  .flatMap((srs: BarSeries<Metadata>) =>
485
- (srs.color_values ?? []).filter(
486
- (val): val is number => typeof val === `number`,
487
- )
500
+ (srs.color_values ?? []).filter((val): val is number => typeof val === `number`),
488
501
  ),
489
502
  )
490
503
 
491
504
  // Create auto color range (safely handle empty arrays or undefined extent results)
492
- let auto_color_range: [number, number] = $derived.by(() => {
505
+ let auto_color_range: Vec2 = $derived.by(() => {
493
506
  if (all_color_values.length === 0) return [0, 1]
494
507
  const [min_val, max_val] = extent(all_color_values)
495
508
  return [min_val ?? 0, max_val ?? 1]
@@ -500,9 +513,7 @@
500
513
  visible_series
501
514
  .filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
502
515
  .flatMap((srs: BarSeries<Metadata>) =>
503
- [...(srs.size_values ?? [])].filter(
504
- (val): val is number => typeof val === `number`,
505
- )
516
+ [...(srs.size_values ?? [])].filter((val): val is number => typeof val === `number`),
506
517
  ),
507
518
  )
508
519
 
@@ -520,57 +531,37 @@
520
531
  // Only respect user ticks when they're a Record (custom label mapping),
521
532
  // not a number (tick count) or array (tick positions)
522
533
  const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
523
- if (
524
- user_ticks != null && typeof user_ticks === `object` &&
525
- !Array.isArray(user_ticks)
526
- ) return user_ticks
527
- return Object.fromEntries(
528
- category_list.map((cat, idx) => [idx, cat]),
529
- ) as Record<number, string>
534
+ if (user_ticks != null && typeof user_ticks === `object` && !Array.isArray(user_ticks))
535
+ return user_ticks
536
+ return Object.fromEntries(category_list.map((cat, idx): [number, string] => [idx, cat]))
530
537
  })
531
538
 
532
- // Ticks
533
- let ticks = $derived({
534
- x: width && height
535
- ? (category_indices && cat_axis === `x` ? cat_tick_indices : generate_ticks(
536
- ranges.current.x,
537
- x_axis.scale_type ?? `linear`,
538
- x_axis.ticks,
539
- scales.x,
540
- { default_count: 8 },
541
- ))
542
- : [],
543
- y: width && height
544
- ? (category_indices && cat_axis === `y` ? cat_tick_indices : generate_ticks(
545
- ranges.current.y,
546
- y_axis.scale_type ?? `linear`,
547
- y_axis.ticks,
548
- scales.y,
549
- { default_count: 6 },
550
- ))
551
- : [],
552
- y2: width && height && y2_series.length > 0 && orientation === `vertical`
553
- ? generate_ticks(
554
- ranges.current.y2,
555
- y2_axis.scale_type ?? `linear`,
556
- y2_axis.ticks,
557
- scales.y2,
558
- {
559
- default_count: 6,
560
- },
561
- )
562
- : [],
563
- x2: width && height && x2_series.length > 0 && orientation === `vertical`
564
- ? generate_ticks(
565
- ranges.current.x2,
566
- x2_axis.scale_type ?? `linear`,
567
- x2_axis.ticks,
568
- scales.x2,
569
- {
570
- default_count: 8,
571
- },
572
- )
573
- : [],
539
+ let ticks = $derived.by(() => {
540
+ const axis_ticks = (
541
+ axis: typeof x_axis,
542
+ range: Vec2,
543
+ scale: typeof scales.x,
544
+ default_count: number,
545
+ show = true,
546
+ ) =>
547
+ width && height && show
548
+ ? generate_ticks(range, axis.scale_type ?? `linear`, axis.ticks, scale, {
549
+ default_count,
550
+ })
551
+ : []
552
+ // categorical axes show one tick per category instead of generated numeric ticks
553
+ return {
554
+ x:
555
+ category_indices && cat_axis === `x` && width && height
556
+ ? cat_tick_indices
557
+ : axis_ticks(x_axis, ranges.current.x, scales.x, 8),
558
+ y:
559
+ category_indices && cat_axis === `y` && width && height
560
+ ? cat_tick_indices
561
+ : axis_ticks(y_axis, ranges.current.y, scales.y, 6),
562
+ y2: axis_ticks(y2_axis, ranges.current.y2, scales.y2, 6, show_y2),
563
+ x2: axis_ticks(x2_axis, ranges.current.x2, scales.x2, 8, show_x2),
564
+ }
574
565
  })
575
566
 
576
567
  // Cache measured tick-label widths so expensive canvas text measurement
@@ -581,212 +572,45 @@
581
572
  x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
582
573
  })
583
574
 
584
- // Zoom drag state
585
- let drag_state = $state<{
586
- start: { x: number; y: number } | null
587
- current: { x: number; y: number } | null
588
- bounds: DOMRect | null
589
- }>({ start: null, current: null, bounds: null })
590
-
591
- // Pan state
592
- let is_focused = $state(false)
593
- let shift_held = $state(false)
594
- let pan_drag_state = $state<
595
- InitialRanges & { start: { x: number; y: number } } | null
596
- >(null)
597
- let touch_state = $state<
598
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
599
- >(null)
600
- const on_window_mouse_move = (evt: MouseEvent) => {
601
- if (!drag_state.start || !drag_state.bounds) return
602
- drag_state.current = {
603
- x: evt.clientX - drag_state.bounds.left,
604
- y: evt.clientY - drag_state.bounds.top,
605
- }
606
- }
607
- const on_window_mouse_up = () => {
608
- if (drag_state.start && drag_state.current) {
609
- const x1_raw = scales.x.invert(drag_state.start.x) as number | Date
610
- const x2_raw = scales.x.invert(drag_state.current.x) as number | Date
611
- const y1 = scales.y.invert(drag_state.start.y)
612
- const y2 = scales.y.invert(drag_state.current.y)
613
- const y2_1 = scales.y2.invert(drag_state.start.y)
614
- const y2_2 = scales.y2.invert(drag_state.current.y)
615
- const x2a_1_raw = scales.x2.invert(drag_state.start.x) as number | Date
616
- const x2a_2_raw = scales.x2.invert(drag_state.current.x) as number | Date
617
- const dx = Math.abs(drag_state.start.x - drag_state.current.x)
618
- const dy = Math.abs(drag_state.start.y - drag_state.current.y)
619
-
620
- // Same scale inverts both coords, so each pair is all-number or all-Date
621
- const [xr1, xr2] = [to_epoch_num(x1_raw), to_epoch_num(x2_raw)]
622
- const [x2r1, x2r2] = [to_epoch_num(x2a_1_raw), to_epoch_num(x2a_2_raw)]
623
-
624
- if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
625
- // Update axis ranges to trigger reactivity and prevent effect from overriding
626
- x_axis = { ...x_axis, range: sorted_range(xr1, xr2) }
627
- if (x2_series.length > 0 && Number.isFinite(x2r1) && Number.isFinite(x2r2)) {
628
- x2_axis_prop = { ...x2_axis_prop, range: sorted_range(x2r1, x2r2) }
629
- }
630
- y_axis = { ...y_axis, range: sorted_range(y1, y2) }
631
- // gate on y2 series presence (like x2): the y2 scale is a [0, 1] sentinel
632
- // otherwise, so inverting would store a phantom range in the bindable prop
633
- if (y2_series.length > 0) {
634
- y2_axis_prop = { ...y2_axis_prop, range: sorted_range(y2_1, y2_2) }
635
- }
575
+ // Shared pan/zoom/touch/drag-rect interaction controller
576
+ const pan_zoom = create_pan_zoom({
577
+ ranges: () => ranges.current,
578
+ scale_type: (axis) =>
579
+ ({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis })[axis].scale_type,
580
+ plot_dims: () => ({ width: chart_width, height: chart_height }),
581
+ pan: () => pan,
582
+ set_range: (axis, range) => (ranges.current[axis] = range),
583
+ svg: () => svg_element,
584
+ on_rect_zoom: (start, current) => {
585
+ // Update axis ranges to trigger reactivity and prevent effect from overriding
586
+ const next_x = invert_rect_range(scales.x, start.x, current.x)
587
+ if (!next_x) return
588
+ x_axis = { ...x_axis, range: next_x }
589
+ // gate x2/y2 on whether they actually render (show_x2/show_y2 also require vertical);
590
+ // otherwise their [0, 1] sentinel scales would store a phantom range in the bindable prop
591
+ const next_x2 = show_x2 ? invert_rect_range(scales.x2, start.x, current.x) : null
592
+ if (next_x2) x2_axis_prop = { ...x2_axis_prop, range: next_x2 }
593
+ const next_y = invert_rect_range(scales.y, start.y, current.y)
594
+ if (next_y) y_axis = { ...y_axis, range: next_y }
595
+ const next_y2 = show_y2 ? invert_rect_range(scales.y2, start.y, current.y) : null
596
+ if (next_y2) y2_axis_prop = { ...y2_axis_prop, range: next_y2 }
597
+ },
598
+ on_reset: () => {
599
+ // Reset zoom to initial ranges (undo any pan/zoom)
600
+ ranges.current = {
601
+ x: [...ranges.initial.x] as Vec2,
602
+ x2: [...ranges.initial.x2] as Vec2,
603
+ y: [...ranges.initial.y] as Vec2,
604
+ y2: [...ranges.initial.y2] as Vec2,
636
605
  }
637
- }
638
- drag_state = { start: null, current: null, bounds: null }
639
- window.removeEventListener(`mousemove`, on_window_mouse_move)
640
- window.removeEventListener(`mouseup`, on_window_mouse_up)
641
- document.body.style.cursor = `default`
642
- }
643
-
644
- // Pan/zoom all four axes from an interaction-start snapshot, each in its own
645
- // scale's transform space (log axes pan by a constant factor, linear by a shift)
646
- const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
647
- ranges.current.x = pan_range_by_pixels(init.initial_x_range, dx_px, chart_width, x_axis.scale_type)
648
- ranges.current.x2 = pan_range_by_pixels(init.initial_x2_range, dx_px, chart_width, x2_axis.scale_type)
649
- ranges.current.y = pan_range_by_pixels(init.initial_y_range, dy_px, chart_height, y_axis.scale_type)
650
- ranges.current.y2 = pan_range_by_pixels(init.initial_y2_range, dy_px, chart_height, y2_axis.scale_type)
651
- }
652
- const zoom_all_axes = (init: InitialRanges, factor: number) => {
653
- ranges.current.x = zoom_range_by_factor(init.initial_x_range, factor, x_axis.scale_type)
654
- ranges.current.x2 = zoom_range_by_factor(init.initial_x2_range, factor, x2_axis.scale_type)
655
- ranges.current.y = zoom_range_by_factor(init.initial_y_range, factor, y_axis.scale_type)
656
- ranges.current.y2 = zoom_range_by_factor(init.initial_y2_range, factor, y2_axis.scale_type)
657
- }
658
-
659
- // Pan drag handler (drag direction inverted on x for natural pan feel)
660
- const on_pan_move = (evt: MouseEvent) => {
661
- if (!pan_drag_state) return
662
- const sensitivity = pan?.drag_sensitivity ?? 1
663
- pan_all_axes(
664
- pan_drag_state,
665
- -(evt.clientX - pan_drag_state.start.x) * sensitivity,
666
- (evt.clientY - pan_drag_state.start.y) * sensitivity,
667
- )
668
- }
669
-
670
- const on_pan_end = () => {
671
- pan_drag_state = null
672
- document.body.style.cursor = ``
673
- window.removeEventListener(`mousemove`, on_pan_move)
674
- window.removeEventListener(`mouseup`, on_pan_end)
675
- }
676
-
677
- // Tear down any window listeners + cursor override if the component unmounts mid-drag
678
- // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
679
- // onDestroy also runs during SSR teardown, where window/document don't exist.
680
- onDestroy(() => {
681
- remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
682
- drag_state = { start: null, current: null, bounds: null }
683
- pan_drag_state = null
606
+ // Also reset axis props so future data changes recalculate auto ranges
607
+ x_axis = { ...x_axis, range: [null, null] }
608
+ x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
609
+ y_axis = { ...y_axis, range: [null, null] }
610
+ y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
611
+ },
684
612
  })
685
-
686
- function handle_mouse_down(evt: MouseEvent) {
687
- const coords = get_relative_coords(evt)
688
- if (!coords || !svg_element) return
689
-
690
- // Check if pan is enabled and shift is held for pan mode
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 [number, number],
697
- initial_x2_range: [...ranges.current.x2] as [number, number],
698
- initial_y_range: [...ranges.current.y] as [number, number],
699
- initial_y2_range: [...ranges.current.y2] as [number, number],
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
-
707
- drag_state = {
708
- start: coords,
709
- current: coords,
710
- bounds: svg_element.getBoundingClientRect(),
711
- }
712
- window.addEventListener(`mousemove`, on_window_mouse_move)
713
- window.addEventListener(`mouseup`, on_window_mouse_up)
714
- evt.preventDefault()
715
- }
716
-
717
- // Wheel handler for pan (requires focus and shift)
718
- function handle_wheel(evt: WheelEvent) {
719
- const pan_enabled = pan?.enabled !== false
720
- // Only capture wheel when focused AND Shift is held
721
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
722
- if (!pan_enabled || !is_focused || !shift_held) return
723
-
724
- evt.preventDefault()
725
-
726
- const sensitivity = pan?.wheel_sensitivity ?? 1
727
-
728
- // Pan along the dominant wheel direction
729
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
730
- const dx = evt.deltaX * sensitivity
731
- ranges.current.x = pan_range_by_pixels(ranges.current.x, dx, chart_width, x_axis.scale_type)
732
- ranges.current.x2 = pan_range_by_pixels(ranges.current.x2, dx, chart_width, x2_axis.scale_type)
733
- } else {
734
- const dy = evt.deltaY * sensitivity
735
- ranges.current.y = pan_range_by_pixels(ranges.current.y, dy, chart_height, y_axis.scale_type)
736
- ranges.current.y2 = pan_range_by_pixels(ranges.current.y2, dy, chart_height, y2_axis.scale_type)
737
- }
738
- }
739
-
740
- // Touch handlers for pinch-zoom and two-finger pan
741
- function handle_touch_start(evt: TouchEvent) {
742
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
743
- if (!touch_enabled || evt.touches.length !== 2) return
744
-
745
- evt.preventDefault()
746
- const touches = Array.from(evt.touches)
747
- touch_state = {
748
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
749
- initial_x_range: [...ranges.current.x] as [number, number],
750
- initial_x2_range: [...ranges.current.x2] as [number, number],
751
- initial_y_range: [...ranges.current.y] as [number, number],
752
- initial_y2_range: [...ranges.current.y2] as [number, number],
753
- }
754
- }
755
-
756
- function handle_touch_move(evt: TouchEvent) {
757
- if (!touch_state || evt.touches.length !== 2) return
758
- evt.preventDefault()
759
-
760
- const [t1, t2] = Array.from(evt.touches)
761
- const [s1, s2] = touch_state.start_touches
762
-
763
- // Calculate center movement for pan
764
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
765
- const curr_center = {
766
- x: (t1.clientX + t2.clientX) / 2,
767
- y: (t1.clientY + t2.clientY) / 2,
768
- }
769
- const dx = curr_center.x - start_center.x
770
- const dy = curr_center.y - start_center.y
771
-
772
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
773
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
774
- // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
775
- if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
776
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
777
- const scale = curr_dist / start_dist
778
-
779
- // If scale changed significantly, treat as pinch-zoom
780
- // Also guard against scale being too small to avoid division by zero
781
- // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
782
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
783
- zoom_all_axes(touch_state, scale)
784
- } else pan_all_axes(touch_state, -dx, dy)
785
- }
786
-
787
- function handle_touch_end() {
788
- touch_state = null
789
- }
613
+ onDestroy(() => pan_zoom.destroy())
790
614
 
791
615
  // Legend data and handlers
792
616
  let legend_data = $derived.by<LegendItem[]>(() =>
@@ -794,8 +618,7 @@
794
618
  const is_line = srs.render_mode === `line`
795
619
  const series_markers = srs.markers ?? DEFAULT_MARKERS
796
620
  const has_line = series_markers === `line` || series_markers === `line+points`
797
- const has_points = series_markers === `points` ||
798
- series_markers === `line+points`
621
+ const has_points = series_markers === `points` || series_markers === `line+points`
799
622
  const series_color = srs.color ?? (is_line ? line_state.color : bar_state.color)
800
623
 
801
624
  // Get point style for symbol color (handle array or single object)
@@ -803,9 +626,10 @@
803
626
  ? srs.point_style[0]
804
627
  : srs.point_style
805
628
  const first_color_value = srs.color_values?.[0]
806
- const point_color = first_color_value != null
807
- ? color_scale_fn(first_color_value)
808
- : first_point_style?.fill ?? series_color
629
+ const point_color =
630
+ first_color_value != null
631
+ ? color_scale_fn(first_color_value)
632
+ : (first_point_style?.fill ?? series_color)
809
633
 
810
634
  if (is_line) {
811
635
  // Line series: show line and/or symbol based on markers
@@ -817,16 +641,15 @@
817
641
  display_style: {
818
642
  ...(has_line
819
643
  ? {
820
- line_color: series_color,
821
- line_dash: srs.line_style?.line_dash,
822
- }
644
+ line_color: series_color,
645
+ line_dash: srs.line_style?.line_dash,
646
+ }
823
647
  : {}),
824
648
  ...(has_points
825
649
  ? {
826
- symbol_type: first_point_style?.symbol_type ??
827
- DEFAULTS.scatter.symbol_type,
828
- symbol_color: point_color,
829
- }
650
+ symbol_type: first_point_style?.symbol_type ?? DEFAULTS.scatter.symbol_type,
651
+ symbol_color: point_color,
652
+ }
830
653
  : {}),
831
654
  },
832
655
  }
@@ -842,10 +665,13 @@
842
665
  symbol_color: series_color,
843
666
  },
844
667
  }
845
- })
668
+ }),
846
669
  )
847
670
 
848
- const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
671
+ const legend_vis = create_legend_visibility(
672
+ () => series,
673
+ (next) => (series = next),
674
+ )
849
675
 
850
676
  // Collect bar and line positions for legend placement
851
677
  let bar_points_for_placement = $derived.by(() => {
@@ -862,12 +688,11 @@
862
688
  return srs.x
863
689
  .map((x_val, bar_idx) => {
864
690
  const y_val = srs.y[bar_idx]
865
- const base = !is_line && mode === `stacked`
866
- ? (series_offsets[bar_idx] ?? 0)
867
- : 0
868
- const [bar_x, bar_y] = orientation === `vertical`
869
- ? [x_scale_pl(x_val), y_scale(base + y_val)]
870
- : [x_scale_pl(base + y_val), scales.y(x_val)]
691
+ const base = !is_line && mode === `stacked` ? (series_offsets[bar_idx] ?? 0) : 0
692
+ const [bar_x, bar_y] =
693
+ orientation === `vertical`
694
+ ? [x_scale_pl(x_val), y_scale(base + y_val)]
695
+ : [x_scale_pl(base + y_val), scales.y(x_val)]
871
696
  return { x: bar_x, y: bar_y }
872
697
  })
873
698
  .filter(({ x, y }) => isFinite(x) && isFinite(y))
@@ -876,12 +701,6 @@
876
701
 
877
702
  // Legend placement stability state (legend_element declared above for the auto-place block)
878
703
  let hovered_legend_series_idx = $state<number | null>(null)
879
- const legend_hover = create_hover_lock()
880
- const dim_tracker = create_dimension_tracker()
881
- let has_initial_legend_placement = $state(false)
882
-
883
- // Clear pending hover lock timeout on unmount
884
- $effect(() => () => legend_hover.cleanup())
885
704
 
886
705
  // Calculate best legend placement using continuous grid sampling
887
706
  let legend_placement = $derived.by(() => {
@@ -900,37 +719,13 @@
900
719
  return result
901
720
  })
902
721
 
903
- // Tweened legend coordinates for smooth animation - create once, update target via effect
904
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
905
- const tweened_legend_coords = new Tween(
906
- { x: 0, y: 0 },
907
- untrack(() => ({ duration: 400, ...legend?.tween })),
908
- )
909
-
910
- // Update legend position with stability checks
911
- $effect(() => {
912
- if (!width || !height || !legend_placement) return
913
-
914
- // Track dimensions for resize detection
915
- const dims_changed = dim_tracker.has_changed(width, height)
916
- if (dims_changed) dim_tracker.update(width, height)
917
-
918
- const is_responsive = legend?.responsive ?? false
919
- // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
920
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
921
- (is_responsive || !has_initial_legend_placement))
922
-
923
- if (should_update) {
924
- tweened_legend_coords.set(
925
- { x: legend_placement.x, y: legend_placement.y },
926
- // Skip animation on initial placement to avoid jump from (0, 0)
927
- has_initial_legend_placement ? undefined : { duration: 0 },
928
- )
929
- // Only lock position after we have actual measured size
930
- if (legend_element) {
931
- has_initial_legend_placement = true
932
- }
933
- }
722
+ // Tweened legend coordinates with shared placement stability gating
723
+ const legend_tween = create_placed_tween({
724
+ placement: () => legend_placement,
725
+ dims: () => ({ width, height }),
726
+ responsive: () => legend?.responsive ?? false,
727
+ element: () => legend_element,
728
+ tween: () => legend?.tween,
934
729
  })
935
730
 
936
731
  // Tooltip state
@@ -944,9 +739,7 @@
944
739
  const srs = internal_series[series_idx]
945
740
  const [x, y] = [srs.x[bar_idx], srs.y[bar_idx]]
946
741
  const [orient_x, orient_y] = orientation === `horizontal` ? [y, x] : [x, y]
947
- const metadata = Array.isArray(srs.metadata)
948
- ? srs.metadata[bar_idx]
949
- : srs.metadata
742
+ const metadata = Array.isArray(srs.metadata) ? srs.metadata[bar_idx] : srs.metadata
950
743
  const label = srs.labels?.[bar_idx] ?? null
951
744
  const active_y_axis = srs.y_axis ?? `y1`
952
745
  const active_x_axis = srs.x_axis ?? `x1`
@@ -1001,7 +794,7 @@
1001
794
  const line_point_fill = (pt: LineSeriesPoint, series_color: string): string =>
1002
795
  pt.color_value != null
1003
796
  ? color_scale_fn(pt.color_value)
1004
- : pt.point_style?.fill ?? series_color
797
+ : (pt.point_style?.fill ?? series_color)
1005
798
 
1006
799
  const handle_bar_hover =
1007
800
  (series_idx: number, bar_idx: number, color: string) => (event: MouseEvent) => {
@@ -1023,31 +816,31 @@
1023
816
  })
1024
817
 
1025
818
  // State accessors for shared axis change handler
819
+ // Secondary axes read the merged $derived (x2_axis/y2_axis) but write the raw $bindable props
820
+ // (x2_axis_prop/y2_axis_prop) so library defaults aren't pushed into the parent's bound state
1026
821
  const axis_state: AxisChangeState<BarSeries<Metadata>> = {
1027
- get_axis: (axis) => {
1028
- if (axis === `x`) return x_axis
1029
- if (axis === `x2`) return x2_axis
1030
- if (axis === `y`) return y_axis
1031
- return y2_axis
822
+ axes: {
823
+ x: { get: () => x_axis, set: (config) => (x_axis = { ...x_axis, ...config }) },
824
+ x2: {
825
+ get: () => x2_axis,
826
+ set: (config) => (x2_axis_prop = { ...x2_axis_prop, ...config }),
827
+ },
828
+ y: { get: () => y_axis, set: (config) => (y_axis = { ...y_axis, ...config }) },
829
+ y2: {
830
+ get: () => y2_axis,
831
+ set: (config) => (y2_axis_prop = { ...y2_axis_prop, ...config }),
832
+ },
1032
833
  },
1033
- set_axis: (axis, config) => {
1034
- // Spread into existing state to preserve merged type structure
1035
- if (axis === `x`) x_axis = { ...x_axis, ...config }
1036
- else if (axis === `x2`) x2_axis_prop = { ...x2_axis_prop, ...config }
1037
- else if (axis === `y`) y_axis = { ...y_axis, ...config }
1038
- else y2_axis_prop = { ...y2_axis_prop, ...config }
1039
- },
1040
- get_series: () => series,
1041
- set_series: (new_series) => (series = new_series),
1042
- get_loading: () => axis_loading,
1043
- set_loading: (axis) => (axis_loading = axis),
834
+ series: { get: () => series, set: (next) => (series = next) },
835
+ loading: { get: () => axis_loading, set: (axis) => (axis_loading = axis) },
1044
836
  }
1045
837
 
1046
838
  // Shared handler + one-shot auto-load bound to this component's state
1047
- const { handle_axis_change, try_auto_load } = create_axis_loader(
1048
- axis_state,
1049
- () => ({ data_loader, on_axis_change, on_error }),
1050
- )
839
+ const { handle_axis_change, try_auto_load } = create_axis_loader(axis_state, () => ({
840
+ data_loader,
841
+ on_axis_change,
842
+ on_error,
843
+ }))
1051
844
  $effect(try_auto_load)
1052
845
  </script>
1053
846
 
@@ -1085,11 +878,9 @@
1085
878
  evt.preventDefault()
1086
879
  fullscreen = false
1087
880
  }
1088
- if (evt.key === `Shift`) shift_held = true
1089
- }}
1090
- onkeyup={(evt) => {
1091
- if (evt.key === `Shift`) shift_held = false
881
+ pan_zoom.on_window_key_down(evt)
1092
882
  }}
883
+ onkeyup={pan_zoom.on_window_key_up}
1093
884
  />
1094
885
 
1095
886
  <div
@@ -1097,7 +888,7 @@
1097
888
  bind:clientWidth={width}
1098
889
  bind:clientHeight={height}
1099
890
  {...rest}
1100
- class="bar-plot {rest.class ?? ``}"
891
+ class={[`bar-plot`, rest.class]}
1101
892
  class:fullscreen
1102
893
  >
1103
894
  {#if width && height}
@@ -1113,41 +904,27 @@
1113
904
  bind:this={svg_element}
1114
905
  role="application"
1115
906
  aria-label={rest[`aria-label`] ??
1116
- ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Bar chart`)}
907
+ ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Bar chart`)}
1117
908
  tabindex="0"
1118
- onfocusin={() => (is_focused = true)}
1119
- onfocusout={() => (is_focused = false)}
1120
- onmousedown={handle_mouse_down}
1121
- ondblclick={() => {
1122
- // Reset zoom to initial ranges (undo any pan/zoom)
1123
- ranges.current.x = [...ranges.initial.x] as [number, number]
1124
- ranges.current.x2 = [...ranges.initial.x2] as [number, number]
1125
- ranges.current.y = [...ranges.initial.y] as [number, number]
1126
- ranges.current.y2 = [...ranges.initial.y2] as [number, number]
1127
- // Also reset axis props so future data changes recalculate auto ranges
1128
- x_axis = { ...x_axis, range: [null, null] }
1129
- x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
1130
- y_axis = { ...y_axis, range: [null, null] }
1131
- y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
1132
- }}
909
+ onfocusin={() => pan_zoom.set_focused(true)}
910
+ onfocusout={() => pan_zoom.set_focused(false)}
911
+ onmousedown={pan_zoom.on_mouse_down}
912
+ ondblclick={pan_zoom.reset_view}
913
+ onkeydown={pan_zoom.on_key_down}
1133
914
  onmouseleave={() => {
1134
915
  hovered = false
1135
916
  hover_info = null
1136
917
  change(null)
1137
918
  on_bar_hover?.(null)
1138
919
  }}
1139
- onwheel={handle_wheel}
1140
- ontouchstart={handle_touch_start}
1141
- ontouchmove={handle_touch_move}
1142
- ontouchend={handle_touch_end}
1143
- ontouchcancel={handle_touch_end}
1144
- style:cursor={pan_drag_state
1145
- ? `grabbing`
1146
- : shift_held && pan?.enabled !== false
1147
- ? `grab`
1148
- : `crosshair`}
920
+ onwheel={pan_zoom.on_wheel}
921
+ ontouchstart={pan_zoom.on_touch_start}
922
+ ontouchmove={pan_zoom.on_touch_move}
923
+ ontouchend={pan_zoom.on_touch_end}
924
+ ontouchcancel={pan_zoom.on_touch_end}
925
+ style:cursor={pan_zoom.cursor}
1149
926
  >
1150
- <ZoomRect start={drag_state.start} current={drag_state.current} />
927
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
1151
928
 
1152
929
  <!-- User content (custom overlays, reference lines, etc.) -->
1153
930
  {@render user_content?.({
@@ -1171,38 +948,38 @@
1171
948
  <!-- X-axis -->
1172
949
  <PlotAxis
1173
950
  side="x"
1174
- ticks={ticks.x as number[]}
951
+ ticks={ticks.x}
1175
952
  place={scales.x}
1176
953
  axis={x_axis}
1177
- domain={ranges.current.x as Vec2}
954
+ domain={ranges.current.x}
1178
955
  {pad}
1179
956
  {width}
1180
957
  {height}
1181
958
  show_grid={display.x_grid}
1182
959
  tick_label={(tick) =>
1183
- get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
960
+ get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
1184
961
  label_x={pad.l + chart_width / 2 + (x_axis.label_shift?.x ?? 0)}
1185
- label_y={height - pad.b / 3 + (x_axis.label_shift?.y ?? 0)}
962
+ label_y={height - pad.b + AXIS_TITLE_OFFSET + (x_axis.label_shift?.y ?? 0)}
1186
963
  axis_loading={axis_loading === `x`}
1187
964
  on_axis_change={(key) => handle_axis_change(`x`, key)}
1188
965
  />
1189
966
 
1190
967
  <!-- X2-axis (Top) -->
1191
968
  <!-- Note: x2 axis is only supported for vertical orientation -->
1192
- {#if x2_series.length > 0 && orientation === `vertical`}
969
+ {#if show_x2}
1193
970
  <PlotAxis
1194
971
  side="x2"
1195
- ticks={ticks.x2 as number[]}
972
+ ticks={ticks.x2}
1196
973
  place={scales.x2}
1197
974
  axis={x2_axis}
1198
- domain={ranges.current.x2 as Vec2}
975
+ domain={ranges.current.x2}
1199
976
  {pad}
1200
977
  {width}
1201
978
  {height}
1202
979
  show_grid={display.x2_grid}
1203
980
  tick_label={(tick) => get_tick_label(tick, x2_axis.ticks)}
1204
981
  label_x={pad.l + chart_width / 2 + (x2_axis.label_shift?.x ?? 0)}
1205
- label_y={Math.max(12, pad.t - (x2_axis.label_shift?.y ?? 40))}
982
+ label_y={Math.max(12, pad.t - (x2_axis.label_shift?.y ?? AXIS_TITLE_OFFSET))}
1206
983
  axis_loading={axis_loading === `x2`}
1207
984
  on_axis_change={(key) => handle_axis_change(`x2`, key)}
1208
985
  />
@@ -1211,19 +988,20 @@
1211
988
  <!-- Y-axis -->
1212
989
  <PlotAxis
1213
990
  side="y"
1214
- ticks={ticks.y as number[]}
991
+ ticks={ticks.y}
1215
992
  place={scales.y}
1216
993
  axis={y_axis}
1217
- domain={ranges.current.y as Vec2}
994
+ domain={ranges.current.y}
1218
995
  {pad}
1219
996
  {width}
1220
997
  {height}
1221
998
  show_grid={display.y_grid}
1222
999
  tick_label={(tick) =>
1223
- get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
1000
+ get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
1224
1001
  label_x={Math.max(
1225
1002
  12,
1226
- pad.l - (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1003
+ pad.l -
1004
+ (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) -
1227
1005
  LABEL_GAP_DEFAULT,
1228
1006
  ) + (y_axis.label_shift?.x ?? 0)}
1229
1007
  label_y={pad.t + chart_height / 2 + (y_axis.label_shift?.y ?? 0)}
@@ -1233,13 +1011,13 @@
1233
1011
 
1234
1012
  <!-- Y2-axis (Right) -->
1235
1013
  <!-- Note: y2 axis is only supported for vertical orientation. Implementing x2 for horizontal mode requires additional complexity. -->
1236
- {#if y2_series.length > 0 && orientation === `vertical`}
1014
+ {#if show_y2}
1237
1015
  <PlotAxis
1238
1016
  side="y2"
1239
- ticks={ticks.y2 as number[]}
1017
+ ticks={ticks.y2}
1240
1018
  place={scales.y2}
1241
1019
  axis={y2_axis}
1242
- domain={ranges.current.y2 as Vec2}
1020
+ domain={ranges.current.y2}
1243
1021
  {pad}
1244
1022
  {width}
1245
1023
  {height}
@@ -1280,8 +1058,8 @@
1280
1058
  y2_scale_type={y2_axis.scale_type}
1281
1059
  x_is_time={x_axis.format?.startsWith(`%`) ?? false}
1282
1060
  x2_is_time={x2_axis.format?.startsWith(`%`) ?? false}
1283
- has_x2={x2_series.length > 0}
1284
- has_y2={y2_series.length > 0}
1061
+ has_x2={show_x2}
1062
+ has_y2={show_y2}
1285
1063
  {width}
1286
1064
  {height}
1287
1065
  {pad}
@@ -1299,7 +1077,7 @@
1299
1077
  class={is_line ? `line-series` : `bar-series`}
1300
1078
  data-series-idx={series_idx}
1301
1079
  opacity={hovered_legend_series_idx !== null &&
1302
- hovered_legend_series_idx !== series_idx
1080
+ hovered_legend_series_idx !== series_idx
1303
1081
  ? 0.25
1304
1082
  : 1}
1305
1083
  >
@@ -1313,21 +1091,22 @@
1313
1091
  {@const use_x2 = srs.x_axis === `x2`}
1314
1092
  {@const x_scale = use_x2 ? scales.x2 : scales.x}
1315
1093
  {@const series_markers = srs.markers ?? DEFAULT_MARKERS}
1316
- {@const show_line = series_markers === `line` ||
1317
- series_markers === `line+points`}
1318
- {@const show_points = series_markers === `points` ||
1319
- series_markers === `line+points`}
1094
+ {@const show_line =
1095
+ series_markers === `line` || series_markers === `line+points`}
1096
+ {@const show_points =
1097
+ series_markers === `points` || series_markers === `line+points`}
1320
1098
  {@const points = compute_line_points({
1321
- series: srs,
1322
- series_idx,
1323
- orientation,
1324
- x_scale,
1325
- y_scale,
1326
- cat_y_scale: scales.y,
1327
- })}
1328
- {@const polyline_str = show_line && points.length > 1
1329
- ? points.map((pt) => `${pt.x},${pt.y}`).join(` `)
1330
- : ``}
1099
+ series: srs,
1100
+ series_idx,
1101
+ orientation,
1102
+ x_scale,
1103
+ y_scale,
1104
+ cat_y_scale: scales.y,
1105
+ })}
1106
+ {@const polyline_str =
1107
+ show_line && points.length > 1
1108
+ ? points.map((pt) => `${pt.x},${pt.y}`).join(` `)
1109
+ : ``}
1331
1110
  {#if polyline_str}
1332
1111
  <polyline
1333
1112
  points={polyline_str}
@@ -1376,44 +1155,41 @@
1376
1155
  {#if show_points}
1377
1156
  {@const clickable = on_bar_click || on_point_click}
1378
1157
  {@const get_pt = (evt: Event) => {
1379
- const attr = evt.target instanceof Element
1380
- ? evt.target.closest(`[data-bar-idx]`)?.getAttribute(
1381
- `data-bar-idx`,
1382
- )
1383
- : null
1384
- return points.find((pt) => pt.idx === parseInt(attr ?? ``, 10))
1385
- }}
1158
+ const attr =
1159
+ evt.target instanceof Element
1160
+ ? evt.target.closest(`[data-bar-idx]`)?.getAttribute(`data-bar-idx`)
1161
+ : null
1162
+ return points.find((pt) => pt.idx === parseInt(attr ?? ``, 10))
1163
+ }}
1386
1164
  {@const set_hover = (
1387
- pt: LineSeriesPoint | null,
1388
- evt: MouseEvent | FocusEvent,
1389
- ) => {
1390
- if (pt) {
1391
- hovered = true
1392
- const fill = line_point_fill(pt, color)
1393
- hover_info = get_bar_data(series_idx, pt.idx, fill)
1394
- change(hover_info)
1395
- } else {
1396
- change(null)
1397
- hover_info = null
1398
- }
1399
- on_bar_hover?.(pt ? { ...hover_info!, event: evt } : null)
1400
- on_point_hover?.(
1401
- pt ? { ...hover_info!, event: evt, point: pt } : null,
1402
- )
1403
- }}
1165
+ pt: LineSeriesPoint | null,
1166
+ evt: MouseEvent | FocusEvent,
1167
+ ) => {
1168
+ if (pt) {
1169
+ hovered = true
1170
+ const fill = line_point_fill(pt, color)
1171
+ hover_info = get_bar_data(series_idx, pt.idx, fill)
1172
+ change(hover_info)
1173
+ } else {
1174
+ change(null)
1175
+ hover_info = null
1176
+ }
1177
+ on_bar_hover?.(pt ? { ...hover_info!, event: evt } : null)
1178
+ on_point_hover?.(pt ? { ...hover_info!, event: evt, point: pt } : null)
1179
+ }}
1404
1180
  {@const do_click = (
1405
- pt: LineSeriesPoint,
1406
- evt: MouseEvent | KeyboardEvent,
1407
- ) => {
1408
- const fill = line_point_fill(pt, color)
1409
- const bar_data = get_bar_data(series_idx, pt.idx, fill)
1410
- on_bar_click?.({ ...bar_data, event: evt })
1411
- on_point_click?.({ ...bar_data, event: evt, point: pt })
1412
- }}
1181
+ pt: LineSeriesPoint,
1182
+ evt: MouseEvent | KeyboardEvent,
1183
+ ) => {
1184
+ const fill = line_point_fill(pt, color)
1185
+ const bar_data = get_bar_data(series_idx, pt.idx, fill)
1186
+ on_bar_click?.({ ...bar_data, event: evt })
1187
+ on_point_click?.({ ...bar_data, event: evt, point: pt })
1188
+ }}
1413
1189
  {@const leaving = (evt: MouseEvent | FocusEvent) =>
1414
- (evt.relatedTarget instanceof Element
1415
- ? evt.relatedTarget.closest(`.line-points`)
1416
- : null) !== evt.currentTarget}
1190
+ (evt.relatedTarget instanceof Element
1191
+ ? evt.relatedTarget.closest(`.line-points`)
1192
+ : null) !== evt.currentTarget}
1417
1193
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_mouse_events_have_key_events -->
1418
1194
  <g
1419
1195
  class="line-points"
@@ -1447,11 +1223,13 @@
1447
1223
  {#each points as pt (pt.idx)}
1448
1224
  {@const sty = pt.point_style}
1449
1225
  {@const fl = line_point_fill(pt, color)}
1450
- {@const rad = pt.size_value != null
1451
- ? size_scale_fn(pt.size_value)
1452
- : sty?.radius ?? 4}
1453
- {@const hov = hover_info?.series_idx === series_idx &&
1454
- hover_info?.bar_idx === pt.idx}
1226
+ {@const rad =
1227
+ pt.size_value != null
1228
+ ? size_scale_fn(pt.size_value)
1229
+ : (sty?.radius ?? 4)}
1230
+ {@const hov =
1231
+ hover_info?.series_idx === series_idx &&
1232
+ hover_info?.bar_idx === pt.idx}
1455
1233
  <ScatterPoint
1456
1234
  x={pt.x}
1457
1235
  y={pt.y}
@@ -1482,31 +1260,30 @@
1482
1260
  <!-- Render as bars -->
1483
1261
  {#each srs.x as x_val, bar_idx (bar_idx)}
1484
1262
  {@const y_val = srs.y[bar_idx]}
1485
- {@const base = mode === `stacked`
1486
- ? (stacked_offsets[series_idx]?.[bar_idx] ?? 0)
1487
- : 0}
1263
+ {@const base =
1264
+ mode === `stacked` ? (stacked_offsets[series_idx]?.[bar_idx] ?? 0) : 0}
1488
1265
  {@const color = srs.color ?? bar_state.color ?? `steelblue`}
1489
1266
  {@const bar_width_val = Array.isArray(srs.bar_width)
1490
- ? (srs.bar_width[bar_idx] ?? 0.5)
1491
- : (srs.bar_width ?? 0.5)}
1267
+ ? (srs.bar_width[bar_idx] ?? 0.5)
1268
+ : (srs.bar_width ?? 0.5)}
1492
1269
  {@const is_vertical = orientation === `vertical`}
1493
1270
  {@const x_scale_bar = srs.x_axis === `x2` ? scales.x2 : scales.x}
1494
1271
  {@const [cat_scale, val_scale] = is_vertical
1495
- ? [x_scale_bar, srs.y_axis === `y2` ? scales.y2 : scales.y]
1496
- : [scales.y, x_scale_bar]}
1272
+ ? [x_scale_bar, srs.y_axis === `y2` ? scales.y2 : scales.y]
1273
+ : [scales.y, x_scale_bar]}
1497
1274
  {@const { c0, c1, v0, v1, rect_x, rect_y, rect_w, rect_h } =
1498
- compute_bar_rect({
1499
- cat_val: x_val,
1500
- val: y_val,
1501
- base,
1502
- bar_width_val,
1503
- series_idx,
1504
- mode,
1505
- orientation,
1506
- group_info,
1507
- cat_scale,
1508
- val_scale,
1509
- })}
1275
+ compute_bar_rect({
1276
+ cat_val: x_val,
1277
+ val: y_val,
1278
+ base,
1279
+ bar_width_val,
1280
+ series_idx,
1281
+ mode,
1282
+ orientation,
1283
+ group_info,
1284
+ cat_scale,
1285
+ val_scale,
1286
+ })}
1510
1287
  {#if (is_vertical ? rect_h : rect_w) > 0}
1511
1288
  <path
1512
1289
  d={bar_path(
@@ -1533,10 +1310,10 @@
1533
1310
  on_bar_hover?.(null)
1534
1311
  }}
1535
1312
  onclick={(evt) =>
1536
- on_bar_click?.({
1537
- ...get_bar_data(series_idx, bar_idx, color),
1538
- event: evt,
1539
- })}
1313
+ on_bar_click?.({
1314
+ ...get_bar_data(series_idx, bar_idx, color),
1315
+ event: evt,
1316
+ })}
1540
1317
  onkeydown={(evt) => {
1541
1318
  if (evt.key === `Enter` || evt.key === ` `) {
1542
1319
  evt.preventDefault()
@@ -1568,20 +1345,44 @@
1568
1345
 
1569
1346
  {@render ref_lines_layer(ref_lines_by_z.below_points)}
1570
1347
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
1348
+
1349
+ <!-- Marginal distribution strips -->
1350
+ <PlotMarginals
1351
+ marginals={resolved_marginals}
1352
+ series={marginal_series}
1353
+ {width}
1354
+ {height}
1355
+ {pad}
1356
+ has_axis={marginal_has_axis}
1357
+ axes={{
1358
+ x1: marginal_axis(
1359
+ scales.x,
1360
+ ranges.current.x,
1361
+ x_axis,
1362
+ cat_axis === `x` ? (pos) => category_list[Math.round(pos)] : undefined,
1363
+ ),
1364
+ x2: marginal_axis(scales.x2, ranges.current.x2, x2_axis),
1365
+ y1: marginal_axis(
1366
+ scales.y,
1367
+ ranges.current.y,
1368
+ y_axis,
1369
+ cat_axis === `y` ? (pos) => category_list[Math.round(pos)] : undefined,
1370
+ ),
1371
+ y2: marginal_axis(scales.y2, ranges.current.y2, y2_axis),
1372
+ }}
1373
+ id={clip_path_id}
1374
+ />
1571
1375
  </svg>
1572
1376
 
1573
1377
  <!-- Legend -->
1574
1378
  {#if legend && (show_legend !== undefined ? show_legend : series.length > 1)}
1575
- {@const legend_left = legend_auto_outside
1576
- ? legend_outside_x
1577
- : legend_placement
1578
- ? tweened_legend_coords.current.x
1579
- : pad.l + 10}
1580
- {@const legend_top = legend_auto_outside
1581
- ? legend_outside_y
1582
- : legend_placement
1583
- ? tweened_legend_coords.current.y
1584
- : pad.t + 10}
1379
+ {@const legend_pos = placed_coords(
1380
+ legend_auto_outside,
1381
+ { x: legend_outside_x, y: legend_outside_y },
1382
+ legend_placement,
1383
+ legend_tween.coords.current,
1384
+ { x: pad.l + 10, y: pad.t + 10 },
1385
+ )}
1585
1386
  <PlotLegend
1586
1387
  bind:root_element={legend_element}
1587
1388
  {...legend}
@@ -1589,16 +1390,15 @@
1589
1390
  on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1590
1391
  on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
1591
1392
  on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1592
- on_hover_change={legend_hover.set_locked}
1393
+ on_hover_change={legend_tween.set_locked}
1593
1394
  on_item_hover={(item) =>
1594
- (hovered_legend_series_idx = item != null && item.series_idx >= 0
1595
- ? item.series_idx
1596
- : null)}
1395
+ (hovered_legend_series_idx =
1396
+ item != null && item.series_idx >= 0 ? item.series_idx : null)}
1597
1397
  active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1598
1398
  style={`
1599
1399
  position: absolute;
1600
- left: ${legend_left}px;
1601
- top: ${legend_top}px;
1400
+ left: ${legend_pos.x}px;
1401
+ top: ${legend_pos.y}px;
1602
1402
  pointer-events: auto;
1603
1403
  ${legend?.style || ``}
1604
1404
  `}
@@ -1607,11 +1407,11 @@
1607
1407
 
1608
1408
  {#if hover_info && hovered}
1609
1409
  {@const cx = (hover_info.active_x_axis === `x2` ? scales.x2 : scales.x)(
1610
- hover_info.orient_x,
1611
- )}
1410
+ hover_info.orient_x,
1411
+ )}
1612
1412
  {@const cy = (hover_info.active_y_axis === `y2` ? scales.y2 : scales.y)(
1613
- hover_info.orient_y,
1614
- )}
1413
+ hover_info.orient_y,
1414
+ )}
1615
1415
  <PlotTooltip
1616
1416
  x={cx}
1617
1417
  y={cy}
@@ -1628,16 +1428,16 @@
1628
1428
  <div><strong>{series_label}</strong></div>
1629
1429
  {/if}
1630
1430
  <div>
1631
- {@html sanitize_html(hover_info.x_axis.label || `x`)}: {
1632
- (cat_axis === `x` ? hover_info.category_label : undefined) ??
1633
- format_value(hover_info.orient_x, hover_info.x_axis.format || `.3~s`)
1634
- }
1431
+ {@html sanitize_html(hover_info.x_axis.label || `x`)}: {(cat_axis === `x`
1432
+ ? hover_info.category_label
1433
+ : undefined) ??
1434
+ format_value_or_num(hover_info.orient_x, hover_info.x_axis.format)}
1635
1435
  </div>
1636
1436
  <div>
1637
- {@html sanitize_html(hover_info.y_axis.label || `y`)}: {
1638
- (cat_axis === `y` ? hover_info.category_label : undefined) ??
1639
- format_value(hover_info.orient_y, hover_info.y_axis.format || `.3~s`)
1640
- }
1437
+ {@html sanitize_html(hover_info.y_axis.label || `y`)}: {(cat_axis === `y`
1438
+ ? hover_info.category_label
1439
+ : undefined) ??
1440
+ format_value_or_num(hover_info.orient_y, hover_info.y_axis.format)}
1641
1441
  </div>
1642
1442
  {/if}
1643
1443
  </PlotTooltip>
@@ -1661,12 +1461,12 @@
1661
1461
  bind:y_axis
1662
1462
  bind:y2_axis={y2_axis_prop}
1663
1463
  bind:display
1664
- auto_x_range={auto_ranges.x as Vec2}
1665
- auto_x2_range={auto_ranges.x2 as Vec2}
1666
- auto_y_range={auto_ranges.y as Vec2}
1667
- auto_y2_range={auto_ranges.y2 as Vec2}
1668
- has_x2_points={x2_series.length > 0}
1669
- has_y2_points={y2_series.length > 0}
1464
+ auto_x_range={auto_ranges.x}
1465
+ auto_x2_range={auto_ranges.x2}
1466
+ auto_y_range={auto_ranges.y}
1467
+ auto_y2_range={auto_ranges.y2}
1468
+ has_x2_points={show_x2}
1469
+ has_y2_points={show_y2}
1670
1470
  children={controls_extra}
1671
1471
  />
1672
1472
  {/if}
@@ -1726,7 +1526,9 @@
1726
1526
  .bar-plot :global(.pane-toggle),
1727
1527
  .bar-plot .header-controls {
1728
1528
  opacity: 0;
1729
- transition: opacity 0.2s, background-color 0.2s;
1529
+ transition:
1530
+ opacity 0.2s,
1531
+ background-color 0.2s;
1730
1532
  }
1731
1533
  .bar-plot:hover :global(.pane-toggle),
1732
1534
  .bar-plot:hover .header-controls,