matterviz 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/dist/Icon.svelte +7 -4
  2. package/dist/MillerIndexInput.svelte +1 -1
  3. package/dist/api/optimade.js +32 -26
  4. package/dist/app.css +0 -3
  5. package/dist/brillouin/BrillouinZone.svelte +8 -3
  6. package/dist/brillouin/BrillouinZone.svelte.d.ts +2 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +52 -6
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -0
  9. package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
  10. package/dist/brillouin/compute.js +10 -14
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
  12. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +12 -15
  13. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +8 -10
  14. package/dist/chempot-diagram/async-compute.svelte.js +3 -1
  15. package/dist/chempot-diagram/chempot-worker.js +2 -1
  16. package/dist/chempot-diagram/compute.d.ts +1 -1
  17. package/dist/chempot-diagram/compute.js +17 -19
  18. package/dist/colors/index.js +6 -5
  19. package/dist/composition/FormulaFilter.svelte +12 -6
  20. package/dist/composition/PieChart.svelte +6 -5
  21. package/dist/composition/chem-sys.d.ts +8 -0
  22. package/dist/composition/chem-sys.js +85 -0
  23. package/dist/composition/format.js +4 -2
  24. package/dist/composition/index.d.ts +1 -0
  25. package/dist/composition/index.js +1 -0
  26. package/dist/composition/parse.js +25 -13
  27. package/dist/convex-hull/ConvexHull2D.svelte +12 -10
  28. package/dist/convex-hull/ConvexHull3D.svelte +5 -5
  29. package/dist/convex-hull/ConvexHull4D.svelte +5 -9
  30. package/dist/convex-hull/ConvexHullStats.svelte +12 -12
  31. package/dist/convex-hull/GasPressureControls.svelte +4 -4
  32. package/dist/convex-hull/TemperatureSlider.svelte +2 -2
  33. package/dist/convex-hull/demo-temperature.d.ts +1 -1
  34. package/dist/convex-hull/demo-temperature.js +20 -22
  35. package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
  36. package/dist/convex-hull/gas-thermodynamics.js +22 -30
  37. package/dist/convex-hull/helpers.d.ts +3 -0
  38. package/dist/convex-hull/helpers.js +17 -9
  39. package/dist/convex-hull/index.d.ts +1 -1
  40. package/dist/convex-hull/thermodynamics.js +83 -78
  41. package/dist/convex-hull/types.d.ts +1 -1
  42. package/dist/coordination/CoordinationBarPlot.svelte +23 -23
  43. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  44. package/dist/element/ElementTile.svelte.d.ts +1 -1
  45. package/dist/fermi-surface/FermiSlice.svelte +13 -5
  46. package/dist/fermi-surface/FermiSurface.svelte +11 -5
  47. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  48. package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
  49. package/dist/fermi-surface/FermiSurfaceScene.svelte +3 -0
  50. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
  51. package/dist/fermi-surface/compute.js +59 -59
  52. package/dist/fermi-surface/export.js +3 -2
  53. package/dist/fermi-surface/parse.js +7 -4
  54. package/dist/fermi-surface/types.d.ts +1 -0
  55. package/dist/heatmap-matrix/HeatmapMatrix.svelte +23 -21
  56. package/dist/heatmap-matrix/index.js +1 -1
  57. package/dist/io/decompress.js +4 -2
  58. package/dist/io/export.d.ts +4 -4
  59. package/dist/io/export.js +47 -25
  60. package/dist/io/fetch.js +5 -1
  61. package/dist/io/file-drop.d.ts +1 -1
  62. package/dist/io/file-drop.js +35 -36
  63. package/dist/io/url-drop.js +64 -33
  64. package/dist/isosurface/parse.js +6 -7
  65. package/dist/isosurface/slice.js +5 -4
  66. package/dist/isosurface/types.js +1 -1
  67. package/dist/keyboard.d.ts +3 -0
  68. package/dist/keyboard.js +23 -0
  69. package/dist/labels.d.ts +1 -1
  70. package/dist/labels.js +8 -7
  71. package/dist/layout/PropertyFilter.svelte +3 -2
  72. package/dist/layout/SettingsSection.svelte +1 -1
  73. package/dist/layout/json-tree/JsonNode.svelte +1 -1
  74. package/dist/layout/json-tree/JsonTree.svelte +2 -2
  75. package/dist/layout/json-tree/utils.js +5 -4
  76. package/dist/marching-cubes.js +8 -13
  77. package/dist/math.d.ts +5 -1
  78. package/dist/math.js +24 -9
  79. package/dist/overlays/DraggablePane.svelte +4 -4
  80. package/dist/periodic-table/PeriodicTable.svelte +20 -9
  81. package/dist/periodic-table/PropertySelect.svelte +1 -0
  82. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
  83. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  84. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  85. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  86. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
  87. package/dist/phase-diagram/build-diagram.js +2 -2
  88. package/dist/phase-diagram/parse.js +6 -5
  89. package/dist/phase-diagram/types.d.ts +1 -1
  90. package/dist/phase-diagram/utils.d.ts +3 -3
  91. package/dist/phase-diagram/utils.js +8 -12
  92. package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +229 -587
  93. package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +5 -5
  94. package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
  95. package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
  96. package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +6 -6
  97. package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
  98. package/dist/plot/bar/data.d.ts +40 -0
  99. package/dist/plot/bar/data.js +154 -0
  100. package/dist/plot/bar/geometry.d.ts +39 -0
  101. package/dist/plot/bar/geometry.js +60 -0
  102. package/dist/plot/bar/index.d.ts +3 -0
  103. package/dist/plot/bar/index.js +3 -0
  104. package/dist/plot/box/BoxPlot.svelte +1462 -0
  105. package/dist/plot/box/BoxPlot.svelte.d.ts +94 -0
  106. package/dist/plot/box/BoxPlotControls.svelte +109 -0
  107. package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
  108. package/dist/plot/box/Violin.svelte +14 -0
  109. package/dist/plot/box/Violin.svelte.d.ts +70 -0
  110. package/dist/plot/box/box-plot.d.ts +55 -0
  111. package/dist/plot/box/box-plot.js +126 -0
  112. package/dist/plot/box/index.d.ts +5 -0
  113. package/dist/plot/box/index.js +5 -0
  114. package/dist/plot/box/kde.d.ts +16 -0
  115. package/dist/plot/box/kde.js +160 -0
  116. package/dist/plot/box/quantile.d.ts +3 -0
  117. package/dist/plot/box/quantile.js +53 -0
  118. package/dist/plot/{auto-place.js → core/auto-place.js} +2 -2
  119. package/dist/plot/core/axis-utils.d.ts +46 -0
  120. package/dist/plot/core/axis-utils.js +110 -0
  121. package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
  122. package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
  123. package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +36 -33
  124. package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +2 -2
  125. package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
  126. package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
  127. package/dist/plot/core/components/ControlPane.svelte +46 -0
  128. package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
  129. package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
  130. package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
  131. package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
  132. package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
  133. package/dist/plot/{Line.svelte → core/components/Line.svelte} +30 -13
  134. package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +7 -5
  135. package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +3 -2
  136. package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
  137. package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
  138. package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
  139. package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
  140. package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
  141. package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
  142. package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
  143. package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
  144. package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
  145. package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +4 -4
  146. package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +2 -2
  147. package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +7 -7
  148. package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +2 -2
  149. package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
  150. package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
  151. package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
  152. package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
  153. package/dist/plot/core/components/index.d.ts +17 -0
  154. package/dist/plot/core/components/index.js +17 -0
  155. package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
  156. package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +3 -5
  157. package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
  158. package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
  159. package/dist/plot/core/fill-utils.d.ts +33 -0
  160. package/dist/plot/core/fill-utils.js +388 -0
  161. package/dist/plot/{hover-lock.svelte.js → core/hover-lock.svelte.js} +5 -6
  162. package/dist/plot/core/index.d.ts +10 -0
  163. package/dist/plot/core/index.js +11 -0
  164. package/dist/plot/core/interactions.d.ts +35 -0
  165. package/dist/plot/core/interactions.js +195 -0
  166. package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
  167. package/dist/plot/{layout.js → core/layout.js} +16 -8
  168. package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +1 -1
  169. package/dist/plot/{reference-line.js → core/reference-line.js} +23 -36
  170. package/dist/plot/{scales.d.ts → core/scales.d.ts} +2 -2
  171. package/dist/plot/{scales.js → core/scales.js} +84 -85
  172. package/dist/plot/core/svg.d.ts +2 -0
  173. package/dist/plot/core/svg.js +41 -0
  174. package/dist/plot/{types.d.ts → core/types.d.ts} +19 -79
  175. package/dist/plot/{types.js → core/types.js} +1 -1
  176. package/dist/plot/{utils → core/utils}/label-placement.d.ts +2 -2
  177. package/dist/plot/core/utils/series-visibility.d.ts +26 -0
  178. package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
  179. package/dist/plot/core/utils.d.ts +11 -0
  180. package/dist/plot/core/utils.js +27 -0
  181. package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +154 -294
  182. package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
  183. package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
  184. package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
  185. package/dist/plot/histogram/index.d.ts +2 -0
  186. package/dist/plot/histogram/index.js +2 -0
  187. package/dist/plot/index.d.ts +8 -41
  188. package/dist/plot/index.js +10 -39
  189. package/dist/plot/sankey/Sankey.svelte +700 -0
  190. package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
  191. package/dist/plot/sankey/SankeyControls.svelte +98 -0
  192. package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
  193. package/dist/plot/sankey/index.d.ts +4 -0
  194. package/dist/plot/sankey/index.js +3 -0
  195. package/dist/plot/sankey/sankey-types.d.ts +42 -0
  196. package/dist/plot/sankey/sankey-types.js +4 -0
  197. package/dist/plot/sankey/sankey.d.ts +52 -0
  198. package/dist/plot/sankey/sankey.js +187 -0
  199. package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +61 -59
  200. package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +4 -4
  201. package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
  202. package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
  203. package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +221 -642
  204. package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +7 -7
  205. package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
  206. package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +1 -1
  207. package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
  208. package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
  209. package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
  210. package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
  211. package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +3 -3
  212. package/dist/plot/scatter/index.d.ts +7 -0
  213. package/dist/plot/scatter/index.js +5 -0
  214. package/dist/plot/scatter/scatter-data.d.ts +19 -0
  215. package/dist/plot/scatter/scatter-data.js +212 -0
  216. package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +12 -10
  217. package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +7 -7
  218. package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +5 -4
  219. package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +2 -2
  220. package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +11 -11
  221. package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +3 -3
  222. package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +1 -1
  223. package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +1 -1
  224. package/dist/plot/scatter-3d/index.d.ts +4 -0
  225. package/dist/plot/scatter-3d/index.js +4 -0
  226. package/dist/plot/sunburst/Sunburst.svelte +1045 -0
  227. package/dist/plot/sunburst/Sunburst.svelte.d.ts +96 -0
  228. package/dist/plot/sunburst/SunburstControls.svelte +200 -0
  229. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
  230. package/dist/plot/sunburst/index.d.ts +4 -0
  231. package/dist/plot/sunburst/index.js +4 -0
  232. package/dist/plot/sunburst/render.d.ts +34 -0
  233. package/dist/plot/sunburst/render.js +122 -0
  234. package/dist/plot/sunburst/sunburst.d.ts +62 -0
  235. package/dist/plot/sunburst/sunburst.js +266 -0
  236. package/dist/rdf/RdfPlot.svelte +2 -1
  237. package/dist/rdf/calc-rdf.js +11 -24
  238. package/dist/sanitize.js +1 -1
  239. package/dist/settings.d.ts +65 -1
  240. package/dist/settings.js +262 -0
  241. package/dist/spectral/Bands.svelte +39 -29
  242. package/dist/spectral/Bands.svelte.d.ts +3 -4
  243. package/dist/spectral/BandsAndDos.svelte +1 -1
  244. package/dist/spectral/BrillouinBandsDos.svelte +39 -27
  245. package/dist/spectral/Dos.svelte +10 -19
  246. package/dist/spectral/Dos.svelte.d.ts +2 -2
  247. package/dist/spectral/helpers.d.ts +3 -1
  248. package/dist/spectral/helpers.js +95 -29
  249. package/dist/structure/AtomLegend.svelte +8 -9
  250. package/dist/structure/CellSelect.svelte +1 -2
  251. package/dist/structure/Cylinder.svelte +12 -8
  252. package/dist/structure/Cylinder.svelte.d.ts +4 -1
  253. package/dist/structure/Structure.svelte +78 -72
  254. package/dist/structure/Structure.svelte.d.ts +1 -1
  255. package/dist/structure/StructureInfoPane.svelte +5 -6
  256. package/dist/structure/StructureScene.svelte +11 -10
  257. package/dist/structure/atom-properties.js +6 -6
  258. package/dist/structure/bond-order-perception.js +1 -1
  259. package/dist/structure/bonding.d.ts +1 -0
  260. package/dist/structure/bonding.js +43 -15
  261. package/dist/structure/export.js +27 -23
  262. package/dist/structure/index.d.ts +2 -4
  263. package/dist/structure/index.js +1 -3
  264. package/dist/structure/label-placement.js +4 -4
  265. package/dist/structure/measure.d.ts +3 -2
  266. package/dist/structure/measure.js +6 -5
  267. package/dist/structure/parse.js +121 -103
  268. package/dist/structure/pbc.js +4 -0
  269. package/dist/symmetry/SymmetryStats.svelte +2 -2
  270. package/dist/symmetry/index.d.ts +1 -1
  271. package/dist/symmetry/index.js +22 -24
  272. package/dist/symmetry/spacegroups.d.ts +7 -0
  273. package/dist/symmetry/spacegroups.js +48 -13
  274. package/dist/table/HeatmapTable.svelte +63 -11
  275. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  276. package/dist/table/index.d.ts +1 -3
  277. package/dist/table/index.js +1 -1
  278. package/dist/theme/index.js +8 -8
  279. package/dist/tooltip/KCoords.svelte +45 -0
  280. package/dist/tooltip/KCoords.svelte.d.ts +8 -0
  281. package/dist/tooltip/index.d.ts +1 -0
  282. package/dist/tooltip/index.js +1 -0
  283. package/dist/trajectory/Trajectory.svelte +66 -40
  284. package/dist/trajectory/Trajectory.svelte.d.ts +2 -1
  285. package/dist/trajectory/TrajectoryExportPane.svelte +2 -1
  286. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -1
  287. package/dist/trajectory/format-detect.d.ts +1 -0
  288. package/dist/trajectory/format-detect.js +25 -11
  289. package/dist/trajectory/frame-reader.js +17 -50
  290. package/dist/trajectory/helpers.js +1 -1
  291. package/dist/trajectory/index.js +1 -1
  292. package/dist/trajectory/parse/hdf5.js +1 -1
  293. package/dist/trajectory/parse/index.js +14 -6
  294. package/dist/trajectory/parse/vasp.js +36 -17
  295. package/dist/trajectory/parse/xyz.d.ts +24 -0
  296. package/dist/trajectory/parse/xyz.js +102 -89
  297. package/dist/trajectory/plotting.d.ts +1 -1
  298. package/dist/trajectory/plotting.js +15 -15
  299. package/dist/utils.d.ts +1 -0
  300. package/dist/utils.js +6 -4
  301. package/dist/xrd/XrdPlot.svelte +2 -1
  302. package/dist/xrd/calc-xrd.js +15 -12
  303. package/dist/xrd/parse.js +2 -2
  304. package/package.json +22 -18
  305. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  306. package/dist/plot/axis-utils.d.ts +0 -19
  307. package/dist/plot/axis-utils.js +0 -78
  308. package/dist/plot/defaults.d.ts +0 -19
  309. package/dist/plot/defaults.js +0 -9
  310. package/dist/plot/fill-utils.d.ts +0 -46
  311. package/dist/plot/fill-utils.js +0 -322
  312. package/dist/plot/interactions.d.ts +0 -12
  313. package/dist/plot/interactions.js +0 -101
  314. package/dist/plot/svg.d.ts +0 -1
  315. package/dist/plot/svg.js +0 -11
  316. package/dist/plot/utils/series-visibility.d.ts +0 -15
  317. package/dist/plot/utils.d.ts +0 -1
  318. package/dist/plot/utils.js +0 -14
  319. /package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +0 -0
  320. /package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +0 -0
  321. /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
  322. /package/dist/plot/{hover-lock.svelte.d.ts → core/hover-lock.svelte.d.ts} +0 -0
  323. /package/dist/plot/{utils → core/utils}/label-placement.js +0 -0
  324. /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
