matterviz 0.3.1 → 0.3.3

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 (358) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +154 -96
  3. package/dist/Icon.svelte +20 -14
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -178
  7. package/dist/brillouin/BrillouinZone.svelte +299 -198
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +327 -0
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  23. package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
  24. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  25. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  26. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  27. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  28. package/dist/chempot-diagram/chempot-worker.js +11 -0
  29. package/dist/chempot-diagram/color.d.ts +10 -0
  30. package/dist/chempot-diagram/color.js +32 -0
  31. package/dist/chempot-diagram/compute.d.ts +48 -0
  32. package/dist/chempot-diagram/compute.js +812 -0
  33. package/dist/chempot-diagram/index.d.ts +6 -0
  34. package/dist/chempot-diagram/index.js +6 -0
  35. package/dist/chempot-diagram/pointer.d.ts +16 -0
  36. package/dist/chempot-diagram/pointer.js +40 -0
  37. package/dist/chempot-diagram/temperature.d.ts +15 -0
  38. package/dist/chempot-diagram/temperature.js +36 -0
  39. package/dist/chempot-diagram/types.d.ts +86 -0
  40. package/dist/chempot-diagram/types.js +28 -0
  41. package/dist/colors/index.d.ts +3 -1
  42. package/dist/colors/index.js +9 -3
  43. package/dist/composition/BarChart.svelte +141 -77
  44. package/dist/composition/BubbleChart.svelte +107 -52
  45. package/dist/composition/Composition.svelte +100 -79
  46. package/dist/composition/Formula.svelte +108 -62
  47. package/dist/composition/FormulaFilter.svelte +973 -353
  48. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  49. package/dist/composition/PieChart.svelte +199 -99
  50. package/dist/composition/PieChart.svelte.d.ts +1 -1
  51. package/dist/composition/format.d.ts +5 -0
  52. package/dist/composition/format.js +20 -3
  53. package/dist/composition/parse.js +14 -9
  54. package/dist/convex-hull/ConvexHull.svelte +93 -38
  55. package/dist/convex-hull/ConvexHull2D.svelte +551 -393
  56. package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
  57. package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
  58. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  59. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  60. package/dist/convex-hull/ConvexHullStats.svelte +821 -249
  61. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  62. package/dist/convex-hull/ConvexHullTooltip.svelte +41 -16
  63. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  64. package/dist/convex-hull/StructurePopup.svelte +25 -4
  65. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  66. package/dist/convex-hull/barycentric-coords.js +13 -7
  67. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  68. package/dist/convex-hull/demo-temperature.js +40 -0
  69. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  70. package/dist/convex-hull/helpers.d.ts +10 -1
  71. package/dist/convex-hull/helpers.js +79 -38
  72. package/dist/convex-hull/index.d.ts +1 -0
  73. package/dist/convex-hull/index.js +1 -0
  74. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  75. package/dist/convex-hull/thermodynamics.js +163 -69
  76. package/dist/convex-hull/types.d.ts +12 -12
  77. package/dist/convex-hull/types.js +0 -12
  78. package/dist/coordination/CoordinationBarPlot.svelte +232 -176
  79. package/dist/element/BohrAtom.svelte +56 -13
  80. package/dist/element/ElementHeading.svelte +7 -2
  81. package/dist/element/ElementPhoto.svelte +15 -9
  82. package/dist/element/ElementStats.svelte +10 -4
  83. package/dist/element/ElementTile.svelte +137 -73
  84. package/dist/element/Nucleus.svelte +39 -11
  85. package/dist/element/data.js +2 -14
  86. package/dist/element/data.json.gz +0 -0
  87. package/dist/element/types.d.ts +1 -0
  88. package/dist/feedback/ClickFeedback.svelte +16 -5
  89. package/dist/feedback/DragOverlay.svelte +10 -2
  90. package/dist/feedback/Spinner.svelte +4 -2
  91. package/dist/feedback/StatusMessage.svelte +8 -2
  92. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  93. package/dist/fermi-surface/FermiSurface.svelte +336 -239
  94. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  95. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  96. package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
  97. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  98. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  99. package/dist/fermi-surface/compute.js +16 -20
  100. package/dist/fermi-surface/parse.js +37 -33
  101. package/dist/fermi-surface/symmetry.js +2 -7
  102. package/dist/fermi-surface/types.d.ts +3 -5
  103. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
  104. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  105. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  106. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
  107. package/dist/heatmap-matrix/index.d.ts +53 -0
  108. package/dist/heatmap-matrix/index.js +100 -0
  109. package/dist/heatmap-matrix/shared.d.ts +2 -0
  110. package/dist/heatmap-matrix/shared.js +4 -0
  111. package/dist/icons.d.ts +111 -0
  112. package/dist/icons.js +158 -0
  113. package/dist/index.d.ts +5 -2
  114. package/dist/index.js +5 -2
  115. package/dist/io/decompress.js +1 -1
  116. package/dist/io/export.d.ts +3 -0
  117. package/dist/io/export.js +138 -140
  118. package/dist/io/file-drop.d.ts +7 -0
  119. package/dist/io/file-drop.js +43 -0
  120. package/dist/io/index.d.ts +2 -2
  121. package/dist/io/index.js +2 -112
  122. package/dist/io/is-binary.js +2 -3
  123. package/dist/io/types.d.ts +1 -0
  124. package/dist/io/url-drop.d.ts +2 -0
  125. package/dist/io/url-drop.js +117 -0
  126. package/dist/isosurface/Isosurface.svelte +220 -110
  127. package/dist/isosurface/IsosurfaceControls.svelte +65 -28
  128. package/dist/isosurface/parse.js +104 -56
  129. package/dist/isosurface/slice.d.ts +2 -1
  130. package/dist/isosurface/slice.js +8 -13
  131. package/dist/isosurface/types.d.ts +14 -1
  132. package/dist/isosurface/types.js +152 -5
  133. package/dist/labels.d.ts +2 -1
  134. package/dist/labels.js +12 -8
  135. package/dist/layout/FullscreenToggle.svelte +11 -2
  136. package/dist/layout/InfoCard.svelte +38 -6
  137. package/dist/layout/InfoTag.svelte +125 -94
  138. package/dist/layout/PropertyFilter.svelte +82 -37
  139. package/dist/layout/SettingsSection.svelte +85 -55
  140. package/dist/layout/SubpageGrid.svelte +82 -0
  141. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  142. package/dist/layout/index.d.ts +1 -0
  143. package/dist/layout/index.js +1 -0
  144. package/dist/layout/json-tree/JsonNode.svelte +266 -223
  145. package/dist/layout/json-tree/JsonTree.svelte +516 -429
  146. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  147. package/dist/layout/json-tree/JsonValue.svelte +281 -173
  148. package/dist/layout/json-tree/types.d.ts +10 -2
  149. package/dist/layout/json-tree/utils.d.ts +2 -0
  150. package/dist/layout/json-tree/utils.js +37 -2
  151. package/dist/marching-cubes.js +25 -2
  152. package/dist/math.d.ts +20 -17
  153. package/dist/math.js +474 -57
  154. package/dist/overlays/ContextMenu.svelte +66 -40
  155. package/dist/overlays/DraggablePane.svelte +331 -154
  156. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  157. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  158. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  159. package/dist/periodic-table/PropertySelect.svelte +25 -7
  160. package/dist/periodic-table/TableInset.svelte +8 -3
  161. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +559 -267
  162. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  163. package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
  164. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  165. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  166. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  167. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
  168. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
  169. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
  170. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  171. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  172. package/dist/phase-diagram/build-diagram.js +9 -9
  173. package/dist/phase-diagram/colors.js +1 -3
  174. package/dist/phase-diagram/index.d.ts +2 -0
  175. package/dist/phase-diagram/index.js +2 -0
  176. package/dist/phase-diagram/parse.js +10 -9
  177. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  178. package/dist/phase-diagram/svg-to-diagram.js +869 -0
  179. package/dist/phase-diagram/types.d.ts +10 -0
  180. package/dist/phase-diagram/utils.d.ts +8 -4
  181. package/dist/phase-diagram/utils.js +219 -74
  182. package/dist/plot/AxisLabel.svelte +51 -0
  183. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  184. package/dist/plot/BarPlot.svelte +1461 -768
  185. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  186. package/dist/plot/BarPlotControls.svelte +33 -6
  187. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  188. package/dist/plot/ColorBar.svelte +533 -383
  189. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  190. package/dist/plot/ColorScaleSelect.svelte +28 -7
  191. package/dist/plot/ElementScatter.svelte +38 -16
  192. package/dist/plot/FillArea.svelte +152 -92
  193. package/dist/plot/Histogram.svelte +1162 -709
  194. package/dist/plot/Histogram.svelte.d.ts +1 -1
  195. package/dist/plot/HistogramControls.svelte +81 -18
  196. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  197. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  198. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  199. package/dist/plot/Line.svelte +63 -28
  200. package/dist/plot/PlotControls.svelte +221 -96
  201. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  202. package/dist/plot/PlotLegend.svelte +174 -91
  203. package/dist/plot/PlotTooltip.svelte +45 -6
  204. package/dist/plot/PortalSelect.svelte +175 -146
  205. package/dist/plot/ReferenceLine.svelte +77 -22
  206. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  207. package/dist/plot/ReferenceLine3D.svelte +132 -107
  208. package/dist/plot/ReferencePlane.svelte +146 -123
  209. package/dist/plot/ScatterPlot.svelte +1880 -1156
  210. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  211. package/dist/plot/ScatterPlot3D.svelte +256 -131
  212. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  213. package/dist/plot/ScatterPlot3DControls.svelte +300 -297
  214. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  215. package/dist/plot/ScatterPlot3DScene.svelte +608 -406
  216. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  217. package/dist/plot/ScatterPlotControls.svelte +150 -70
  218. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  219. package/dist/plot/ScatterPoint.svelte +98 -26
  220. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  221. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  222. package/dist/plot/Surface3D.svelte +159 -108
  223. package/dist/plot/ZeroLines.svelte +96 -0
  224. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  225. package/dist/plot/ZoomRect.svelte +23 -0
  226. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  227. package/dist/plot/axis-utils.d.ts +1 -1
  228. package/dist/plot/axis-utils.js +1 -3
  229. package/dist/plot/data-cleaning.js +12 -28
  230. package/dist/plot/data-transform.js +2 -1
  231. package/dist/plot/fill-utils.js +2 -0
  232. package/dist/plot/index.d.ts +6 -2
  233. package/dist/plot/index.js +6 -2
  234. package/dist/plot/interactions.d.ts +8 -10
  235. package/dist/plot/interactions.js +2 -3
  236. package/dist/plot/layout.d.ts +11 -2
  237. package/dist/plot/layout.js +44 -17
  238. package/dist/plot/reference-line.d.ts +5 -22
  239. package/dist/plot/reference-line.js +12 -84
  240. package/dist/plot/scales.js +24 -36
  241. package/dist/plot/types.d.ts +53 -40
  242. package/dist/plot/types.js +12 -7
  243. package/dist/plot/utils/label-placement.d.ts +32 -15
  244. package/dist/plot/utils/label-placement.js +227 -63
  245. package/dist/plot/utils/series-visibility.js +2 -3
  246. package/dist/plot/utils.d.ts +1 -0
  247. package/dist/plot/utils.js +14 -0
  248. package/dist/rdf/RdfPlot.svelte +173 -132
  249. package/dist/rdf/calc-rdf.js +4 -5
  250. package/dist/sanitize.d.ts +4 -0
  251. package/dist/sanitize.js +107 -0
  252. package/dist/settings.d.ts +21 -6
  253. package/dist/settings.js +63 -19
  254. package/dist/spectral/Bands.svelte +963 -412
  255. package/dist/spectral/Bands.svelte.d.ts +22 -2
  256. package/dist/spectral/BandsAndDos.svelte +90 -49
  257. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  258. package/dist/spectral/Dos.svelte +389 -258
  259. package/dist/spectral/helpers.d.ts +23 -1
  260. package/dist/spectral/helpers.js +119 -51
  261. package/dist/spectral/types.d.ts +2 -0
  262. package/dist/state.svelte.d.ts +1 -1
  263. package/dist/state.svelte.js +3 -2
  264. package/dist/structure/Arrow.svelte +59 -20
  265. package/dist/structure/AtomLegend.svelte +231 -129
  266. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  267. package/dist/structure/Bond.svelte +73 -47
  268. package/dist/structure/CanvasTooltip.svelte +10 -2
  269. package/dist/structure/CellSelect.svelte +148 -51
  270. package/dist/structure/Cylinder.svelte +33 -17
  271. package/dist/structure/Lattice.svelte +88 -33
  272. package/dist/structure/Structure.svelte +1077 -821
  273. package/dist/structure/Structure.svelte.d.ts +1 -1
  274. package/dist/structure/StructureControls.svelte +373 -139
  275. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  276. package/dist/structure/StructureExportPane.svelte +124 -89
  277. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  278. package/dist/structure/StructureInfoPane.svelte +304 -231
  279. package/dist/structure/StructureScene.svelte +919 -445
  280. package/dist/structure/StructureScene.svelte.d.ts +16 -7
  281. package/dist/structure/atom-properties.d.ts +6 -2
  282. package/dist/structure/atom-properties.js +42 -29
  283. package/dist/structure/bonding.js +6 -7
  284. package/dist/structure/export.js +22 -34
  285. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  286. package/dist/structure/ferrox-wasm-types.js +0 -3
  287. package/dist/structure/ferrox-wasm.d.ts +3 -2
  288. package/dist/structure/ferrox-wasm.js +2 -3
  289. package/dist/structure/index.d.ts +16 -0
  290. package/dist/structure/index.js +88 -6
  291. package/dist/structure/measure.d.ts +2 -2
  292. package/dist/structure/measure.js +4 -44
  293. package/dist/structure/parse.js +130 -155
  294. package/dist/structure/partial-occupancy.d.ts +25 -0
  295. package/dist/structure/partial-occupancy.js +99 -0
  296. package/dist/structure/pbc.d.ts +1 -0
  297. package/dist/structure/pbc.js +16 -6
  298. package/dist/structure/supercell.d.ts +2 -2
  299. package/dist/structure/supercell.js +12 -22
  300. package/dist/structure/validation.js +5 -3
  301. package/dist/symmetry/SymmetryStats.svelte +94 -37
  302. package/dist/symmetry/WyckoffTable.svelte +42 -14
  303. package/dist/symmetry/cell-transform.js +5 -3
  304. package/dist/symmetry/index.d.ts +7 -4
  305. package/dist/symmetry/index.js +87 -21
  306. package/dist/symmetry/spacegroups.js +148 -148
  307. package/dist/table/HeatmapTable.svelte +1112 -516
  308. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  309. package/dist/table/ToggleMenu.svelte +125 -90
  310. package/dist/table/index.d.ts +2 -0
  311. package/dist/table/index.js +2 -4
  312. package/dist/theme/ThemeControl.svelte +21 -12
  313. package/dist/time.js +4 -1
  314. package/dist/tooltip/TooltipContent.svelte +33 -8
  315. package/dist/trajectory/Trajectory.svelte +889 -687
  316. package/dist/trajectory/TrajectoryError.svelte +14 -3
  317. package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
  318. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  319. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  320. package/dist/trajectory/constants.d.ts +6 -0
  321. package/dist/trajectory/constants.js +7 -0
  322. package/dist/trajectory/extract.js +13 -31
  323. package/dist/trajectory/format-detect.d.ts +9 -0
  324. package/dist/trajectory/format-detect.js +76 -0
  325. package/dist/trajectory/frame-reader.d.ts +17 -0
  326. package/dist/trajectory/frame-reader.js +332 -0
  327. package/dist/trajectory/helpers.d.ts +14 -0
  328. package/dist/trajectory/helpers.js +172 -0
  329. package/dist/trajectory/index.d.ts +1 -0
  330. package/dist/trajectory/index.js +23 -14
  331. package/dist/trajectory/parse/ase.d.ts +2 -0
  332. package/dist/trajectory/parse/ase.js +77 -0
  333. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  334. package/dist/trajectory/parse/hdf5.js +129 -0
  335. package/dist/trajectory/parse/index.d.ts +12 -0
  336. package/dist/trajectory/parse/index.js +299 -0
  337. package/dist/trajectory/parse/lammps.d.ts +5 -0
  338. package/dist/trajectory/parse/lammps.js +179 -0
  339. package/dist/trajectory/parse/vasp.d.ts +2 -0
  340. package/dist/trajectory/parse/vasp.js +68 -0
  341. package/dist/trajectory/parse/xyz.d.ts +2 -0
  342. package/dist/trajectory/parse/xyz.js +110 -0
  343. package/dist/trajectory/plotting.js +13 -8
  344. package/dist/trajectory/types.d.ts +11 -0
  345. package/dist/trajectory/types.js +1 -0
  346. package/dist/utils.d.ts +3 -0
  347. package/dist/utils.js +17 -0
  348. package/dist/xrd/XrdPlot.svelte +337 -245
  349. package/dist/xrd/broadening.js +14 -9
  350. package/dist/xrd/calc-xrd.js +12 -19
  351. package/dist/xrd/parse.d.ts +1 -1
  352. package/dist/xrd/parse.js +17 -17
  353. package/package.json +103 -101
  354. package/readme.md +4 -4
  355. package/dist/trajectory/parse.d.ts +0 -42
  356. package/dist/trajectory/parse.js +0 -1267
  357. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
  358. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,567 +1,1024 @@
