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,12 +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 type { D3SymbolName } from '../labels'
7
- import { format_value, symbol_names } from '../labels'
8
- import { sanitize_html } from '../sanitize'
9
- import { FullscreenToggle, set_fullscreen_bg } from '../layout'
10
- 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'
11
10
  import type {
12
11
  AxisLoadError,
13
12
  BasePlotProps,
@@ -32,7 +31,7 @@
32
31
  ScatterHandlerProps,
33
32
  StyleOverrides,
34
33
  UserContentProps,
35
- } from './'
34
+ } from '..'
36
35
  import {
37
36
  ColorBar,
38
37
  compute_element_placement,
@@ -47,86 +46,79 @@
47
46
  ScatterPoint,
48
47
  ZeroLines,
49
48
  ZoomRect,
50
- } from './'
49
+ } from '..'
51
50
  import {
52
51
  build_obstacles_norm,
53
52
  has_explicit_position,
54
53
  measured_footprint,
55
54
  place_decorations,
56
- } from './auto-place'
57
- import type { AxisChangeState } from './axis-utils'
58
- import { create_axis_change_handler } from './axis-utils'
59
- import {
60
- get_series_color,
61
- get_series_symbol,
62
- process_prop,
63
- } from './data-transform'
64
- import { AXIS_DEFAULTS } from './defaults'
55
+ } from '../core/auto-place'
56
+ import type { AxisChangeState } from '../core/axis-utils'
57
+ import { AXIS_DEFAULTS, create_axis_loader } from '../core/axis-utils'
58
+ import { get_series_color, get_series_symbol } from '../core/data-transform'
65
59
  import {
66
60
  create_dimension_tracker,
67
61
  create_hover_lock,
68
- } from './hover-lock.svelte'
62
+ } from '../core/hover-lock.svelte'
69
63
  import {
70
64
  DEFAULT_MARKERS,
71
65
  get_scale_type_name,
72
66
  is_time_scale,
73
- } from './types'
74
- import { compute_label_positions } from './utils/label-placement'
75
- import type { SeriesVisibilitySnapshot } from './utils/series-visibility'
76
- import {
77
- handle_legend_double_click,
78
- toggle_group_visibility,
79
- toggle_series_visibility,
80
- } from './utils/series-visibility'
81
- import { DEFAULTS } from '../settings'
67
+ } from '../core/types'
68
+ import { compute_label_positions } from '../core/utils/label-placement'
69
+ import { create_legend_visibility } from '../core/utils/series-visibility'
70
+ import { DEFAULTS } from '../../settings'
82
71
  import { extent } from 'd3-array'
83
72
  import { scaleTime } from 'd3-scale'
84
73
  import type { ComponentProps, Snippet } from 'svelte'
85
- import { untrack } from 'svelte'
74
+ import { onDestroy, untrack } from 'svelte'
86
75
  import type { HTMLAttributes } from 'svelte/elements'
87
76
  import { Tween, type TweenOptions } from 'svelte/motion'
88
77
  import { SvelteSet } from 'svelte/reactivity'
89
- import type { FillPathPoint } from './fill-utils'
78
+ import type { Pt } from '../core/fill-utils'
90
79
  import {
91
- apply_range_constraints,
92
- apply_where_condition,
93
- clamp_for_log_scale,
80
+ compute_fill_segments,
94
81
  convert_error_band_to_fill_region,
95
82
  generate_fill_path,
96
- is_fill_gradient,
97
- resolve_boundary,
98
- } from './fill-utils'
83
+ } from '../core/fill-utils'
99
84
  import {
100
85
  expand_range_if_needed,
101
86
  get_relative_coords,
87
+ MIN_TOUCH_DISTANCE_PIXELS,
102
88
  normalize_y2_sync,
103
- pan_range,
89
+ pan_range_by_pixels,
104
90
  PINCH_ZOOM_THRESHOLD,
105
- pixels_to_data_delta,
91
+ remove_drag_listeners,
92
+ sorted_range,
106
93
  sync_y2_range,
107
- } from './interactions'
108
- import type { Rect, Sides } from './layout'
94
+ to_epoch_num,
95
+ zoom_range_by_factor,
96
+ } from '../core/interactions'
97
+ import type { Rect, Sides } from '../core/layout'
109
98
  import {
110
99
  calc_auto_padding,
111
- constrain_tooltip_position,
112
100
  filter_padding,
113
101
  LABEL_GAP_DEFAULT,
102
+ y2_axis_label_x,
114
103
  measure_full_footprint,
115
104
  measure_max_tick_width,
116
105
  sample_series_obstacle_points,
117
- } from './layout'
118
- import type { IndexedRefLine } from './reference-line'
119
- import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
106
+ } from '../core/layout'
107
+ import type { IndexedRefLine } from '../core/reference-line'
108
+ import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
120
109
  import {
121
110
  create_color_scale,
122
111
  create_scale,
123
112
  create_size_scale,
124
113
  generate_ticks,
125
114
  get_nice_data_range,
126
- } from './scales'
127
-
128
- const in_range = (val: number | null | undefined, lo: number, hi: number) =>
129
- val != null && !isNaN(val) && val >= Math.min(lo, hi) && val <= Math.max(lo, hi)
115
+ } from '../core/scales'
116
+ import { resolve_line_tween, unique_id } from '../core/utils'
117
+ import {
118
+ build_legend_data,
119
+ filter_series_to_ranges,
120
+ pick_tooltip_bg,
121
+ } from './scatter-data'
130
122
 
131
123
  let {
132
124
  series = $bindable([]),
@@ -291,7 +283,7 @@
291
283
  let touched = new SvelteSet<string>()
292
284
 
293
285
  // Unique component ID to avoid clipPath conflicts between multiple instances
294
- let component_id = $state(`scatter-${crypto.randomUUID()}`)
286
+ let component_id = $state(unique_id(`scatter`))
295
287
  let clip_path_id = $derived(`plot-area-clip-${component_id}`)
296
288
 
297
289
  // Assign stable IDs to series for keying
@@ -317,7 +309,7 @@
317
309
  let zoom_x2_range = $state<[number, number]>([0, 1])
318
310
  let zoom_y_range = $state<[number, number]>([0, 1])
319
311
  let zoom_y2_range = $state<[number, number]>([0, 1])
320
- let prev_series_visibility: SeriesVisibilitySnapshot | null = $state(null)
312
+ const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
321
313
 
322
314
  // Y2 axis sync configuration
323
315
  let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
@@ -390,9 +382,6 @@
390
382
  colorbar_hover.cleanup()
391
383
  })
392
384
 