@@ -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 { 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'
10
10
  import type {
11
11
  AxisLoadError,
12
12
  BarHandlerProps,
@@ -27,7 +27,7 @@
27
27
  RefLineEvent,
28
28
  ScaleType,
29
29
  UserContentProps,
30
- } from './'
30
+ } from '..'
31
31
  import {
32
32
  BarPlotControls,
33
33
  compute_element_placement,
@@ -35,56 +35,69 @@
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'
42
41
  import {
43
42
  create_dimension_tracker,
44
43
  create_hover_lock,
45
- } from './hover-lock.svelte'
44
+ } from '../core/hover-lock.svelte'
45
+ import { create_legend_visibility } from '../core/utils/series-visibility'
46
46
  import {
47
+ axis_ranges_equal,
47
48
  get_relative_coords,
48
- pan_range,
49
+ MIN_TOUCH_DISTANCE_PIXELS,
50
+ pan_range_by_pixels,
49
51
  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'
52
+ remove_drag_listeners,
53
+ resolve_axis_ranges,
54
+ sorted_range,
55
+ to_epoch_num,
56
+ zoom_range_by_factor,
57
+ } from '../core/interactions'
58
+ import type { IndexedRefLine } from '../core/reference-line'
59
+ import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
54
60
  import {
55
61
  create_color_scale,
56
62
  create_scale,
57
63
  create_size_scale,
58
64
  generate_ticks,
59
- get_nice_data_range,
60
65
  get_tick_label,
61
- } from './scales'
62
- import { DEFAULT_MARKERS, get_scale_type_name } from './types'
63
- import { DEFAULTS } from '../settings'
66
+ } from '../core/scales'
67
+ import { DEFAULT_MARKERS } from '../core/types'
68
+ import { DEFAULTS } from '../../settings'
64
69
  import { extent } from 'd3-array'
