matterviz 0.3.7 → 0.4.1

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 (486) hide show
  1. package/dist/Icon.svelte +7 -4
  2. package/dist/MillerIndexInput.svelte +1 -1
  3. package/dist/api/optimade.js +32 -26
  4. package/dist/app.css +0 -3
  5. package/dist/brillouin/BrillouinZone.svelte +76 -148
  6. package/dist/brillouin/BrillouinZone.svelte.d.ts +6 -14
  7. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  10. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  11. package/dist/brillouin/BrillouinZoneScene.svelte +97 -205
  12. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +4 -23
  13. package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
  14. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  15. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  16. package/dist/brillouin/compute.d.ts +2 -0
  17. package/dist/brillouin/compute.js +89 -90
  18. package/dist/brillouin/geometry.d.ts +8 -0
  19. package/dist/brillouin/geometry.js +57 -0
  20. package/dist/brillouin/index.d.ts +2 -0
  21. package/dist/brillouin/index.js +2 -0
  22. package/dist/brillouin/types.d.ts +2 -2
  23. package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  25. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +109 -203
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  27. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +180 -470
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  29. package/dist/chempot-diagram/async-compute.svelte.js +3 -1
  30. package/dist/chempot-diagram/chempot-worker.js +2 -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 +4 -4
  34. package/dist/chempot-diagram/compute.js +20 -20
  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.js +8 -7
  45. package/dist/composition/FormulaFilter.svelte +18 -11
  46. package/dist/composition/PieChart.svelte +11 -10
  47. package/dist/composition/chem-sys.d.ts +8 -0
  48. package/dist/composition/chem-sys.js +86 -0
  49. package/dist/composition/format.js +7 -4
  50. package/dist/composition/index.d.ts +1 -0
  51. package/dist/composition/index.js +1 -0
  52. package/dist/composition/parse.d.ts +0 -1
  53. package/dist/composition/parse.js +41 -31
  54. package/dist/controls.d.ts +1 -0
  55. package/dist/controls.js +0 -1
  56. package/dist/convex-hull/ConvexHull.svelte +8 -10
  57. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  58. package/dist/convex-hull/ConvexHull2D.svelte +106 -185
  59. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  60. package/dist/convex-hull/ConvexHull3D.svelte +179 -683
  61. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  62. package/dist/convex-hull/ConvexHull4D.svelte +183 -687
  63. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  64. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  65. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  66. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  67. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  68. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  69. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  70. package/dist/convex-hull/ConvexHullStats.svelte +36 -175
  71. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  72. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  73. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  74. package/dist/convex-hull/GasPressureControls.svelte +4 -4
  75. package/dist/convex-hull/TemperatureSlider.svelte +2 -2
  76. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  77. package/dist/convex-hull/barycentric-coords.js +6 -33
  78. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  79. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  80. package/dist/convex-hull/demo-temperature.d.ts +1 -1
  81. package/dist/convex-hull/demo-temperature.js +20 -22
  82. package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
  83. package/dist/convex-hull/gas-thermodynamics.js +22 -30
  84. package/dist/convex-hull/helpers.d.ts +42 -7
  85. package/dist/convex-hull/helpers.js +171 -78
  86. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  87. package/dist/convex-hull/hull-state.svelte.js +124 -0
  88. package/dist/convex-hull/index.d.ts +10 -8
  89. package/dist/convex-hull/index.js +7 -2
  90. package/dist/convex-hull/thermodynamics.js +136 -960
  91. package/dist/convex-hull/types.d.ts +13 -5
  92. package/dist/convex-hull/types.js +12 -0
  93. package/dist/coordination/CoordinationBarPlot.svelte +27 -34
  94. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  95. package/dist/element/BohrAtom.svelte +2 -1
  96. package/dist/element/index.d.ts +4 -0
  97. package/dist/element/index.js +18 -0
  98. package/dist/feedback/DragOverlay.svelte +3 -1
  99. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  100. package/dist/feedback/StatusMessage.svelte +13 -3
  101. package/dist/fermi-surface/FermiSlice.svelte +13 -5
  102. package/dist/fermi-surface/FermiSurface.svelte +78 -151
  103. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  104. package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
  105. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  106. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -221
  107. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  108. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
  109. package/dist/fermi-surface/compute.js +67 -66
  110. package/dist/fermi-surface/export.js +6 -16
  111. package/dist/fermi-surface/index.d.ts +0 -1
  112. package/dist/fermi-surface/index.js +0 -1
  113. package/dist/fermi-surface/parse.d.ts +1 -1
  114. package/dist/fermi-surface/parse.js +71 -79
  115. package/dist/fermi-surface/types.d.ts +3 -2
  116. package/dist/heatmap-matrix/HeatmapMatrix.svelte +69 -52
  117. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  118. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  119. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  120. package/dist/heatmap-matrix/index.d.ts +3 -2
  121. package/dist/heatmap-matrix/index.js +1 -1
  122. package/dist/index.d.ts +1 -0
  123. package/dist/index.js +1 -0
  124. package/dist/io/ExportPane.svelte +166 -0
  125. package/dist/io/ExportPane.svelte.d.ts +17 -0
  126. package/dist/io/decompress.js +5 -4
  127. package/dist/io/export.d.ts +9 -5
  128. package/dist/io/export.js +77 -51
  129. package/dist/io/fetch.d.ts +2 -1
  130. package/dist/io/fetch.js +5 -1
  131. package/dist/io/file-drop.d.ts +8 -1
  132. package/dist/io/file-drop.js +48 -36
  133. package/dist/io/index.d.ts +2 -0
  134. package/dist/io/index.js +10 -0
  135. package/dist/io/types.d.ts +13 -0
  136. package/dist/io/url-drop.js +64 -33
  137. package/dist/isosurface/parse.js +52 -51
  138. package/dist/isosurface/slice.js +5 -4
  139. package/dist/isosurface/types.js +1 -1
  140. package/dist/keyboard.d.ts +3 -0
  141. package/dist/keyboard.js +23 -0
  142. package/dist/labels.d.ts +1 -1
  143. package/dist/labels.js +9 -8
  144. package/dist/layout/FullscreenButton.svelte +33 -0
  145. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  146. package/dist/layout/FullscreenToggle.svelte +8 -14
  147. package/dist/layout/PropertyFilter.svelte +3 -2
  148. package/dist/layout/SettingsSection.svelte +1 -1
  149. package/dist/layout/ViewerChrome.svelte +116 -0
  150. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  151. package/dist/layout/fullscreen.d.ts +4 -0
  152. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  153. package/dist/layout/fullscreen.svelte.js +37 -0
  154. package/dist/layout/index.d.ts +3 -0
  155. package/dist/layout/index.js +3 -0
  156. package/dist/layout/json-tree/JsonNode.svelte +1 -1
  157. package/dist/layout/json-tree/JsonTree.svelte +2 -2
  158. package/dist/layout/json-tree/utils.js +5 -4
  159. package/dist/marching-cubes.js +8 -13
  160. package/dist/math.d.ts +12 -4
  161. package/dist/math.js +42 -30
  162. package/dist/overlays/DraggablePane.svelte +4 -4
  163. package/dist/overlays/index.d.ts +4 -0
  164. package/dist/periodic-table/PeriodicTable.svelte +27 -15
  165. package/dist/periodic-table/PropertySelect.svelte +1 -0
  166. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
  167. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  168. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  169. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  170. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  171. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  172. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  173. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  174. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
  175. package/dist/phase-diagram/build-diagram.js +2 -2
  176. package/dist/phase-diagram/colors.js +1 -1
  177. package/dist/phase-diagram/parse.d.ts +2 -1
  178. package/dist/phase-diagram/parse.js +6 -5
  179. package/dist/phase-diagram/types.d.ts +1 -1
  180. package/dist/phase-diagram/utils.d.ts +3 -3
  181. package/dist/phase-diagram/utils.js +8 -12
  182. package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +246 -841
  183. package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +8 -16
  184. package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
  185. package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
  186. package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +8 -7
  187. package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
  188. package/dist/plot/bar/data.d.ts +40 -0
  189. package/dist/plot/bar/data.js +154 -0
  190. package/dist/plot/bar/geometry.d.ts +39 -0
  191. package/dist/plot/bar/geometry.js +60 -0
  192. package/dist/plot/bar/index.d.ts +3 -0
  193. package/dist/plot/bar/index.js +3 -0
  194. package/dist/plot/box/BoxPlot.svelte +1292 -0
  195. package/dist/plot/box/BoxPlot.svelte.d.ts +95 -0
  196. package/dist/plot/box/BoxPlotControls.svelte +109 -0
  197. package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
  198. package/dist/plot/box/Violin.svelte +14 -0
  199. package/dist/plot/box/Violin.svelte.d.ts +70 -0
  200. package/dist/plot/box/box-plot.d.ts +56 -0
  201. package/dist/plot/box/box-plot.js +129 -0
  202. package/dist/plot/box/index.d.ts +5 -0
  203. package/dist/plot/box/index.js +5 -0
  204. package/dist/plot/box/kde.d.ts +17 -0
  205. package/dist/plot/box/kde.js +160 -0
  206. package/dist/plot/box/quantile.d.ts +3 -0
  207. package/dist/plot/box/quantile.js +53 -0
  208. package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +1 -1
  209. package/dist/plot/{auto-place.js → core/auto-place.js} +6 -3
  210. package/dist/plot/core/axis-utils.d.ts +46 -0
  211. package/dist/plot/core/axis-utils.js +110 -0
  212. package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
  213. package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
  214. package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +41 -38
  215. package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +7 -6
  216. package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
  217. package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
  218. package/dist/plot/core/components/ControlPane.svelte +46 -0
  219. package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
  220. package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
  221. package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
  222. package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
  223. package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
  224. package/dist/plot/{Line.svelte → core/components/Line.svelte} +33 -15
  225. package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +3 -2
  226. package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +9 -6
  227. package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +5 -3
  228. package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
  229. package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
  230. package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
  231. package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
  232. package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
  233. package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
  234. package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
  235. package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
  236. package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
  237. package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +5 -5
  238. package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +5 -5
  239. package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +8 -8
  240. package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +5 -5
  241. package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
  242. package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
  243. package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
  244. package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
  245. package/dist/plot/core/components/index.d.ts +17 -0
  246. package/dist/plot/core/components/index.js +17 -0
  247. package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
  248. package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +21 -23
  249. package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
  250. package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
  251. package/dist/plot/core/fill-utils.d.ts +34 -0
  252. package/dist/plot/core/fill-utils.js +391 -0
  253. package/dist/plot/core/index.d.ts +10 -0
  254. package/dist/plot/core/index.js +11 -0
  255. package/dist/plot/core/interactions.d.ts +39 -0
  256. package/dist/plot/core/interactions.js +209 -0
  257. package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
  258. package/dist/plot/{layout.js → core/layout.js} +16 -8
  259. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  260. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  261. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  262. package/dist/plot/core/placed-tween.svelte.js +68 -0
  263. package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +11 -11
  264. package/dist/plot/{reference-line.js → core/reference-line.js} +29 -42
  265. package/dist/plot/core/scales.d.ts +40 -0
  266. package/dist/plot/{scales.js → core/scales.js} +94 -93
  267. package/dist/plot/core/svg.d.ts +3 -0
  268. package/dist/plot/core/svg.js +41 -0
  269. package/dist/plot/{types.d.ts → core/types.d.ts} +36 -85
  270. package/dist/plot/{types.js → core/types.js} +1 -1
  271. package/dist/plot/{utils → core/utils}/label-placement.d.ts +3 -3
  272. package/dist/plot/{utils → core/utils}/label-placement.js +3 -3
  273. package/dist/plot/core/utils/series-visibility.d.ts +26 -0
  274. package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
  275. package/dist/plot/core/utils.d.ts +12 -0
  276. package/dist/plot/core/utils.js +27 -0
  277. package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +174 -551
  278. package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
  279. package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
  280. package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
  281. package/dist/plot/histogram/index.d.ts +2 -0
  282. package/dist/plot/histogram/index.js +2 -0
  283. package/dist/plot/index.d.ts +8 -41
  284. package/dist/plot/index.js +10 -39
  285. package/dist/plot/sankey/Sankey.svelte +697 -0
  286. package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
  287. package/dist/plot/sankey/SankeyControls.svelte +98 -0
  288. package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
  289. package/dist/plot/sankey/index.d.ts +4 -0
  290. package/dist/plot/sankey/index.js +3 -0
  291. package/dist/plot/sankey/sankey-types.d.ts +42 -0
  292. package/dist/plot/sankey/sankey-types.js +4 -0
  293. package/dist/plot/sankey/sankey.d.ts +52 -0
  294. package/dist/plot/sankey/sankey.js +189 -0
  295. package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +64 -64
  296. package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +6 -6
  297. package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
  298. package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
  299. package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +297 -1008
  300. package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +10 -18
  301. package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
  302. package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +2 -2
  303. package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
  304. package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
  305. package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
  306. package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
  307. package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +5 -12
  308. package/dist/plot/scatter/index.d.ts +7 -0
  309. package/dist/plot/scatter/index.js +5 -0
  310. package/dist/plot/scatter/scatter-data.d.ts +19 -0
  311. package/dist/plot/scatter/scatter-data.js +212 -0
  312. package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +25 -34
  313. package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +9 -17
  314. package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +14 -14
  315. package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +6 -6
  316. package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +129 -128
  317. package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +6 -15
  318. package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +7 -6
  319. package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +5 -4
  320. package/dist/plot/scatter-3d/index.d.ts +4 -0
  321. package/dist/plot/scatter-3d/index.js +4 -0
  322. package/dist/plot/sunburst/Sunburst.svelte +1041 -0
  323. package/dist/plot/sunburst/Sunburst.svelte.d.ts +97 -0
  324. package/dist/plot/sunburst/SunburstControls.svelte +200 -0
  325. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
  326. package/dist/plot/sunburst/index.d.ts +4 -0
  327. package/dist/plot/sunburst/index.js +4 -0
  328. package/dist/plot/sunburst/render.d.ts +34 -0
  329. package/dist/plot/sunburst/render.js +122 -0
  330. package/dist/plot/sunburst/sunburst.d.ts +62 -0
  331. package/dist/plot/sunburst/sunburst.js +269 -0
  332. package/dist/rdf/RdfPlot.svelte +2 -1
  333. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  334. package/dist/rdf/calc-rdf.js +11 -24
  335. package/dist/sanitize.js +14 -3
  336. package/dist/scene/SceneCamera.svelte +62 -0
  337. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  338. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  339. package/dist/scene/bind-renderer.svelte.js +14 -0
  340. package/dist/scene/index.d.ts +4 -0
  341. package/dist/scene/index.js +5 -0
  342. package/dist/scene/props.js +52 -0
  343. package/dist/scene/types.d.ts +26 -0
  344. package/dist/scene/types.js +1 -0
  345. package/dist/settings.d.ts +79 -3
  346. package/dist/settings.js +321 -1
  347. package/dist/spectral/Bands.svelte +47 -36
  348. package/dist/spectral/Bands.svelte.d.ts +6 -6
  349. package/dist/spectral/BandsAndDos.svelte +23 -25
  350. package/dist/spectral/BrillouinBandsDos.svelte +42 -30
  351. package/dist/spectral/Dos.svelte +15 -23
  352. package/dist/spectral/Dos.svelte.d.ts +4 -3
  353. package/dist/spectral/helpers.d.ts +8 -6
  354. package/dist/spectral/helpers.js +137 -65
  355. package/dist/state.svelte.d.ts +0 -7
  356. package/dist/state.svelte.js +0 -6
  357. package/dist/structure/Arrow.svelte +2 -4
  358. package/dist/structure/AtomLegend.svelte +8 -9
  359. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  360. package/dist/structure/CanvasTooltip.svelte +1 -0
  361. package/dist/structure/CellSelect.svelte +12 -5
  362. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  363. package/dist/structure/Cylinder.svelte +12 -8
  364. package/dist/structure/Cylinder.svelte.d.ts +4 -1
  365. package/dist/structure/Lattice.svelte +2 -2
  366. package/dist/structure/Structure.svelte +365 -423
  367. package/dist/structure/Structure.svelte.d.ts +5 -15
  368. package/dist/structure/StructureControls.svelte +217 -2
  369. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  370. package/dist/structure/StructureExportPane.svelte +54 -156
  371. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  372. package/dist/structure/StructureInfoPane.svelte +10 -9
  373. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  374. package/dist/structure/StructureScene.svelte +376 -208
  375. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  376. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  377. package/dist/structure/{label-placement.js → atom-label-placement.js} +15 -5
  378. package/dist/structure/atom-properties.d.ts +1 -1
  379. package/dist/structure/atom-properties.js +17 -22
  380. package/dist/structure/bond-order-perception.js +3 -5
  381. package/dist/structure/bonding.d.ts +4 -0
  382. package/dist/structure/bonding.js +134 -63
  383. package/dist/structure/export.d.ts +24 -4
  384. package/dist/structure/export.js +89 -143
  385. package/dist/structure/index.d.ts +4 -4
  386. package/dist/structure/index.js +3 -3
  387. package/dist/structure/measure.d.ts +3 -2
  388. package/dist/structure/measure.js +6 -5
  389. package/dist/structure/parse.d.ts +3 -2
  390. package/dist/structure/parse.js +419 -438
  391. package/dist/structure/partial-occupancy.d.ts +0 -1
  392. package/dist/structure/partial-occupancy.js +1 -1
  393. package/dist/structure/pbc.d.ts +1 -1
  394. package/dist/structure/pbc.js +190 -13
  395. package/dist/structure/polyhedra.d.ts +41 -0
  396. package/dist/structure/polyhedra.js +602 -0
  397. package/dist/structure/site.d.ts +4 -0
  398. package/dist/structure/site.js +1 -0
  399. package/dist/structure/supercell.js +3 -2
  400. package/dist/structure/validation.js +5 -6
  401. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  402. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  403. package/dist/symmetry/SymmetryElements.svelte +354 -0
  404. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  405. package/dist/symmetry/SymmetryStats.svelte +113 -8
  406. package/dist/symmetry/WyckoffTable.svelte +68 -7
  407. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  408. package/dist/symmetry/cell-transform.js +7 -14
  409. package/dist/symmetry/index.d.ts +14 -4
  410. package/dist/symmetry/index.js +291 -72
  411. package/dist/symmetry/spacegroups.d.ts +12 -1
  412. package/dist/symmetry/spacegroups.js +63 -14
  413. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  414. package/dist/symmetry/symmetry-elements.js +521 -0
  415. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  416. package/dist/symmetry/wyckoff-db.js +87 -0
  417. package/dist/table/HeatmapTable.svelte +66 -25
  418. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  419. package/dist/table/index.d.ts +1 -3
  420. package/dist/table/index.js +1 -1
  421. package/dist/theme/index.js +8 -8
  422. package/dist/tooltip/KCoords.svelte +45 -0
  423. package/dist/tooltip/KCoords.svelte.d.ts +8 -0
  424. package/dist/tooltip/index.d.ts +1 -0
  425. package/dist/tooltip/index.js +1 -0
  426. package/dist/trajectory/Trajectory.svelte +123 -100
  427. package/dist/trajectory/Trajectory.svelte.d.ts +11 -22
  428. package/dist/trajectory/TrajectoryExportPane.svelte +17 -25
  429. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  430. package/dist/trajectory/TrajectoryInfoPane.svelte +5 -3
  431. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  432. package/dist/trajectory/constants.js +6 -2
  433. package/dist/trajectory/extract.js +17 -37
  434. package/dist/trajectory/format-detect.d.ts +1 -1
  435. package/dist/trajectory/format-detect.js +27 -19
  436. package/dist/trajectory/frame-reader.d.ts +0 -1
  437. package/dist/trajectory/frame-reader.js +63 -162
  438. package/dist/trajectory/helpers.d.ts +10 -2
  439. package/dist/trajectory/helpers.js +56 -36
  440. package/dist/trajectory/index.js +1 -1
  441. package/dist/trajectory/parse/ase.d.ts +9 -1
  442. package/dist/trajectory/parse/ase.js +47 -32
  443. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  444. package/dist/trajectory/parse/diagnostics.js +14 -0
  445. package/dist/trajectory/parse/hdf5.js +1 -1
  446. package/dist/trajectory/parse/index.d.ts +1 -1
  447. package/dist/trajectory/parse/index.js +65 -105
  448. package/dist/trajectory/parse/lammps.d.ts +0 -2
  449. package/dist/trajectory/parse/lammps.js +8 -6
  450. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  451. package/dist/trajectory/parse/pymatgen.js +74 -0
  452. package/dist/trajectory/parse/vasp.js +38 -18
  453. package/dist/trajectory/parse/xyz.d.ts +13 -1
  454. package/dist/trajectory/parse/xyz.js +102 -94
  455. package/dist/trajectory/plotting.d.ts +1 -2
  456. package/dist/trajectory/plotting.js +16 -113
  457. package/dist/utils.d.ts +2 -0
  458. package/dist/utils.js +7 -5
  459. package/dist/xrd/XrdPlot.svelte +16 -30
  460. package/dist/xrd/broadening.d.ts +2 -1
  461. package/dist/xrd/calc-xrd.js +18 -20
  462. package/dist/xrd/index.d.ts +2 -2
  463. package/dist/xrd/parse.js +2 -2
  464. package/package.json +43 -26
  465. package/dist/element/data.json +0 -11864
  466. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  467. package/dist/fermi-surface/marching-cubes.js +0 -2
  468. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  469. package/dist/plot/axis-utils.d.ts +0 -19
  470. package/dist/plot/axis-utils.js +0 -78
  471. package/dist/plot/defaults.d.ts +0 -19
  472. package/dist/plot/defaults.js +0 -9
  473. package/dist/plot/fill-utils.d.ts +0 -46
  474. package/dist/plot/fill-utils.js +0 -322
  475. package/dist/plot/hover-lock.svelte.d.ts +0 -14
  476. package/dist/plot/hover-lock.svelte.js +0 -46
  477. package/dist/plot/interactions.d.ts +0 -12
  478. package/dist/plot/interactions.js +0 -101
  479. package/dist/plot/scales.d.ts +0 -48
  480. package/dist/plot/svg.d.ts +0 -1
  481. package/dist/plot/svg.js +0 -11
  482. package/dist/plot/utils/series-visibility.d.ts +0 -15
  483. package/dist/plot/utils.d.ts +0 -1
  484. package/dist/plot/utils.js +0 -14
  485. /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
  486. /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