393
- // Tooltip element reference for dynamic sizing
394
- let tooltip_el = $state<HTMLDivElement | undefined>()
395
-
396
385
  // Module-level constants to avoid repeated allocations
397
386
  // Create and categorize points in a single pass (instead of 3 separate iterations)
398
387
  type SimplePoint = { x: number; y: number }
@@ -431,7 +420,7 @@
431
420
  // Update padding when format or ticks change
432
421
  $effect(() => {
433
422
  const new_pad = width && height &&
434
- (y_tick_values.length || y2_tick_values.length || x2_tick_values.length)
423
+ (y_tick_values.length > 0 || y2_tick_values.length > 0 || x2_tick_values.length > 0)
435
424
  ? calc_auto_padding({
436
425
  padding,
437
426
  default_padding,
@@ -650,7 +639,14 @@
650
639
  if (result.changed) initial_y2_range = result.range
651
640
  // Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
652
641
  if (y2_sync_config.mode !== `none`) {
653
- zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
642
+ // untrack the read of zoom_y_range: this effect also writes it (fresh array per
643
+ // run when y.explicit), so a tracked read would loop until
644
+ // effect_update_depth_exceeded. Pan/zoom handlers sync y2 themselves.
645
+ zoom_y2_range = sync_y2_range(
646
+ untrack(() => zoom_y_range),
647
+ initial_y2_range,
648
+ y2_sync_config,
649
+ )
654
650
  } else if (result.changed) {
655
651
  zoom_y2_range = result.range
656
652
  }
@@ -723,80 +719,29 @@
723
719
 
724
720
  // Filter series data to only include points within bounds and augment with internal data
725
721
  let filtered_series = $derived(
726
- series_with_ids
727
- .map((data_series: DataSeries<Metadata>, series_idx): DataSeries<Metadata> => {
728
- // Handle null/undefined series first
729
- if (!data_series) {
730
- return {
731
- x: [],
732
- y: [],
733
- visible: true,
734
- filtered_data: [],
735
- _id: series_idx,
736
- orig_series_idx: series_idx,
737
- }
738
- }
739
-
740
- // Handle explicitly hidden series
741
- if (!(data_series.visible ?? true)) {
742
- return {
743
- ...data_series,
744
- visible: false,
745
- filtered_data: [],
746
- orig_series_idx: series_idx,
747
- }
748
- }
749
-
750
- const { x: xs, y: ys, color_values, size_values, ...series_rest } = data_series
751
-
752
- // Process points internally, adding properties beyond the base Point type
753
- const processed_points: InternalPoint<Metadata>[] = xs.map(
754
- (x_val: number, point_idx: number) => ({
755
- x: x_val,
756
- y: ys[point_idx],
757
- color_value: color_values?.[point_idx],
758
- metadata: process_prop(series_rest.metadata, point_idx) as Metadata | undefined,
759
- point_style: process_prop(series_rest.point_style, point_idx),
760
- point_hover: process_prop(series_rest.point_hover, point_idx),
761
- point_label: process_prop(series_rest.point_label, point_idx),
762
- point_offset: process_prop(series_rest.point_offset, point_idx),
763
- series_idx,
764
- point_idx,
765
- size_value: size_values?.[point_idx],
766
- }),
767
- )
768
-
769
- // Filter to points within the plot bounds (handles inverted ranges like [3.5, 1.4])
770
- // Determine which ranges to use based on series axis properties
771
- const [series_x_min, series_x_max] = (data_series.x_axis ?? `x1`) === `x2`
772
- ? [x2_min, x2_max]
773
- : [x_min, x_max]
774
- const [series_y_min, series_y_max] = (data_series.y_axis ?? `y1`) === `y2`
775
- ? [y2_min, y2_max]
776
- : [y_min, y_max]
777
-
778
- const filtered_data_with_extras = processed_points.filter(
779
- ({ x, y }) =>
780
- in_range(x, series_x_min, series_x_max) &&
781
- in_range(y, series_y_min, series_y_max),
782
- )
783
-
784
- // Return structure consistent with DataSeries but acknowledge internal data structure (filtered_data)
785
- return {
786
- ...data_series,
787
- visible: true, // Mark series as visible here
788
- filtered_data: filtered_data_with_extras,
789
- orig_series_idx: series_idx, // Store original index for auto-cycling colors/symbols
790
- }
791
- })
792
- // Filter series end up completely empty after point filtering
793
- .filter((
794
- srs,
795
- ): srs is DataSeries<Metadata> & { filtered_data: InternalPoint<Metadata>[] } =>
796
- !!srs.filtered_data && srs.filtered_data.length > 0
797
- ),
722
+ filter_series_to_ranges(series_with_ids, {
723
+ x: [x_min, x_max],
724
+ x2: [x2_min, x2_max],
725
+ y: [y_min, y_max],
726
+ y2: [y2_min, y2_max],
727
+ }),
798
728
  )
799
729
 
730
+ // Tally line series/points to budget path-morph tweens (see resolve_line_tween).
731
+ // Disabling the morph for high-cardinality plots (e.g. phonon bands) keeps them
732
+ // snappy; Line.svelte short-circuits the Tween when duration <= 0.
733
+ let line_tween_load = $derived.by(() => {
734
+ if (!styles.show_lines) return { series: 0, points: 0 }
735
+ let [n_series, n_points] = [0, 0]
736
+ for (const srs of filtered_series ?? []) {
737
+ if (!(srs.markers ?? DEFAULT_MARKERS).includes(`line`)) continue
738
+ n_series += 1
739
+ n_points += srs.x.length
740
+ }
741
+ return { series: n_series, points: n_points }
742
+ })
743
+ let effective_line_tween = $derived(resolve_line_tween(line_tween, line_tween_load))
744
+
800
745
  // Obstacle field for legend/colorbar auto-placement. Sampling only data points lets the
801
746
  // legend land on top of a steep connecting line whose markers are sparse (e.g. y=x^2), so
802
747
  // sample_series_obstacle_points also walks each drawn segment at a fixed pixel cadence.
@@ -824,14 +769,6 @@
824
769
  return points
825
770
  })
826
771
 
827
- // Explicitly define the type for display_style matching PlotLegend expectations
828
- type LegendDisplayStyle = {
829
- symbol_type?: D3SymbolName
830
- symbol_color?: string
831
- line_color?: string
832
- line_dash?: string
833
- }
834
-
835
772
  const fill_hover_key = (
836
773
  source_type: `fill_region` | `error_band`,
837
774
  source_idx: number,
@@ -894,19 +831,22 @@
894
831
  })),
895
832
  ]
896
833
 