65
70
  import type { Snippet } from 'svelte'
66
- import { untrack } from 'svelte'
71
+ import { onDestroy, untrack } from 'svelte'
67
72
  import type { HTMLAttributes } from 'svelte/elements'
68
73
  import { Tween, type TweenOptions } from 'svelte/motion'
69
- import { SvelteMap } from 'svelte/reactivity'
70
74
  import {
71
75
  build_obstacles_norm,
72
76
  clip_bar,
73
77
  has_explicit_position,
74
78
  measured_footprint,
75
79
  place_decorations,
76
- } from './auto-place'
80
+ } from '../core/auto-place'
77
81
  import {
78
82
  calc_auto_padding,
79
- constrain_tooltip_position,
80
83
  filter_padding,
81
84
  LABEL_GAP_DEFAULT,
85
+ y2_axis_label_x,
82
86
  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'
87
+ } from '../core/layout'
88
+ import PlotTooltip from '../core/components/PlotTooltip.svelte'
89
+ import { bar_path } from '../core/svg'
90
+ import { unique_id } from '../core/utils'
91
+ import ZeroLines from '../core/components/ZeroLines.svelte'
92
+ import ZoomRect from '../core/components/ZoomRect.svelte'
93
+ import {
94
+ compute_bar_auto_ranges,
95
+ compute_group_info,
96
+ compute_stacked_offsets,
97
+ normalize_categorical,
98
+ } from './data'
99
+ import { compute_bar_rect, compute_line_points } from './geometry'
100
+ import type { LineSeriesPoint as BarLineSeriesPoint } from './geometry'
88
101
 