1
- <script lang="ts">import { format_value } from '../labels';
2
- import { FullscreenToggle, set_fullscreen_bg } from '../layout';
3
- import { compute_element_placement, HistogramControls, InteractiveAxisLabel, PlotLegend, ReferenceLine, } from './';
4
- import { AXIS_LABEL_CONTAINER, create_axis_change_handler, } from './axis-utils';
5
- import { extract_series_color, prepare_legend_data } from './data-transform';
6
- import { AXIS_DEFAULTS } from './defaults';
7
- import { create_dimension_tracker, create_hover_lock, } from './hover-lock.svelte';
8
- import { get_relative_coords, pan_range, PINCH_ZOOM_THRESHOLD, pixels_to_data_delta, } from './interactions';
9
- import { calc_auto_padding, constrain_tooltip_position, filter_padding, LABEL_GAP_DEFAULT, measure_text_width, } from './layout';
10
- import { group_ref_lines_by_z, index_ref_lines } from './reference-line';
11
- import { create_scale, generate_ticks, get_nice_data_range, get_tick_label, } from './scales';
12
- import { get_scale_type_name } from './types';
13
- import { DEFAULTS } from '../settings';
14
- import { bin, max } from 'd3-array';
15
- import { untrack } from 'svelte';
16
- import { Tween } from 'svelte/motion';
17
- import PlotTooltip from './PlotTooltip.svelte';
18
- import { bar_path } from './svg';
19
- let { series = $bindable([]), x_axis: x_axis_init = {}, y_axis: y_axis_init = {}, y2_axis: y2_axis_init = {}, display: display_init = DEFAULTS.histogram.display, x_range = [null, null], y_range = [null, null], y2_range = [null, null], range_padding = 0.05, padding = { t: 20, b: 60, l: 60, r: 20 }, bins = $bindable(100), show_legend = $bindable(true), legend = {}, bar: bar_init = {}, selected_property = $bindable(``), mode = $bindable(`single`), tooltip, hovered = $bindable(false), change = () => { }, on_bar_click, on_bar_hover, ref_lines = $bindable([]), on_ref_line_click, on_ref_line_hover, show_controls = $bindable(true), controls_open = $bindable(false), on_series_toggle = () => { }, controls_toggle_props, controls_pane_props, fullscreen = $bindable(false), fullscreen_toggle = true, children, header_controls, controls_extra, data_loader, on_axis_change, on_error, pan = {}, ...rest } = $props();
20
- // Local state for controls (initialized from props, owned by this component)
21
- // Include key AXIS_DEFAULTS props (range, ticks, scale_type) that PlotControls needs
22
- // Using $state because these have bindings in HistogramControls/PlotControls
23
- // untrack() explicitly captures initial prop values (intentional - props provide initial config)
24
- const { format: _, ...axis_state_defaults } = AXIS_DEFAULTS; // Exclude format (has component-specific default)
25
- let bar = $state(untrack(() => ({ ...DEFAULTS.histogram.bar, ...bar_init })));
26
- let x_axis = $state(untrack(() => ({ ...axis_state_defaults, ...x_axis_init })));
27
- let y_axis = $state(untrack(() => ({ ...axis_state_defaults, ...y_axis_init })));
28
- // y2-axis needs different default label_shift for right-side positioning
29
- let y2_axis = $state(untrack(() => ({
1
+ <script lang="ts">
2
+ import { format_value } from '../labels'
3
+ import { FullscreenToggle, set_fullscreen_bg } from '../layout'
4
+ import type {
5
+ AxisLoadError,
6
+ BarStyle,
7
+ DataLoaderFn,
8
+ HistogramHandlerProps,
9
+ PanConfig,
10
+ RefLine,
11
+ RefLineEvent,
12
+ } from './'
13
+ import {
14
+ AxisLabel,
15
+ compute_element_placement,
16
+ HistogramControls,
17
+ PlotLegend,
18
+ ReferenceLine,
19
+ } from './'
20
+ import type { AxisChangeState } from './axis-utils'
21
+ import { create_axis_change_handler } from './axis-utils'
22
+ import { extract_series_color, prepare_legend_data } from './data-transform'
23
+ import { AXIS_DEFAULTS } from './defaults'
24
+ import {
25
+ create_dimension_tracker,
26
+ create_hover_lock,
27
+ } from './hover-lock.svelte'
28
+ import {
29
+ get_relative_coords,
30
+ pan_range,
31
+ PINCH_ZOOM_THRESHOLD,
32
+ pixels_to_data_delta,
33
+ } from './interactions'
34
+ import {
35
+ calc_auto_padding,
36
+ constrain_tooltip_position,
37
+ filter_padding,
38
+ LABEL_GAP_DEFAULT,
39
+ measure_max_tick_width,
40
+ } from './layout'
41
+ import type { IndexedRefLine } from './reference-line'
42
+ import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
43
+ import {
44
+ create_scale,
45
+ generate_ticks,
46
+ get_nice_data_range,
47
+ get_tick_label,
48
+ } from './scales'
49
+ import type {
50
+ BasePlotProps,
51
+ DataSeries,
52
+ InitialRanges,
53
+ LegendConfig,
54
+ PlotConfig,
55
+ ScaleType,
56
+ } from './types'
57
+ import { get_scale_type_name } from './types'
58
+ import ZeroLines from './ZeroLines.svelte'
59
+ import ZoomRect from './ZoomRect.svelte'
60
+ import { DEFAULTS } from '../settings'
61
+ import { bin, max } from 'd3-array'
62
+ import type { Snippet } from 'svelte'
63
+ import { untrack } from 'svelte'
64
+ import type { HTMLAttributes } from 'svelte/elements'
65
+ import { Tween } from 'svelte/motion'
66
+ import type { Vec2 } from '../math'
67
+ import PlotTooltip from './PlotTooltip.svelte'
68
+ import { bar_path } from './svg'
69
+
70
+ let {
71
+ series = $bindable([]),
72
+ x_axis: x_axis_init = {},
73
+ x2_axis: x2_axis_init = {},
74
+ y_axis: y_axis_init = {},
75
+ y2_axis: y2_axis_init = {},
76
+ display: display_init = DEFAULTS.histogram.display,
77
+ x_range = [null, null],
78
+ x2_range = [null, null],
79
+ y_range = [null, null],
80
+ y2_range = [null, null],
81
+ range_padding = 0.05,
82
+ padding = { t: 20, b: 60, l: 60, r: 20 },
83
+ bins = $bindable(100),
84
+ show_legend = $bindable(true),
85
+ legend = {},
86
+ bar: bar_init = {},
87
+ selected_property = $bindable(``),
88
+ mode = $bindable(`single`),
89
+ tooltip,
90
+ hovered = $bindable(false),
91
+ change = () => {},
92
+ on_bar_click,
93
+ on_bar_hover,
94
+ ref_lines = $bindable([]),
95
+ on_ref_line_click,
96
+ on_ref_line_hover,
97
+ show_controls = $bindable(true),
98
+ controls_open = $bindable(false),
99
+ on_series_toggle = () => {},
100
+ controls_toggle_props,
101
+ controls_pane_props,
102
+ fullscreen = $bindable(false),
103
+ fullscreen_toggle = true,
104
+ children,
105
+ header_controls,
106
+ controls_extra,
107
+ data_loader,
108
+ on_axis_change,
109
+ on_error,
110
+ pan = {},
111
+ ...rest
112
+ }: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
113
+ series: DataSeries[]
114
+ // Component-specific props
115
+ bins?: number
116
+ show_legend?: boolean
117
+ legend?: LegendConfig | null
118
+ bar?: BarStyle
119
+ selected_property?: string
120
+ mode?: `single` | `overlay`
121
+ tooltip?: Snippet<[HistogramHandlerProps]>
122
+ header_controls?: Snippet<
123
+ [{ height: number; width: number; fullscreen: boolean }]
124
+ >
125
+ controls_extra?: Snippet<[Required<PlotConfig>]>
126
+ change?: (data: { value: number; count: number; property: string } | null) => void
127
+ on_bar_click?: (
128
+ data: {
129
+ value: number
130
+ count: number
131
+ property: string
132
+ event: MouseEvent | KeyboardEvent
133
+ },
134
+ ) => void
135
+ on_bar_hover?: (
136
+ data:
137
+ | { value: number; count: number; property: string; event: MouseEvent }
138
+ | null,
139
+ ) => void
140
+ ref_lines?: RefLine[]
141
+ on_ref_line_click?: (event: RefLineEvent) => void
142
+ on_ref_line_hover?: (event: RefLineEvent | null) => void
143
+ on_series_toggle?: (series_idx: number) => void
144
+ // Interactive axis props
145
+ data_loader?: DataLoaderFn
146
+ on_axis_change?: (
147
+ axis: `x` | `x2` | `y` | `y2`,
148
+ key: string,
149
+ new_series: DataSeries[],
150
+ ) => void
151
+ on_error?: (error: AxisLoadError) => void
152
+ pan?: PanConfig
153
+ } = $props()
154
+
155
+ // Local state for controls (initialized from props, owned by this component)
156
+ // Include key AXIS_DEFAULTS props (range, ticks, scale_type) that PlotControls needs
157
+ // Using $state because these have bindings in HistogramControls/PlotControls
158
+ // untrack() explicitly captures initial prop values (intentional - props provide initial config)
159
+ const { format: _, ...axis_state_defaults } = AXIS_DEFAULTS // Exclude format (has component-specific default)
160
+ let bar = $state(untrack(() => ({ ...DEFAULTS.histogram.bar, ...bar_init })))
161
+ let x_axis = $state(untrack(() => ({ ...axis_state_defaults, ...x_axis_init })))
162
+ // x2-axis needs different default label_shift for top-side positioning
163
+ let x2_axis = $state(untrack(() => ({
164
+ ...axis_state_defaults,
165
+ label_shift: { x: 0, y: 40 },
166
+ ...x2_axis_init,
167
+ })))
168
+ let y_axis = $state(untrack(() => ({ ...axis_state_defaults, ...y_axis_init })))
169
+ // y2-axis needs different default label_shift for right-side positioning
170
+ let y2_axis = $state(untrack(() => ({
30
171
  ...axis_state_defaults,
31
172
  label_shift: { x: 0, y: 60 },
32
173
  ...y2_axis_init,
33
- })));
34
- let display = $state(untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })));
35
- // Merge component-specific defaults with local state (format comes from here, not AXIS_DEFAULTS)
36
- const final_x_axis = $derived({ label: `Value`, format: `.2~s`, ...x_axis });
37
- const final_y_axis = $derived({ label: `Count`, format: `d`, ...y_axis });
38
- const final_bar = $derived({ ...DEFAULTS.histogram.bar, ...bar });
39
- const final_y2_axis = $derived({ label: `Count`, format: `d`, ...y2_axis });
40
- // Core state
41
- let [width, height] = $state([0, 0]);
42
- let wrapper = $state();
43
- let svg_element = $state(null);
44
- let clip_path_id = `histogram-clip-${crypto?.randomUUID?.()}`;
45
- let hover_info = $state(null);
46
- // Reference line hover state
47
- let hovered_ref_line_idx = $state(null);
48
- // Interactive axis loading state
49
- let axis_loading = $state(null);
50
- // Compute ref_lines with index and group by z-index (using shared utilities)
51
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines));
52
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines));
53
- let tooltip_el = $state();
54
- let drag_state = $state({ start: null, current: null, bounds: null });
55
- // Pan state
56
- let is_focused = $state(false);
57
- let shift_held = $state(false);
58
- let pan_drag_state = $state(null);
59
- let touch_state = $state(null);
60
- // Legend placement stability state
61
- let legend_element = $state();
62
- const legend_hover = create_hover_lock();
63
- const dim_tracker = create_dimension_tracker();
64
- let has_initial_legend_placement = $state(false);
65
- // Clear pending hover lock timeout on unmount
66
- $effect(() => () => legend_hover.cleanup());
67
- // Derived data
68
- let selected_series = $derived(mode === `single` && selected_property
69
- ? series.filter((srs) => (srs.visible ?? true) && srs.label === selected_property)
70
- : series.filter((srs) => srs.visible ?? true));
71
- // Separate series by y-axis
72
- let y1_series = $derived(selected_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`));
73
- let y2_series = $derived(selected_series.filter((srs) => srs.y_axis === `y2`));
74
- let auto_ranges = $derived.by(() => {
75
- const all_values = selected_series.flatMap((srs) => srs.y);
76
- const auto_x = get_nice_data_range(all_values.map((val) => ({ x: val, y: 0 })), ({ x }) => x, x_range, final_x_axis.scale_type ?? `linear`, range_padding, false);
174
+ })))
175
+ let display = $state(
176
+ untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })),
177
+ )
178
+
179
+ // Merge component-specific defaults with local state (format comes from here, not AXIS_DEFAULTS)
180
+ const final_x_axis = $derived({ label: `Value`, format: `.2~s`, ...x_axis })
181
+ const final_x2_axis = $derived({ label: `Value`, format: `.2~s`, ...x2_axis })
182
+ const final_y_axis = $derived({ label: `Count`, format: `d`, ...y_axis })
183
+ const final_bar = $derived({ ...DEFAULTS.histogram.bar, ...bar })
184
+ const final_y2_axis = $derived({ label: `Count`, format: `d`, ...y2_axis })
185
+
186
+ // Core state
187
+ let [width, height] = $state([0, 0])
188
+ let wrapper: HTMLDivElement | undefined = $state()
189
+ let svg_element: SVGElement | null = $state(null)
190
+ let clip_path_id = `histogram-clip-${crypto?.randomUUID?.()}`
191
+ let hover_info = $state<HistogramHandlerProps | null>(null)
192
+
193
+ // Reference line hover state
194
+ let hovered_ref_line_idx = $state<number | null>(null)
195
+
196
+ // Interactive axis loading state
197
+ let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
198
+
199
+ // Compute ref_lines with index and group by z-index (using shared utilities)
200
+ let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
201
+ let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
202
+ let tooltip_el = $state<HTMLDivElement | undefined>()
203
+ let drag_state = $state<{
204
+ start: { x: number; y: number } | null
205
+ current: { x: number; y: number } | null
206
+ bounds: DOMRect | null
207
+ }>({ start: null, current: null, bounds: null })
208
+
209
+ // Pan state
210
+ let is_focused = $state(false)
211
+ let shift_held = $state(false)
212
+ let pan_drag_state = $state<
213
+ InitialRanges & { start: { x: number; y: number } } | null
214
+ >(null)
215
+ let touch_state = $state<
216
+ InitialRanges & { start_touches: { x: number; y: number }[] } | null
217
+ >(null)
218
+
219
+ // Legend placement stability state
220
+ let legend_element = $state<HTMLDivElement | undefined>()
221
+ const legend_hover = create_hover_lock()
222
+ const dim_tracker = create_dimension_tracker()
223
+ let has_initial_legend_placement = $state(false)
224
+
225
+ // Clear pending hover lock timeout on unmount
226
+ $effect(() => () => legend_hover.cleanup())
227
+
228
+ // Derived data
229
+ let selected_series = $derived(
230
+ mode === `single` && selected_property
231
+ ? series.filter((srs: DataSeries) =>
232
+ (srs.visible ?? true) && srs.label === selected_property
233
+ )
234
+ : series.filter((srs: DataSeries) => srs.visible ?? true),
235
+ )
236
+
237
+ // Separate series by y-axis
238
+ let y1_series = $derived(
239
+ selected_series.filter((srs: DataSeries) => (srs.y_axis ?? `y1`) === `y1`),
240
+ )
241
+ let y2_series = $derived(
242
+ selected_series.filter((srs: DataSeries) => srs.y_axis === `y2`),
243
+ )
244
+ let x2_series = $derived(
245
+ selected_series.filter((srs: DataSeries) => srs.x_axis === `x2`),
246
+ )
247
+
248
+ let auto_ranges = $derived.by(() => {
249
+ const all_values = selected_series.flatMap((srs: DataSeries) => srs.y)
250
+ const auto_x = get_nice_data_range(
251
+ all_values.map((val) => ({ x: val, y: 0 })),
252
+ ({ x }) => x,
253
+ x_range,
254
+ final_x_axis.scale_type ?? `linear`,
255
+ range_padding,
256
+ false,
257
+ )
258
+
259
+ const x2_values = x2_series.flatMap((srs: DataSeries) => srs.y)
260
+ const auto_x2 = x2_values.length > 0
261
+ ? get_nice_data_range(
262
+ x2_values.map((val) => ({ x: val, y: 0 })),
263
+ ({ x }) => x,
264
+ x2_range,
265
+ final_x2_axis.scale_type ?? `linear`,
266
+ range_padding,
267
+ false,
268
+ )
269
+ : [0, 1] as Vec2
270
+
77
271
  // Calculate y-range for a specific set of series
78
- const calc_y_range = (series_list, y_limit, scale_type) => {
79
- const type_name = get_scale_type_name(scale_type);
80
- if (!series_list.length) {
81
- const fallback = type_name === `log` ? 1 : 0;
82
- return [fallback, 1];
83
- }
84
- const hist = bin().domain([auto_x[0], auto_x[1]]).thresholds(bins);
85
- const max_count = Math.max(0, ...series_list.map((srs) => max(hist(srs.y), (data) => data.length) || 0));
86
- // If there's effectively no data, avoid log-range issues (counts can't be <= 0 on log)
87
- if (max_count <= 0) {
88
- const fallback = type_name === `log` ? 1 : 0;
89
- return [fallback, 1];
90
- }
91
- const [y0, y1] = get_nice_data_range([{ x: 0, y: 0 }, { x: max_count, y: 0 }], ({ x }) => x, y_limit, scale_type, range_padding, false);
92
- // For log scale, minimum must be >= 1 (count can't be 0 on log)
93
- // For linear/arcsinh, start from 0
94
- const y_min = type_name === `log` ? Math.max(1, y0) : Math.max(0, y0);
95
- return [y_min, y1];
96
- };
97
- const y1_range = calc_y_range(y1_series, y_range, final_y_axis.scale_type ?? `linear`);
98
- const y2_auto_range = calc_y_range(y2_series, y2_range, final_y2_axis.scale_type ?? `linear`);
99
- return { x: auto_x, y: y1_range, y2: y2_auto_range };
100
- });
101
- // Initialize ranges
102
- let ranges = $state({
272
+ const calc_y_range = (
273
+ series_list: typeof selected_series,
274
+ y_limit: typeof y_range,
275
+ scale_type: ScaleType,
276
+ ): Vec2 => {
277
+ const type_name = get_scale_type_name(scale_type)
278
+ if (!series_list.length) {
279
+ const fallback = type_name === `log` ? 1 : 0
280
+ return [fallback, 1]
281
+ }
282
+ const hist = bin().domain([auto_x[0], auto_x[1]]).thresholds(bins)
283
+ const max_count = Math.max(
284
+ 0,
285
+ ...series_list.map((srs: DataSeries) =>
286
+ max(hist(srs.y), (data) => data.length) || 0
287
+ ),
288
+ )
289
+
290
+ // If there's effectively no data, avoid log-range issues (counts can't be <= 0 on log)
291
+ if (max_count <= 0) {
292
+ const fallback = type_name === `log` ? 1 : 0
293
+ return [fallback, 1]
294
+ }
295
+
296
+ const [y0, y1] = get_nice_data_range(
297
+ [{ x: 0, y: 0 }, { x: max_count, y: 0 }],
298
+ ({ x }) => x,
299
+ y_limit,
300
+ scale_type,
301
+ range_padding,
302
+ false,
303
+ )
304
+ // For log scale, minimum must be >= 1 (count can't be 0 on log)
305
+ // For linear/arcsinh, start from 0
306
+ const y_min = type_name === `log` ? Math.max(1, y0) : Math.max(0, y0)
307
+ return [y_min, y1]
308
+ }
309
+
310
+ const y1_range = calc_y_range(
311
+ y1_series,
312
+ y_range,
313
+ final_y_axis.scale_type ?? `linear`,
314
+ )
315
+ const y2_auto_range = calc_y_range(
316
+ y2_series,
317
+ y2_range,
318
+ final_y2_axis.scale_type ?? `linear`,
319
+ )
320
+
321
+ return { x: auto_x, x2: auto_x2, y: y1_range, y2: y2_auto_range }
322
+ })
323
+
324
+ // Initialize ranges
325
+ let ranges = $state({
103
326
  initial: {
104
- x: [0, 1],
105
- y: [0, 1],
106
- y2: [0, 1],
327
+ x: [0, 1] as Vec2,
328
+ x2: [0, 1] as Vec2,
329
+ y: [0, 1] as Vec2,
330
+ y2: [0, 1] as Vec2,
107
331
  },
108
332
  current: {
109
- x: [0, 1],
110
- y: [0, 1],
111
- y2: [0, 1],
333
+ x: [0, 1] as Vec2,
334
+ x2: [0, 1] as Vec2,
335
+ y: [0, 1] as Vec2,
336
+ y2: [0, 1] as Vec2,
112
337
  },
113
- });
114
- $effect(() => {
338
+ })
339
+
340
+ $effect(() => {
115
341
  // Support one-sided range pinning: merge user range with auto range for null values
116
- const new_x = final_x_axis.range
117
- ? [
118
- final_x_axis.range[0] ?? auto_ranges.x[0],
119
- final_x_axis.range[1] ?? auto_ranges.x[1],
120
- ]
121
- : auto_ranges.x;
122
- const new_y = final_y_axis.range
123
- ? [
124
- final_y_axis.range[0] ?? auto_ranges.y[0],
125
- final_y_axis.range[1] ?? auto_ranges.y[1],
126
- ]
127
- : auto_ranges.y;
128
- const new_y2 = final_y2_axis.range
129
- ? [
130
- final_y2_axis.range[0] ?? auto_ranges.y2[0],
131
- final_y2_axis.range[1] ?? auto_ranges.y2[1],
132
- ]
133
- : auto_ranges.y2;
342
+ const new_x: [number, number] = final_x_axis.range
343
+ ? [
344
+ final_x_axis.range[0] ?? auto_ranges.x[0],
345
+ final_x_axis.range[1] ?? auto_ranges.x[1],
346
+ ]
347
+ : auto_ranges.x
348
+ const new_x2: [number, number] = final_x2_axis.range
349
+ ? [
350
+ final_x2_axis.range[0] ?? auto_ranges.x2[0],
351
+ final_x2_axis.range[1] ?? auto_ranges.x2[1],
352
+ ]
353
+ : auto_ranges.x2
354
+ const new_y: [number, number] = final_y_axis.range
355
+ ? [
356
+ final_y_axis.range[0] ?? auto_ranges.y[0],
357
+ final_y_axis.range[1] ?? auto_ranges.y[1],
358
+ ]
359
+ : auto_ranges.y
360
+ const new_y2: [number, number] = final_y2_axis.range
361
+ ? [
362
+ final_y2_axis.range[0] ?? auto_ranges.y2[0],
363
+ final_y2_axis.range[1] ?? auto_ranges.y2[1],
364
+ ]
365
+ : auto_ranges.y2
366
+
134
367
  // Only update if the initial (data-driven) ranges changed, not when user pans
135
368
  // Comparing against initial preserves user's pan/zoom state
136
369
  const x_changed = new_x[0] !== ranges.initial.x[0] ||
137
- new_x[1] !== ranges.initial.x[1];
370
+ new_x[1] !== ranges.initial.x[1]
371
+ const x2_changed = new_x2[0] !== ranges.initial.x2[0] ||
372
+ new_x2[1] !== ranges.initial.x2[1]
138
373
  const y_changed = new_y[0] !== ranges.initial.y[0] ||
139
- new_y[1] !== ranges.initial.y[1];
374
+ new_y[1] !== ranges.initial.y[1]
140
375
  const y2_changed = new_y2[0] !== ranges.initial.y2[0] ||
141
- new_y2[1] !== ranges.initial.y2[1];
142
- if (x_changed)
143
- [ranges.initial.x, ranges.current.x] = [new_x, new_x];
144
- if (y_changed)
145
- [ranges.initial.y, ranges.current.y] = [new_y, new_y];
146
- if (y2_changed)
147
- [ranges.initial.y2, ranges.current.y2] = [new_y2, new_y2];
148
- });
149
- // Layout: dynamic padding based on tick label widths
150
- const default_padding = { t: 20, b: 60, l: 60, r: 20 };
151
- let pad = $derived(filter_padding(padding, default_padding));
152
- // Update padding based on tick label widths (untrack breaks circular dependency)
153
- $effect(() => {
154
- const current_ticks_y = untrack(() => ticks.y);
155
- const current_ticks_y2 = untrack(() => ticks.y2);
376
+ new_y2[1] !== ranges.initial.y2[1]
377
+
378
+ if (x_changed) [ranges.initial.x, ranges.current.x] = [new_x, new_x]
379
+ if (x2_changed) [ranges.initial.x2, ranges.current.x2] = [new_x2, new_x2]
380
+ if (y_changed) [ranges.initial.y, ranges.current.y] = [new_y, new_y]
381
+ if (y2_changed) [ranges.initial.y2, ranges.current.y2] = [new_y2, new_y2]
382
+ })
383
+
384
+ // Layout: dynamic padding based on tick label widths
385
+ const default_padding = { t: 20, b: 60, l: 60, r: 20 }
386
+ let pad = $derived(filter_padding(padding, default_padding))
387
+
388
+ // Update padding based on tick label widths (untrack breaks circular dependency)
389
+ $effect(() => {
390
+ const current_ticks_x2 = untrack(() => ticks.x2)
391
+ const current_ticks_y = untrack(() => ticks.y)
392
+ const current_ticks_y2 = untrack(() => ticks.y2)
393
+
156
394
  const new_pad = width && height && current_ticks_y.length
157
- ? calc_auto_padding({
158
- padding,
159
- default_padding,
160
- y_axis: { ...final_y_axis, tick_values: current_ticks_y },
161
- y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
162
- })
163
- : filter_padding(padding, default_padding);
395
+ ? calc_auto_padding({
396
+ padding,
397
+ default_padding,
398
+ x2_axis: { ...final_x2_axis, tick_values: current_ticks_x2 },
399
+ y_axis: { ...final_y_axis, tick_values: current_ticks_y },
400
+ y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
401
+ })
402
+ : filter_padding(padding, default_padding)
403
+
164
404
  // Add y2 axis label space (calc_auto_padding only accounts for tick labels)
165
- if (width && height && y2_series.length && current_ticks_y2.length &&
166
- final_y2_axis.label) {
167
- const y2_tick_width = Math.max(0, ...current_ticks_y2.map((tick) => measure_text_width(format_value(tick, final_y2_axis.format), `12px sans-serif`)));
168
- const inside = final_y2_axis.tick?.label?.inside ?? false;
169
- // When ticks are inside, they don't contribute to padding
170
- const tick_shift = inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8;
171
- const tick_width_contribution = inside ? 0 : y2_tick_width;
172
- const label_thickness = Math.round(12 * 1.2);
173
- new_pad.r = Math.max(new_pad.r, tick_width_contribution + LABEL_GAP_DEFAULT + tick_shift + label_thickness);
405
+ if (
406
+ width && height && y2_series.length && current_ticks_y2.length &&
407
+ final_y2_axis.label
408
+ ) {
409
+ const inside = final_y2_axis.tick?.label?.inside ?? false
410
+ // When ticks are inside, they don't contribute to padding
411
+ const tick_shift = inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8
412
+ const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
413
+ const label_thickness = Math.round(12 * 1.2)
414
+ new_pad.r = Math.max(
415
+ new_pad.r,
416
+ tick_width_contribution + LABEL_GAP_DEFAULT + tick_shift + label_thickness,
417
+ )
418
+ }
419
+
420
+ // Add x2 axis label space (mirroring y2 logic for top padding)
421
+ if (
422
+ width && height && x2_series.length && current_ticks_x2.length &&
423
+ final_x2_axis.label
424
+ ) {
425
+ const inside = final_x2_axis.tick?.label?.inside ?? false
426
+ const tick_shift = inside
427
+ ? 0
428
+ : Math.abs(final_x2_axis.tick?.label?.shift?.y ?? 0) + 8
429
+ const label_thickness = Math.round(12 * 1.2)
430
+ new_pad.t = Math.max(
431
+ new_pad.t,
432
+ tick_shift + LABEL_GAP_DEFAULT + label_thickness,
433
+ )
174
434
  }
435
+
175
436
  // Only update if padding actually changed
176
- if (pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
177
- pad.r !== new_pad.r)
178
- pad = new_pad;
179
- });
180
- // Scales and data
181
- let scales = $derived({
182
- x: create_scale(final_x_axis.scale_type ?? `linear`, ranges.current.x, [pad.l, width - pad.r]),
183
- y: create_scale(final_y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
184
- y2: create_scale(final_y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
185
- });
186
- let histogram_data = $derived.by(() => {
187
- if (!selected_series.length || !width || !height)
188
- return [];
437
+ if (
438
+ pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
439
+ pad.r !== new_pad.r
440
+ ) pad = new_pad
441
+ })
442
+
443
+ // Scales and data
444
+ let scales = $derived({
445
+ x: create_scale(
446
+ final_x_axis.scale_type ?? `linear`,
447
+ ranges.current.x,
448
+ [pad.l, width - pad.r],
449
+ ),
450
+ x2: create_scale(
451
+ final_x2_axis.scale_type ?? `linear`,
452
+ ranges.current.x2,
453
+ [pad.l, width - pad.r],
454
+ ),
455
+ y: create_scale(
456
+ final_y_axis.scale_type ?? `linear`,
457
+ ranges.current.y,
458
+ [height - pad.b, pad.t],
459
+ ),
460
+ y2: create_scale(
461
+ final_y2_axis.scale_type ?? `linear`,
462
+ ranges.current.y2,
463
+ [height - pad.b, pad.t],
464
+ ),
465
+ })
466
+
467
+ let histogram_data = $derived.by(() => {
468
+ if (!selected_series.length || !width || !height) return []
189
469
  const hist_generator = bin()
190
- .domain([ranges.current.x[0], ranges.current.x[1]])
191
- .thresholds(bins);
470
+ .domain([ranges.current.x[0], ranges.current.x[1]])
471
+ .thresholds(bins)
472
+ const x2_hist_generator = x2_series.length > 0
473
+ ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
474
+ : null
192
475
  return selected_series.map((series_data, series_idx) => {
193
- const bins_arr = hist_generator(series_data.y);
194
- const use_y2 = series_data.y_axis === `y2`;
195
- return {
196
- id: series_data.id ?? series_idx,
197
- series_idx,
198
- label: series_data.label || `Series ${series_idx + 1}`,
199
- color: selected_series.length === 1
200
- ? final_bar.color
201
- : extract_series_color(series_data),
202
- bins: bins_arr,
203
- max_count: max(bins_arr, (data) => data.length) || 0,
204
- y_axis: series_data.y_axis,
205
- y_scale: use_y2 ? scales.y2 : scales.y,
206
- };
207
- });
208
- });
209
- let ticks = $derived({
476
+ const use_x2 = series_data.x_axis === `x2`
477
+ const active_hist = use_x2 && x2_hist_generator
478
+ ? x2_hist_generator
479
+ : hist_generator
480
+ const bins_arr = active_hist(series_data.y)
481
+ const use_y2 = series_data.y_axis === `y2`
482
+ return {
483
+ id: series_data.id ?? series_idx,
484
+ series_idx,
485
+ label: series_data.label || `Series ${series_idx + 1}`,
486
+ color: selected_series.length === 1
487
+ ? final_bar.color
488
+ : extract_series_color(series_data),
489
+ bins: bins_arr,
490
+ max_count: max(bins_arr, (data) => data.length) || 0,
491
+ x_axis: series_data.x_axis,
492
+ y_axis: series_data.y_axis,
493
+ x_scale: use_x2 ? scales.x2 : scales.x,
494
+ y_scale: use_y2 ? scales.y2 : scales.y,
495
+ }
496
+ })
497
+ })
498
+
499
+ let ticks = $derived({
210
500
  x: width && height
211
- ? generate_ticks(ranges.current.x, final_x_axis.scale_type ?? `linear`, final_x_axis.ticks, scales.x, { default_count: 8 })
212
- : [],
501
+ ? generate_ticks(
502
+ ranges.current.x,
503
+ final_x_axis.scale_type ?? `linear`,
504
+ final_x_axis.ticks,
505
+ scales.x,
506
+ { default_count: 8 },
507
+ )
508
+ : [],
509
+ x2: width && height && x2_series.length > 0
510
+ ? generate_ticks(
511
+ ranges.current.x2,
512
+ final_x2_axis.scale_type ?? `linear`,
513
+ final_x2_axis.ticks,
514
+ scales.x2,
515
+ { default_count: 8 },
516
+ )
517
+ : [],
213
518
  y: width && height
214
- ? generate_ticks(ranges.current.y, final_y_axis.scale_type ?? `linear`, final_y_axis.ticks, scales.y, { default_count: 6 })
215
- : [],
519
+ ? generate_ticks(
520
+ ranges.current.y,
521
+ final_y_axis.scale_type ?? `linear`,
522
+ final_y_axis.ticks,
523
+ scales.y,
524
+ { default_count: 6 },
525
+ )
526
+ : [],
216
527
  y2: width && height && y2_series.length > 0
217
- ? generate_ticks(ranges.current.y2, final_y2_axis.scale_type ?? `linear`, final_y2_axis.ticks, scales.y2, { default_count: 6 })
218
- : [],
219
- });
220
- let legend_data = $derived(prepare_legend_data(series));
221
- // Collect histogram bar positions for legend placement
222
- let hist_points_for_placement = $derived.by(() => {
223
- if (!width || !height || !histogram_data.length)
224
- return [];
225
- const points = [];
226
- for (const { bins, y_scale } of histogram_data) {
227
- for (const bin of bins) {
228
- if (bin.length > 0) {
229
- const bar_x = scales.x((bin.x0 + bin.x1) / 2);
230
- const bar_y = y_scale(bin.length);
231
- if (isFinite(bar_x) && isFinite(bar_y)) {
232
- // Add multiple points for taller bars to increase their weight
233
- // Cap to prevent O(N·count/10) blow-ups for large counts
234
- const weight = Math.min(20, Math.ceil(bin.length / 10));
235
- for (let idx = 0; idx < weight; idx++)
236
- points.push({ x: bar_x, y: bar_y });
237
- }
238
- }
528
+ ? generate_ticks(
529
+ ranges.current.y2,
530
+ final_y2_axis.scale_type ?? `linear`,
531
+ final_y2_axis.ticks,
532
+ scales.y2,
533
+ { default_count: 6 },
534
+ )
535
+ : [],
536
+ })
537
+
538
+ // Cache measured tick-label widths so expensive text measurement only runs
539
+ // when tick values/format change, not on every template rerender.
540
+ let tick_label_widths = $derived({
541
+ x2_max: measure_max_tick_width(ticks.x2, final_x2_axis.format ?? ``),
542
+ y_max: measure_max_tick_width(ticks.y, final_y_axis.format ?? ``),
543
+ y2_max: measure_max_tick_width(ticks.y2, final_y2_axis.format ?? ``),
544
+ })
545
+
546
+ let legend_data = $derived(prepare_legend_data(series))
547
+
548
+ // Collect histogram bar positions for legend placement
549
+ let hist_points_for_placement = $derived.by(() => {
550
+ if (!width || !height || !histogram_data.length) return []
551
+
552
+ const points: { x: number; y: number }[] = []
553
+
554
+ for (const { bins, x_scale, y_scale } of histogram_data) {
555
+ for (const bin of bins) {
556
+ if (bin.length > 0) {
557
+ const bar_x = x_scale(((bin.x0 ?? 0) + (bin.x1 ?? 0)) / 2)
558
+ const bar_y = y_scale(bin.length)
559
+ if (isFinite(bar_x) && isFinite(bar_y)) {
560
+ // Add multiple points for taller bars to increase their weight
561
+ // Cap to prevent O(N·count/10) blow-ups for large counts
562
+ const weight = Math.min(20, Math.ceil(bin.length / 10))
563
+ for (let idx = 0; idx < weight; idx++) points.push({ x: bar_x, y: bar_y })
564
+ }
239
565
  }
566
+ }
240
567
  }
241
- return points;
242
- });
243
- // Calculate best legend placement using continuous grid sampling
244
- let legend_placement = $derived.by(() => {
245
- const should_place = show_legend && legend != null && series.length > 1;
246
- if (!should_place || !width || !height)
247
- return null;
248
- const plot_width = width - pad.l - pad.r;
249
- const plot_height = height - pad.t - pad.b;
568
+ return points
569
+ })
570
+
571
+ // Calculate best legend placement using continuous grid sampling
572
+ let legend_placement = $derived.by(() => {
573
+ const should_place = show_legend && legend != null && series.length > 1
574
+ if (!should_place || !width || !height) return null
575
+
576
+ const plot_width = width - pad.l - pad.r
577
+ const plot_height = height - pad.t - pad.b
578
+
250
579
  // Use measured size if available, otherwise estimate
251
580
  const legend_size = legend_element
252
- ? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
253
- : { width: 120, height: 60 };
581
+ ? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
582
+ : { width: 120, height: 60 }
583
+
254
584
  const result = compute_element_placement({
255
- plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
256
- element_size: legend_size,
257
- axis_clearance: legend?.axis_clearance,
258
- exclude_rects: [],
259
- points: hist_points_for_placement,
260
- });
261
- return result;
262
- });
263
- // Tweened legend coordinates for smooth animation - create once, update target via effect
264
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
265
- const tweened_legend_coords = new Tween({ x: 0, y: 0 }, untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })));
266
- // Update legend position with stability checks
267
- $effect(() => {
268
- if (!width || !height || !legend_placement)
269
- return;
585
+ plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
586
+ element_size: legend_size,
587
+ axis_clearance: legend?.axis_clearance,
588
+ exclude_rects: [],
589
+ points: hist_points_for_placement,
590
+ })
591
+
592
+ return result
593
+ })
594
+
595
+ // Tweened legend coordinates for smooth animation - create once, update target via effect
596
+ // untrack() explicitly captures initial tween config (intentional - config set once at mount)
597
+ const tweened_legend_coords = new Tween(
598
+ { x: 0, y: 0 },
599
+ untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
600
+ )
601
+
602
+ // Update legend position with stability checks
603
+ $effect(() => {
604
+ if (!width || !height || !legend_placement) return
605
+
270
606
  // Track dimensions for resize detection
271
- const dims_changed = dim_tracker.has_changed(width, height);
272
- if (dims_changed)
273
- dim_tracker.update(width, height);
607
+ const dims_changed = dim_tracker.has_changed(width, height)
608
+ if (dims_changed) dim_tracker.update(width, height)
609
+
274
610
  // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
275
- const is_responsive = legend?.responsive ?? false;
611
+ const is_responsive = legend?.responsive ?? false
276
612
  const should_update = dims_changed || (!legend_hover.is_locked.current &&
277
- (is_responsive || !has_initial_legend_placement));
613
+ (is_responsive || !has_initial_legend_placement))
614
+
278
615
  if (should_update) {
279
- tweened_legend_coords.set({ x: legend_placement.x, y: legend_placement.y },
616
+ tweened_legend_coords.set(
617
+ { x: legend_placement.x, y: legend_placement.y },
280
618
  // Skip animation on initial placement to avoid jump from (0, 0)
281
- has_initial_legend_placement ? undefined : { duration: 0 });
282
- // Only lock position after we have actual measured size
283
- if (legend_element) {
284
- has_initial_legend_placement = true;
285
- }
619
+ has_initial_legend_placement ? undefined : { duration: 0 },
620
+ )
621
+ // Only lock position after we have actual measured size
622
+ if (legend_element) {
623
+ has_initial_legend_placement = true
624
+ }
286
625
  }
287
- });
288
- // Event handlers
289
- const handle_zoom = () => {
290
- if (!drag_state.start || !drag_state.current)
291
- return;
292
- const start_x = scales.x.invert(drag_state.start.x);
293
- const end_x = scales.x.invert(drag_state.current.x);
294
- const start_y = scales.y.invert(drag_state.start.y);
295
- const end_y = scales.y.invert(drag_state.current.y);
296
- const start_y2 = scales.y2.invert(drag_state.start.y);
297
- const end_y2 = scales.y2.invert(drag_state.current.y);
626
+ })
627
+
628
+ // Event handlers
629
+ const handle_zoom = () => {
630
+ if (!drag_state.start || !drag_state.current) return
631
+ const start_x = scales.x.invert(drag_state.start.x)
632
+ const end_x = scales.x.invert(drag_state.current.x)
633
+ const start_x2 = scales.x2.invert(drag_state.start.x)
634
+ const end_x2 = scales.x2.invert(drag_state.current.x)
635
+ const start_y = scales.y.invert(drag_state.start.y)
636
+ const end_y = scales.y.invert(drag_state.current.y)
637
+ const start_y2 = scales.y2.invert(drag_state.start.y)
638
+ const end_y2 = scales.y2.invert(drag_state.current.y)
639
+
298
640
  if (typeof start_x === `number` && typeof end_x === `number`) {
299
- const dx = Math.abs(drag_state.start.x - drag_state.current.x);
300
- const dy = Math.abs(drag_state.start.y - drag_state.current.y);
301
- if (dx > 5 && dy > 5) {
302
- // Update axis ranges to trigger reactivity and prevent effect from overriding
303
- x_axis = {
304
- ...x_axis,
305
- range: [Math.min(start_x, end_x), Math.max(start_x, end_x)],
306
- };
307
- y_axis = {
308
- ...y_axis,
309
- range: [Math.min(start_y, end_y), Math.max(start_y, end_y)],
310
- };
311
- y2_axis = {
312
- ...y2_axis,
313
- range: [Math.min(start_y2, end_y2), Math.max(start_y2, end_y2)],
314
- };
641
+ const dx = Math.abs(drag_state.start.x - drag_state.current.x)
642
+ const dy = Math.abs(drag_state.start.y - drag_state.current.y)
643
+ if (dx > 5 && dy > 5) {
644
+ // Update axis ranges to trigger reactivity and prevent effect from overriding
645
+ x_axis = {
646
+ ...x_axis,
647
+ range: [Math.min(start_x, end_x), Math.max(start_x, end_x)],
315
648
  }
649
+ if (x2_series.length > 0) {
650
+ x2_axis = {
651
+ ...x2_axis,
652
+ range: [Math.min(start_x2, end_x2), Math.max(start_x2, end_x2)],
653
+ }
654
+ }
655
+ y_axis = {
656
+ ...y_axis,
657
+ range: [Math.min(start_y, end_y), Math.max(start_y, end_y)],
658
+ }
659
+ y2_axis = {
660
+ ...y2_axis,
661
+ range: [Math.min(start_y2, end_y2), Math.max(start_y2, end_y2)],
662
+ }
663
+ }
316
664
  }
317
- };
318
- const on_window_mouse_move = (evt) => {
319
- if (!drag_state.start || !drag_state.bounds)
320
- return;
665
+ }
666
+
667
+ const on_window_mouse_move = (evt: MouseEvent) => {
668
+ if (!drag_state.start || !drag_state.bounds) return
321
669
  drag_state.current = {
322
- x: evt.clientX - drag_state.bounds.left,
323
- y: evt.clientY - drag_state.bounds.top,
324
- };
325
- };
326
- const on_window_mouse_up = () => {
327
- handle_zoom();
328
- drag_state = { start: null, current: null, bounds: null };
329
- window.removeEventListener(`mousemove`, on_window_mouse_move);
330
- window.removeEventListener(`mouseup`, on_window_mouse_up);
331
- document.body.style.cursor = `default`;
332
- };
333
- // Pan drag handlers
334
- const on_pan_move = (evt) => {
335
- if (!pan_drag_state)
336
- return;
337
- const dx = evt.clientX - pan_drag_state.start.x;
338
- const dy = evt.clientY - pan_drag_state.start.y;
670
+ x: evt.clientX - drag_state.bounds.left,
671
+ y: evt.clientY - drag_state.bounds.top,
672
+ }
673
+ }
674
+
675
+ const on_window_mouse_up = () => {
676
+ handle_zoom()
677
+ drag_state = { start: null, current: null, bounds: null }
678
+ window.removeEventListener(`mousemove`, on_window_mouse_move)
679
+ window.removeEventListener(`mouseup`, on_window_mouse_up)
680
+ document.body.style.cursor = `default`
681
+ }
682
+
683
+ // Pan drag handlers
684
+ const on_pan_move = (evt: MouseEvent) => {
685
+ if (!pan_drag_state) return
686
+ const dx = evt.clientX - pan_drag_state.start.x
687
+ const dy = evt.clientY - pan_drag_state.start.y
688
+
339
689
  // Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
340
- const plot_width = width - pad.l - pad.r;
341
- const plot_height = height - pad.t - pad.b;
342
- const sensitivity = pan?.drag_sensitivity ?? 1;
343
- const x_delta = pixels_to_data_delta(-dx * sensitivity, pan_drag_state.initial_x_range, plot_width);
344
- const y_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y_range, plot_height);
345
- const y2_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y2_range, plot_height);
346
- ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta);
347
- ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta);
348
- ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta);
349
- };
350
- const on_pan_end = () => {
351
- pan_drag_state = null;
352
- document.body.style.cursor = ``;
353
- window.removeEventListener(`mousemove`, on_pan_move);
354
- window.removeEventListener(`mouseup`, on_pan_end);
355
- };
356
- function handle_mouse_down(evt) {
357
- const coords = get_relative_coords(evt);
358
- if (!coords || !svg_element)
359
- return;
690
+ const plot_width = width - pad.l - pad.r
691
+ const plot_height = height - pad.t - pad.b
692
+ const sensitivity = pan?.drag_sensitivity ?? 1
693
+
694
+ const x_delta = pixels_to_data_delta(
695
+ -dx * sensitivity,
696
+ pan_drag_state.initial_x_range,
697
+ plot_width,
698
+ )
699
+ const x2_delta = pixels_to_data_delta(
700
+ -dx * sensitivity,
701
+ pan_drag_state.initial_x2_range,
702
+ plot_width,
703
+ )
704
+ const y_delta = pixels_to_data_delta(
705
+ dy * sensitivity,
706
+ pan_drag_state.initial_y_range,
707
+ plot_height,
708
+ )
709
+ const y2_delta = pixels_to_data_delta(
710
+ dy * sensitivity,
711
+ pan_drag_state.initial_y2_range,
712
+ plot_height,
713
+ )
714
+
715
+ ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta)
716
+ ranges.current.x2 = pan_range(pan_drag_state.initial_x2_range, x2_delta)
717
+ ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta)
718
+ ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta)
719
+ }
720
+
721
+ const on_pan_end = () => {
722
+ pan_drag_state = null
723
+ document.body.style.cursor = ``
724
+ window.removeEventListener(`mousemove`, on_pan_move)
725
+ window.removeEventListener(`mouseup`, on_pan_end)
726
+ }
727
+
728
+ function handle_mouse_down(evt: MouseEvent) {
729
+ const coords = get_relative_coords(evt)
730
+ if (!coords || !svg_element) return
731
+
360
732
  // Check if pan is enabled and shift is held for pan mode
361
- const pan_enabled = pan?.enabled !== false;
733
+ const pan_enabled = pan?.enabled !== false
362
734
  if (pan_enabled && evt.shiftKey) {
363
- evt.preventDefault();
364
- pan_drag_state = {
365
- start: { x: evt.clientX, y: evt.clientY },
366
- initial_x_range: [...ranges.current.x],
367
- initial_y_range: [...ranges.current.y],
368
- initial_y2_range: [...ranges.current.y2],
369
- };
370
- document.body.style.cursor = `grabbing`;
371
- window.addEventListener(`mousemove`, on_pan_move);
372
- window.addEventListener(`mouseup`, on_pan_end);
373
- return;
735
+ evt.preventDefault()
736
+ pan_drag_state = {
737
+ start: { x: evt.clientX, y: evt.clientY },
738
+ initial_x_range: [...ranges.current.x] as [number, number],
739
+ initial_x2_range: [...ranges.current.x2] as [number, number],
740
+ initial_y_range: [...ranges.current.y] as [number, number],
741
+ initial_y2_range: [...ranges.current.y2] as [number, number],
742
+ }
743
+ document.body.style.cursor = `grabbing`
744
+ window.addEventListener(`mousemove`, on_pan_move)
745
+ window.addEventListener(`mouseup`, on_pan_end)
746
+ return
374
747
  }
748
+
375
749
  drag_state = {
376
- start: coords,
377
- current: coords,
378
- bounds: svg_element.getBoundingClientRect(),
379
- };
380
- window.addEventListener(`mousemove`, on_window_mouse_move);
381
- window.addEventListener(`mouseup`, on_window_mouse_up);
382
- evt.preventDefault();
383
- }
384
- // Wheel handler for pan (requires focus and shift)
385
- function handle_wheel(evt) {
386
- const pan_enabled = pan?.enabled !== false;
750
+ start: coords,
751
+ current: coords,
752
+ bounds: svg_element.getBoundingClientRect(),
753
+ }
754
+ window.addEventListener(`mousemove`, on_window_mouse_move)
755
+ window.addEventListener(`mouseup`, on_window_mouse_up)
756
+ evt.preventDefault()
757
+ }
758
+
759
+ // Wheel handler for pan (requires focus and shift)
760
+ function handle_wheel(evt: WheelEvent) {
761
+ const pan_enabled = pan?.enabled !== false
387
762
  // Only capture wheel when focused AND Shift is held
388
763
  // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
389
- if (!pan_enabled || !is_focused || !shift_held)
390
- return;
391
- evt.preventDefault();
764
+ if (!pan_enabled || !is_focused || !shift_held) return
765
+
766
+ evt.preventDefault()
767
+
392
768
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
393
- const plot_width = Math.max(1, width - pad.l - pad.r);
394
- const plot_height = Math.max(1, height - pad.t - pad.b);
395
- const sensitivity = pan?.wheel_sensitivity ?? 1;
769
+ const plot_width = Math.max(1, width - pad.l - pad.r)
770
+ const plot_height = Math.max(1, height - pad.t - pad.b)
771
+ const sensitivity = pan?.wheel_sensitivity ?? 1
772
+
396
773
  // Determine pan direction based on wheel delta
397
- const x_delta = pixels_to_data_delta(evt.deltaX * sensitivity, ranges.current.x, plot_width);
398
- const y_delta = pixels_to_data_delta(evt.deltaY * sensitivity, ranges.current.y, plot_height);
399
- const y2_delta = pixels_to_data_delta(evt.deltaY * sensitivity, ranges.current.y2, plot_height);
774
+ const x_delta = pixels_to_data_delta(
775
+ evt.deltaX * sensitivity,
776
+ ranges.current.x,
777
+ plot_width,
778
+ )
779
+ const x2_delta = pixels_to_data_delta(
780
+ evt.deltaX * sensitivity,
781
+ ranges.current.x2,
782
+ plot_width,
783
+ )
784
+ const y_delta = pixels_to_data_delta(
785
+ evt.deltaY * sensitivity,
786
+ ranges.current.y,
787
+ plot_height,
788
+ )
789
+ const y2_delta = pixels_to_data_delta(
790
+ evt.deltaY * sensitivity,
791
+ ranges.current.y2,
792
+ plot_height,
793
+ )
794
+
400
795
  if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
401
- ranges.current.x = pan_range(ranges.current.x, x_delta);
796
+ ranges.current.x = pan_range(ranges.current.x, x_delta)
797
+ ranges.current.x2 = pan_range(ranges.current.x2, x2_delta)
798
+ } else {
799
+ ranges.current.y = pan_range(ranges.current.y, y_delta)
800
+ ranges.current.y2 = pan_range(ranges.current.y2, y2_delta)
402
801
  }
403
- else {
404
- ranges.current.y = pan_range(ranges.current.y, y_delta);
405
- ranges.current.y2 = pan_range(ranges.current.y2, y2_delta);
406
- }
407
- }
408
- // Touch handlers for pinch-zoom and two-finger pan
409
- function handle_touch_start(evt) {
410
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false;
411
- if (!touch_enabled || evt.touches.length !== 2)
412
- return;
413
- evt.preventDefault();
414
- const touches = Array.from(evt.touches);
802
+ }
803
+
804
+ // Touch handlers for pinch-zoom and two-finger pan
805
+ function handle_touch_start(evt: TouchEvent) {
806
+ const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
807
+ if (!touch_enabled || evt.touches.length !== 2) return
808
+
809
+ evt.preventDefault()
810
+ const touches = Array.from(evt.touches)
415
811
  touch_state = {
416
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
417
- initial_x_range: [...ranges.current.x],
418
- initial_y_range: [...ranges.current.y],
419
- initial_y2_range: [...ranges.current.y2],
420
- };
421
- }
422
- function handle_touch_move(evt) {
423
- if (!touch_state || evt.touches.length !== 2)
424
- return;
425
- evt.preventDefault();
426
- const [t1, t2] = Array.from(evt.touches);
427
- const [s1, s2] = touch_state.start_touches;
812
+ start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
813
+ initial_x_range: [...ranges.current.x] as [number, number],
814
+ initial_x2_range: [...ranges.current.x2] as [number, number],
815
+ initial_y_range: [...ranges.current.y] as [number, number],
816
+ initial_y2_range: [...ranges.current.y2] as [number, number],
817
+ }
818
+ }
819
+
820
+ function handle_touch_move(evt: TouchEvent) {
821
+ if (!touch_state || evt.touches.length !== 2) return
822
+ evt.preventDefault()
823
+
824
+ const [t1, t2] = Array.from(evt.touches)
825
+ const [s1, s2] = touch_state.start_touches
826
+
428
827
  // Calculate center movement for pan
429
- const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 };
828
+ const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
430
829
  const curr_center = {
431
- x: (t1.clientX + t2.clientX) / 2,
432
- y: (t1.clientY + t2.clientY) / 2,
433
- };
434
- const dx = curr_center.x - start_center.x;
435
- const dy = curr_center.y - start_center.y;
830
+ x: (t1.clientX + t2.clientX) / 2,
831
+ y: (t1.clientY + t2.clientY) / 2,
832
+ }
833
+ const dx = curr_center.x - start_center.x
834
+ const dy = curr_center.y - start_center.y
835
+
436
836
  // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
437
- const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y);
837
+ const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
438
838
  // Guard against zero-distance pinch to avoid Infinity scale
439
- if (start_dist < Number.EPSILON)
440
- return;
441
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
442
- const scale = curr_dist / start_dist;
839
+ if (start_dist < Number.EPSILON) return
840
+ const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
841
+ const scale = curr_dist / start_dist
842
+
443
843
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
444
- const plot_width = Math.max(1, width - pad.l - pad.r);
445
- const plot_height = Math.max(1, height - pad.t - pad.b);
844
+ const plot_width = Math.max(1, width - pad.l - pad.r)
845
+ const plot_height = Math.max(1, height - pad.t - pad.b)
846
+
446
847
  // If scale changed significantly, treat as pinch-zoom
447
848
  // Also guard against scale being too small to avoid division by zero
448
849
  if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
449
- // Pinch zoom centered on gesture center
450
- // Divide by scale so spread (scale > 1) = smaller span (zoom in)
451
- const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0];
452
- const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0];
453
- const y2_span = touch_state.initial_y2_range[1] -
454
- touch_state.initial_y2_range[0];
455
- const x_center = (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2;
456
- const y_center = (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2;
457
- const y2_center = (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2;
458
- ranges.current.x = [
459
- x_center - x_span / scale / 2,
460
- x_center + x_span / scale / 2,
461
- ];
462
- ranges.current.y = [
463
- y_center - y_span / scale / 2,
464
- y_center + y_span / scale / 2,
465
- ];
466
- ranges.current.y2 = [
467
- y2_center - y2_span / scale / 2,
468
- y2_center + y2_span / scale / 2,
469
- ];
470
- }
471
- else {
472
- // Pan
473
- const x_delta = pixels_to_data_delta(-dx, touch_state.initial_x_range, plot_width);
474
- const y_delta = pixels_to_data_delta(dy, touch_state.initial_y_range, plot_height);
475
- const y2_delta = pixels_to_data_delta(dy, touch_state.initial_y2_range, plot_height);
476
- ranges.current.x = pan_range(touch_state.initial_x_range, x_delta);
477
- ranges.current.y = pan_range(touch_state.initial_y_range, y_delta);
478
- ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta);
850
+ // Pinch zoom centered on gesture center
851
+ // Divide by scale so spread (scale > 1) = smaller span (zoom in)
852
+ const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
853
+ const x2_span = touch_state.initial_x2_range[1] -
854
+ touch_state.initial_x2_range[0]
855
+ const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
856
+ const y2_span = touch_state.initial_y2_range[1] -
857
+ touch_state.initial_y2_range[0]
858
+ const x_center =
859
+ (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
860
+ const x2_center =
861
+ (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
862
+ const y_center =
863
+ (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
864
+ const y2_center =
865
+ (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
866
+
867
+ ranges.current.x = [
868
+ x_center - x_span / scale / 2,
869
+ x_center + x_span / scale / 2,
870
+ ]
871
+ ranges.current.x2 = [
872
+ x2_center - x2_span / scale / 2,
873
+ x2_center + x2_span / scale / 2,
874
+ ]
875
+ ranges.current.y = [
876
+ y_center - y_span / scale / 2,
877
+ y_center + y_span / scale / 2,
878
+ ]
879
+ ranges.current.y2 = [
880
+ y2_center - y2_span / scale / 2,
881
+ y2_center + y2_span / scale / 2,
882
+ ]
883
+ } else {
884
+ // Pan
885
+ const x_delta = pixels_to_data_delta(
886
+ -dx,
887
+ touch_state.initial_x_range,
888
+ plot_width,
889
+ )
890
+ const x2_delta = pixels_to_data_delta(
891
+ -dx,
892
+ touch_state.initial_x2_range,
893
+ plot_width,
894
+ )
895
+ const y_delta = pixels_to_data_delta(
896
+ dy,
897
+ touch_state.initial_y_range,
898
+ plot_height,
899
+ )
900
+ const y2_delta = pixels_to_data_delta(
901
+ dy,
902
+ touch_state.initial_y2_range,
903
+ plot_height,
904
+ )
905
+ ranges.current.x = pan_range(touch_state.initial_x_range, x_delta)
906
+ ranges.current.x2 = pan_range(touch_state.initial_x2_range, x2_delta)
907
+ ranges.current.y = pan_range(touch_state.initial_y_range, y_delta)
908
+ ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta)
479
909
  }
480
- }
481
- function handle_touch_end() {
482
- touch_state = null;
483
- }
484
- function handle_double_click() {
910
+ }
911
+
912
+ function handle_touch_end() {
913
+ touch_state = null
914
+ }
915
+
916
+ function handle_double_click() {
485
917
  // Reset zoom to initial ranges (undo any pan/zoom)
486
- ranges.current.x = [...ranges.initial.x];
487
- ranges.current.y = [...ranges.initial.y];
488
- ranges.current.y2 = [...ranges.initial.y2];
918
+ ranges.current.x = [...ranges.initial.x] as [number, number]
919
+ ranges.current.x2 = [...ranges.initial.x2] as [number, number]
920
+ ranges.current.y = [...ranges.initial.y] as [number, number]
921
+ ranges.current.y2 = [...ranges.initial.y2] as [number, number]
489
922
  // Also reset axis props so future data changes recalculate auto ranges
490
- x_axis = { ...x_axis, range: [null, null] };
491
- y_axis = { ...y_axis, range: [null, null] };
492
- y2_axis = { ...y2_axis, range: [null, null] };
493
- }
494
- function handle_mouse_move(evt, value, count, property, active_y_axis = `y1`, series_idx = 0) {
495
- hovered = true;
923
+ x_axis = { ...x_axis, range: [null, null] }
924
+ x2_axis = { ...x2_axis, range: [null, null] }
925
+ y_axis = { ...y_axis, range: [null, null] }
926
+ y2_axis = { ...y2_axis, range: [null, null] }
927
+ }
928
+
929
+ function handle_mouse_move(
930
+ evt: MouseEvent,
931
+ value: number,
932
+ count: number,
933
+ property: string,
934
+ active_y_axis: `y1` | `y2` = `y1`,
935
+ series_idx: number = 0,
936
+ active_x_axis: `x1` | `x2` = `x1`,
937
+ ) {
938
+ hovered = true
496
939
  hover_info = {
497
- value,
498
- count,
499
- property,
500
- active_y_axis,
501
- x: value,
502
- y: count,
503
- series_idx,
504
- metadata: null,
505
- label: property,
506
- x_axis,
507
- y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
508
- y2_axis,
509
- };
510
- change({ value, count, property });
511
- on_bar_hover?.({ value, count, property, event: evt });
512
- }
513
- function toggle_series_visibility(series_idx) {
940
+ value,
941
+ count,
942
+ property,
943
+ active_y_axis,
944
+ active_x_axis,
945
+ x: value,
946
+ y: count,
947
+ series_idx,
948
+ metadata: null,
949
+ label: property,
950
+ x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
951
+ x2_axis,
952
+ y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
953
+ y2_axis,
954
+ }
955
+ change({ value, count, property })
956
+ on_bar_hover?.({ value, count, property, event: evt })
957
+ }
958
+
959
+ function toggle_series_visibility(series_idx: number) {
514
960
  if (series_idx >= 0 && series_idx < series.length) {
515
- // Toggle series visibility
516
- series = series.map((srs, idx) => {
517
- if (idx === series_idx)
518
- return { ...srs, visible: !(srs.visible ?? true) };
519
- return srs;
520
- });
521
- (legend?.on_toggle || on_series_toggle)(series_idx);
961
+ // Toggle series visibility
962
+ series = series.map((srs: DataSeries, idx: number) => {
963
+ if (idx === series_idx) return { ...srs, visible: !(srs.visible ?? true) }
964
+ return srs
965
+ })
966
+ ;(legend?.on_toggle || on_series_toggle)(series_idx)
522
967
  }
523
- }
524
- // Set theme-aware background when entering fullscreen
525
- $effect(() => {
526
- set_fullscreen_bg(wrapper, fullscreen, `--histogram-fullscreen-bg`);
527
- });
528
- // State accessors for shared axis change handler
529
- const axis_state = {
530
- get_axis: (axis) => (axis === `x` ? x_axis : axis === `y` ? y_axis : y2_axis),
968
+ }
969
+
970
+ // Set theme-aware background when entering fullscreen
971
+ $effect(() => {
972
+ set_fullscreen_bg(wrapper, fullscreen, `--histogram-fullscreen-bg`)
973
+ })
974
+
975
+ // State accessors for shared axis change handler
976
+ const axis_state: AxisChangeState<DataSeries> = {
977
+ get_axis: (axis) => {
978
+ if (axis === `x`) return x_axis
979
+ if (axis === `x2`) return x2_axis
980
+ if (axis === `y`) return y_axis
981
+ return y2_axis
982
+ },
531
983
  set_axis: (axis, config) => {
532
- // Spread into existing state to preserve merged type structure
533
- if (axis === `x`)
534
- x_axis = { ...x_axis, ...config };
535
- else if (axis === `y`)
536
- y_axis = { ...y_axis, ...config };
537
- else
538
- y2_axis = { ...y2_axis, ...config };
984
+ // Spread into existing state to preserve merged type structure
985
+ if (axis === `x`) x_axis = { ...x_axis, ...config }
986
+ else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
987
+ else if (axis === `y`) y_axis = { ...y_axis, ...config }
988
+ else y2_axis = { ...y2_axis, ...config }
539
989
  },
540
990
  get_series: () => series,
541
991
  set_series: (new_series) => (series = new_series),
542
992
  get_loading: () => axis_loading,
543
993
  set_loading: (axis) => (axis_loading = axis),
544
- };
545
- // Create shared handler bound to this component's state
546
- // Using $derived so handler updates when callback props change
547
- const handle_axis_change = $derived(create_axis_change_handler(axis_state, data_loader, on_axis_change, on_error));
548
- let auto_load_attempted = false; // prevent infinite retries on failure
549
- // Auto-load data if series is empty but options exist (runs once)
550
- $effect(() => {
994
+ }
995
+
996
+ // Create shared handler bound to this component's state
997
+ // Using $derived so handler updates when callback props change
998
+ const handle_axis_change = $derived(create_axis_change_handler(
999
+ axis_state,
1000
+ data_loader,
1001
+ on_axis_change,
1002
+ on_error,
1003
+ ))
1004
+
1005
+ let auto_load_attempted = false // prevent infinite retries on failure
1006
+
1007
+ // Auto-load data if series is empty but options exist (runs once)
1008
+ $effect(() => {
551
1009
  if (series.length === 0 && data_loader && !auto_load_attempted) {
552
- // Check x-axis first, then y-axis
553
- if (x_axis.options?.length) {
554
- auto_load_attempted = true;
555
- const first_key = x_axis.selected_key ?? x_axis.options[0].key;
556
- handle_axis_change(`x`, first_key).catch(() => { });
557
- }
558
- else if (y_axis.options?.length) {
559
- auto_load_attempted = true;
560
- const first_key = y_axis.selected_key ?? y_axis.options[0].key;
561
- handle_axis_change(`y`, first_key).catch(() => { });
562
- }
1010
+ // Check x-axis first, then y-axis
1011
+ if (x_axis.options?.length) {
1012
+ auto_load_attempted = true
1013
+ const first_key = x_axis.selected_key ?? x_axis.options[0].key
1014
+ handle_axis_change(`x`, first_key).catch(() => {})
1015
+ } else if (y_axis.options?.length) {
1016
+ auto_load_attempted = true
1017
+ const first_key = y_axis.selected_key ?? y_axis.options[0].key
1018
+ handle_axis_change(`y`, first_key).catch(() => {})
1019
+ }
563
1020
  }
564
- });
1021
+ })
565
1022
  </script>
566
1023
 
567
1024
  {#snippet ref_lines_layer(lines: IndexedRefLine[])}
@@ -569,11 +1026,12 @@ $effect(() => {
569
1026
  <ReferenceLine
570
1027
  ref_line={line}
571
1028
  line_idx={line.idx}
572
- x_min={ranges.current.x[0]}
573
- x_max={ranges.current.x[1]}
1029
+ x_min={line.x_axis === `x2` ? ranges.current.x2[0] : ranges.current.x[0]}
1030
+ x_max={line.x_axis === `x2` ? ranges.current.x2[1] : ranges.current.x[1]}
574
1031
  y_min={line.y_axis === `y2` ? ranges.current.y2[0] : ranges.current.y[0]}
575
1032
  y_max={line.y_axis === `y2` ? ranges.current.y2[1] : ranges.current.y[1]}
576
1033
  x_scale={scales.x}
1034
+ x2_scale={scales.x2}
577
1035
  y_scale={scales.y}
578
1036
  y2_scale={scales.y2}
579
1037
  {clip_path_id}
@@ -625,6 +1083,9 @@ $effect(() => {
625
1083
  <svg
626
1084
  bind:this={svg_element}
627
1085
  role="application"
1086
+ aria-label={rest[`aria-label`] ??
1087
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1088
+ `Histogram`)}
628
1089
  tabindex="0"
629
1090
  onfocusin={() => (is_focused = true)}
630
1091
  onfocusout={() => (is_focused = false)}
@@ -670,79 +1131,33 @@ $effect(() => {
670
1131
  <!-- Reference lines: below grid (must render first to appear behind grid) -->
671
1132
  {@render ref_lines_layer(ref_lines_by_z.below_grid)}
672
1133
 
673
- <!-- Zoom Selection Rectangle -->
674
- {#if drag_state.start && drag_state.current && isFinite(drag_state.start.x) &&
675
- isFinite(drag_state.start.y) && isFinite(drag_state.current.x) &&
676
- isFinite(drag_state.current.y)}
677
- {@const x = Math.min(drag_state.start.x, drag_state.current.x)}
678
- {@const y = Math.min(drag_state.start.y, drag_state.current.y)}
679
- {@const rect_width = Math.abs(drag_state.start.x - drag_state.current.x)}
680
- {@const rect_height = Math.abs(drag_state.start.y - drag_state.current.y)}
681
- <rect class="zoom-rect" {x} {y} width={rect_width} height={rect_height} />
682
- {/if}
1134
+ <ZoomRect start={drag_state.start} current={drag_state.current} />
1135
+
1136
+ <ZeroLines
1137
+ {display}
1138
+ x_scale_fn={scales.x}
1139
+ x2_scale_fn={scales.x2}
1140
+ y_scale_fn={scales.y}
1141
+ y2_scale_fn={scales.y2}
1142
+ x_range={ranges.current.x}
1143
+ x2_range={ranges.current.x2}
1144
+ y_range={ranges.current.y}
1145
+ y2_range={ranges.current.y2}
1146
+ x_scale_type={final_x_axis.scale_type}
1147
+ x2_scale_type={final_x2_axis.scale_type}
1148
+ y_scale_type={final_y_axis.scale_type}
1149
+ y2_scale_type={final_y2_axis.scale_type}
1150
+ has_x2={x2_series.length > 0}
1151
+ has_y2={y2_series.length > 0}
1152
+ {width}
1153
+ {height}
1154
+ {pad}
1155
+ />
683
1156
 
684
1157
  <!-- Reference lines: below lines -->
685
1158
  {@render ref_lines_layer(ref_lines_by_z.below_lines)}
686
1159
 
687
- <!-- Histogram bars (rendered before axes so tick labels appear on top) -->
688
- {#each histogram_data as
689
- { id, bins, color, label, y_scale, y_axis },
690
- series_idx
691
- (id ?? series_idx)
692
- }
693
- <g class="histogram-series" data-series-idx={series_idx}>
694
- {#each bins as bin, bin_idx (bin_idx)}
695
- {@const bar_x = scales.x(bin.x0!)}
696
- {@const bar_width = Math.max(1, Math.abs(scales.x(bin.x1!) - bar_x))}
697
- {@const bar_height = Math.max(0, (height - pad.b) - y_scale(bin.length))}
698
- {@const bar_y = y_scale(bin.length)}
699
- {@const value = (bin.x0! + bin.x1!) / 2}
700
- {#if bar_height > 0}
701
- <path
702
- d={bar_path(
703
- bar_x,
704
- bar_y,
705
- bar_width,
706
- bar_height,
707
- Math.min(final_bar.border_radius ?? 0, bar_width / 2, bar_height / 2),
708
- )}
709
- fill={color}
710
- opacity={final_bar.opacity}
711
- stroke={final_bar.stroke_color}
712
- stroke-opacity={final_bar.stroke_opacity}
713
- stroke-width={final_bar.stroke_width}
714
- role="button"
715
- tabindex="0"
716
- onmousemove={(evt) =>
717
- handle_mouse_move(
718
- evt,
719
- value,
720
- bin.length,
721
- label,
722
- (y_axis ?? `y1`) as `y1` | `y2`,
723
- series_idx,
724
- )}
725
- onmouseleave={() => {
726
- hover_info = null
727
- change(null)
728
- on_bar_hover?.(null)
729
- }}
730
- onclick={(event) =>
731
- on_bar_click?.({ value, count: bin.length, property: label, event })}
732
- onkeydown={(event: KeyboardEvent) => {
733
- if ([`Enter`, ` `].includes(event.key)) {
734
- event.preventDefault()
735
- on_bar_click?.({ value, count: bin.length, property: label, event })
736
- }
737
- }}
738
- style:cursor={on_bar_click ? `pointer` : undefined}
739
- />
740
- {/if}
741
- {/each}
742
- </g>
743
- {/each}
744
-
745
- <!-- Reference lines: below points (after bars, before axes/labels) -->
1160
+ <!-- Reference lines: below points -->
746
1161
  {@render ref_lines_layer(ref_lines_by_z.below_points)}
747
1162
 
748
1163
  <!-- X-axis -->
@@ -761,7 +1176,7 @@ $effect(() => {
761
1176
  {@const inside = final_x_axis.tick?.label?.inside ?? false}
762
1177
  {@const shift_x = final_x_axis.tick?.label?.shift?.x ?? 0}
763
1178
  {@const shift_y = final_x_axis.tick?.label?.shift?.y ?? 0}
764
- {@const base_y = inside ? -8 : 18}
1179
+ {@const base_y = inside ? -8 : 8}
765
1180
  {@const text_y = base_y + shift_y}
766
1181
  {@const dominant_baseline = inside ? `auto` : `hanging`}
767
1182
  <g class="tick" transform="translate({tick_x}, {height - pad.b})">
@@ -793,31 +1208,87 @@ $effect(() => {
793
1208
  </g>
794
1209
  {/each}
795
1210
  {#if final_x_axis.label || x_axis.options?.length}
796
- <foreignObject
797
- x={(pad.l + width - pad.r) / 2 + (final_x_axis.label_shift?.x ?? 0) -
798
- AXIS_LABEL_CONTAINER.x_offset}
799
- y={height - 10 + (final_x_axis.label_shift?.y ?? 0) -
800
- AXIS_LABEL_CONTAINER.y_offset}
801
- width={AXIS_LABEL_CONTAINER.width}
802
- height={AXIS_LABEL_CONTAINER.height}
803
- style="overflow: visible; pointer-events: none"
804
- >
805
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
806
- <InteractiveAxisLabel
807
- label={final_x_axis.label ?? ``}
808
- options={x_axis.options}
809
- selected_key={x_axis.selected_key}
810
- loading={axis_loading === `x`}
811
- axis_type="x"
812
- color={final_x_axis.color}
813
- on_select={(key) => handle_axis_change(`x`, key)}
814
- class="axis-label x-label"
815
- />
816
- </div>
817
- </foreignObject>
1211
+ {@const { label_shift, label = ``, options, selected_key, color } = final_x_axis}
1212
+ <AxisLabel
1213
+ x={(pad.l + width - pad.r) / 2 + (label_shift?.x ?? 0)}
1214
+ y={height - 10 + (label_shift?.y ?? 0)}
1215
+ {label}
1216
+ {options}
1217
+ {selected_key}
1218
+ loading={axis_loading === `x`}
1219
+ axis_type="x"
1220
+ {color}
1221
+ on_select={(key) => handle_axis_change(`x`, key)}
1222
+ />
818
1223
  {/if}
819
1224
  </g>
820
1225
 
1226
+ <!-- X2-axis (Top) -->
1227
+ {#if x2_series.length > 0}
1228
+ <g class="x2-axis">
1229
+ <line
1230
+ x1={pad.l}
1231
+ x2={width - pad.r}
1232
+ y1={pad.t}
1233
+ y2={pad.t}
1234
+ stroke={final_x2_axis.color || `var(--border-color, gray)`}
1235
+ stroke-width="1"
1236
+ />
1237
+ {#each ticks.x2 as tick (tick)}
1238
+ {@const tick_x = scales.x2(tick as number)}
1239
+ {@const custom_label = get_tick_label(tick as number, final_x2_axis.ticks)}
1240
+ {@const inside = final_x2_axis.tick?.label?.inside ?? false}
1241
+ {@const shift_x = final_x2_axis.tick?.label?.shift?.x ?? 0}
1242
+ {@const shift_y = final_x2_axis.tick?.label?.shift?.y ?? 0}
1243
+ {@const base_y = inside ? 8 : -20}
1244
+ {@const text_y = base_y + shift_y}
1245
+ {@const dominant_baseline = inside ? `hanging` : `auto`}
1246
+ <g class="tick" transform="translate({tick_x}, {pad.t})">
1247
+ {#if display.x2_grid}
1248
+ <line
1249
+ y1="0"
1250
+ y2={height - pad.b - pad.t}
1251
+ stroke="var(--border-color, gray)"
1252
+ stroke-dasharray="4"
1253
+ stroke-width="1"
1254
+ {...final_x2_axis.grid_style ?? {}}
1255
+ />
1256
+ {/if}
1257
+ <line
1258
+ y1={inside ? 0 : -5}
1259
+ y2={inside ? 5 : 0}
1260
+ stroke={final_x2_axis.color || `var(--border-color, gray)`}
1261
+ stroke-width="1"
1262
+ />
1263
+ <text
1264
+ x={shift_x}
1265
+ y={text_y}
1266
+ text-anchor="middle"
1267
+ dominant-baseline={dominant_baseline}
1268
+ fill={final_x2_axis.color || `var(--text-color)`}
1269
+ >
1270
+ {custom_label ?? format_value(tick, final_x2_axis.format)}
1271
+ </text>
1272
+ </g>
1273
+ {/each}
1274
+ {#if final_x2_axis.label || x2_axis.options?.length}
1275
+ {@const { label_shift, label = ``, options, selected_key, color } =
1276
+ final_x2_axis}
1277
+ <AxisLabel
1278
+ x={(pad.l + width - pad.r) / 2 + (label_shift?.x ?? 0)}
1279
+ y={Math.max(12, pad.t - (label_shift?.y ?? 40))}
1280
+ {label}
1281
+ {options}
1282
+ {selected_key}
1283
+ loading={axis_loading === `x2`}
1284
+ axis_type="x2"
1285
+ {color}
1286
+ on_select={(key) => handle_axis_change(`x2`, key)}
1287
+ />
1288
+ {/if}
1289
+ </g>
1290
+ {/if}
1291
+
821
1292
  <!-- Y-axis -->
822
1293
  <g class="y-axis">
823
1294
  <line
@@ -866,41 +1337,25 @@ $effect(() => {
866
1337
  </g>
867
1338
  {/each}
868
1339
  {#if final_y_axis.label || y_axis.options?.length}
869
- {@const max_y_tick_width = Math.max(
870
- 0,
871
- ...ticks.y.map((tick) =>
872
- measure_text_width(
873
- format_value(tick, final_y_axis.format),
874
- `12px sans-serif`,
875
- )
876
- ),
877
- )}
878
- {@const shift_x = final_y_axis.label_shift?.x ?? 0}
879
- {@const shift_y = final_y_axis.label_shift?.y ?? 0}
880
- {@const y_label_x = Math.max(12, pad.l - max_y_tick_width - LABEL_GAP_DEFAULT) +
881
- shift_x}
882
- {@const y_label_y = pad.t + (height - pad.t - pad.b) / 2 + shift_y}
883
- <foreignObject
884
- x={y_label_x - AXIS_LABEL_CONTAINER.x_offset}
885
- y={y_label_y - AXIS_LABEL_CONTAINER.y_offset}
886
- width={AXIS_LABEL_CONTAINER.width}
887
- height={AXIS_LABEL_CONTAINER.height}
888
- style="overflow: visible; pointer-events: none"
889
- transform="rotate(-90, {y_label_x}, {y_label_y})"
890
- >
891
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
892
- <InteractiveAxisLabel
893
- label={final_y_axis.label ?? ``}
894
- options={y_axis.options}
895
- selected_key={y_axis.selected_key}
896
- loading={axis_loading === `y`}
897
- axis_type="y"
898
- color={final_y_axis.color}
899
- on_select={(key) => handle_axis_change(`y`, key)}
900
- class="axis-label y-label"
901
- />
902
- </div>
903
- </foreignObject>
1340
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
1341
+ final_y_axis}
1342
+ {@const y_inside = tick?.label?.inside ?? false}
1343
+ <AxisLabel
1344
+ x={Math.max(
1345
+ 12,
1346
+ pad.l - (y_inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
1347
+ ) +
1348
+ (label_shift?.x ?? 0)}
1349
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
1350
+ rotate
1351
+ {label}
1352
+ {options}
1353
+ {selected_key}
1354
+ loading={axis_loading === `y`}
1355
+ axis_type="y"
1356
+ {color}
1357
+ on_select={(key) => handle_axis_change(`y`, key)}
1358
+ />
904
1359
  {/if}
905
1360
  </g>
906
1361
 
@@ -952,80 +1407,86 @@ $effect(() => {
952
1407
  </g>
953
1408
  {/each}
954
1409
  {#if final_y2_axis.label || y2_axis.options?.length}
955
- {@const max_y2_tick_width = Math.max(
956
- 0,
957
- ...ticks.y2.map((tick) =>
958
- measure_text_width(
959
- format_value(tick, final_y2_axis.format),
960
- `12px sans-serif`,
961
- )
962
- ),
963
- )}
964
- {@const shift_x = final_y2_axis.label_shift?.x ?? 0}
965
- {@const shift_y = final_y2_axis.label_shift?.y ?? 0}
966
- {@const inside = final_y2_axis.tick?.label?.inside ?? false}
967
- {@const tick_shift = inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8}
968
- {@const tick_width_contribution = inside ? 0 : max_y2_tick_width}
969
- {@const y2_label_x = width - pad.r + tick_shift + tick_width_contribution +
970
- LABEL_GAP_DEFAULT +
971
- shift_x}
972
- {@const y2_label_y = pad.t + (height - pad.t - pad.b) / 2 + shift_y}
973
- <foreignObject
974
- x={y2_label_x - AXIS_LABEL_CONTAINER.x_offset}
975
- y={y2_label_y - AXIS_LABEL_CONTAINER.y_offset}
976
- width={AXIS_LABEL_CONTAINER.width}
977
- height={AXIS_LABEL_CONTAINER.height}
978
- style="overflow: visible; pointer-events: none"
979
- transform="rotate(-90, {y2_label_x}, {y2_label_y})"
980
- >
981
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
982
- <InteractiveAxisLabel
983
- label={final_y2_axis.label ?? ``}
984
- options={y2_axis.options}
985
- selected_key={y2_axis.selected_key}
986
- loading={axis_loading === `y2`}
987
- axis_type="y2"
988
- color={final_y2_axis.color}
989
- on_select={(key) => handle_axis_change(`y2`, key)}
990
- class="axis-label y2-label"
991
- />
992
- </div>
993
- </foreignObject>
1410
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
1411
+ final_y2_axis}
1412
+ {@const inside = tick?.label?.inside ?? false}
1413
+ {@const tick_shift = inside ? 0 : (tick?.label?.shift?.x ?? 0) + 8}
1414
+ {@const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max}
1415
+ <AxisLabel
1416
+ x={width - pad.r + tick_shift + tick_width_contribution +
1417
+ LABEL_GAP_DEFAULT + (label_shift?.x ?? 0)}
1418
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
1419
+ rotate
1420
+ {label}
1421
+ {options}
1422
+ {selected_key}
1423
+ loading={axis_loading === `y2`}
1424
+ axis_type="y2"
1425
+ {color}
1426
+ on_select={(key) => handle_axis_change(`y2`, key)}
1427
+ />
994
1428
  {/if}
995
1429
  </g>
996
1430
  {/if}
997
1431
 
998
- <!-- Zero lines (shown for linear and arcsinh scales, not log) -->
999
- {#if display.x_zero_line &&
1000
- get_scale_type_name(final_x_axis.scale_type) !== `log` &&
1001
- ranges.current.x[0] <= 0 && ranges.current.x[1] >= 0}
1002
- {@const x0 = scales.x(0)}
1003
- {#if isFinite(x0)}
1004
- <line class="zero-line" x1={x0} x2={x0} y1={pad.t} y2={height - pad.b} />
1005
- {/if}
1006
- {/if}
1007
- {#if display.y_zero_line &&
1008
- get_scale_type_name(final_y_axis.scale_type) !== `log` &&
1009
- ranges.current.y[0] <= 0 && ranges.current.y[1] >= 0}
1010
- {@const zero_y = scales.y(0)}
1011
- {#if isFinite(zero_y)}
1012
- <line class="zero-line" x1={pad.l} x2={width - pad.r} y1={zero_y} y2={zero_y} />
1013
- {/if}
1014
- {/if}
1015
- {#if display.y_zero_line && y2_series.length > 0 &&
1016
- get_scale_type_name(final_y2_axis.scale_type) !== `log` &&
1017
- ranges.current.y2[0] <= 0 && ranges.current.y2[1] >= 0}
1018
- {@const zero_y2 = scales.y2(0)}
1019
- {#if isFinite(zero_y2)}
1020
- <line
1021
- class="zero-line"
1022
- x1={pad.l}
1023
- x2={width - pad.r}
1024
- y1={zero_y2}
1025
- y2={zero_y2}
1026
- />
1027
- {/if}
1028
- {/if}
1432
+ <!-- Histogram bars (rendered after axes so bars appear above grid lines) -->
1433
+ {#each histogram_data as
1434
+ { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis },
1435
+ series_idx
1436
+ (id ?? series_idx)
1437
+ }
1438
+ <g class="histogram-series" data-series-idx={series_idx}>
1439
+ {#each bins as bin, bin_idx (bin_idx)}
1440
+ {@const bar_x = x_scale(bin.x0!)}
1441
+ {@const bar_width = Math.max(1, Math.abs(x_scale(bin.x1!) - bar_x))}
1442
+ {@const bar_height = Math.max(0, (height - pad.b) - y_scale(bin.length))}
1443
+ {@const bar_y = y_scale(bin.length)}
1444
+ {@const value = (bin.x0! + bin.x1!) / 2}
1445
+ {#if bar_height > 0}
1446
+ <path
1447
+ d={bar_path(
1448
+ bar_x,
1449
+ bar_y,
1450
+ bar_width,
1451
+ bar_height,
1452
+ Math.min(final_bar.border_radius ?? 0, bar_width / 2, bar_height / 2),
1453
+ )}
1454
+ fill={color}
1455
+ opacity={final_bar.opacity}
1456
+ stroke={final_bar.stroke_color}
1457
+ stroke-opacity={final_bar.stroke_opacity}
1458
+ stroke-width={final_bar.stroke_width}
1459
+ role="button"
1460
+ tabindex="0"
1461
+ onmousemove={(evt) =>
1462
+ handle_mouse_move(
1463
+ evt,
1464
+ value,
1465
+ bin.length,
1466
+ label,
1467
+ (y_axis ?? `y1`) as `y1` | `y2`,
1468
+ series_idx,
1469
+ (srs_x_axis ?? `x1`) as `x1` | `x2`,
1470
+ )}
1471
+ onmouseleave={() => {
1472
+ hover_info = null
1473
+ change(null)
1474
+ on_bar_hover?.(null)
1475
+ }}
1476
+ onclick={(event) =>
1477
+ on_bar_click?.({ value, count: bin.length, property: label, event })}
1478
+ onkeydown={(event: KeyboardEvent) => {
1479
+ if ([`Enter`, ` `].includes(event.key)) {
1480
+ event.preventDefault()
1481
+ on_bar_click?.({ value, count: bin.length, property: label, event })
1482
+ }
1483
+ }}
1484
+ style:cursor={on_bar_click ? `pointer` : undefined}
1485
+ />
1486
+ {/if}
1487
+ {/each}
1488
+ </g>
1489
+ {/each}
1029
1490
 
1030
1491
  <!-- Reference lines: above all -->
1031
1492
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
@@ -1033,8 +1494,8 @@ $effect(() => {
1033
1494
 
1034
1495
  <!-- Tooltip (outside SVG for proper HTML rendering) -->
1035
1496
  {#if hover_info}
1036
- {@const { value, count, property, active_y_axis } = hover_info}
1037
- {@const tooltip_x = scales.x(value)}
1497
+ {@const { value, count, property, active_y_axis, active_x_axis } = hover_info}
1498
+ {@const tooltip_x = (active_x_axis === `x2` ? scales.x2 : scales.x)(value)}
1038
1499
  {@const tooltip_y = (active_y_axis === `y2` ? scales.y2 : scales.y)(count)}
1039
1500
  {@const tooltip_pos = constrain_tooltip_position(
1040
1501
  tooltip_x,
@@ -1045,7 +1506,6 @@ $effect(() => {
1045
1506
  height,
1046
1507
  { offset_x: 5, offset_y: -10 },
1047
1508
  )}
1048
- {@const active_y_config = active_y_axis === `y2` ? final_y2_axis : final_y_axis}
1049
1509
  <PlotTooltip
1050
1510
  x={tooltip_pos.x}
1051
1511
  y={tooltip_pos.y}
@@ -1055,8 +1515,8 @@ $effect(() => {
1055
1515
  {#if tooltip}
1056
1516
  {@render tooltip({ ...hover_info, fullscreen })}
1057
1517
  {:else}
1058
- <div>Value: {format_value(value, final_x_axis.format || `.3~s`)}</div>
1059
- <div>Count: {format_value(count, active_y_config.format || `.3~s`)}</div>
1518
+ <div>Value: {format_value(value, hover_info.x_axis.format || `.3~s`)}</div>
1519
+ <div>Count: {format_value(count, hover_info.y_axis.format || `.3~s`)}</div>
1060
1520
  {#if mode === `overlay`}<div>{property}</div>{/if}
1061
1521
  {/if}
1062
1522
  </PlotTooltip>
@@ -1066,7 +1526,7 @@ $effect(() => {
1066
1526
  <HistogramControls
1067
1527
  toggle_props={{
1068
1528
  ...controls_toggle_props,
1069
- style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 36px); top: 4px; ${
1529
+ style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); ${
1070
1530
  controls_toggle_props?.style ?? ``
1071
1531
  }`,
1072
1532
  }}