897
- // Compute unique x-values once for all fills
898
- // Optimization: deduplicate first (O(n)), then sort only unique values (O(k log k))
899
- // This is faster for datasets with many duplicate x-values across series
900
- const x_set = new SvelteSet<number>()
901
- for (const data_series of series_with_ids) {
902
- if (!data_series) continue
903
- for (const val of data_series.x) {
904
- if (typeof val === `number` && isFinite(val)) x_set.add(val)
905
- }
906
- }
907
- const unique_x = [...x_set].sort((val_a, val_b) => val_a - val_b)
834
+ // On log axes, clamp non-positive coords to the scale's domain floor (x_min/y_min) before
835
+ // scaling. A fixed tiny epsilon can sit far below the domain and map to extreme pixel coords.
836
+ const x_scale_type = final_x_axis.scale_type ?? `linear`
837
+ const y_scale_type = final_y_axis.scale_type ?? `linear`
838
+ const to_px = (pt: Pt): Pt => ({
839
+ x: x_scale_fn(x_scale_type === `log` && pt.x <= 0 ? x_min : pt.x),
840
+ y: y_scale_fn(y_scale_type === `log` && pt.y <= 0 ? y_min : pt.y),
841
+ })
908
842
 
909
- if (unique_x.length === 0) return []
843
+ // Each boundary is traced through its own points with the same curve the series line uses,
844
+ // so fill edges coincide exactly with the lines they border (x_domain anchors flat boundaries).
845
+ const domains = {
846
+ x_domain: [x_min, x_max] as Vec2,
847
+ y_domain: [y_min, y_max] as Vec2,
848
+ y2_domain: [y2_min, y2_max] as Vec2,
849
+ }
910
850
 
911
851
  return all_regions
912
852
  .filter((
@@ -918,71 +858,24 @@
918
858
  hover_key: string
919
859
  } => entry.region !== null)
920
860
  .map(({ region, source_type, source_idx, hover_key }, idx) => {
921
- if (region.visible === false) return null
922
-
923
- // Domain context for boundary resolution
924
- const domains = {
925
- y_domain: [y_min, y_max] as Vec2,
926
- y2_domain: [y2_min, y2_max] as Vec2,
927
- }
928
-
929
- // Resolve upper and lower boundaries
930
- const upper_values = resolve_boundary(
931
- region.upper,
932
- series_with_ids,
933
- unique_x,
934
- domains,
935
- )
936
- const lower_values = resolve_boundary(
937
- region.lower,
938
- series_with_ids,
939
- unique_x,
940
- domains,
941
- )
942
-
943
- if (!upper_values || !lower_values) return null
944
-
945
- // Apply range constraints
946
- const range_filtered = apply_range_constraints(
947
- unique_x,
948
- lower_values,
949
- upper_values,
950
- region,
951
- )
952
-
953
- // Clamp for log scale if needed
954
- const y_scale_type = final_y_axis.scale_type ?? `linear`
955
- const x_scale_type = final_x_axis.scale_type ?? `linear`
956
- const clamped = clamp_for_log_scale(
957
- range_filtered.x,
958
- range_filtered.y1,
959
- range_filtered.y2,
960
- y_scale_type,
961
- x_scale_type,
962
- )
963
-
964
- // Apply where condition (splits into segments)
965
- const conditioned = apply_where_condition(
966
- clamped.x,
967
- clamped.y1,
968
- clamped.y2,
969
- region,
970
- )
971
-
972
- // Generate paths for each segment (convert to pixel coordinates)
973
- const path_segments = conditioned.segments
974
- .filter((segment) => segment.length > 1)
975
- .map((segment) => {
976
- const pixel_data: FillPathPoint[] = segment.map((point) => ({
977
- x: x_scale_fn(point.x),
978
- y1: y_scale_fn(point.y1),
979
- y2: y_scale_fn(point.y2),
980
- }))
981
- return generate_fill_path(pixel_data, region.curve ?? `monotoneX`)
982
- })
983
- .filter((path) => path.length > 0)
861
+ // Hidden fills keep their entry (with empty path_segments -> nothing renders) so the
862
+ // legend item persists greyed-out and can be toggled back on.
863
+ const hidden = region.visible === false
864
+ const path_segments = hidden
865
+ ? []
866
+ : compute_fill_segments(region, series_with_ids, domains)
867
+ .map((seg) =>
868
+ generate_fill_path(
869
+ seg.upper.map(to_px),
870
+ seg.lower.map(to_px),
871
+ seg.upper_curve,
872
+ seg.lower_curve,
873
+ )
874
+ )
875
+ .filter((path) => path.length > 0)
984
876
 
985
- if (path_segments.length === 0) return null
877
+ // Drop only visible fills with no geometry; keep hidden ones for the legend
878
+ if (!hidden && path_segments.length === 0) return null
986
879
 
987
880
  return { ...region, idx, source_type, source_idx, hover_key, path_segments }
988
881
  })
@@ -990,161 +883,9 @@
990
883
  })
991
884
 
992
885
  // Prepare data needed for the legend component