89
102
  // Handler props for line marker events (extends BarHandlerProps with point-specific data)
90
103
  interface LineMarkerHandlerProps extends BarHandlerProps<Metadata> {
@@ -92,27 +105,17 @@
92
105
  }
93
106
 
94
107
  // 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
- }
108
+ type LineSeriesPoint = BarLineSeriesPoint<Metadata>
102
109
 
103
110
  let {
104
111
  series = $bindable([]),
105
112
  orientation = $bindable(`vertical`),
106
113
  mode = $bindable(`overlay`),
107
114
  x_axis = $bindable({}),
108
- x2_axis = $bindable({}),
115
+ x2_axis: x2_axis_prop = $bindable({}),
109
116
  y_axis = $bindable({}),
110
- y2_axis = $bindable({}),
117
+ y2_axis: y2_axis_prop = $bindable({}),
111
118
  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
119
  range_padding = 0.05,
117
120
  padding = { t: 20, b: 60, l: 60, r: 20 },
118
121
  legend = {},
@@ -222,29 +225,31 @@
222
225
  // Initialize bar, line, y2_axis with defaults - using $derived for reactivity
223
226
  let bar_state = $derived({ ...DEFAULTS.bar.bar, ...bar })
224
227
  let line_state = $derived({ ...DEFAULTS.bar.line, ...line })
225
- y2_axis = {
228
+ // Merge secondary-axis defaults as deriveds instead of assigning back into the
229
+ // $bindable props (which would push library defaults into the parent's bound state)
230
+ let y2_axis = $derived({
226
231
  format: ``,
227
232
  scale_type: `linear`,
228
233
  ticks: 5,
229
- label_shift: { y: 60 },
234
+ label_shift: { x: 0, y: 0 }, // y2 title stays vertically centered (x pos set by y2_axis_label_x)
230
235
  tick: { label: { shift: { x: 0, y: 0 } } }, // base offset handled in rendering
231
236
  range: [null, null],
232
- ...y2_axis,
233
- }
234
- x2_axis = {
237
+ ...y2_axis_prop,
238
+ } as typeof y2_axis_prop)
239
+ let x2_axis = $derived({
235
240
  format: ``,
236
241
  scale_type: `linear`,
237
242
  ticks: 5,
238
243
  label_shift: { x: 0, y: 40 },
239
244
  tick: { label: { shift: { x: 0, y: 0 } } },
240
245
  range: [null, null],
241
- ...x2_axis,
242
- }
246
+ ...x2_axis_prop,
247
+ } as typeof x2_axis_prop)
243
248
 
244
249
  let [width, height] = $state([0, 0])
245
250
  let wrapper: HTMLDivElement | undefined = $state()
246
251
  let svg_element: SVGElement | null = $state(null)
247
- let clip_path_id = `chart-clip-${crypto?.randomUUID?.()}`
252
+ const clip_path_id = unique_id(`chart-clip`) // stable, collision-resistant (see unique_id)
248
253
 
249
254
  // Reference line hover state
250
255
  let hovered_ref_line_idx = $state<number | null>(null)
@@ -256,55 +261,25 @@
256
261
  let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
257
262
  let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
258
263
 
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
- })
264
+ // === Categorical Normalization (string x values -> integer indices, see ./data) ===
265
+ let cat_norm = $derived(normalize_categorical(series, x_axis.categories))
266
+ let category_list = $derived(cat_norm.category_list)
267
+ let internal_series = $derived(cat_norm.internal_series)
272
268
 