@@ -2,11 +2,11 @@
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'
7
- import { sanitize_html } from '../sanitize'
8
- import { FullscreenToggle, set_fullscreen_bg } from '../layout'
9
- import type { Point2D, Vec2 } from '../math'
5
+ import type { D3InterpolateName } from '../../colors'
6
+ import { format_value } from '../../labels'
7
+ import { sanitize_html } from '../../sanitize'
8
+ import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
9
+ import type { Point2D, Vec2 } from '../../math'
10
10
  import type {
11
11
  AxisLoadError,
12
12
  BarHandlerProps,
@@ -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,9 +25,9 @@
25
25
  PlotConfig,
26
26
  RefLine,
27
27
  RefLineEvent,
28
- ScaleType,
28
+ SizeScaleConfig,
29
29
  UserContentProps,
30
- } from './'
30
+ } from '..'
31
31
  import {
32
32
  BarPlotControls,
33
33
  compute_element_placement,
@@ -35,56 +35,61 @@
35
35
  PlotLegend,
36
36
  ReferenceLine,
37
37
  ScatterPoint,
38
- } from './'
39
- import type { AxisChangeState } from './axis-utils'
40
- import { create_axis_change_handler } from './axis-utils'
41
- import { process_prop } from './data-transform'
38
+ } from '..'
39
+ import type { AxisChangeState } from '../core/axis-utils'
40
+ import { create_axis_loader } from '../core/axis-utils'
41
+ import { create_placed_tween } from '../core/placed-tween.svelte'
42
+ import { create_pan_zoom } from '../core/pan-zoom.svelte'
43
+ import { create_legend_visibility } from '../core/utils/series-visibility'
42
44
  import {
43
- create_dimension_tracker,
44
- create_hover_lock,
45
- } from './hover-lock.svelte'
46
- import {
47
- get_relative_coords,
48
- pan_range,
49
- PINCH_ZOOM_THRESHOLD,
50
- pixels_to_data_delta,
51
- } from './interactions'
52
- import type { IndexedRefLine } from './reference-line'
53
- import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
45
+ axis_ranges_equal,
46
+ invert_rect_range,
47
+ resolve_axis_ranges,
48
+ } from '../core/interactions'
49
+ import type { IndexedRefLine } from '../core/reference-line'
50
+ import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
54
51
  import {
55
52
  create_color_scale,
56
53
  create_scale,
57
54
  create_size_scale,
58
55
  generate_ticks,
59
- get_nice_data_range,
60
56
  get_tick_label,
61
- } from './scales'
62
- import { DEFAULT_MARKERS, get_scale_type_name } from './types'
63
- import { DEFAULTS } from '../settings'
57
+ } from '../core/scales'
58
+ import { DEFAULT_MARKERS } from '../core/types'
59
+ import { DEFAULTS } from '../../settings'
64
60
  import { extent } from 'd3-array'