993
- let legend_data = $derived.by(() => {
994
- const items = series_with_ids.map(
995
- (data_series: DataSeries & { _id?: string | number }, series_idx: number) => {
996
- const is_visible = data_series?.visible ?? true
997
- // Prefer top-level label, fallback to metadata label
998
- const explicit_label = data_series?.label ??
999
- (typeof data_series?.metadata === `object` &&
1000
- data_series.metadata !== null &&
1001
- `label` in data_series.metadata &&
1002
- typeof data_series.metadata.label === `string`
1003
- ? data_series.metadata.label
1004
- : null)
1005
- // Use explicit label or generate default
1006
- const label = explicit_label ?? `Series ${series_idx + 1}`
1007
- const has_explicit_label = explicit_label != null
1008
-
1009
- // Use series-specific defaults for auto-differentiation
1010
- const series_default_color = get_series_color(series_idx)
1011
- const series_default_symbol = get_series_symbol(series_idx)
1012
-
1013
- const display_style: LegendDisplayStyle = {
1014
- symbol_type: series_default_symbol,
1015
- symbol_color: series_default_color,
1016
- line_color: series_default_color,
1017
- }
1018
- const series_markers = data_series?.markers ?? DEFAULT_MARKERS
1019
-
1020
- // Check point_style (could be object or array)
1021
- const first_point_style = Array.isArray(data_series?.point_style)
1022
- ? data_series.point_style[0]
1023
- : data_series?.point_style
1024
-
1025
- if (series_markers?.includes(`points`)) {
1026
- if (first_point_style) {
1027
- // Use explicit symbol_type if provided and valid, otherwise keep series default
1028
- if (
1029
- typeof first_point_style.symbol_type === `string` &&
1030
- symbol_names.includes(first_point_style.symbol_type as D3SymbolName)
1031
- ) {
1032
- display_style.symbol_type = first_point_style
1033
- .symbol_type as D3SymbolName
1034
- }
1035
-
1036
- // Use explicit fill color if provided
1037
- if (first_point_style.fill) {
1038
- display_style.symbol_color = first_point_style.fill
1039
- }
1040
- if (first_point_style.stroke) {
1041
- // Use stroke color if fill is none or transparent
1042
- if (
1043
- !display_style.symbol_color ||
1044
- display_style.symbol_color === `none` ||
1045
- display_style.symbol_color.startsWith(`rgba(`, 0) // Check if transparent
1046
- ) display_style.symbol_color = first_point_style.stroke
1047
- }
1048
- }
1049
- // else: keep series-specific defaults for symbol_type and symbol_color
1050
- } else {
1051
- // If no points marker, explicitly remove marker style for legend
1052
- display_style.symbol_type = undefined
1053
- display_style.symbol_color = undefined
1054
- }
1055
-
1056
- // Check line_style
1057
- if (series_markers?.includes(`line`)) {
1058
- // Prefer explicit line stroke, then other explicit colors, then series default
1059
- let legend_line_color = data_series?.line_style?.stroke
1060
- if (!legend_line_color) {
1061
- // Try color scale if available
1062
- const first_cv = Array.isArray(data_series?.color_values)
1063
- ? data_series?.color_values?.find((color_val: number | null) =>
1064
- color_val != null
1065
- )
1066
- : undefined
1067
- legend_line_color =
1068
- (first_cv != null ? color_scale_fn(first_cv) : undefined) ||
1069
- first_point_style?.fill ||
1070
- first_point_style?.stroke ||
1071
- series_default_color
1072
- }
1073
- display_style.line_color = legend_line_color
1074
- display_style.line_dash = data_series?.line_style?.line_dash
1075
- } else {
1076
- // If no line marker, explicitly remove line style for legend
1077
- display_style.line_dash = undefined
1078
- display_style.line_color = undefined
1079
- }
1080
-
1081
- return {
1082
- series_idx,
1083
- label,
1084
- visible: is_visible,
1085
- display_style,
1086
- has_explicit_label,
1087
- legend_group: data_series?.legend_group,
1088
- }
1089
- },
1090
- )
1091
-
1092
- // Deduplicate by label+legend_group - keep first occurrence of each unique combination
1093
- const seen_labels = new SvelteSet<string>()
1094
- const series_items = items.filter(
1095
- (
1096
- legend_item: {
1097
- label: string
1098
- series_idx: number
1099
- visible: boolean
1100
- display_style: LegendDisplayStyle
1101
- has_explicit_label: boolean
1102
- legend_group?: string
1103
- },
1104
- ) => {
1105
- // Use label+group as unique key (group may be undefined)
1106
- const unique_key = `${legend_item.legend_group ?? ``}::${legend_item.label}`
1107
- if (seen_labels.has(unique_key)) return false
1108
- seen_labels.add(unique_key)
1109
- return true
1110
- },
1111
- )
1112
-
1113
- // Add fill region items to legend (deduplicated using same key format as series)
1114
- const fill_items = computed_fills
1115
- .filter((fill) => fill.show_in_legend !== false && fill.label)
1116
- .filter((fill) => {
1117
- // Use same composite key as series: legend_group::label
1118
- const unique_key = `${fill.legend_group ?? ``}::${fill.label ?? ``}`
1119
- if (seen_labels.has(unique_key)) return false
1120
- seen_labels.add(unique_key)
1121
- return true
1122
- })
1123
- .map((fill) => {
1124
- // Pass gradient for swatch rendering, or solid color as fallback
1125
- const fill_gradient = is_fill_gradient(fill.fill) ? fill.fill : undefined
1126
- const fill_color = typeof fill.fill === `string` ? fill.fill : undefined
1127
-
1128
- return {
1129
- series_idx: -1, // Not a series
1130
- fill_idx: fill.idx,
1131
- fill_source_type: fill.source_type,
1132
- fill_source_idx: fill.source_idx,
1133
- item_type: `fill` as const,
1134
- label: fill.label ?? ``,
1135
- visible: fill.visible !== false,
1136
- legend_group: fill.legend_group,
1137
- display_style: {
1138
- fill_color,
1139
- fill_opacity: fill.fill_opacity ?? 0.3,
1140
- edge_color: fill.edge_upper?.color,
1141
- fill_gradient,
1142
- },
1143
- }
1144
- })
1145
-
1146
- return [...series_items, ...fill_items]
1147
- })
886
+ let legend_data = $derived(
887
+ build_legend_data(series_with_ids, computed_fills, color_scale_fn),
888
+ )
1148
889
 
1149
890
  // Group fills by z-index for ordered rendering (single pass instead of 4 filters)