273
269
  let category_indices = $derived(
274
- category_list.length ? category_list.map((_, idx) => idx) : null,
270
+ category_list.length > 0 ? category_list.map((_, idx) => idx) : null,
275
271
  )
276
272
 
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
- })
273
+ // Thin categorical tick labels + grid lines when many categories would overlap.
274
+ // Bars still render for every category (this only reduces drawn ticks/labels/grid).
275
+ let cat_tick_indices = $derived.by<number[]>(() => {
276
+ if (!category_indices) return []
277
+ const axis_px = (orientation === `horizontal` ? height : width) || 0
278
+ const max_ticks = Math.max(1, Math.floor(axis_px / 28)) // ~28px per category label
279
+ const step = Math.ceil(category_indices.length / max_ticks)
280
+ return step <= 1
281
+ ? category_indices
282
+ : category_indices.filter((_, idx) => idx % step === 0)
308
283
  })
309
284
 
310
285
  // Compute auto ranges from visible series
@@ -323,124 +298,26 @@
323
298
  visible_series.filter((srs) => srs.x_axis === `x2`),
324
299
  )
325
300
 
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
- })
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
+ }))
444
321
 
445
322
  // Initialize and current ranges
446
323
  let ranges = $state<{
@@ -452,38 +329,16 @@
452
329
  })
453
330
 
454
331
  $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
- }
332
+ // resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
333
+ // 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)
335
+ if (!next) return
336
+ // Only update if the initial (data-driven) ranges changed, not when user pans.
337
+ // untrack the read of `ranges` so the assignment below can't re-trigger this effect
338
+ // (reading + writing the same state otherwise causes effect_update_depth_exceeded).
339
+ const init = untrack(() => ranges.initial)
340
+ if (!axis_ranges_equal(init, next)) {
341
+ ranges = { initial: { ...next }, current: { ...next } }
487
342
  }
488
343
  })
489
344
 
@@ -494,7 +349,7 @@
494
349
 
495
350
  // Update padding when format or ticks change