65
61
  import type { Snippet } from 'svelte'
66
- import { untrack } from 'svelte'
62
+ import { onDestroy, untrack } from 'svelte'
67
63
  import type { HTMLAttributes } from 'svelte/elements'
68
- import { Tween, type TweenOptions } from 'svelte/motion'
69
- import { SvelteMap } from 'svelte/reactivity'
64
+ import type { TweenOptions } from 'svelte/motion'
70
65
  import {
71
66
  build_obstacles_norm,
72
67
  clip_bar,
73
68
  has_explicit_position,
74
69
  measured_footprint,
75
70
  place_decorations,
76
- } from './auto-place'
71
+ placed_coords,
72
+ } from '../core/auto-place'
77
73
  import {
78
74
  calc_auto_padding,
79
- constrain_tooltip_position,
80
75
  filter_padding,
81
76
  LABEL_GAP_DEFAULT,
77
+ y2_axis_label_x,
82
78
  measure_max_tick_width,
83
- } from './layout'
84
- import PlotTooltip from './PlotTooltip.svelte'
85
- import { bar_path } from './svg'
86
- import ZeroLines from './ZeroLines.svelte'
87
- import ZoomRect from './ZoomRect.svelte'
79
+ } from '../core/layout'
80
+ import PlotTooltip from '../core/components/PlotTooltip.svelte'
81
+ import { bar_path } from '../core/svg'
82
+ import { unique_id } from '../core/utils'
83
+ import ZeroLines from '../core/components/ZeroLines.svelte'
84
+ import ZoomRect from '../core/components/ZoomRect.svelte'
85
+ import {
86
+ compute_bar_auto_ranges,
87
+ compute_group_info,
88
+ compute_stacked_offsets,
89
+ normalize_categorical,
90
+ } from './data'
91
+ import { compute_bar_rect, compute_line_points } from './geometry'
92
+ import type { LineSeriesPoint as BarLineSeriesPoint } from './geometry'
88
93
 
89
94
  // Handler props for line marker events (extends BarHandlerProps with point-specific data)
90
95
  interface LineMarkerHandlerProps extends BarHandlerProps<Metadata> {
@@ -92,27 +97,17 @@
92
97
  }
93
98
 
94
99
  // Extended point type with computed screen coordinates (used internally for rendering)
95
- type LineSeriesPoint = InternalPoint<Metadata> & {
96
- x: number // Screen x coordinate
97
- y: number // Screen y coordinate
98
- data_x: number // Original data x value
99
- data_y: number // Original data y value
100
- idx: number // Index in series
101
- }
100
+ type LineSeriesPoint = BarLineSeriesPoint<Metadata>
102
101
 
103
102
  let {
104
103
  series = $bindable([]),
105
104
  orientation = $bindable(`vertical`),
106
105
  mode = $bindable(`overlay`),
107
106
  x_axis = $bindable({}),
108
- x2_axis = $bindable({}),
107
+ x2_axis: x2_axis_prop = $bindable({}),
109
108
  y_axis = $bindable({}),
110
- y2_axis = $bindable({}),
109
+ y2_axis: y2_axis_prop = $bindable({}),
111
110
  display = $bindable(DEFAULTS.bar.display),
112
- x_range = [null, null],
113
- x2_range = [null, null],
114
- y_range = [null, null],
115
- y2_range = [null, null],
116
111
  range_padding = 0.05,
117
112
  padding = { t: 20, b: 60, l: 60, r: 20 },
118
113
  legend = {},
@@ -174,26 +169,14 @@
174
169
  data: BarHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
175
170
  ) => void
176
171
  on_bar_hover?: (
177
- data:
178
- | (BarHandlerProps<Metadata> & {
179
- event: MouseEvent | FocusEvent | KeyboardEvent
180
- })
181
- | null,
172
+ data: (BarHandlerProps<Metadata> & { event: MouseEvent | FocusEvent | KeyboardEvent }) | null,
182
173
  ) => void
183
174
  // Line marker props (matching ScatterPlot)
184
175
  // Note: For line series with markers, BOTH on_bar_* AND on_point_* events fire.
185
176
  // Use on_point_* for marker-specific data (includes `point` with InternalPoint details)
186
177
  // or on_bar_* for backward compatibility with bar-style event handling.
187
- color_scale?: {
188
- type?: ScaleType
189
- scheme?: D3ColorSchemeName | D3InterpolateName
190
- value_range?: [number, number]
191
- } | D3InterpolateName
192
- size_scale?: {
193
- type?: ScaleType
194
- radius_range?: [number, number]
195
- value_range?: [number, number]
196
- }
178
+ color_scale?: ColorScaleConfig | D3InterpolateName
179
+ size_scale?: SizeScaleConfig
197
180
  point_tween?: TweenOptions<Point2D>