@@ -1080,12 +1540,16 @@ $effect(() => {
1080
1540
  bind:display
1081
1541
  bind:bar
1082
1542
  bind:x_axis
1543
+ bind:x2_axis
1083
1544
  bind:y_axis
1084
1545
  bind:y2_axis
1085
1546
  auto_x_range={auto_ranges.x}
1547
+ auto_x2_range={auto_ranges.x2}
1086
1548
  auto_y_range={auto_ranges.y}
1087
1549
  auto_y2_range={auto_ranges.y2}
1088
1550
  {series}
1551
+ has_x2_points={x2_series.length > 0}
1552
+ has_y2_points={y2_series.length > 0}
1089
1553
  children={controls_extra}
1090
1554
  />
1091
1555
  {/if}
@@ -1178,7 +1642,7 @@ $effect(() => {
1178
1642
  font-weight: var(--histogram-font-weight);
1179
1643
  font-size: var(--histogram-font-size);
1180
1644
  }
1181
- g:is(.x-axis, .y-axis, .y2-axis) .tick text {
1645
+ g:is(.x-axis, .x2-axis, .y-axis, .y2-axis) .tick text {
1182
1646
  font-size: var(--tick-font-size, 0.8em); /* shrink tick labels */
1183
1647
  }
1184
1648
  .histogram-series path {
@@ -1187,15 +1651,4 @@ $effect(() => {
1187
1651
  .histogram-series path:hover {
1188
1652
  opacity: 1 !important;
1189
1653
  }
1190
- .zoom-rect {
1191
- fill: var(--histogram-zoom-rect-fill, rgba(100, 100, 255, 0.2));
1192
- stroke: var(--histogram-zoom-rect-stroke, rgba(100, 100, 255, 0.8));
1193
- stroke-width: var(--histogram-zoom-rect-stroke-width, 1);
1194
- pointer-events: none;
1195
- }
1196
- .zero-line {
1197
- stroke: var(--histogram-zero-line-color, light-dark(black, white));
1198
- stroke-width: var(--histogram-zero-line-width, 1);
1199
- opacity: var(--histogram-zero-line-opacity);
1200
- }
1201
1654
  </style>