496
351
  $effect(() => {
497
- const new_pad = width && height && ticks.y.length
352
+ const new_pad = width && height && ticks.y.length > 0
498
353
  ? calc_auto_padding({
499
354
  padding,
500
355
  default_padding,
@@ -505,7 +360,7 @@
505
360
  : filter_padding(padding, default_padding)
506
361
  // Expand right padding if y2 ticks are shown (only for vertical orientation)
507
362
  if (
508
- width && height && y2_series.length && ticks.y2.length &&
363
+ width && height && y2_series.length > 0 && ticks.y2.length > 0 &&
509
364
  orientation === `vertical`
510
365
  ) {
511
366
  // Need space for: tick shift + tick width + gap (30px) + label space (20px if present)
@@ -521,7 +376,7 @@
521
376
  }
522
377
  // Expand top padding if x2 ticks are shown (only for vertical orientation)
523
378
  if (
524
- width && height && x2_series.length && ticks.x2.length &&
379
+ width && height && x2_series.length > 0 && ticks.x2.length > 0 &&
525
380
  orientation === `vertical`
526
381
  ) {
527
382
  const inside = x2_axis.tick?.label?.inside ?? false
@@ -546,7 +401,7 @@
546
401
  // from baseline to its tip so the legend can't hide inside a tall bar. Built from internal_series
547
402
  // (pad-independent) + ranges so the crowding decision can't see its own reservation.
548
403
  const obstacles_norm = $derived.by(() => {
549
- if (!width || !height || !visible_series.length) return []
404
+ if (!width || !height || visible_series.length === 0) return []
550
405
  const base_w = width - base_pad.l - base_pad.r
551
406
  const base_h = height - base_pad.t - base_pad.b
552
407
  if (base_w <= 0 || base_h <= 0) return []
@@ -661,7 +516,7 @@
661
516
  // In vertical mode categories are on x-axis; in horizontal mode on y-axis
662
517
  let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
663
518
  let effective_cat_ticks = $derived.by(() => {
664
- if (!category_list.length) return undefined
519
+ if (category_list.length === 0) return undefined
665
520
  // Only respect user ticks when they're a Record (custom label mapping),
666
521
  // not a number (tick count) or array (tick positions)
667
522
  const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
@@ -677,7 +532,7 @@
677
532
  // Ticks
678
533
  let ticks = $derived({
679
534
  x: width && height
680
- ? (category_indices && cat_axis === `x` ? category_indices : generate_ticks(
535
+ ? (category_indices && cat_axis === `x` ? cat_tick_indices : generate_ticks(
681
536
  ranges.current.x,
682
537
  x_axis.scale_type ?? `linear`,
683
538
  x_axis.ticks,
@@ -686,7 +541,7 @@
686
541
  ))
687
542
  : [],
688
543
  y: width && height
689
- ? (category_indices && cat_axis === `y` ? category_indices : generate_ticks(
544
+ ? (category_indices && cat_axis === `y` ? cat_tick_indices : generate_ticks(
690
545
  ranges.current.y,
691
546
  y_axis.scale_type ?? `linear`,
692
547
  y_axis.ticks,
@@ -762,31 +617,22 @@
762
617
  const dx = Math.abs(drag_state.start.x - drag_state.current.x)
763
618
  const dy = Math.abs(drag_state.start.y - drag_state.current.y)
764
619
 
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]
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)]
778
623
 
779
624
  if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
780
625
  // 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)] }
626
+ x_axis = { ...x_axis, range: sorted_range(xr1, xr2) }
782
627
  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
- }
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) }
787
635
  }
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
636
  }
791
637
  }
792
638
  drag_state = { start: null, current: null, bounds: null }
@@ -795,40 +641,30 @@
795
641
  document.body.style.cursor = `default`
796
642
  }
797
643
 
798
- // Pan drag handlers
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)
799
660
  const on_pan_move = (evt: MouseEvent) => {
800
661
  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
662
  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,
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,
821
667
  )
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
668
  }
833
669
 
834
670
  const on_pan_end = () => {
@@ -838,6 +674,15 @@
838
674
  window.removeEventListener(`mouseup`, on_pan_end)
839
675
  }
840
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
684
+ })
685
+
841
686
  function handle_mouse_down(evt: MouseEvent) {
842
687
  const coords = get_relative_coords(evt)
843
688
  if (!coords || !svg_element) return
@@ -880,34 +725,15 @@
880
725
 
881
726
  const sensitivity = pan?.wheel_sensitivity ?? 1
882
727
 
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
-
728
+ // Pan along the dominant wheel direction
905
729
  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)
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)
908
733
  } else {
909
- ranges.current.y = pan_range(ranges.current.y, y_delta)
910
- ranges.current.y2 = pan_range(ranges.current.y2, y2_delta)
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)
911
737
  }
912
738
  }
913
739
 
@@ -945,74 +771,17 @@
945
771
 
946
772
  // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
947
773
  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
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
950
776
  const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
951
777
  const scale = curr_dist / start_dist
952
778
 
953
779
  // If scale changed significantly, treat as pinch-zoom
954
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)
955
782
  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
- }
783
+ zoom_all_axes(touch_state, scale)
784
+ } else pan_all_axes(touch_state, -dx, dy)
1016
785
  }
1017
786
 
1018
787
  function handle_touch_end() {
@@ -1076,34 +845,11 @@
1076
845
  })
1077
846
  )
1078
847
 
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
- }
848
+ const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
1103
849
 
1104
850
  // Collect bar and line positions for legend placement
1105
851
  let bar_points_for_placement = $derived.by(() => {
1106
- if (!width || !height || !visible_series.length) return []
852
+ if (!width || !height || visible_series.length === 0) return []
1107
853
 
1108
854
  return internal_series.flatMap((srs, series_idx) => {
1109
855
  if (!(srs?.visible ?? true)) return []
@@ -1189,7 +935,6 @@
1189
935
 
1190
936
  // Tooltip state
1191
937
  let hover_info = $state<BarHandlerProps<Metadata> | null>(null)
1192
- let tooltip_el = $state<HTMLDivElement | undefined>()
1193
938
 
1194
939
  function get_bar_data(
1195
940
  series_idx: number,
@@ -1267,49 +1012,10 @@
1267
1012
  }
1268
1013
 
1269
1014
  // 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
- })
1015
+ let stacked_offsets = $derived(compute_stacked_offsets(internal_series, mode))
1302
1016
 
1303
1017
  // 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
- })
1018
+ let group_info = $derived(compute_group_info(internal_series, mode))
1313
1019
 