198
181
  on_point_click?: (
199
182
  data: LineMarkerHandlerProps & { event: MouseEvent | KeyboardEvent },
@@ -222,29 +205,31 @@
222
205
  // Initialize bar, line, y2_axis with defaults - using $derived for reactivity
223
206
  let bar_state = $derived({ ...DEFAULTS.bar.bar, ...bar })
224
207
  let line_state = $derived({ ...DEFAULTS.bar.line, ...line })
225
- y2_axis = {
208
+ // Merge secondary-axis defaults as deriveds instead of assigning back into the
209
+ // $bindable props (which would push library defaults into the parent's bound state)
210
+ let y2_axis = $derived({
226
211
  format: ``,
227
212
  scale_type: `linear`,
228
213
  ticks: 5,
229
- label_shift: { y: 60 },
214
+ label_shift: { x: 0, y: 0 }, // y2 title stays vertically centered (x pos set by y2_axis_label_x)
230
215
  tick: { label: { shift: { x: 0, y: 0 } } }, // base offset handled in rendering
231
216
  range: [null, null],
232
- ...y2_axis,
233
- }
234
- x2_axis = {
217
+ ...y2_axis_prop,
218
+ } as typeof y2_axis_prop)
219
+ let x2_axis = $derived({
235
220
  format: ``,
236
221
  scale_type: `linear`,
237
222
  ticks: 5,
238
223
  label_shift: { x: 0, y: 40 },
239
224
  tick: { label: { shift: { x: 0, y: 0 } } },
240
225
  range: [null, null],
241
- ...x2_axis,
242
- }
226
+ ...x2_axis_prop,
227
+ } as typeof x2_axis_prop)
243
228
 
244
229
  let [width, height] = $state([0, 0])
245
230
  let wrapper: HTMLDivElement | undefined = $state()
246
231
  let svg_element: SVGElement | null = $state(null)
247
- let clip_path_id = `chart-clip-${crypto?.randomUUID?.()}`
232
+ const clip_path_id = unique_id(`chart-clip`) // stable, collision-resistant (see unique_id)
248
233
 
249
234
  // Reference line hover state
250
235
  let hovered_ref_line_idx = $state<number | null>(null)
@@ -256,55 +241,25 @@
256
241
  let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
257
242
  let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
258
243
 
259
- // === Categorical Normalization ===
260
- // Internal type with guaranteed numeric x (for downstream scale/rendering code)
261
- type NumericBarSeries = Omit<BarSeries<Metadata>, `x`> & { x: readonly number[] }
262
-
263
- let is_categorical = $derived(
264
- series.some((srs) => srs.x.some((val) => typeof val === `string`)),
265
- )
266
-
267
- let category_list = $derived.by(() => {
268
- if (!is_categorical) return [] as string[]
269
- if (x_axis.categories?.length) return [...x_axis.categories]
270
- return [...new Set(series.flatMap((srs) => srs.x.map(String)))]
271
- })
244
+ // === Categorical Normalization (string x values -> integer indices, see ./data) ===
245
+ let cat_norm = $derived(normalize_categorical(series, x_axis.categories))
246
+ let category_list = $derived(cat_norm.category_list)
247
+ let internal_series = $derived(cat_norm.internal_series)
272
248
 
273
249
  let category_indices = $derived(
274
- category_list.length ? category_list.map((_, idx) => idx) : null,
250
+ category_list.length > 0 ? category_list.map((_, idx) => idx) : null,
275
251
  )
276
252
 
277
- let internal_series = $derived.by<NumericBarSeries[]>(() => {
278
- // safe: when !category_indices, all x values are numeric (is_categorical is false)
279
- if (!category_indices) return series as unknown as NumericBarSeries[]
280
- return series.map((srs) => {
281
- const orig_map = new Map(srs.x.map((val, idx) => [String(val), idx]))
282
- if (orig_map.size < srs.x.length) {
283
- console.warn(
284
- `BarPlot: series "${
285
- srs.label ?? `?`
286
- }" has duplicate x values last occurrence wins`,
287
- )
288
- }
289
- // Resolve original index for each category (undefined if series lacks it)
290
- const orig_indices = category_list.map((cat) => orig_map.get(cat))
291
- const remap = <T>(arr: readonly T[] | null | undefined, fallback: T): T[] =>
292
- orig_indices.map((oi) => oi != null ? (arr?.[oi] ?? fallback) : fallback)
293
- const bw_arr = Array.isArray(srs.bar_width) ? srs.bar_width : null
294
- const meta_arr = Array.isArray(srs.metadata) ? srs.metadata : null
295
- return {
296
- ...srs,
297
- x: category_indices,
298
- y: remap(srs.y, srs.render_mode === `line` ? NaN : 0),
299
- labels: remap(srs.labels, null),
300
- metadata: orig_indices.map((oi) =>
301
- oi != null ? (meta_arr ? meta_arr[oi] : srs.metadata) : undefined
302
- ) as Metadata[],
303
- ...(bw_arr ? { bar_width: remap(bw_arr, 0.5) } : {}),
304
- ...(srs.color_values ? { color_values: remap(srs.color_values, null) } : {}),
305
- ...(srs.size_values ? { size_values: remap(srs.size_values, null) } : {}),
306
- } as NumericBarSeries
307
- })
253
+ // Thin categorical tick labels + grid lines when many categories would overlap.
254
+ // Bars still render for every category (this only reduces drawn ticks/labels/grid).
255
+ let cat_tick_indices = $derived.by<number[]>(() => {
256
+ if (!category_indices) return []
257
+ const axis_px = (orientation === `horizontal` ? height : width) || 0
258
+ const max_ticks = Math.max(1, Math.floor(axis_px / 28)) // ~28px per category label
259
+ const step = Math.ceil(category_indices.length / max_ticks)
260
+ return step <= 1
261
+ ? category_indices
262
+ : category_indices.filter((_, idx) => idx % step === 0)
308
263
  })
309
264
 
310
265
  // Compute auto ranges from visible series
@@ -323,124 +278,26 @@
323
278
  visible_series.filter((srs) => srs.x_axis === `x2`),
324
279
  )
325
280
 
326
- let auto_ranges = $derived.by(() => {
327
- // Calculate separate ranges for y1 and y2 axes
328
- const calc_y_range = (
329
- series_list: typeof visible_series,
330
- y_limit: typeof y_range,
331
- scale_type: ScaleType,
332
- ) => {
333
- let points = series_list.flatMap((srs) =>
334
- srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
335
- )
336
-
337
- // In stacked mode, calculate stacked totals for accurate range (only for bars on the same axis)
338
- if (mode === `stacked`) {
339
- const stacked_totals = new SvelteMap<number, { pos: number; neg: number }>()
340
-
341
- // Only include visible bar series (not lines) in stacking
342
- series_list
343
- .filter((srs) => srs.render_mode !== `line`)
344
- .forEach((srs) =>
345
- srs.x.forEach((x_val, idx) => {
346
- const y_val = srs.y[idx] ?? 0
347
- const totals = stacked_totals.get(x_val) ?? { pos: 0, neg: 0 }
348
- if (y_val >= 0) totals.pos += y_val
349
- else totals.neg += y_val
350
- stacked_totals.set(x_val, totals)
351
- })
352
- )
353
-
354
- // Replace points with stacked totals + line series (which don't stack)
355
- points = [
356
- ...Array.from(stacked_totals).flatMap(([x_val, { pos, neg }]) => [
357
- ...(pos > 0 ? [{ x: x_val, y: pos }] : []),
358
- ...(neg < 0 ? [{ x: x_val, y: neg }] : []),
359
- ]),
360
- ...series_list
361
- .filter((srs) => srs.render_mode === `line`)
362
- .flatMap((srs) =>
363
- srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
364
- ),
365
- ]
366
- }
367
-
368
- if (!points.length) return [0, 1]
369
-
370
- let computed_y_range = get_nice_data_range(
371
- points,
372
- (pt) => pt.y,
373
- y_limit,
374
- scale_type,
375
- range_padding,
376
- false,
377
- )
378
-
379
- // For bar plots, ensure the value axis starts at 0 unless there are negative values
380
- // Only apply zero-clamping for linear and arcsinh scales (not log)
381
- const type_name = get_scale_type_name(scale_type)
382
- if (type_name === `linear` || type_name === `arcsinh`) {
383
- const has_negative = points.some((pt) => pt.y < 0)
384
- const has_positive = points.some((pt) => pt.y > 0)
385
-
386
- // Only adjust if no explicit y_range is set
387
- if (y_limit?.[0] == null && y_limit?.[1] == null) {
388
- if (has_positive && !has_negative) computed_y_range = [0, computed_y_range[1]]
389
- else if (has_negative && !has_positive) computed_y_range = [computed_y_range[0], 0]
390
- }
391
- }
392
-
393
- return computed_y_range
394
- }
395
-
396
- // Get x values split by axis for range calculation
397
- // For categorical data, use fixed range centered on integer indices
398
- let x_auto_range: number[]
399
- if (category_list.length) {
400
- x_auto_range = [-0.5, category_list.length - 0.5]
401
- } else {
402
- const x1_x_points = visible_series
403
- .filter((srs) => (srs.x_axis ?? `x1`) === `x1`)
404
- .flatMap((srs) => srs.x.map((x_val) => ({ x: x_val, y: 0 })))
405
- x_auto_range = x1_x_points.length
406
- ? get_nice_data_range(
407
- x1_x_points,
408
- (pt) => pt.x,
409
- x_range,
410
- x_axis.scale_type ?? `linear`,
411
- range_padding,
412
- x_axis.format?.startsWith(`%`) || false,
413
- )
414
- : [0, 1]
415
- }
416
-
417
- const x2_x_points = x2_series.flatMap((srs) =>
418
- srs.x.map((x_val) => ({ x: x_val, y: 0 }))
419
- )
420
- const x2_scale_type = x2_axis.scale_type ?? `linear`
421
- const x2_auto_range = x2_x_points.length
422
- ? get_nice_data_range(
423
- x2_x_points,
424
- (pt) => pt.x,
425
- x2_range,
426
- x2_scale_type,
427
- range_padding,
428
- x2_axis.format?.startsWith(`%`) || false,
429
- )
430
- : [0, 1]
431
-
432
- const y1_range = calc_y_range(y1_series, y_range, y_axis.scale_type ?? `linear`)
433
- const y2_auto_range = calc_y_range(
434
- y2_series,
435
- y2_range,
436
- y2_axis.scale_type ?? `linear`,
437
- )
438
-
439
- // Map data ranges to axis ranges depending on orientation
440
- return orientation === `horizontal`
441
- ? ({ x: y1_range, x2: x2_auto_range, y: x_auto_range, y2: y2_auto_range })
442
- : ({ x: x_auto_range, x2: x2_auto_range, y: y1_range, y2: y2_auto_range })
443
- })
281
+ let auto_ranges = $derived(compute_bar_auto_ranges({
282
+ visible_series,
283
+ y1_series,
284
+ y2_series,
285
+ x2_series,
286
+ mode,
287
+ orientation,
288
+ range_padding,
289
+ category_count: category_list.length,
290
+ x_range: x_axis.range ?? [null, null],
291
+ x_scale_type: x_axis.scale_type ?? `linear`,
292
+ x_is_time: x_axis.format?.startsWith(`%`) || false,
293
+ x2_range: x2_axis.range ?? [null, null],
294
+ x2_scale_type: x2_axis.scale_type ?? `linear`,
295
+ x2_is_time: x2_axis.format?.startsWith(`%`) || false,
296
+ y_range: y_axis.range ?? [null, null],
297
+ y_scale_type: y_axis.scale_type ?? `linear`,
298
+ y2_range: y2_axis.range ?? [null, null],
299
+ y2_scale_type: y2_axis.scale_type ?? `linear`,
300
+ }))
444
301
 
445
302
  // Initialize and current ranges
446
303
  let ranges = $state<{
@@ -452,38 +309,16 @@
452
309
  })
453
310
 
454
311
  $effect(() => { // handle x_axis.range / x2_axis.range / y_axis.range / y2_axis.range changes
455
- const new_x = [
456
- x_axis.range?.[0] ?? auto_ranges.x[0],
457
- x_axis.range?.[1] ?? auto_ranges.x[1],
458
- ] as Vec2
459
- const new_x2 = [
460
- x2_axis.range?.[0] ?? auto_ranges.x2[0],
461
- x2_axis.range?.[1] ?? auto_ranges.x2[1],
462
- ] as Vec2
463
- const new_y = [
464
- y_axis.range?.[0] ?? auto_ranges.y[0],
465
- y_axis.range?.[1] ?? auto_ranges.y[1],
466
- ] as Vec2
467
- const new_y2 = [
468
- y2_axis.range?.[0] ?? auto_ranges.y2[0],
469
- y2_axis.range?.[1] ?? auto_ranges.y2[1],
470
- ] as Vec2
471
- // Only update if the initial (data-driven) ranges changed, not when user pans
472
- // Comparing against initial preserves user's pan/zoom state
473
- if (
474
- ranges.initial.x[0] !== new_x[0] ||
475
- ranges.initial.x[1] !== new_x[1] ||
476
- ranges.initial.x2[0] !== new_x2[0] ||
477
- ranges.initial.x2[1] !== new_x2[1] ||
478
- ranges.initial.y[0] !== new_y[0] ||
479
- ranges.initial.y[1] !== new_y[1] ||
480
- ranges.initial.y2[0] !== new_y2[0] ||
481
- ranges.initial.y2[1] !== new_y2[1]
482
- ) {
483
- ranges = {
484
- initial: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
485
- current: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
486
- }
312
+ // resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
313
+ // NaN breaks scales and, since NaN !== NaN, loops the effect)
314
+ const next = resolve_axis_ranges({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis }, auto_ranges)
315
+ if (!next) return
316
+ // Only update if the initial (data-driven) ranges changed, not when user pans.
317
+ // untrack the read of `ranges` so the assignment below can't re-trigger this effect
318
+ // (reading + writing the same state otherwise causes effect_update_depth_exceeded).
319
+ const init = untrack(() => ranges.initial)
320
+ if (!axis_ranges_equal(init, next)) {
321
+ ranges = { initial: { ...next }, current: { ...next } }
487
322
  }
488
323
  })
489
324
 
@@ -494,7 +329,7 @@
494
329
 
495
330
  // Update padding when format or ticks change
496
331
  $effect(() => {
497
- const new_pad = width && height && ticks.y.length
332
+ const new_pad = width && height && ticks.y.length > 0
498
333
  ? calc_auto_padding({
499
334
  padding,
500
335
  default_padding,
@@ -505,7 +340,7 @@
505
340
  : filter_padding(padding, default_padding)
506
341
  // Expand right padding if y2 ticks are shown (only for vertical orientation)
507
342
  if (
508
- width && height && y2_series.length && ticks.y2.length &&
343
+ width && height && y2_series.length > 0 && ticks.y2.length > 0 &&
509
344
  orientation === `vertical`
510
345
  ) {
511
346
  // Need space for: tick shift + tick width + gap (30px) + label space (20px if present)
@@ -521,7 +356,7 @@
521
356
  }
522
357
  // Expand top padding if x2 ticks are shown (only for vertical orientation)
523
358
  if (
524
- width && height && x2_series.length && ticks.x2.length &&
359
+ width && height && x2_series.length > 0 && ticks.x2.length > 0 &&
525
360
  orientation === `vertical`
526
361
  ) {
527
362
  const inside = x2_axis.tick?.label?.inside ?? false
@@ -546,7 +381,7 @@
546
381
  // from baseline to its tip so the legend can't hide inside a tall bar. Built from internal_series
547
382
  // (pad-independent) + ranges so the crowding decision can't see its own reservation.
548
383
  const obstacles_norm = $derived.by(() => {
549
- if (!width || !height || !visible_series.length) return []
384
+ if (!width || !height || visible_series.length === 0) return []
550
385
  const base_w = width - base_pad.l - base_pad.r
551
386
  const base_h = height - base_pad.t - base_pad.b
552
387
  if (base_w <= 0 || base_h <= 0) return []
@@ -634,7 +469,7 @@
634
469
  )
635
470
 
636
471
  // Create auto color range (safely handle empty arrays or undefined extent results)
637
- let auto_color_range: [number, number] = $derived.by(() => {
472
+ let auto_color_range: Vec2 = $derived.by(() => {
638
473
  if (all_color_values.length === 0) return [0, 1]
639
474
  const [min_val, max_val] = extent(all_color_values)
640
475
  return [min_val ?? 0, max_val ?? 1]
@@ -661,7 +496,7 @@
661
496
  // In vertical mode categories are on x-axis; in horizontal mode on y-axis
662
497
  let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
663
498
  let effective_cat_ticks = $derived.by(() => {
664
- if (!category_list.length) return undefined
499
+ if (category_list.length === 0) return undefined
665
500
  // Only respect user ticks when they're a Record (custom label mapping),
666
501
  // not a number (tick count) or array (tick positions)
667
502
  const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
@@ -677,7 +512,7 @@
677
512
  // Ticks
678
513
  let ticks = $derived({
679
514
  x: width && height
680
- ? (category_indices && cat_axis === `x` ? category_indices : generate_ticks(
515
+ ? (category_indices && cat_axis === `x` ? cat_tick_indices : generate_ticks(
681
516
  ranges.current.x,
682
517
  x_axis.scale_type ?? `linear`,
683
518
  x_axis.ticks,
@@ -686,7 +521,7 @@
686
521
  ))
687
522
  : [],
688
523
  y: width && height
689
- ? (category_indices && cat_axis === `y` ? category_indices : generate_ticks(
524
+ ? (category_indices && cat_axis === `y` ? cat_tick_indices : generate_ticks(
690
525
  ranges.current.y,
691
526
  y_axis.scale_type ?? `linear`,
692
527
  y_axis.ticks,
@@ -726,298 +561,44 @@
726
561
  x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
727
562
  })
728
563
 
729
- // Zoom drag state
730
- let drag_state = $state<{
731
- start: { x: number; y: number } | null
732
- current: { x: number; y: number } | null
733
- bounds: DOMRect | null
734
- }>({ start: null, current: null, bounds: null })
735
-
736
- // Pan state
737
- let is_focused = $state(false)
738
- let shift_held = $state(false)
739
- let pan_drag_state = $state<
740
- InitialRanges & { start: { x: number; y: number } } | null
741
- >(null)
742
- let touch_state = $state<
743
- InitialRanges & { start_touches: { x: number; y: number }[] } | null
744
- >(null)
745
- const on_window_mouse_move = (evt: MouseEvent) => {
746
- if (!drag_state.start || !drag_state.bounds) return
747
- drag_state.current = {
748
- x: evt.clientX - drag_state.bounds.left,
749
- y: evt.clientY - drag_state.bounds.top,
750
- }
751
- }
752
- const on_window_mouse_up = () => {
753
- if (drag_state.start && drag_state.current) {
754
- const x1_raw = scales.x.invert(drag_state.start.x) as number | Date
755
- const x2_raw = scales.x.invert(drag_state.current.x) as number | Date
756
- const y1 = scales.y.invert(drag_state.start.y)
757
- const y2 = scales.y.invert(drag_state.current.y)
758
- const y2_1 = scales.y2.invert(drag_state.start.y)
759
- const y2_2 = scales.y2.invert(drag_state.current.y)
760
- const x2a_1_raw = scales.x2.invert(drag_state.start.x) as number | Date
761
- const x2a_2_raw = scales.x2.invert(drag_state.current.x) as number | Date
762
- const dx = Math.abs(drag_state.start.x - drag_state.current.x)
763
- const dy = Math.abs(drag_state.start.y - drag_state.current.y)
764
-
765
- let xr1: number, xr2: number
766
- if (x1_raw instanceof Date && x2_raw instanceof Date) {
767
- ;[xr1, xr2] = [x1_raw.getTime(), x2_raw.getTime()]
768
- } else if (typeof x1_raw === `number` && typeof x2_raw === `number`) {
769
- ;[xr1, xr2] = [x1_raw, x2_raw]
770
- } else [xr1, xr2] = [NaN, NaN] // bail: mixed types
771
-
772
- let x2r1: number, x2r2: number
773
- if (x2a_1_raw instanceof Date && x2a_2_raw instanceof Date) {
774
- ;[x2r1, x2r2] = [x2a_1_raw.getTime(), x2a_2_raw.getTime()]
775
- } else if (typeof x2a_1_raw === `number` && typeof x2a_2_raw === `number`) {
776
- ;[x2r1, x2r2] = [x2a_1_raw, x2a_2_raw]
777
- } else [x2r1, x2r2] = [NaN, NaN]
778
-
779
- if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
780
- // Update axis ranges to trigger reactivity and prevent effect from overriding
781
- x_axis = { ...x_axis, range: [Math.min(xr1, xr2), Math.max(xr1, xr2)] }
782
- if (x2_series.length > 0 && Number.isFinite(x2r1) && Number.isFinite(x2r2)) {
783
- x2_axis = {
784
- ...x2_axis,
785
- range: [Math.min(x2r1, x2r2), Math.max(x2r1, x2r2)],
786
- }
787
- }
788
- y_axis = { ...y_axis, range: [Math.min(y1, y2), Math.max(y1, y2)] }
789
- y2_axis = { ...y2_axis, range: [Math.min(y2_1, y2_2), Math.max(y2_1, y2_2)] }
790
- }
791
- }
792
- drag_state = { start: null, current: null, bounds: null }
793
- window.removeEventListener(`mousemove`, on_window_mouse_move)
794
- window.removeEventListener(`mouseup`, on_window_mouse_up)
795
- document.body.style.cursor = `default`
796
- }
797
-
798
- // Pan drag handlers
799
- const on_pan_move = (evt: MouseEvent) => {
800
- if (!pan_drag_state) return
801
- const dx = evt.clientX - pan_drag_state.start.x
802
- const dy = evt.clientY - pan_drag_state.start.y
803
-
804
- // Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
805
- const sensitivity = pan?.drag_sensitivity ?? 1
806
-
807
- const x_delta = pixels_to_data_delta(
808
- -dx * sensitivity,
809
- pan_drag_state.initial_x_range,
810
- chart_width,
811
- )
812
- const x2_delta = pixels_to_data_delta(
813
- -dx * sensitivity,
814
- pan_drag_state.initial_x2_range,
815
- chart_width,
816
- )
817
- const y_delta = pixels_to_data_delta(
818
- dy * sensitivity,
819
- pan_drag_state.initial_y_range,
820
- chart_height,
821
- )
822
- const y2_delta = pixels_to_data_delta(
823
- dy * sensitivity,
824
- pan_drag_state.initial_y2_range,
825
- chart_height,
826
- )
827
-
828
- ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta)
829
- ranges.current.x2 = pan_range(pan_drag_state.initial_x2_range, x2_delta)
830
- ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta)
831
- ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta)
832
- }
833
-
834
- const on_pan_end = () => {
835
- pan_drag_state = null
836
- document.body.style.cursor = ``
837
- window.removeEventListener(`mousemove`, on_pan_move)
838
- window.removeEventListener(`mouseup`, on_pan_end)
839
- }
840
-
841
- function handle_mouse_down(evt: MouseEvent) {
842
- const coords = get_relative_coords(evt)
843
- if (!coords || !svg_element) return
844
-
845
- // Check if pan is enabled and shift is held for pan mode
846
- const pan_enabled = pan?.enabled !== false
847
- if (pan_enabled && evt.shiftKey) {
848
- evt.preventDefault()
849
- pan_drag_state = {
850
- start: { x: evt.clientX, y: evt.clientY },
851
- initial_x_range: [...ranges.current.x] as [number, number],
852
- initial_x2_range: [...ranges.current.x2] as [number, number],
853
- initial_y_range: [...ranges.current.y] as [number, number],
854
- initial_y2_range: [...ranges.current.y2] as [number, number],
564
+ // Shared pan/zoom/touch/drag-rect interaction controller
565
+ const pan_zoom = create_pan_zoom({
566
+ ranges: () => ranges.current,
567
+ scale_type: (axis) => ({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis })[axis].scale_type,
568
+ plot_dims: () => ({ width: chart_width, height: chart_height }),
569
+ pan: () => pan,
570
+ set_range: (axis, range) => (ranges.current[axis] = range),
571
+ svg: () => svg_element,
572
+ on_rect_zoom: (start, current) => {
573
+ // Update axis ranges to trigger reactivity and prevent effect from overriding
574
+ const next_x = invert_rect_range(scales.x, start.x, current.x)
575
+ if (!next_x) return
576
+ x_axis = { ...x_axis, range: next_x }
577
+ // gate x2/y2 on series presence: their scales are [0, 1] sentinels otherwise,
578
+ // so inverting would store a phantom range in the bindable prop
579
+ const next_x2 = x2_series.length > 0 ? invert_rect_range(scales.x2, start.x, current.x) : null
580
+ if (next_x2) x2_axis_prop = { ...x2_axis_prop, range: next_x2 }
581
+ const next_y = invert_rect_range(scales.y, start.y, current.y)
582
+ if (next_y) y_axis = { ...y_axis, range: next_y }
583
+ const next_y2 = y2_series.length > 0 ? invert_rect_range(scales.y2, start.y, current.y) : null
584
+ if (next_y2) y2_axis_prop = { ...y2_axis_prop, range: next_y2 }
585
+ },
586
+ on_reset: () => {
587
+ // Reset zoom to initial ranges (undo any pan/zoom)
588
+ ranges.current = {
589
+ x: [...ranges.initial.x] as Vec2,
590
+ x2: [...ranges.initial.x2] as Vec2,
591
+ y: [...ranges.initial.y] as Vec2,
592
+ y2: [...ranges.initial.y2] as Vec2,
855
593
  }
856
- document.body.style.cursor = `grabbing`
857
- window.addEventListener(`mousemove`, on_pan_move)
858
- window.addEventListener(`mouseup`, on_pan_end)
859
- return
860
- }
861
-
862
- drag_state = {
863
- start: coords,
864
- current: coords,
865
- bounds: svg_element.getBoundingClientRect(),
866
- }
867
- window.addEventListener(`mousemove`, on_window_mouse_move)
868
- window.addEventListener(`mouseup`, on_window_mouse_up)
869
- evt.preventDefault()
870
- }
871
-
872
- // Wheel handler for pan (requires focus and shift)
873
- function handle_wheel(evt: WheelEvent) {
874
- const pan_enabled = pan?.enabled !== false
875
- // Only capture wheel when focused AND Shift is held
876
- // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
877
- if (!pan_enabled || !is_focused || !shift_held) return
878
-
879
- evt.preventDefault()
880
-
881
- const sensitivity = pan?.wheel_sensitivity ?? 1
882
-
883
- // Determine pan direction based on wheel delta
884
- const x_delta = pixels_to_data_delta(
885
- evt.deltaX * sensitivity,
886
- ranges.current.x,
887
- chart_width,
888
- )
889
- const x2_delta = pixels_to_data_delta(
890
- evt.deltaX * sensitivity,
891
- ranges.current.x2,
892
- chart_width,
893
- )
894
- const y_delta = pixels_to_data_delta(
895
- evt.deltaY * sensitivity,
896
- ranges.current.y,
897
- chart_height,
898
- )
899
- const y2_delta = pixels_to_data_delta(
900
- evt.deltaY * sensitivity,
901
- ranges.current.y2,
902
- chart_height,
903
- )
904
-
905
- if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
906
- ranges.current.x = pan_range(ranges.current.x, x_delta)
907
- ranges.current.x2 = pan_range(ranges.current.x2, x2_delta)
908
- } else {
909
- ranges.current.y = pan_range(ranges.current.y, y_delta)
910
- ranges.current.y2 = pan_range(ranges.current.y2, y2_delta)
911
- }
912
- }
913
-
914
- // Touch handlers for pinch-zoom and two-finger pan
915
- function handle_touch_start(evt: TouchEvent) {
916
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
917
- if (!touch_enabled || evt.touches.length !== 2) return
918
-
919
- evt.preventDefault()
920
- const touches = Array.from(evt.touches)
921
- touch_state = {
922
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
923
- initial_x_range: [...ranges.current.x] as [number, number],
924
- initial_x2_range: [...ranges.current.x2] as [number, number],
925
- initial_y_range: [...ranges.current.y] as [number, number],
926
- initial_y2_range: [...ranges.current.y2] as [number, number],
927
- }
928
- }
929
-
930
- function handle_touch_move(evt: TouchEvent) {
931
- if (!touch_state || evt.touches.length !== 2) return
932
- evt.preventDefault()
933
-
934
- const [t1, t2] = Array.from(evt.touches)
935
- const [s1, s2] = touch_state.start_touches
936
-
937
- // Calculate center movement for pan
938
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
939
- const curr_center = {
940
- x: (t1.clientX + t2.clientX) / 2,
941
- y: (t1.clientY + t2.clientY) / 2,
942
- }
943
- const dx = curr_center.x - start_center.x
944
- const dy = curr_center.y - start_center.y
945
-
946
- // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
947
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
948
- // Guard against zero-distance pinch to avoid Infinity scale
949
- if (start_dist < Number.EPSILON) return
950
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
951
- const scale = curr_dist / start_dist
952
-
953
- // If scale changed significantly, treat as pinch-zoom
954
- // Also guard against scale being too small to avoid division by zero
955
- if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
956
- // Pinch zoom centered on gesture center
957
- // Divide by scale so spread (scale > 1) = smaller span (zoom in)
958
- const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
959
- const x2_span = touch_state.initial_x2_range[1] -
960
- touch_state.initial_x2_range[0]
961
- const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
962
- const y2_span = touch_state.initial_y2_range[1] -
963
- touch_state.initial_y2_range[0]
964
- const x_center =
965
- (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
966
- const x2_center =
967
- (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
968
- const y_center =
969
- (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
970
- const y2_center =
971
- (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
972
-
973
- ranges.current.x = [
974
- x_center - x_span / scale / 2,
975
- x_center + x_span / scale / 2,
976
- ]
977
- ranges.current.x2 = [
978
- x2_center - x2_span / scale / 2,
979
- x2_center + x2_span / scale / 2,
980
- ]
981
- ranges.current.y = [
982
- y_center - y_span / scale / 2,
983
- y_center + y_span / scale / 2,
984
- ]
985
- ranges.current.y2 = [
986
- y2_center - y2_span / scale / 2,
987
- y2_center + y2_span / scale / 2,
988
- ]
989
- } else {
990
- // Pan
991
- const x_delta = pixels_to_data_delta(
992
- -dx,
993
- touch_state.initial_x_range,
994
- chart_width,
995
- )
996
- const x2_delta = pixels_to_data_delta(
997
- -dx,
998
- touch_state.initial_x2_range,
999
- chart_width,
1000
- )
1001
- const y_delta = pixels_to_data_delta(
1002
- dy,
1003
- touch_state.initial_y_range,
1004
- chart_height,
1005
- )
1006
- const y2_delta = pixels_to_data_delta(
1007
- dy,
1008
- touch_state.initial_y2_range,
1009
- chart_height,
1010
- )
1011
- ranges.current.x = pan_range(touch_state.initial_x_range, x_delta)
1012
- ranges.current.x2 = pan_range(touch_state.initial_x2_range, x2_delta)
1013
- ranges.current.y = pan_range(touch_state.initial_y_range, y_delta)
1014
- ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta)
1015
- }
1016
- }
1017
-
1018
- function handle_touch_end() {
1019
- touch_state = null
1020
- }
594
+ // Also reset axis props so future data changes recalculate auto ranges
595
+ x_axis = { ...x_axis, range: [null, null] }
596
+ x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
597
+ y_axis = { ...y_axis, range: [null, null] }
598
+ y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
599
+ },
600
+ })
601
+ onDestroy(() => pan_zoom.destroy())
1021
602
 
1022
603
  // Legend data and handlers
1023
604
  let legend_data = $derived.by<LegendItem[]>(() =>
@@ -1076,34 +657,11 @@
1076
657
  })
1077
658
  )
1078
659
 
1079
- function toggle_series_visibility(series_idx: number) {
1080
- if (series_idx >= 0 && series_idx < series.length) {
1081
- series = series.map((srs, idx) =>
1082
- idx === series_idx ? { ...srs, visible: !(srs.visible ?? true) } : srs
1083
- )
1084
- }
1085
- }
1086
-
1087
- function toggle_group_visibility(_group_name: string, series_indices: number[]) {
1088
- // Filter to valid indices upfront (consistent with shared toggle_group_visibility)
1089
- const valid_indices = series_indices.filter((idx) =>
1090
- idx >= 0 && idx < series.length
1091
- )
1092
- if (valid_indices.length === 0) return
1093
-
1094
- const idx_set = new Set(valid_indices)
1095
- // Check if all series in the group are currently visible
1096
- const all_visible = valid_indices.every((idx) => series[idx].visible ?? true)
1097
- // Toggle: if all visible, hide all; otherwise show all
1098
- const new_visibility = !all_visible
1099
- series = series.map((srs, idx) =>
1100
- idx_set.has(idx) ? { ...srs, visible: new_visibility } : srs
1101
- )
1102
- }
660
+ const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
1103
661
 
1104
662
  // Collect bar and line positions for legend placement
1105
663
  let bar_points_for_placement = $derived.by(() => {
1106
- if (!width || !height || !visible_series.length) return []
664
+ if (!width || !height || visible_series.length === 0) return []
1107
665
 
1108
666
  return internal_series.flatMap((srs, series_idx) => {
1109
667
  if (!(srs?.visible ?? true)) return []
@@ -1130,12 +688,6 @@
1130
688
 
1131
689
  // Legend placement stability state (legend_element declared above for the auto-place block)
1132
690
  let hovered_legend_series_idx = $state<number | null>(null)
1133
- const legend_hover = create_hover_lock()
1134
- const dim_tracker = create_dimension_tracker()
1135
- let has_initial_legend_placement = $state(false)
1136
-
1137
- // Clear pending hover lock timeout on unmount
1138
- $effect(() => () => legend_hover.cleanup())
1139
691
 
1140
692
  // Calculate best legend placement using continuous grid sampling
1141
693
  let legend_placement = $derived.by(() => {
@@ -1154,42 +706,17 @@
1154
706
  return result
1155
707
  })
1156
708
 
1157
- // Tweened legend coordinates for smooth animation - create once, update target via effect
1158
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
1159
- const tweened_legend_coords = new Tween(
1160
- { x: 0, y: 0 },
1161
- untrack(() => ({ duration: 400, ...legend?.tween })),
1162
- )
1163
-
1164
- // Update legend position with stability checks
1165
- $effect(() => {
1166
- if (!width || !height || !legend_placement) return
1167
-
1168
- // Track dimensions for resize detection
1169
- const dims_changed = dim_tracker.has_changed(width, height)
1170
- if (dims_changed) dim_tracker.update(width, height)
1171
-
1172
- const is_responsive = legend?.responsive ?? false
1173
- // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
1174
- const should_update = dims_changed || (!legend_hover.is_locked.current &&
1175
- (is_responsive || !has_initial_legend_placement))
1176
-
1177
- if (should_update) {
1178
- tweened_legend_coords.set(
1179
- { x: legend_placement.x, y: legend_placement.y },
1180
- // Skip animation on initial placement to avoid jump from (0, 0)
1181
- has_initial_legend_placement ? undefined : { duration: 0 },
1182
- )
1183
- // Only lock position after we have actual measured size
1184
- if (legend_element) {
1185
- has_initial_legend_placement = true
1186
- }
1187
- }
709
+ // Tweened legend coordinates with shared placement stability gating
710
+ const legend_tween = create_placed_tween({
711
+ placement: () => legend_placement,
712
+ dims: () => ({ width, height }),
713
+ responsive: () => legend?.responsive ?? false,
714
+ element: () => legend_element,
715
+ tween: () => legend?.tween,
1188
716
  })
1189
717
 
1190
718
  // Tooltip state
1191
719
  let hover_info = $state<BarHandlerProps<Metadata> | null>(null)
1192
- let tooltip_el = $state<HTMLDivElement | undefined>()
1193
720
 
1194
721
  function get_bar_data(
1195
722
  series_idx: number,
@@ -1267,49 +794,10 @@
1267
794
  }
1268
795
 
1269
796
  // Stack offsets (only for bar series in stacked mode, grouped by y-axis)
1270
- let stacked_offsets = $derived.by(() => {
1271
- if (mode !== `stacked`) return [] as number[][]
1272
- const max_len = Math.max(
1273
- 0,
1274
- ...internal_series.map((srs) => srs.y.length),
1275
- )
1276
- const offsets = internal_series.map(() =>
1277
- Array.from({ length: max_len }, () => 0)
1278
- )
1279
-
1280
- // Separate accumulators for y1 and y2 axes
1281
- const y1_pos_acc = Array.from({ length: max_len }, () => 0)
1282
- const y1_neg_acc = Array.from({ length: max_len }, () => 0)
1283
- const y2_pos_acc = Array.from({ length: max_len }, () => 0)
1284
- const y2_neg_acc = Array.from({ length: max_len }, () => 0)
1285
-
1286
- internal_series.forEach((srs, series_idx) => {
1287
- if (!(srs?.visible ?? true) || srs.render_mode === `line`) return
1288
-
1289
- const use_y2 = srs.y_axis === `y2`
1290
- const pos_acc = use_y2 ? y2_pos_acc : y1_pos_acc
1291
- const neg_acc = use_y2 ? y2_neg_acc : y1_neg_acc
1292
-
1293
- for (let bar_idx = 0; bar_idx < max_len; bar_idx++) {
1294
- const y_val = srs.y[bar_idx] ?? 0
1295
- const acc = y_val >= 0 ? pos_acc : neg_acc
1296
- offsets[series_idx][bar_idx] = acc[bar_idx]
1297
- acc[bar_idx] += y_val
1298
- }
1299
- })
1300
- return offsets
1301
- })
797
+ let stacked_offsets = $derived(compute_stacked_offsets(internal_series, mode))
1302
798
 
1303
799
  // Calculate group positions for grouped mode (side-by-side bars)
1304
- let group_info = $derived.by(() => {
1305
- if (mode !== `grouped`) return { bar_series_count: 0, bar_series_indices: [] }
1306
- const bar_series_indices = internal_series
1307
- .map((srs, idx) =>
1308
- (srs?.visible ?? true) && srs.render_mode !== `line` ? idx : -1
1309
- )
1310
- .filter((idx) => idx >= 0)
1311
- return { bar_series_count: bar_series_indices.length, bar_series_indices }
1312
- })
800
+ let group_info = $derived(compute_group_info(internal_series, mode))
1313
801
 
1314
802
  // Set theme-aware background when entering fullscreen
1315
803
  $effect(() => {
@@ -1327,9 +815,9 @@
1327
815
  set_axis: (axis, config) => {
1328
816
  // Spread into existing state to preserve merged type structure
1329
817
  if (axis === `x`) x_axis = { ...x_axis, ...config }
1330
- else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
818
+ else if (axis === `x2`) x2_axis_prop = { ...x2_axis_prop, ...config }
1331
819
  else if (axis === `y`) y_axis = { ...y_axis, ...config }
1332
- else y2_axis = { ...y2_axis, ...config }
820
+ else y2_axis_prop = { ...y2_axis_prop, ...config }
1333
821
  },
1334
822
  get_series: () => series,
1335
823
  set_series: (new_series) => (series = new_series),
@@ -1337,32 +825,12 @@
1337
825
  set_loading: (axis) => (axis_loading = axis),
1338
826
  }
1339
827
 
1340
- // Create shared handler bound to this component's state
1341
- // Using $derived so handler updates when callback props change
1342
- const handle_axis_change = $derived(create_axis_change_handler(
828
+ // Shared handler + one-shot auto-load bound to this component's state
829
+ const { handle_axis_change, try_auto_load } = create_axis_loader(
1343
830
  axis_state,
1344
- data_loader,
1345
- on_axis_change,
1346
- on_error,
1347
- ))
1348
-
1349
- let auto_load_attempted = false // prevent infinite retries on failure
1350
-
1351
- // Auto-load data if series is empty but options exist (runs once)
1352
- $effect(() => {
1353
- if (series.length === 0 && data_loader && !auto_load_attempted) {
1354
- // Check x-axis first, then y-axis
1355
- if (x_axis.options?.length) {
1356
- auto_load_attempted = true
1357
- const first_key = x_axis.selected_key ?? x_axis.options[0].key
1358
- handle_axis_change(`x`, first_key).catch(() => {})
1359
- } else if (y_axis.options?.length) {
1360
- auto_load_attempted = true
1361
- const first_key = y_axis.selected_key ?? y_axis.options[0].key
1362
- handle_axis_change(`y`, first_key).catch(() => {})
1363
- }
1364
- }
1365
- })
831
+ () => ({ data_loader, on_axis_change, on_error }),
832
+ )
833
+ $effect(try_auto_load)
1366
834
  </script>
1367
835
 
1368
836
  {#snippet ref_lines_layer(lines: IndexedRefLine[])}
@@ -1399,11 +867,9 @@
1399
867
  evt.preventDefault()
1400
868
  fullscreen = false
1401
869
  }
1402
- if (evt.key === `Shift`) shift_held = true
1403
- }}
1404
- onkeyup={(evt) => {
1405
- if (evt.key === `Shift`) shift_held = false
870
+ pan_zoom.on_window_key_down(evt)
1406
871
  }}
872
+ onkeyup={pan_zoom.on_window_key_up}
1407
873
  />
1408
874
 
1409
875
  <div
@@ -1429,38 +895,25 @@
1429
895
  aria-label={rest[`aria-label`] ??
1430
896
  ([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Bar chart`)}
1431
897
  tabindex="0"
1432
- onfocusin={() => (is_focused = true)}
1433
- onfocusout={() => (is_focused = false)}
1434
- onmousedown={handle_mouse_down}
1435
- ondblclick={() => {
1436
- // Reset zoom to initial ranges (undo any pan/zoom)
1437
- ranges.current.x = [...ranges.initial.x] as [number, number]
1438
- ranges.current.x2 = [...ranges.initial.x2] as [number, number]
1439
- ranges.current.y = [...ranges.initial.y] as [number, number]
1440
- ranges.current.y2 = [...ranges.initial.y2] as [number, number]
1441
- // Also reset axis props so future data changes recalculate auto ranges
1442
- x_axis = { ...x_axis, range: [null, null] }
1443
- x2_axis = { ...x2_axis, range: [null, null] }
1444
- y_axis = { ...y_axis, range: [null, null] }
1445
- y2_axis = { ...y2_axis, range: [null, null] }
1446
- }}
898
+ onfocusin={() => pan_zoom.set_focused(true)}
899
+ onfocusout={() => pan_zoom.set_focused(false)}
900
+ onmousedown={pan_zoom.on_mouse_down}
901
+ ondblclick={pan_zoom.reset_view}
902
+ onkeydown={pan_zoom.on_key_down}
1447
903
  onmouseleave={() => {
1448
904
  hovered = false
1449
905
  hover_info = null
1450
906
  change(null)
1451
907
  on_bar_hover?.(null)
1452
908
  }}
1453
- onwheel={handle_wheel}
1454
- ontouchstart={handle_touch_start}
1455
- ontouchmove={handle_touch_move}
1456
- ontouchend={handle_touch_end}
1457
- style:cursor={pan_drag_state
1458
- ? `grabbing`
1459
- : shift_held && pan?.enabled !== false
1460
- ? `grab`
1461
- : `crosshair`}
909
+ onwheel={pan_zoom.on_wheel}
910
+ ontouchstart={pan_zoom.on_touch_start}
911
+ ontouchmove={pan_zoom.on_touch_move}
912
+ ontouchend={pan_zoom.on_touch_end}
913
+ ontouchcancel={pan_zoom.on_touch_end}
914
+ style:cursor={pan_zoom.cursor}
1462
915
  >
1463
- <ZoomRect start={drag_state.start} current={drag_state.current} />
916
+ <ZoomRect start={pan_zoom.drag_start} current={pan_zoom.drag_current} />
1464
917
 
1465
918
  <!-- User content (custom overlays, reference lines, etc.) -->
1466
919
  {@render user_content?.({
@@ -1487,6 +940,7 @@
1487
940
  ticks={ticks.x as number[]}
1488
941
  place={scales.x}
1489
942
  axis={x_axis}
943
+ domain={ranges.current.x as Vec2}
1490
944
  {pad}
1491
945
  {width}
1492
946
  {height}
@@ -1507,6 +961,7 @@
1507
961
  ticks={ticks.x2 as number[]}
1508
962
  place={scales.x2}
1509
963
  axis={x2_axis}
964
+ domain={ranges.current.x2 as Vec2}
1510
965
  {pad}
1511
966
  {width}
1512
967
  {height}
@@ -1525,6 +980,7 @@
1525
980
  ticks={ticks.y as number[]}
1526
981
  place={scales.y}
1527
982
  axis={y_axis}
983
+ domain={ranges.current.y as Vec2}
1528
984
  {pad}
1529
985
  {width}
1530
986
  {height}
@@ -1544,21 +1000,18 @@
1544
1000
  <!-- Y2-axis (Right) -->
1545
1001
  <!-- Note: y2 axis is only supported for vertical orientation. Implementing x2 for horizontal mode requires additional complexity. -->
1546
1002
  {#if y2_series.length > 0 && orientation === `vertical`}
1547
- {@const y2_inside = y2_axis.tick?.label?.inside ?? false}
1548
- {@const y2_tick_shift = y2_inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8}
1549
- {@const y2_tick_width = y2_inside ? 0 : tick_label_widths.y2_max}
1550
1003
  <PlotAxis
1551
1004
  side="y2"
1552
1005
  ticks={ticks.y2 as number[]}
1553
1006
  place={scales.y2}
1554
1007
  axis={y2_axis}
1008
+ domain={ranges.current.y2 as Vec2}
1555
1009
  {pad}
1556
1010
  {width}
1557
1011
  {height}
1558
1012
  show_grid={display.y2_grid}
1559
1013
  tick_label={(tick) => get_tick_label(tick, y2_axis.ticks)}
1560
- label_x={width - pad.r + y2_tick_shift + y2_tick_width + LABEL_GAP_DEFAULT +
1561
- (y2_axis.label_shift?.x ?? 0)}
1014
+ label_x={y2_axis_label_x(y2_axis, width, pad.r, tick_label_widths.y2_max)}
1562
1015
  label_y={pad.t + chart_height / 2 + (y2_axis.label_shift?.y ?? 0)}
1563
1016
  axis_loading={axis_loading === `y2`}
1564
1017
  on_axis_change={(key) => handle_axis_change(`y2`, key)}
@@ -1572,7 +1025,10 @@
1572
1025
  </clipPath>
1573
1026
  </defs>
1574
1027
 
1575
- <!-- Clipped content: zero lines, bars, and lines -->
1028
+ <!-- Chart content is clipped in two groups so reference lines can interleave
1029
+ at their z positions while staying outside the chart clip: each line still
1030
+ self-clips to the plot area inside ReferenceLine, only its annotation text
1031
+ is allowed to overflow the plot edges. -->
1576
1032
  <g clip-path="url(#{clip_path_id})">
1577
1033
  <ZeroLines
1578
1034
  {display}
@@ -1596,11 +1052,12 @@
1596
1052
  {height}
1597
1053
  {pad}
1598
1054
  />
1055
+ </g>
1599
1056
 
1600
- <!-- Reference lines: below lines -->
1601
- {@render ref_lines_layer(ref_lines_by_z.below_lines)}
1057
+ {@render ref_lines_layer(ref_lines_by_z.below_lines)}
1602
1058
 
1603
- <!-- Bars and Lines -->
1059
+ <!-- Bars and Lines -->
1060
+ <g clip-path="url(#{clip_path_id})">
1604
1061
  {#each internal_series as srs, series_idx (srs?.id ?? series_idx)}
1605
1062
  {#if srs?.visible ?? true}
1606
1063
  {@const is_line = srs.render_mode === `line`}
@@ -1626,42 +1083,14 @@
1626
1083
  series_markers === `line+points`}
1627
1084
  {@const show_points = series_markers === `points` ||
1628
1085
  series_markers === `line+points`}
1629
- {@const points = srs.x.map((x_val, idx) => {
1630
- const y_val = srs.y[idx]
1631
- // Lines don't stack - they show absolute values (useful for totals/trends)
1632
- const plot_x = orientation === `vertical`
1633
- ? x_scale(x_val)
1634
- : x_scale(y_val)
1635
- const plot_y = orientation === `vertical`
1636
- ? y_scale(y_val)
1637
- : scales.y(x_val)
1638
- // Create internal point with all needed data
1639
- const color_value = srs.color_values?.[idx] ?? null
1640
- const size_value = srs.size_values?.[idx] ?? null
1641
- const point_style = process_prop(srs.point_style, idx)
1642
- const point_hover = process_prop(srs.point_hover, idx)
1643
- const point_label = process_prop(srs.point_label, idx)
1644
- const point_offset = process_prop(srs.point_offset, idx)
1645
- const metadata = Array.isArray(srs.metadata)
1646
- ? srs.metadata[idx]
1647
- : srs.metadata
1648
- return {
1649
- x: plot_x,
1650
- y: plot_y,
1651
- data_x: x_val,
1652
- data_y: y_val,
1653
- idx,
1654
- color_value,
1655
- size_value,
1656
- point_style,
1657
- point_hover,
1658
- point_label,
1659
- point_offset,
1660
- metadata,
1661
- series_idx,
1662
- point_idx: idx,
1663
- } as LineSeriesPoint
1664
- }).filter((pt) => isFinite(pt.x) && isFinite(pt.y))}
1086
+ {@const points = compute_line_points({
1087
+ series: srs,
1088
+ series_idx,
1089
+ orientation,
1090
+ x_scale,
1091
+ y_scale,
1092
+ cat_y_scale: scales.y,
1093
+ })}
1665
1094
  {@const polyline_str = show_line && points.length > 1
1666
1095
  ? points.map((pt) => `${pt.x},${pt.y}`).join(` `)
1667
1096
  : ``}
@@ -1826,37 +1255,24 @@
1826
1255
  {@const bar_width_val = Array.isArray(srs.bar_width)
1827
1256
  ? (srs.bar_width[bar_idx] ?? 0.5)
1828
1257
  : (srs.bar_width ?? 0.5)}
1829
- {@const half = mode === `grouped` && group_info.bar_series_count > 1
1830
- ? bar_width_val / (2 * group_info.bar_series_count)
1831
- : bar_width_val / 2}
1832
- {@const calculate_group_offset = (idx: number) => {
1833
- const position = group_info.bar_series_indices.indexOf(idx)
1834
- const offset = position - (group_info.bar_series_count - 1) / 2
1835
- return offset * (bar_width_val / group_info.bar_series_count)
1836
- }}
1837
- {@const group_offset = mode === `grouped` && group_info.bar_series_count > 1
1838
- ? calculate_group_offset(series_idx)
1839
- : 0}
1840
1258
  {@const is_vertical = orientation === `vertical`}
1841
- {@const cat_val = x_val}
1842
- {@const val = y_val}
1843
- {@const use_y2 = srs.y_axis === `y2`}
1844
- {@const y_scale = use_y2 ? scales.y2 : scales.y}
1845
- {@const use_x2_bar = srs.x_axis === `x2`}
1846
- {@const x_scale_bar = use_x2_bar ? scales.x2 : scales.x}
1259
+ {@const x_scale_bar = srs.x_axis === `x2` ? scales.x2 : scales.x}
1847
1260
  {@const [cat_scale, val_scale] = is_vertical
1848
- ? [x_scale_bar, y_scale]
1261
+ ? [x_scale_bar, srs.y_axis === `y2` ? scales.y2 : scales.y]
1849
1262
  : [scales.y, x_scale_bar]}
1850
- {@const c0 = cat_scale(cat_val + group_offset - half)}
1851
- {@const c1 = cat_scale(cat_val + group_offset + half)}
1852
- {@const v0 = val_scale(base)}
1853
- {@const v1 = val_scale(base + val)}
1854
- {@const [rect_x, rect_y] = is_vertical
1855
- ? [Math.min(c0, c1), Math.min(v0, v1)]
1856
- : [Math.min(v0, v1), Math.min(c0, c1)]}
1857
- {@const [rect_w, rect_h] = is_vertical
1858
- ? [Math.max(1, Math.abs(c1 - c0)), Math.max(0, Math.abs(v1 - v0))]
1859
- : [Math.max(1, Math.abs(v1 - v0)), Math.max(0, Math.abs(c1 - c0))]}
1263
+ {@const { c0, c1, v0, v1, rect_x, rect_y, rect_w, rect_h } =
1264
+ compute_bar_rect({
1265
+ cat_val: x_val,
1266
+ val: y_val,
1267
+ base,
1268
+ bar_width_val,
1269
+ series_idx,
1270
+ mode,
1271
+ orientation,
1272
+ group_info,
1273
+ cat_scale,
1274
+ val_scale,
1275
+ })}
1860
1276
  {#if (is_vertical ? rect_h : rect_w) > 0}
1861
1277
  <path
1862
1278
  d={bar_path(
@@ -1914,43 +1330,38 @@
1914
1330
  </g>
1915
1331
  {/if}
1916
1332
  {/each}
1917
-
1918
- <!-- Reference lines: below points -->
1919
- {@render ref_lines_layer(ref_lines_by_z.below_points)}
1920
-
1921
- <!-- Reference lines: above all -->
1922
- {@render ref_lines_layer(ref_lines_by_z.above_all)}
1923
1333
  </g>
1334
+
1335
+ {@render ref_lines_layer(ref_lines_by_z.below_points)}
1336
+ {@render ref_lines_layer(ref_lines_by_z.above_all)}
1924
1337
  </svg>
1925
1338
 
1926
1339
  <!-- Legend -->
1927
1340
  {#if legend && (show_legend !== undefined ? show_legend : series.length > 1)}
1928
- {@const legend_left = legend_auto_outside
1929
- ? legend_outside_x
1930
- : legend_placement
1931
- ? tweened_legend_coords.current.x
1932
- : pad.l + 10}
1933
- {@const legend_top = legend_auto_outside
1934
- ? legend_outside_y
1935
- : legend_placement
1936
- ? tweened_legend_coords.current.y
1937
- : pad.t + 10}
1341
+ {@const legend_pos = placed_coords(
1342
+ legend_auto_outside,
1343
+ { x: legend_outside_x, y: legend_outside_y },
1344
+ legend_placement,
1345
+ legend_tween.coords.current,
1346
+ { x: pad.l + 10, y: pad.t + 10 },
1347
+ )}
1938
1348
  <PlotLegend
1939
1349
  bind:root_element={legend_element}
1940
1350
  {...legend}
1941
1351
  series_data={legend_data}
1942
- on_toggle={legend?.on_toggle || toggle_series_visibility}
1943
- on_group_toggle={legend?.on_group_toggle || toggle_group_visibility}
1944
- on_hover_change={legend_hover.set_locked}
1945
- on_item_hover={(series_idx) =>
1946
- (hovered_legend_series_idx = series_idx != null && series_idx >= 0
1947
- ? series_idx
1352
+ on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1353
+ on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
1354
+ on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1355
+ on_hover_change={legend_tween.set_locked}
1356
+ on_item_hover={(item) =>
1357
+ (hovered_legend_series_idx = item != null && item.series_idx >= 0
1358
+ ? item.series_idx
1948
1359
  : null)}
1949
1360
  active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1950
1361
  style={`
1951
1362
  position: absolute;
1952
- left: ${legend_left}px;
1953
- top: ${legend_top}px;
1363
+ left: ${legend_pos.x}px;
1364
+ top: ${legend_pos.y}px;
1954
1365
  pointer-events: auto;
1955
1366
  ${legend?.style || ``}
1956
1367
  `}
@@ -1963,22 +1374,14 @@
1963
1374
  )}
1964
1375
  {@const cy = (hover_info.active_y_axis === `y2` ? scales.y2 : scales.y)(
1965
1376
  hover_info.orient_y,
1966
- )}
1967
- {@const tooltip_pos = constrain_tooltip_position(
1968
- cx,
1969
- cy,
1970
- tooltip_el?.offsetWidth ?? 140,
1971
- tooltip_el?.offsetHeight ?? 50,
1972
- width,
1973
- height,
1974
- { offset_x: 10, offset_y: 5 },
1975
1377
  )}
1976
1378
  <PlotTooltip
1977
- x={tooltip_pos.x}
1978
- y={tooltip_pos.y}
1979
- offset={{ x: 0, y: 0 }}
1379
+ x={cx}
1380
+ y={cy}
1381
+ offset={{ x: 10, y: 5 }}
1382
+ constrain_to={{ width, height }}
1383
+ fallback_size={{ width: 140, height: 50 }}
1980
1384
  bg_color={hover_info.color}
1981
- bind:wrapper={tooltip_el}
1982
1385
  >
1983
1386
  {#if tooltip}
1984
1387
  {@render tooltip({ ...hover_info, fullscreen })}
@@ -2017,9 +1420,9 @@
2017
1420
  bind:orientation
2018
1421
  bind:mode
2019
1422
  bind:x_axis
2020
- bind:x2_axis
1423
+ bind:x2_axis={x2_axis_prop}
2021
1424
  bind:y_axis
2022
- bind:y2_axis
1425
+ bind:y2_axis={y2_axis_prop}
2023
1426
  bind:display
2024
1427
  auto_x_range={auto_ranges.x as Vec2}
2025
1428
  auto_x2_range={auto_ranges.x2 as Vec2}
@@ -2063,8 +1466,10 @@
2063
1466
  background: var(--barplot-fullscreen-bg, var(--barplot-bg, var(--plot-bg)));
2064
1467
  max-height: none !important;
2065
1468
  overflow: hidden;
2066
- /* Add padding to prevent titles from being cropped at top */
2067
- padding-top: var(--plot-fullscreen-padding-top, 2em);
1469
+ /* border-top (not padding-top): bind:clientHeight includes padding but excludes
1470
+ borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
1471
+ border-top: var(--plot-fullscreen-padding-top, 2em) solid
1472
+ var(--barplot-fullscreen-bg, var(--barplot-bg, var(--plot-bg, transparent)));
2068
1473
  box-sizing: border-box;
2069
1474
  }
2070
1475
  .header-controls {