1150
891
  let fills_by_z = $derived.by(() => {
@@ -1171,7 +912,7 @@
1171
912
  // Calculate best legend placement using continuous grid sampling
1172
913
  let legend_placement = $derived.by(() => {
1173
914
  const should_place = legend != null &&
1174
- (legend_data.length > 1 || Object.keys(legend).length > 0)
915
+ (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)
1175
916
 
1176
917
  if (!should_place || !width || !height) return null
1177
918
 
@@ -1192,7 +933,7 @@
1192
933
 
1193
934
  // Calculate color bar placement (coordinates with legend to avoid overlap)
1194
935
  let color_bar_placement = $derived.by(() => {
1195
- if (!color_bar || !all_color_values.length || !width || !height) return null
936
+ if (!color_bar || all_color_values.length === 0 || !width || !height) return null
1196
937
 
1197
938
  const plot_width = width - pad.l - pad.r
1198
939
  const plot_height = height - pad.t - pad.b
@@ -1390,33 +1131,11 @@
1390
1131
  const start_data_y_val = y_scale_fn.invert(drag_start_coords.y)
1391
1132
  const end_data_y_val = y_scale_fn.invert(drag_current_coords.y)
1392
1133
 
1393
- // Ensure range is not zero and order is correct
1394
- let x1: number, x2: number
1395
- if (start_data_x_val instanceof Date && end_data_x_val instanceof Date) {
1396
- x1 = start_data_x_val.getTime()
1397
- x2 = end_data_x_val.getTime()
1398
- } else if (
1399
- typeof start_data_x_val === `number` &&
1400
- typeof end_data_x_val === `number`
1401
- ) {
1402
- x1 = start_data_x_val
1403
- x2 = end_data_x_val
1404
- } else {
1405
- console.error(`Mismatched types for x-axis zoom calculation`)
1406
- // Reset states without zooming if types are wrong
1407
- drag_start_coords = null
1408
- drag_current_coords = null
1409
- window.removeEventListener(`mousemove`, on_window_mouse_move)
1410
- window.removeEventListener(`mouseup`, on_window_mouse_up)
1411
- return
1412
- }
1413
-
1414
- const next_x_range: [number, number] = [Math.min(x1, x2), Math.max(x1, x2)]
1134
+ // Same scale inverts both coords, so both are numbers or both are Dates
1135
+ const [x1, x2] = [to_epoch_num(start_data_x_val), to_epoch_num(end_data_x_val)]
1136
+ const next_x_range = sorted_range(x1, x2)
1415
1137
  // Y axis is always number
1416
- const next_y_range: [number, number] = [
1417
- Math.min(start_data_y_val, end_data_y_val),
1418
- Math.max(start_data_y_val, end_data_y_val),
1419
- ]
1138
+ const next_y_range = sorted_range(start_data_y_val, end_data_y_val)
1420
1139
 
1421
1140
  // Check for minuscule zoom box (e.g. accidental click)
1422
1141
  const min_zoom_size = 5 // Minimum pixels to trigger zoom
@@ -1430,24 +1149,22 @@
1430
1149
  next_y_range[0] !== next_y_range[1]
1431
1150
  ) {
1432
1151
  // Update axis ranges to trigger reactivity (like BarPlot/Histogram do)
1433
- // Y2 sync is handled by the effect that reacts to y_axis changes
1434
1152
  x_axis = { ...x_axis, range: next_x_range }
1435
1153
  y_axis = { ...y_axis, range: next_y_range }
1436
1154
 
1437
1155
  // X2 axis: invert screen coords using x2 scale
1438
1156
  if (x2_points.length > 0) {
1439
- const start_x2_val = x2_scale_fn.invert(drag_start_coords.x)
1440
- const end_x2_val = x2_scale_fn.invert(drag_current_coords.x)
1441
- const x2_a = start_x2_val instanceof Date
1442
- ? start_x2_val.getTime()
1443
- : start_x2_val as number
1444
- const x2_b = end_x2_val instanceof Date
1445
- ? end_x2_val.getTime()
1446
- : end_x2_val as number
1447
- x2_axis = {
1448
- ...x2_axis,
1449
- range: [Math.min(x2_a, x2_b), Math.max(x2_a, x2_b)],
1450
- }
1157
+ const x2_a = to_epoch_num(x2_scale_fn.invert(drag_start_coords.x))
1158
+ const x2_b = to_epoch_num(x2_scale_fn.invert(drag_current_coords.x))
1159
+ x2_axis = { ...x2_axis, range: sorted_range(x2_a, x2_b) }
1160
+ }
1161
+
1162
+ // Y2 axis: when sync is enabled the y_axis effect derives y2; with sync 'none'
1163
+ // y2 must zoom from the rect directly (parity with BarPlot/Histogram/BoxPlot)
1164
+ if (y2_points.length > 0 && y2_sync_config.mode === `none`) {
1165
+ const y2_a = y2_scale_fn.invert(drag_start_coords.y)
1166
+ const y2_b = y2_scale_fn.invert(drag_current_coords.y)
1167
+ y2_axis = { ...y2_axis, range: sorted_range(y2_a, y2_b) }
1451
1168
  }
1452
1169
  }
1453
1170
  }
@@ -1461,45 +1178,38 @@
1461
1178
  document.body.style.cursor = `default`
1462
1179
  }
1463
1180
 
1464
- // Pan drag handlers
1465
- const on_pan_move = (evt: MouseEvent) => {
1466
- if (!pan_drag_state) return
1467
- const dx = evt.clientX - pan_drag_state.start.x
1468
- const dy = evt.clientY - pan_drag_state.start.y
1469
-
1470
- // Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
1471
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1181
+ // Pan/zoom all four axes from an interaction-start snapshot, each in its own
1182
+ // scale's transform space (log axes pan by a constant factor, linear by a shift).
1183
+ // Plot dims clamped to 1px so degenerate containers can't produce Infinity deltas.
1184
+ const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
1472
1185
  const plot_width = Math.max(1, width - pad.l - pad.r)
1473
1186
  const plot_height = Math.max(1, height - pad.t - pad.b)
1474
- const sensitivity = pan?.drag_sensitivity ?? 1
1475
-
1476
- const x_delta = pixels_to_data_delta(
1477
- -dx * sensitivity,
1478
- pan_drag_state.initial_x_range,
1479
- plot_width,
1480
- )
1481
- const x2_delta = pixels_to_data_delta(
1482
- -dx * sensitivity,
1483
- pan_drag_state.initial_x2_range,
1484
- plot_width,
1485
- )
1486
- const y_delta = pixels_to_data_delta(
1487
- dy * sensitivity,
1488
- pan_drag_state.initial_y_range,
1489
- plot_height,
1490
- )
1491
- const y2_delta = pixels_to_data_delta(
1492
- dy * sensitivity,
1493
- pan_drag_state.initial_y2_range,
1494
- plot_height,
1187
+ zoom_x_range = pan_range_by_pixels(init.initial_x_range, dx_px, plot_width, final_x_axis.scale_type)
1188
+ zoom_x2_range = pan_range_by_pixels(init.initial_x2_range, dx_px, plot_width, final_x2_axis.scale_type)
1189
+ zoom_y_range = pan_range_by_pixels(init.initial_y_range, dy_px, plot_height, final_y_axis.scale_type)
1190
+ zoom_y2_range = get_synced_y2(
1191
+ zoom_y_range,
1192
+ pan_range_by_pixels(init.initial_y2_range, dy_px, plot_height, final_y2_axis.scale_type),
1495
1193
  )
1496
-
1497
- zoom_x_range = pan_range(pan_drag_state.initial_x_range, x_delta)
1498
- zoom_x2_range = pan_range(pan_drag_state.initial_x2_range, x2_delta)
1499
- zoom_y_range = pan_range(pan_drag_state.initial_y_range, y_delta)
1194
+ }
1195
+ const zoom_all_axes = (init: InitialRanges, factor: number) => {
1196
+ zoom_x_range = zoom_range_by_factor(init.initial_x_range, factor, final_x_axis.scale_type)
1197
+ zoom_x2_range = zoom_range_by_factor(init.initial_x2_range, factor, final_x2_axis.scale_type)
1198
+ zoom_y_range = zoom_range_by_factor(init.initial_y_range, factor, final_y_axis.scale_type)
1500
1199
  zoom_y2_range = get_synced_y2(
1501
1200
  zoom_y_range,
1502
- pan_range(pan_drag_state.initial_y2_range, y2_delta),
1201
+ zoom_range_by_factor(init.initial_y2_range, factor, final_y2_axis.scale_type),
1202
+ )
1203
+ }
1204
+
1205
+ // Pan drag handler (drag direction inverted on x for natural pan feel)
1206
+ const on_pan_move = (evt: MouseEvent) => {
1207
+ if (!pan_drag_state) return
1208
+ const sensitivity = pan?.drag_sensitivity ?? 1
1209
+ pan_all_axes(
1210
+ pan_drag_state,
1211
+ -(evt.clientX - pan_drag_state.start.x) * sensitivity,
1212
+ (evt.clientY - pan_drag_state.start.y) * sensitivity,
1503
1213
  )
1504
1214
  }
1505
1215
 
@@ -1510,6 +1220,17 @@
1510
1220
  window.removeEventListener(`mouseup`, on_pan_end)
1511
1221
  }
1512
1222
 
1223
+ // Tear down any window listeners + cursor override if the component unmounts mid-drag
1224
+ // (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
1225
+ // onDestroy also runs during SSR teardown, where window/document don't exist.
1226
+ onDestroy(() => {
1227
+ remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
1228
+ drag_start_coords = null
1229
+ drag_current_coords = null
1230
+ svg_bounding_box = null
1231
+ pan_drag_state = null
1232
+ })
1233
+
1513
1234
  function handle_mouse_down(evt: MouseEvent) {
1514
1235
  if (!svg_element) return
1515
1236
 
@@ -1561,35 +1282,19 @@
1561
1282
  const plot_height = Math.max(1, height - pad.t - pad.b)
1562
1283
  const sensitivity = pan?.wheel_sensitivity ?? 1
1563
1284
 
1564
- // Determine pan direction based on wheel delta
1565
- // deltaX for horizontal scroll (trackpad), deltaY for vertical
1566
- const x_delta = pixels_to_data_delta(
1567
- evt.deltaX * sensitivity,
1568
- zoom_x_range,
1569
- plot_width,
1570
- )
1571
- const x2_delta = pixels_to_data_delta(
1572
- evt.deltaX * sensitivity,
1573
- zoom_x2_range,
1574
- plot_width,
1575
- )
1576
- const y_delta = pixels_to_data_delta(
1577
- evt.deltaY * sensitivity,
1578
- zoom_y_range,
1579
- plot_height,
1580
- )
1581
- const y2_delta = pixels_to_data_delta(
1582
- evt.deltaY * sensitivity,
1583
- zoom_y2_range,
1584
- plot_height,
1585
- )
1586
-
1285
+ // Pan along the dominant wheel direction
1286
+ // (deltaX for horizontal scroll on trackpads, deltaY for vertical)
1587
1287
  if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
1588
- zoom_x_range = pan_range(zoom_x_range, x_delta)
1589
- zoom_x2_range = pan_range(zoom_x2_range, x2_delta)
1288
+ const dx = evt.deltaX * sensitivity
1289
+ zoom_x_range = pan_range_by_pixels(zoom_x_range, dx, plot_width, final_x_axis.scale_type)
1290
+ zoom_x2_range = pan_range_by_pixels(zoom_x2_range, dx, plot_width, final_x2_axis.scale_type)
1590
1291
  } else {
1591
- zoom_y_range = pan_range(zoom_y_range, y_delta)
1592
- zoom_y2_range = get_synced_y2(zoom_y_range, pan_range(zoom_y2_range, y2_delta))
1292
+ const dy = evt.deltaY * sensitivity
1293
+ zoom_y_range = pan_range_by_pixels(zoom_y_range, dy, plot_height, final_y_axis.scale_type)
1294
+ zoom_y2_range = get_synced_y2(
1295
+ zoom_y_range,
1296
+ pan_range_by_pixels(zoom_y2_range, dy, plot_height, final_y2_axis.scale_type),
1297
+ )
1593
1298
  }
1594
1299
  }
1595
1300
 
@@ -1627,75 +1332,15 @@
1627
1332
 
1628
1333
  // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
1629
1334
  const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
1630
- // Guard against zero-distance pinch to avoid Infinity scale
1631
- if (start_dist < Number.EPSILON) return
1335
+ // ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
1336
+ if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
1632
1337
  const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
1633
1338
  const scale = curr_dist / start_dist
1634
1339
 
1635
- // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
1636
- const plot_width = Math.max(1, width - pad.l - pad.r)
1637
- const plot_height = Math.max(1, height - pad.t - pad.b)
1638
-
1639
- // If scale changed significantly, treat as pinch-zoom
1640
- // Also guard against scale being too small to avoid division by zero
1340
+ // Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
1641
1341
  if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
1642
- // Pinch zoom centered on gesture center
1643
- // Divide by scale so spread (scale > 1) = smaller span (zoom in)
1644
- const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
1645
- const x2_span = touch_state.initial_x2_range[1] -
1646
- touch_state.initial_x2_range[0]
1647
- const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
1648
- const y2_span = touch_state.initial_y2_range[1] -
1649
- touch_state.initial_y2_range[0]
1650
- const x_center =
1651
- (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
1652
- const x2_center =
1653
- (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
1654
- const y_center =
1655
- (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
1656
- const y2_center =
1657
- (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
1658
-
1659
- zoom_x_range = [x_center - x_span / scale / 2, x_center + x_span / scale / 2]
1660
- zoom_x2_range = [
1661
- x2_center - x2_span / scale / 2,
1662
- x2_center + x2_span / scale / 2,
1663
- ]
1664
- zoom_y_range = [y_center - y_span / scale / 2, y_center + y_span / scale / 2]
1665
- zoom_y2_range = get_synced_y2(zoom_y_range, [
1666
- y2_center - y2_span / scale / 2,
1667
- y2_center + y2_span / scale / 2,
1668
- ])
1669
- } else {
1670
- // Pan
1671
- const x_delta = pixels_to_data_delta(
1672
- -dx,
1673
- touch_state.initial_x_range,
1674
- plot_width,
1675
- )
1676
- const x2_delta = pixels_to_data_delta(
1677
- -dx,
1678
- touch_state.initial_x2_range,
1679
- plot_width,
1680
- )
1681
- const y_delta = pixels_to_data_delta(
1682
- dy,
1683
- touch_state.initial_y_range,
1684
- plot_height,
1685
- )
1686
- const y2_delta = pixels_to_data_delta(
1687
- dy,
1688
- touch_state.initial_y2_range,
1689
- plot_height,
1690
- )
1691
- zoom_x_range = pan_range(touch_state.initial_x_range, x_delta)
1692
- zoom_x2_range = pan_range(touch_state.initial_x2_range, x2_delta)
1693
- zoom_y_range = pan_range(touch_state.initial_y_range, y_delta)
1694
- zoom_y2_range = get_synced_y2(
1695
- zoom_y_range,
1696
- pan_range(touch_state.initial_y2_range, y2_delta),
1697
- )
1698
- }
1342
+ zoom_all_axes(touch_state, scale)
1343
+ } else pan_all_axes(touch_state, -dx, dy)
1699
1344
  }
1700
1345
 
1701
1346
  function handle_touch_end() {
@@ -1906,7 +1551,6 @@
1906
1551
  return construct_handler_props(tooltip_point)
1907
1552
  })
1908
1553
 
1909
- let using_controls = $derived(controls.show)
1910
1554
  let has_multiple_series = $derived(series_with_ids.filter(Boolean).length > 1)
1911
1555
 
1912
1556
  // Precompute non-click event names from point_events so we don't rebuild
@@ -1943,32 +1587,12 @@
1943
1587
  set_loading: (axis) => (axis_loading = axis),
1944
1588
  }
1945
1589
 
1946
- // Create shared handler bound to this component's state
1947
- // Using $derived so handler updates when callback props change
1948
- const handle_axis_change = $derived(create_axis_change_handler(
1590
+ // Shared handler + one-shot auto-load bound to this component's state
1591
+ const { handle_axis_change, try_auto_load } = create_axis_loader(
1949
1592
  axis_state,
1950
- data_loader,
1951
- on_axis_change,
1952
- on_error,
1953
- ))
1954
-
1955
- let auto_load_attempted = false // prevent infinite retries on failure
1956
-
1957
- // Auto-load data if series is empty but options exist (runs once)
1958
- $effect(() => {
1959
- if (series.length === 0 && data_loader && !auto_load_attempted) {
1960
- // Check x-axis first, then y-axis
1961
- if (x_axis.options?.length) {
1962
- auto_load_attempted = true
1963
- const first_key = x_axis.selected_key ?? x_axis.options[0].key
1964
- handle_axis_change(`x`, first_key).catch(() => {})
1965
- } else if (y_axis.options?.length) {
1966
- auto_load_attempted = true
1967
- const first_key = y_axis.selected_key ?? y_axis.options[0].key
1968
- handle_axis_change(`y`, first_key).catch(() => {})
1969
- }
1970
- }
1971
- })
1593
+ () => ({ data_loader, on_axis_change, on_error }),
1594
+ )
1595
+ $effect(try_auto_load)
1972
1596
  </script>
1973
1597
 
1974
1598
  {#snippet fill_regions_layer(fills: typeof computed_fills)}
@@ -2099,6 +1723,7 @@
2099
1723
  ontouchstart={handle_touch_start}
2100
1724
  ontouchmove={handle_touch_move}
2101
1725
  ontouchend={handle_touch_end}
1726
+ ontouchcancel={handle_touch_end}
2102
1727
  style:cursor={pan_drag_state
2103
1728
  ? `grabbing`
2104
1729
  : shift_held && pan?.enabled !== false
@@ -2191,9 +1816,6 @@
2191
1816
 
2192
1817
  <!-- Y2-axis (Right) -->
2193
1818
  {#if y2_points.length > 0}
2194
- {@const y2_inside = final_y2_axis.tick?.label?.inside ?? false}
2195
- {@const y2_tick_shift = y2_inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8}
2196
- {@const y2_tick_width = y2_inside ? 0 : tick_label_widths.y2_max}
2197
1819
  <PlotAxis
2198
1820
  side="y2"
2199
1821
  ticks={y2_tick_values}
@@ -2207,8 +1829,7 @@
2207
1829
  domain={[y2_min, y2_max]}
2208
1830
  unit_on_first_tick
2209
1831
  tick_label={(tick) => get_tick_label(tick, final_y2_axis.ticks)}
2210
- label_x={width - pad.r + y2_tick_shift + y2_tick_width + LABEL_GAP_DEFAULT +
2211
- (final_y2_axis.label_shift?.x ?? 0)}
1832
+ label_x={y2_axis_label_x(final_y2_axis, width, pad.r, tick_label_widths.y2_max)}
2212
1833
  label_y={pad.t + (height - pad.t - pad.b) / 2 + (final_y2_axis.label_shift?.y ?? 0)}
2213
1834
  axis_loading={axis_loading === `y2`}
2214
1835
  on_axis_change={(key) => handle_axis_change(`y2`, key)}
@@ -2300,7 +1921,7 @@
2300
1921
  {@const finite_screen_points = all_line_points
2301
1922
  .map((point) => get_screen_coords(point, series_data))
2302
1923
  .filter(([sx, sy]) => isFinite(sx) && isFinite(sy))}
2303
- {@const apply_line_controls = using_controls &&
1924
+ {@const apply_line_controls = controls.show &&
2304
1925
  (!has_multiple_series ||
2305
1926
  series_data._id === series_with_ids[selected_series_idx]?._id)}
2306
1927
  {@const ls = series_data.line_style}
@@ -2322,7 +1943,7 @@
2322
1943
  line_width={(tc(`line.width`) ? styles.line?.width : null) ?? ls?.stroke_width ?? 2}
2323
1944
  line_dash={(tc(`line.dash`) ? styles.line?.dash : null) ?? ls?.line_dash}
2324
1945
  area_color="transparent"
2325
- {line_tween}
1946
+ line_tween={effective_line_tween}
2326
1947
  />
2327
1948
  {/if}
2328
1949
  </g>
@@ -2371,7 +1992,7 @@
2371
1992
  {@const screen_y = isFinite(raw_screen_y)
2372
1993
  ? raw_screen_y
2373
1994
  : (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn).range()[0]}
2374
- {@const apply_controls = using_controls &&
1995
+ {@const apply_controls = controls.show &&
2375
1996
  (!has_multiple_series ||
2376
1997
  series_data._id === series_with_ids[selected_series_idx]?._id)}
2377
1998
  {@const pt = point.point_style}
@@ -2446,60 +2067,19 @@
2446
2067
 
2447
2068
  <!-- Tooltip overlay above all plot overlays (legend, colorbar) -->
2448
2069
  {#if handler_props && hovered && tooltip_point}
2449
- {@const { color_value, point_label, point_style, series_idx } = tooltip_point}
2450
- {@const hovered_series = series_with_ids[series_idx]}
2451
- {@const series_markers = hovered_series?.markers ?? DEFAULT_MARKERS}
2452
- {@const is_transparent_or_none = (color: string | undefined | null): boolean =>
2453
- !color ||
2454
- color === `none` ||
2455
- color === `transparent` ||
2456
- /rgba\([^)]+[,/]\s*0(\.0*)?\s*\)$/.test(color)}
2457
- {@const tooltip_bg_color = (() => {
2458
- const scale_color = color_value != null
2459
- ? color_scale_fn(color_value)
2460
- : undefined
2461
- if (!is_transparent_or_none(scale_color)) return scale_color
2462
- const fill_color = point_style?.fill
2463
- if (!is_transparent_or_none(fill_color)) return fill_color
2464
- if (series_markers?.includes(`points`)) {
2465
- const stroke_color = point_style?.stroke
2466
- if (!is_transparent_or_none(stroke_color)) return stroke_color
2467
- }
2468
- if (series_markers?.includes(`line`)) {
2469
- const line_style = hovered_series?.line_style ?? {}
2470
- const first_point_style = Array.isArray(hovered_series?.point_style)
2471
- ? hovered_series?.point_style[0]
2472
- : hovered_series?.point_style
2473
- const first_color_value = hovered_series?.color_values?.[0]
2474
- let line_color_candidate = line_style.stroke
2475
- if (is_transparent_or_none(line_color_candidate)) {line_color_candidate =
2476
- first_point_style?.fill}
2477
- if (
2478
- is_transparent_or_none(line_color_candidate) && first_color_value != null
2479
- ) line_color_candidate = color_scale_fn(first_color_value)
2480
- if (
2481
- is_transparent_or_none(line_color_candidate) &&
2482
- series_markers?.includes(`points`)
2483
- ) line_color_candidate = first_point_style?.stroke
2484
- if (!is_transparent_or_none(line_color_candidate)) return line_color_candidate
2485
- }
2486
- return `rgba(0, 0, 0, 0.7)`
2487
- })()}
2488
- {@const tooltip_pos = constrain_tooltip_position(
2489
- handler_props.cx,
2490
- handler_props.cy,
2491
- tooltip_el?.offsetWidth ?? 120,
2492
- tooltip_el?.offsetHeight ?? 50,
2493
- width,
2494
- height,
2495
- { offset_x: 10, offset_y: 5 },
2070
+ {@const { point_label, series_idx } = tooltip_point}
2071
+ {@const tooltip_bg_color = pick_tooltip_bg(
2072
+ tooltip_point,
2073
+ series_with_ids[series_idx],
2074
+ color_scale_fn,
2496
2075
  )}
2497
2076
  <PlotTooltip
2498
- x={tooltip_pos.x}
2499
- y={tooltip_pos.y}
2500
- offset={{ x: 0, y: 0 }}
2077
+ x={handler_props.cx}
2078
+ y={handler_props.cy}
2079
+ offset={{ x: 10, y: 5 }}
2080
+ constrain_to={{ width, height }}
2081
+ fallback_size={{ width: 120, height: 50 }}
2501
2082
  bg_color={tooltip_bg_color}
2502
- bind:wrapper={tooltip_el}
2503
2083
  >
2504
2084
  {#if tooltip}
2505
2085
  {@render tooltip(handler_props)}
@@ -2585,7 +2165,7 @@
2585
2165
  <!-- Legend -->
2586
2166
  <!-- Only render if multiple series or if legend prop was explicitly provided by user (even if empty object) -->
2587
2167
  {#if legend != null && legend_data.length > 0 &&
2588
- (legend_data.length > 1 || Object.keys(legend).length > 0)}
2168
+ (legend_data.length > 1 || Object.keys(legend ?? {}).length > 0)}
2589
2169
  {@const default_x = pad.l + 10}
2590
2170
  {@const default_y = pad.t + 10}
2591
2171
  {@const current_x = legend_is_dragging && legend_manual_position
@@ -2609,31 +2189,28 @@
2609
2189
  on_drag={handle_legend_drag}
2610
2190
  on_drag_end={() => (legend_is_dragging = false)}
2611
2191
  on_hover_change={legend_hover.set_locked}
2612
- on_item_hover={(series_idx: number | null) =>
2613
- (hovered_legend_series_idx = series_idx != null && series_idx >= 0
2614
- ? series_idx
2615
- : null)}
2192
+ on_item_hover={(item) => {
2193
+ if (item?.item_type === `fill`) {
2194
+ // highlight the matching fill in the plot (same state plot fill-hover uses), but skip
2195
+ // hidden fills since they render nothing and would mark the legend item active for naught
2196
+ const fill = computed_fills.find((entry) => entry.idx === item.fill_idx)
2197
+ hovered_fill_key = fill && fill.visible !== false ? fill.hover_key : null
2198
+ hovered_legend_series_idx = null
2199
+ } else {
2200
+ hovered_legend_series_idx = item != null && item.series_idx >= 0
2201
+ ? item.series_idx
2202
+ : null
2203
+ hovered_fill_key = null
2204
+ }
2205
+ }}
2616
2206
  active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
2207
+ active_fill_idx={computed_fills.find((fill) => fill.hover_key === hovered_fill_key)?.idx ??
2208
+ null}
2617
2209
  draggable={legend?.draggable ?? true}
2618
2210
  {...legend}
2619
- on_toggle={legend?.on_toggle ??
2620
- ((series_idx: number) => {
2621
- series = toggle_series_visibility(series, series_idx)
2622
- })}
2623
- on_double_click={legend?.on_double_click ??
2624
- ((double_clicked_idx: number) => {
2625
- const result = handle_legend_double_click(
2626
- series,
2627
- double_clicked_idx,
2628
- prev_series_visibility,
2629
- )
2630
- series = result.series
2631
- prev_series_visibility = result.prev_visibility
2632
- })}
2633
- on_group_toggle={legend?.on_group_toggle ??
2634
- ((_group_name: string, series_indices: number[]) => {
2635
- series = toggle_group_visibility(series, series_indices)
2636
- })}
2211
+ on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
2212
+ on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
2213
+ on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
2637
2214
  on_fill_toggle={(source_type: `fill_region` | `error_band`, source_idx: number) => {
2638
2215
  // Only fill_regions can be toggled (error_bands are not bindable)
2639
2216
  if (source_type === `fill_region`) {
@@ -2707,8 +2284,10 @@
2707
2284
  background: var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg)));
2708
2285
  max-height: none !important;
2709
2286
  overflow: hidden;
2710
- /* Add padding to prevent titles from being cropped at top */
2711
- padding-top: var(--plot-fullscreen-padding-top, 2em);
2287
+ /* border-top (not padding-top): bind:clientHeight includes padding but excludes
2288
+ borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
2289
+ border-top: var(--plot-fullscreen-padding-top, 2em) solid
2290
+ var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg, transparent)));
2712
2291
  box-sizing: border-box;
2713
2292
  }
2714
2293
  /* Center the colorbar within its wrapper when shorter than it (e.g. capped by --cbar-max-height