1314
1020
  // Set theme-aware background when entering fullscreen
1315
1021
  $effect(() => {
@@ -1327,9 +1033,9 @@
1327
1033
  set_axis: (axis, config) => {
1328
1034
  // Spread into existing state to preserve merged type structure
1329
1035
  if (axis === `x`) x_axis = { ...x_axis, ...config }
1330
- else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
1036
+ else if (axis === `x2`) x2_axis_prop = { ...x2_axis_prop, ...config }
1331
1037
  else if (axis === `y`) y_axis = { ...y_axis, ...config }
1332
- else y2_axis = { ...y2_axis, ...config }
1038
+ else y2_axis_prop = { ...y2_axis_prop, ...config }
1333
1039
  },
1334
1040
  get_series: () => series,
1335
1041
  set_series: (new_series) => (series = new_series),
@@ -1337,32 +1043,12 @@
1337
1043
  set_loading: (axis) => (axis_loading = axis),
1338
1044
  }
1339
1045
 
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(
1046
+ // Shared handler + one-shot auto-load bound to this component's state
1047
+ const { handle_axis_change, try_auto_load } = create_axis_loader(
1343
1048
  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
- })
1049
+ () => ({ data_loader, on_axis_change, on_error }),
1050
+ )
1051
+ $effect(try_auto_load)
1366
1052
  </script>
1367
1053
 
1368
1054
  {#snippet ref_lines_layer(lines: IndexedRefLine[])}
@@ -1440,9 +1126,9 @@
1440
1126
  ranges.current.y2 = [...ranges.initial.y2] as [number, number]
1441
1127
  // Also reset axis props so future data changes recalculate auto ranges
1442
1128
  x_axis = { ...x_axis, range: [null, null] }
1443
- x2_axis = { ...x2_axis, range: [null, null] }
1129
+ x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
1444
1130
  y_axis = { ...y_axis, range: [null, null] }
1445
- y2_axis = { ...y2_axis, range: [null, null] }
1131
+ y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
1446
1132
  }}
1447
1133
  onmouseleave={() => {
1448
1134
  hovered = false
@@ -1454,6 +1140,7 @@
1454
1140
  ontouchstart={handle_touch_start}
1455
1141
  ontouchmove={handle_touch_move}
1456
1142
  ontouchend={handle_touch_end}
1143
+ ontouchcancel={handle_touch_end}
1457
1144
  style:cursor={pan_drag_state
1458
1145
  ? `grabbing`
1459
1146
  : shift_held && pan?.enabled !== false
@@ -1487,6 +1174,7 @@
1487
1174
  ticks={ticks.x as number[]}
1488
1175
  place={scales.x}
1489
1176
  axis={x_axis}
1177
+ domain={ranges.current.x as Vec2}
1490
1178
  {pad}
1491
1179
  {width}
1492
1180
  {height}
@@ -1507,6 +1195,7 @@
1507
1195
  ticks={ticks.x2 as number[]}
1508
1196
  place={scales.x2}
1509
1197
  axis={x2_axis}
1198
+ domain={ranges.current.x2 as Vec2}
1510
1199
  {pad}
1511
1200
  {width}
1512
1201
  {height}
@@ -1525,6 +1214,7 @@
1525
1214
  ticks={ticks.y as number[]}
1526
1215
  place={scales.y}
1527
1216
  axis={y_axis}
1217
+ domain={ranges.current.y as Vec2}
1528
1218
  {pad}
1529
1219
  {width}
1530
1220
  {height}
@@ -1544,21 +1234,18 @@
1544
1234
  <!-- Y2-axis (Right) -->
1545
1235
  <!-- Note: y2 axis is only supported for vertical orientation. Implementing x2 for horizontal mode requires additional complexity. -->
1546
1236
  {#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
1237
  <PlotAxis
1551
1238
  side="y2"
1552
1239
  ticks={ticks.y2 as number[]}
1553
1240
  place={scales.y2}
1554
1241
  axis={y2_axis}
1242
+ domain={ranges.current.y2 as Vec2}
1555
1243
  {pad}
1556
1244
  {width}
1557
1245
  {height}
1558
1246
  show_grid={display.y2_grid}
1559
1247
  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)}
1248
+ label_x={y2_axis_label_x(y2_axis, width, pad.r, tick_label_widths.y2_max)}
1562
1249
  label_y={pad.t + chart_height / 2 + (y2_axis.label_shift?.y ?? 0)}
1563
1250
  axis_loading={axis_loading === `y2`}
1564
1251
  on_axis_change={(key) => handle_axis_change(`y2`, key)}
@@ -1572,7 +1259,10 @@
1572
1259
  </clipPath>
1573
1260
  </defs>
1574
1261
 
1575
- <!-- Clipped content: zero lines, bars, and lines -->
1262
+ <!-- Chart content is clipped in two groups so reference lines can interleave
1263
+ at their z positions while staying outside the chart clip: each line still
1264
+ self-clips to the plot area inside ReferenceLine, only its annotation text
1265
+ is allowed to overflow the plot edges. -->
1576
1266
  <g clip-path="url(#{clip_path_id})">
1577
1267
  <ZeroLines
1578
1268
  {display}
@@ -1596,11 +1286,12 @@
1596
1286
  {height}
1597
1287
  {pad}
1598
1288
  />
1289
+ </g>
1599
1290
 
1600
- <!-- Reference lines: below lines -->
1601
- {@render ref_lines_layer(ref_lines_by_z.below_lines)}
1291
+ {@render ref_lines_layer(ref_lines_by_z.below_lines)}
1602
1292
 
1603
- <!-- Bars and Lines -->
1293
+ <!-- Bars and Lines -->
1294
+ <g clip-path="url(#{clip_path_id})">
1604
1295
  {#each internal_series as srs, series_idx (srs?.id ?? series_idx)}
1605
1296
  {#if srs?.visible ?? true}
1606
1297
  {@const is_line = srs.render_mode === `line`}
@@ -1626,42 +1317,14 @@
1626
1317
  series_markers === `line+points`}
1627
1318
  {@const show_points = series_markers === `points` ||
1628
1319
  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))}
1320
+ {@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
+ })}
1665
1328
  {@const polyline_str = show_line && points.length > 1
1666
1329
  ? points.map((pt) => `${pt.x},${pt.y}`).join(` `)
1667
1330
  : ``}
@@ -1826,37 +1489,24 @@
1826
1489
  {@const bar_width_val = Array.isArray(srs.bar_width)
1827
1490
  ? (srs.bar_width[bar_idx] ?? 0.5)
1828
1491
  : (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
1492
  {@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}
1493
+ {@const x_scale_bar = srs.x_axis === `x2` ? scales.x2 : scales.x}
1847
1494
  {@const [cat_scale, val_scale] = is_vertical
1848
- ? [x_scale_bar, y_scale]
1495
+ ? [x_scale_bar, srs.y_axis === `y2` ? scales.y2 : scales.y]
1849
1496
  : [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))]}
1497
+ {@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
+ })}
1860
1510
  {#if (is_vertical ? rect_h : rect_w) > 0}
1861
1511
  <path
1862
1512
  d={bar_path(
@@ -1914,13 +1564,10 @@
1914
1564
  </g>
1915
1565
  {/if}
1916
1566
  {/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
1567
  </g>
1568
+
1569
+ {@render ref_lines_layer(ref_lines_by_z.below_points)}
1570
+ {@render ref_lines_layer(ref_lines_by_z.above_all)}
1924
1571
  </svg>
1925
1572
 
1926
1573
  <!-- Legend -->
@@ -1939,12 +1586,13 @@
1939
1586
  bind:root_element={legend_element}
1940
1587
  {...legend}
1941
1588
  series_data={legend_data}
1942
- on_toggle={legend?.on_toggle || toggle_series_visibility}
1943
- on_group_toggle={legend?.on_group_toggle || toggle_group_visibility}
1589
+ on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
1590
+ on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
1591
+ on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
1944
1592
  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
1593
+ on_item_hover={(item) =>
1594
+ (hovered_legend_series_idx = item != null && item.series_idx >= 0
1595
+ ? item.series_idx
1948
1596
  : null)}
1949
1597
  active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1950
1598
  style={`
@@ -1963,22 +1611,14 @@
1963
1611
  )}
1964
1612
  {@const cy = (hover_info.active_y_axis === `y2` ? scales.y2 : scales.y)(
1965
1613
  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
1614
  )}
1976
1615
  <PlotTooltip
1977
- x={tooltip_pos.x}
1978
- y={tooltip_pos.y}
1979
- offset={{ x: 0, y: 0 }}
1616
+ x={cx}
1617
+ y={cy}
1618
+ offset={{ x: 10, y: 5 }}
1619
+ constrain_to={{ width, height }}
1620
+ fallback_size={{ width: 140, height: 50 }}
1980
1621
  bg_color={hover_info.color}
1981
- bind:wrapper={tooltip_el}
1982
1622
  >
1983
1623
  {#if tooltip}
1984
1624
  {@render tooltip({ ...hover_info, fullscreen })}
@@ -2017,9 +1657,9 @@
2017
1657
  bind:orientation
2018
1658
  bind:mode
2019
1659
  bind:x_axis
2020
- bind:x2_axis
1660
+ bind:x2_axis={x2_axis_prop}
2021
1661
  bind:y_axis
2022
- bind:y2_axis
1662
+ bind:y2_axis={y2_axis_prop}
2023
1663
  bind:display
2024
1664
  auto_x_range={auto_ranges.x as Vec2}
2025
1665
  auto_x2_range={auto_ranges.x2 as Vec2}
@@ -2063,8 +1703,10 @@
2063
1703
  background: var(--barplot-fullscreen-bg, var(--barplot-bg, var(--plot-bg)));
2064
1704
  max-height: none !important;
2065
1705
  overflow: hidden;
2066
- /* Add padding to prevent titles from being cropped at top */
2067
- padding-top: var(--plot-fullscreen-padding-top, 2em);
1706
+ /* border-top (not padding-top): bind:clientHeight includes padding but excludes
1707
+ borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
1708
+ border-top: var(--plot-fullscreen-padding-top, 2em) solid
1709
+ var(--barplot-fullscreen-bg, var(--barplot-bg, var(--plot-bg, transparent)));
2068
1710
  box-sizing: border-box;
2069
1711
  }
2070
1712
  .header-controls {