matterviz 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/MillerIndexInput.svelte +60 -0
  4. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  5. package/dist/app.css +38 -2
  6. package/dist/brillouin/BrillouinZone.svelte +20 -62
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  9. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  10. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  17. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  18. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  19. package/dist/chempot-diagram/color.d.ts +10 -0
  20. package/dist/chempot-diagram/color.js +33 -0
  21. package/dist/chempot-diagram/compute.d.ts +38 -0
  22. package/dist/chempot-diagram/compute.js +650 -0
  23. package/dist/chempot-diagram/index.d.ts +5 -0
  24. package/dist/chempot-diagram/index.js +5 -0
  25. package/dist/chempot-diagram/pointer.d.ts +16 -0
  26. package/dist/chempot-diagram/pointer.js +40 -0
  27. package/dist/chempot-diagram/temperature.d.ts +15 -0
  28. package/dist/chempot-diagram/temperature.js +37 -0
  29. package/dist/chempot-diagram/types.d.ts +83 -0
  30. package/dist/chempot-diagram/types.js +27 -0
  31. package/dist/colors/index.d.ts +3 -1
  32. package/dist/colors/index.js +4 -0
  33. package/dist/composition/BarChart.svelte +13 -22
  34. package/dist/composition/BubbleChart.svelte +5 -3
  35. package/dist/composition/FormulaFilter.svelte +770 -90
  36. package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
  37. package/dist/composition/PieChart.svelte +43 -18
  38. package/dist/composition/PieChart.svelte.d.ts +1 -1
  39. package/dist/constants.d.ts +1 -0
  40. package/dist/constants.js +2 -0
  41. package/dist/convex-hull/ConvexHull.svelte +14 -1
  42. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull2D.svelte +14 -45
  44. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHull3D.svelte +396 -134
  46. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  47. package/dist/convex-hull/ConvexHull4D.svelte +93 -42
  48. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHullControls.svelte +94 -31
  50. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
  51. package/dist/convex-hull/ConvexHullStats.svelte +697 -128
  52. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  53. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  54. package/dist/convex-hull/GasPressureControls.svelte +72 -38
  55. package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
  56. package/dist/convex-hull/TemperatureSlider.svelte +46 -19
  57. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
  58. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  59. package/dist/convex-hull/demo-temperature.js +36 -0
  60. package/dist/convex-hull/gas-thermodynamics.js +16 -5
  61. package/dist/convex-hull/helpers.d.ts +7 -1
  62. package/dist/convex-hull/helpers.js +45 -15
  63. package/dist/convex-hull/index.d.ts +15 -1
  64. package/dist/convex-hull/index.js +1 -0
  65. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  66. package/dist/convex-hull/thermodynamics.js +106 -17
  67. package/dist/convex-hull/types.d.ts +7 -0
  68. package/dist/convex-hull/types.js +11 -0
  69. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  70. package/dist/element/BohrAtom.svelte +1 -1
  71. package/dist/element/data.js +2 -14
  72. package/dist/element/data.json.gz +0 -0
  73. package/dist/element/index.d.ts +1 -1
  74. package/dist/element/index.js +1 -0
  75. package/dist/element/types.d.ts +1 -0
  76. package/dist/fermi-surface/FermiSurface.svelte +21 -65
  77. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  80. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  81. package/dist/fermi-surface/compute.js +1 -21
  82. package/dist/fermi-surface/marching-cubes.d.ts +2 -13
  83. package/dist/fermi-surface/marching-cubes.js +2 -519
  84. package/dist/fermi-surface/parse.js +17 -23
  85. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  86. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  87. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  88. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  89. package/dist/heatmap-matrix/index.d.ts +53 -0
  90. package/dist/heatmap-matrix/index.js +100 -0
  91. package/dist/heatmap-matrix/shared.d.ts +2 -0
  92. package/dist/heatmap-matrix/shared.js +4 -0
  93. package/dist/icons.d.ts +119 -0
  94. package/dist/icons.js +119 -0
  95. package/dist/index.d.ts +6 -1
  96. package/dist/index.js +6 -1
  97. package/dist/io/export.js +15 -3
  98. package/dist/io/file-drop.d.ts +7 -0
  99. package/dist/io/file-drop.js +43 -0
  100. package/dist/io/index.d.ts +2 -2
  101. package/dist/io/index.js +2 -112
  102. package/dist/io/types.d.ts +1 -0
  103. package/dist/io/url-drop.d.ts +2 -0
  104. package/dist/io/url-drop.js +118 -0
  105. package/dist/isosurface/Isosurface.svelte +231 -0
  106. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  107. package/dist/isosurface/IsosurfaceControls.svelte +273 -0
  108. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  109. package/dist/isosurface/index.d.ts +5 -0
  110. package/dist/isosurface/index.js +6 -0
  111. package/dist/isosurface/parse.d.ts +6 -0
  112. package/dist/isosurface/parse.js +548 -0
  113. package/dist/isosurface/slice.d.ts +11 -0
  114. package/dist/isosurface/slice.js +145 -0
  115. package/dist/isosurface/types.d.ts +55 -0
  116. package/dist/isosurface/types.js +178 -0
  117. package/dist/labels.d.ts +2 -1
  118. package/dist/labels.js +1 -0
  119. package/dist/layout/InfoTag.svelte +62 -62
  120. package/dist/layout/SubpageGrid.svelte +74 -0
  121. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +226 -53
  125. package/dist/layout/json-tree/JsonTree.svelte +425 -51
  126. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  127. package/dist/layout/json-tree/JsonValue.svelte +218 -97
  128. package/dist/layout/json-tree/types.d.ts +27 -2
  129. package/dist/layout/json-tree/utils.d.ts +14 -1
  130. package/dist/layout/json-tree/utils.js +254 -0
  131. package/dist/marching-cubes.d.ts +14 -0
  132. package/dist/marching-cubes.js +519 -0
  133. package/dist/math.d.ts +8 -0
  134. package/dist/math.js +374 -7
  135. package/dist/overlays/ContextMenu.svelte +3 -2
  136. package/dist/overlays/DraggablePane.svelte +163 -58
  137. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  138. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  139. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  140. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  141. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  142. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  143. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  144. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  145. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  146. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  147. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  148. package/dist/phase-diagram/index.d.ts +2 -0
  149. package/dist/phase-diagram/index.js +2 -0
  150. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  151. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  152. package/dist/phase-diagram/types.d.ts +10 -0
  153. package/dist/phase-diagram/utils.d.ts +7 -4
  154. package/dist/phase-diagram/utils.js +149 -59
  155. package/dist/plot/AxisLabel.svelte +26 -0
  156. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  157. package/dist/plot/BarPlot.svelte +473 -228
  158. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  159. package/dist/plot/BarPlotControls.svelte +3 -2
  160. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  161. package/dist/plot/ColorBar.svelte +54 -54
  162. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  163. package/dist/plot/ElementScatter.svelte +4 -3
  164. package/dist/plot/FillArea.svelte +4 -1
  165. package/dist/plot/Histogram.svelte +320 -230
  166. package/dist/plot/Histogram.svelte.d.ts +2 -2
  167. package/dist/plot/HistogramControls.svelte +29 -10
  168. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  169. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  170. package/dist/plot/PlotControls.svelte +109 -27
  171. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  172. package/dist/plot/PlotLegend.svelte +1 -1
  173. package/dist/plot/PortalSelect.svelte +2 -1
  174. package/dist/plot/ReferenceLine.svelte +2 -1
  175. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  176. package/dist/plot/ReferencePlane.svelte +1 -3
  177. package/dist/plot/ScatterPlot.svelte +343 -209
  178. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  179. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  180. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  181. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  182. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  183. package/dist/plot/ScatterPlotControls.svelte +95 -55
  184. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  185. package/dist/plot/ZeroLines.svelte +44 -0
  186. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  187. package/dist/plot/ZoomRect.svelte +21 -0
  188. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  189. package/dist/plot/axis-utils.d.ts +1 -1
  190. package/dist/plot/data-cleaning.js +1 -5
  191. package/dist/plot/index.d.ts +6 -2
  192. package/dist/plot/index.js +6 -2
  193. package/dist/plot/interactions.d.ts +8 -10
  194. package/dist/plot/interactions.js +10 -19
  195. package/dist/plot/layout.d.ts +7 -1
  196. package/dist/plot/layout.js +12 -4
  197. package/dist/plot/reference-line.d.ts +4 -21
  198. package/dist/plot/reference-line.js +7 -81
  199. package/dist/plot/types.d.ts +42 -17
  200. package/dist/plot/types.js +10 -0
  201. package/dist/plot/utils/label-placement.js +14 -11
  202. package/dist/plot/utils.d.ts +1 -0
  203. package/dist/plot/utils.js +14 -0
  204. package/dist/rdf/RdfPlot.svelte +55 -66
  205. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  206. package/dist/rdf/index.d.ts +1 -1
  207. package/dist/rdf/index.js +1 -1
  208. package/dist/settings.d.ts +5 -0
  209. package/dist/settings.js +37 -3
  210. package/dist/spectral/Bands.svelte +515 -143
  211. package/dist/spectral/Bands.svelte.d.ts +22 -2
  212. package/dist/spectral/helpers.d.ts +23 -1
  213. package/dist/spectral/helpers.js +65 -9
  214. package/dist/spectral/types.d.ts +2 -0
  215. package/dist/structure/AtomLegend.svelte +31 -10
  216. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  217. package/dist/structure/CellSelect.svelte +92 -22
  218. package/dist/structure/Lattice.svelte +2 -0
  219. package/dist/structure/Structure.svelte +716 -173
  220. package/dist/structure/Structure.svelte.d.ts +7 -2
  221. package/dist/structure/StructureControls.svelte +26 -14
  222. package/dist/structure/StructureControls.svelte.d.ts +5 -1
  223. package/dist/structure/StructureInfoPane.svelte +7 -1
  224. package/dist/structure/StructureScene.svelte +386 -95
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -4
  226. package/dist/structure/atom-properties.d.ts +6 -2
  227. package/dist/structure/atom-properties.js +38 -25
  228. package/dist/structure/export.js +10 -7
  229. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  230. package/dist/structure/ferrox-wasm-types.js +0 -3
  231. package/dist/structure/ferrox-wasm.d.ts +3 -2
  232. package/dist/structure/ferrox-wasm.js +1 -2
  233. package/dist/structure/index.d.ts +7 -0
  234. package/dist/structure/index.js +22 -0
  235. package/dist/structure/parse.js +19 -16
  236. package/dist/structure/partial-occupancy.d.ts +25 -0
  237. package/dist/structure/partial-occupancy.js +102 -0
  238. package/dist/structure/validation.js +6 -3
  239. package/dist/symmetry/SymmetryStats.svelte +18 -4
  240. package/dist/symmetry/WyckoffTable.svelte +18 -10
  241. package/dist/symmetry/index.d.ts +7 -4
  242. package/dist/symmetry/index.js +83 -18
  243. package/dist/table/HeatmapTable.svelte +468 -69
  244. package/dist/table/HeatmapTable.svelte.d.ts +13 -1
  245. package/dist/table/ToggleMenu.svelte +291 -44
  246. package/dist/table/ToggleMenu.svelte.d.ts +4 -1
  247. package/dist/table/index.d.ts +3 -0
  248. package/dist/tooltip/index.d.ts +1 -1
  249. package/dist/tooltip/index.js +1 -0
  250. package/dist/trajectory/Trajectory.svelte +147 -145
  251. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  252. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  253. package/dist/trajectory/constants.d.ts +6 -0
  254. package/dist/trajectory/constants.js +7 -0
  255. package/dist/trajectory/extract.js +3 -5
  256. package/dist/trajectory/format-detect.d.ts +9 -0
  257. package/dist/trajectory/format-detect.js +76 -0
  258. package/dist/trajectory/frame-reader.d.ts +17 -0
  259. package/dist/trajectory/frame-reader.js +339 -0
  260. package/dist/trajectory/helpers.d.ts +15 -0
  261. package/dist/trajectory/helpers.js +187 -0
  262. package/dist/trajectory/index.d.ts +1 -0
  263. package/dist/trajectory/index.js +11 -4
  264. package/dist/trajectory/parse/ase.d.ts +2 -0
  265. package/dist/trajectory/parse/ase.js +76 -0
  266. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  267. package/dist/trajectory/parse/hdf5.js +121 -0
  268. package/dist/trajectory/parse/index.d.ts +12 -0
  269. package/dist/trajectory/parse/index.js +304 -0
  270. package/dist/trajectory/parse/lammps.d.ts +5 -0
  271. package/dist/trajectory/parse/lammps.js +169 -0
  272. package/dist/trajectory/parse/vasp.d.ts +2 -0
  273. package/dist/trajectory/parse/vasp.js +65 -0
  274. package/dist/trajectory/parse/xyz.d.ts +2 -0
  275. package/dist/trajectory/parse/xyz.js +109 -0
  276. package/dist/trajectory/types.d.ts +11 -0
  277. package/dist/trajectory/types.js +1 -0
  278. package/dist/utils.d.ts +2 -0
  279. package/dist/utils.js +4 -0
  280. package/dist/xrd/XrdPlot.svelte +6 -4
  281. package/dist/xrd/calc-xrd.js +0 -1
  282. package/package.json +33 -23
  283. package/readme.md +4 -4
  284. package/dist/trajectory/parse.d.ts +0 -42
  285. package/dist/trajectory/parse.js +0 -1267
  286. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,22 +1,24 @@
1
1
  <script lang="ts">import { format_value } from '../labels';
2
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';
3
+ import { AxisLabel, compute_element_placement, HistogramControls, PlotLegend, ReferenceLine, } from './';
4
+ import { create_axis_change_handler } from './axis-utils';
5
5
  import { extract_series_color, prepare_legend_data } from './data-transform';
6
6
  import { AXIS_DEFAULTS } from './defaults';
7
7
  import { create_dimension_tracker, create_hover_lock, } from './hover-lock.svelte';
8
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';
9
+ import { calc_auto_padding, constrain_tooltip_position, filter_padding, LABEL_GAP_DEFAULT, measure_max_tick_width, } from './layout';
10
10
  import { group_ref_lines_by_z, index_ref_lines } from './reference-line';
11
11
  import { create_scale, generate_ticks, get_nice_data_range, get_tick_label, } from './scales';
12
12
  import { get_scale_type_name } from './types';
13
+ import ZeroLines from './ZeroLines.svelte';
14
+ import ZoomRect from './ZoomRect.svelte';
13
15
  import { DEFAULTS } from '../settings';
14
16
  import { bin, max } from 'd3-array';
15
17
  import { untrack } from 'svelte';
16
18
  import { Tween } from 'svelte/motion';
17
19
  import PlotTooltip from './PlotTooltip.svelte';
18
20
  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();
21
+ let { series = $bindable([]), x_axis: x_axis_init = {}, x2_axis: x2_axis_init = {}, y_axis: y_axis_init = {}, y2_axis: y2_axis_init = {}, display: display_init = DEFAULTS.histogram.display, x_range = [null, null], x2_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
22
  // Local state for controls (initialized from props, owned by this component)
21
23
  // Include key AXIS_DEFAULTS props (range, ticks, scale_type) that PlotControls needs
22
24
  // Using $state because these have bindings in HistogramControls/PlotControls
@@ -24,6 +26,12 @@ let { series = $bindable([]), x_axis: x_axis_init = {}, y_axis: y_axis_init = {}
24
26
  const { format: _, ...axis_state_defaults } = AXIS_DEFAULTS; // Exclude format (has component-specific default)
25
27
  let bar = $state(untrack(() => ({ ...DEFAULTS.histogram.bar, ...bar_init })));
26
28
  let x_axis = $state(untrack(() => ({ ...axis_state_defaults, ...x_axis_init })));
29
+ // x2-axis needs different default label_shift for top-side positioning
30
+ let x2_axis = $state(untrack(() => ({
31
+ ...axis_state_defaults,
32
+ label_shift: { x: 0, y: 40 },
33
+ ...x2_axis_init,
34
+ })));
27
35
  let y_axis = $state(untrack(() => ({ ...axis_state_defaults, ...y_axis_init })));
28
36
  // y2-axis needs different default label_shift for right-side positioning
29
37
  let y2_axis = $state(untrack(() => ({
@@ -34,6 +42,7 @@ let y2_axis = $state(untrack(() => ({
34
42
  let display = $state(untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })));
35
43
  // Merge component-specific defaults with local state (format comes from here, not AXIS_DEFAULTS)
36
44
  const final_x_axis = $derived({ label: `Value`, format: `.2~s`, ...x_axis });
45
+ const final_x2_axis = $derived({ label: `Value`, format: `.2~s`, ...x2_axis });
37
46
  const final_y_axis = $derived({ label: `Count`, format: `d`, ...y_axis });
38
47
  const final_bar = $derived({ ...DEFAULTS.histogram.bar, ...bar });
39
48
  const final_y2_axis = $derived({ label: `Count`, format: `d`, ...y2_axis });
@@ -71,9 +80,14 @@ let selected_series = $derived(mode === `single` && selected_property
71
80
  // Separate series by y-axis
72
81
  let y1_series = $derived(selected_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`));
73
82
  let y2_series = $derived(selected_series.filter((srs) => srs.y_axis === `y2`));
83
+ let x2_series = $derived(selected_series.filter((srs) => srs.x_axis === `x2`));
74
84
  let auto_ranges = $derived.by(() => {
75
85
  const all_values = selected_series.flatMap((srs) => srs.y);
76
86
  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);
87
+ const x2_values = x2_series.flatMap((srs) => srs.y);
88
+ const auto_x2 = x2_values.length > 0
89
+ ? get_nice_data_range(x2_values.map((val) => ({ x: val, y: 0 })), ({ x }) => x, x2_range, final_x2_axis.scale_type ?? `linear`, range_padding, false)
90
+ : [0, 1];
77
91
  // Calculate y-range for a specific set of series
78
92
  const calc_y_range = (series_list, y_limit, scale_type) => {
79
93
  const type_name = get_scale_type_name(scale_type);
@@ -96,17 +110,19 @@ let auto_ranges = $derived.by(() => {
96
110
  };
97
111
  const y1_range = calc_y_range(y1_series, y_range, final_y_axis.scale_type ?? `linear`);
98
112
  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 };
113
+ return { x: auto_x, x2: auto_x2, y: y1_range, y2: y2_auto_range };
100
114
  });
101
115
  // Initialize ranges
102
116
  let ranges = $state({
103
117
  initial: {
104
118
  x: [0, 1],
119
+ x2: [0, 1],
105
120
  y: [0, 1],
106
121
  y2: [0, 1],
107
122
  },
108
123
  current: {
109
124
  x: [0, 1],
125
+ x2: [0, 1],
110
126
  y: [0, 1],
111
127
  y2: [0, 1],
112
128
  },
@@ -119,6 +135,12 @@ $effect(() => {
119
135
  final_x_axis.range[1] ?? auto_ranges.x[1],
120
136
  ]
121
137
  : auto_ranges.x;
138
+ const new_x2 = final_x2_axis.range
139
+ ? [
140
+ final_x2_axis.range[0] ?? auto_ranges.x2[0],
141
+ final_x2_axis.range[1] ?? auto_ranges.x2[1],
142
+ ]
143
+ : auto_ranges.x2;
122
144
  const new_y = final_y_axis.range
123
145
  ? [
124
146
  final_y_axis.range[0] ?? auto_ranges.y[0],
@@ -135,12 +157,16 @@ $effect(() => {
135
157
  // Comparing against initial preserves user's pan/zoom state
136
158
  const x_changed = new_x[0] !== ranges.initial.x[0] ||
137
159
  new_x[1] !== ranges.initial.x[1];
160
+ const x2_changed = new_x2[0] !== ranges.initial.x2[0] ||
161
+ new_x2[1] !== ranges.initial.x2[1];
138
162
  const y_changed = new_y[0] !== ranges.initial.y[0] ||
139
163
  new_y[1] !== ranges.initial.y[1];
140
164
  const y2_changed = new_y2[0] !== ranges.initial.y2[0] ||
141
165
  new_y2[1] !== ranges.initial.y2[1];
142
166
  if (x_changed)
143
167
  [ranges.initial.x, ranges.current.x] = [new_x, new_x];
168
+ if (x2_changed)
169
+ [ranges.initial.x2, ranges.current.x2] = [new_x2, new_x2];
144
170
  if (y_changed)
145
171
  [ranges.initial.y, ranges.current.y] = [new_y, new_y];
146
172
  if (y2_changed)
@@ -151,12 +177,14 @@ const default_padding = { t: 20, b: 60, l: 60, r: 20 };
151
177
  let pad = $derived(filter_padding(padding, default_padding));
152
178
  // Update padding based on tick label widths (untrack breaks circular dependency)
153
179
  $effect(() => {
180
+ const current_ticks_x2 = untrack(() => ticks.x2);
154
181
  const current_ticks_y = untrack(() => ticks.y);
155
182
  const current_ticks_y2 = untrack(() => ticks.y2);
156
183
  const new_pad = width && height && current_ticks_y.length
157
184
  ? calc_auto_padding({
158
185
  padding,
159
186
  default_padding,
187
+ x2_axis: { ...final_x2_axis, tick_values: current_ticks_x2 },
160
188
  y_axis: { ...final_y_axis, tick_values: current_ticks_y },
161
189
  y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
162
190
  })
@@ -164,14 +192,23 @@ $effect(() => {
164
192
  // Add y2 axis label space (calc_auto_padding only accounts for tick labels)
165
193
  if (width && height && y2_series.length && current_ticks_y2.length &&
166
194
  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
195
  const inside = final_y2_axis.tick?.label?.inside ?? false;
169
196
  // When ticks are inside, they don't contribute to padding
170
197
  const tick_shift = inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8;
171
- const tick_width_contribution = inside ? 0 : y2_tick_width;
198
+ const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max;
172
199
  const label_thickness = Math.round(12 * 1.2);
173
200
  new_pad.r = Math.max(new_pad.r, tick_width_contribution + LABEL_GAP_DEFAULT + tick_shift + label_thickness);
174
201
  }
202
+ // Add x2 axis label space (mirroring y2 logic for top padding)
203
+ if (width && height && x2_series.length && current_ticks_x2.length &&
204
+ final_x2_axis.label) {
205
+ const inside = final_x2_axis.tick?.label?.inside ?? false;
206
+ const tick_shift = inside
207
+ ? 0
208
+ : Math.abs(final_x2_axis.tick?.label?.shift?.y ?? 0) + 8;
209
+ const label_thickness = Math.round(12 * 1.2);
210
+ new_pad.t = Math.max(new_pad.t, tick_shift + LABEL_GAP_DEFAULT + label_thickness);
211
+ }
175
212
  // Only update if padding actually changed
176
213
  if (pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
177
214
  pad.r !== new_pad.r)
@@ -180,6 +217,7 @@ $effect(() => {
180
217
  // Scales and data
181
218
  let scales = $derived({
182
219
  x: create_scale(final_x_axis.scale_type ?? `linear`, ranges.current.x, [pad.l, width - pad.r]),
220
+ x2: create_scale(final_x2_axis.scale_type ?? `linear`, ranges.current.x2, [pad.l, width - pad.r]),
183
221
  y: create_scale(final_y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
184
222
  y2: create_scale(final_y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
185
223
  });
@@ -189,8 +227,15 @@ let histogram_data = $derived.by(() => {
189
227
  const hist_generator = bin()
190
228
  .domain([ranges.current.x[0], ranges.current.x[1]])
191
229
  .thresholds(bins);
230
+ const x2_hist_generator = x2_series.length > 0
231
+ ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
232
+ : null;
192
233
  return selected_series.map((series_data, series_idx) => {
193
- const bins_arr = hist_generator(series_data.y);
234
+ const use_x2 = series_data.x_axis === `x2`;
235
+ const active_hist = use_x2 && x2_hist_generator
236
+ ? x2_hist_generator
237
+ : hist_generator;
238
+ const bins_arr = active_hist(series_data.y);
194
239
  const use_y2 = series_data.y_axis === `y2`;
195
240
  return {
196
241
  id: series_data.id ?? series_idx,
@@ -201,7 +246,9 @@ let histogram_data = $derived.by(() => {
201
246
  : extract_series_color(series_data),
202
247
  bins: bins_arr,
203
248
  max_count: max(bins_arr, (data) => data.length) || 0,
249
+ x_axis: series_data.x_axis,
204
250
  y_axis: series_data.y_axis,
251
+ x_scale: use_x2 ? scales.x2 : scales.x,
205
252
  y_scale: use_y2 ? scales.y2 : scales.y,
206
253
  };
207
254
  });
@@ -210,6 +257,9 @@ let ticks = $derived({
210
257
  x: width && height
211
258
  ? generate_ticks(ranges.current.x, final_x_axis.scale_type ?? `linear`, final_x_axis.ticks, scales.x, { default_count: 8 })
212
259
  : [],
260
+ x2: width && height && x2_series.length > 0
261
+ ? generate_ticks(ranges.current.x2, final_x2_axis.scale_type ?? `linear`, final_x2_axis.ticks, scales.x2, { default_count: 8 })
262
+ : [],
213
263
  y: width && height
214
264
  ? generate_ticks(ranges.current.y, final_y_axis.scale_type ?? `linear`, final_y_axis.ticks, scales.y, { default_count: 6 })
215
265
  : [],
@@ -217,16 +267,23 @@ let ticks = $derived({
217
267
  ? generate_ticks(ranges.current.y2, final_y2_axis.scale_type ?? `linear`, final_y2_axis.ticks, scales.y2, { default_count: 6 })
218
268
  : [],
219
269
  });
270
+ // Cache measured tick-label widths so expensive text measurement only runs
271
+ // when tick values/format change, not on every template rerender.
272
+ let tick_label_widths = $derived({
273
+ x2_max: measure_max_tick_width(ticks.x2, final_x2_axis.format ?? ``),
274
+ y_max: measure_max_tick_width(ticks.y, final_y_axis.format ?? ``),
275
+ y2_max: measure_max_tick_width(ticks.y2, final_y2_axis.format ?? ``),
276
+ });
220
277
  let legend_data = $derived(prepare_legend_data(series));
221
278
  // Collect histogram bar positions for legend placement
222
279
  let hist_points_for_placement = $derived.by(() => {
223
280
  if (!width || !height || !histogram_data.length)
224
281
  return [];
225
282
  const points = [];
226
- for (const { bins, y_scale } of histogram_data) {
283
+ for (const { bins, x_scale, y_scale } of histogram_data) {
227
284
  for (const bin of bins) {
228
285
  if (bin.length > 0) {
229
- const bar_x = scales.x((bin.x0 + bin.x1) / 2);
286
+ const bar_x = x_scale((bin.x0 + bin.x1) / 2);
230
287
  const bar_y = y_scale(bin.length);
231
288
  if (isFinite(bar_x) && isFinite(bar_y)) {
232
289
  // Add multiple points for taller bars to increase their weight
@@ -291,6 +348,8 @@ const handle_zoom = () => {
291
348
  return;
292
349
  const start_x = scales.x.invert(drag_state.start.x);
293
350
  const end_x = scales.x.invert(drag_state.current.x);
351
+ const start_x2 = scales.x2.invert(drag_state.start.x);
352
+ const end_x2 = scales.x2.invert(drag_state.current.x);
294
353
  const start_y = scales.y.invert(drag_state.start.y);
295
354
  const end_y = scales.y.invert(drag_state.current.y);
296
355
  const start_y2 = scales.y2.invert(drag_state.start.y);
@@ -304,6 +363,12 @@ const handle_zoom = () => {
304
363
  ...x_axis,
305
364
  range: [Math.min(start_x, end_x), Math.max(start_x, end_x)],
306
365
  };
366
+ if (x2_series.length > 0) {
367
+ x2_axis = {
368
+ ...x2_axis,
369
+ range: [Math.min(start_x2, end_x2), Math.max(start_x2, end_x2)],
370
+ };
371
+ }
307
372
  y_axis = {
308
373
  ...y_axis,
309
374
  range: [Math.min(start_y, end_y), Math.max(start_y, end_y)],
@@ -341,9 +406,11 @@ const on_pan_move = (evt) => {
341
406
  const plot_height = height - pad.t - pad.b;
342
407
  const sensitivity = pan?.drag_sensitivity ?? 1;
343
408
  const x_delta = pixels_to_data_delta(-dx * sensitivity, pan_drag_state.initial_x_range, plot_width);
409
+ const x2_delta = pixels_to_data_delta(-dx * sensitivity, pan_drag_state.initial_x2_range, plot_width);
344
410
  const y_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y_range, plot_height);
345
411
  const y2_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y2_range, plot_height);
346
412
  ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta);
413
+ ranges.current.x2 = pan_range(pan_drag_state.initial_x2_range, x2_delta);
347
414
  ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta);
348
415
  ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta);
349
416
  };
@@ -364,6 +431,7 @@ function handle_mouse_down(evt) {
364
431
  pan_drag_state = {
365
432
  start: { x: evt.clientX, y: evt.clientY },
366
433
  initial_x_range: [...ranges.current.x],
434
+ initial_x2_range: [...ranges.current.x2],
367
435
  initial_y_range: [...ranges.current.y],
368
436
  initial_y2_range: [...ranges.current.y2],
369
437
  };
@@ -395,10 +463,12 @@ function handle_wheel(evt) {
395
463
  const sensitivity = pan?.wheel_sensitivity ?? 1;
396
464
  // Determine pan direction based on wheel delta
397
465
  const x_delta = pixels_to_data_delta(evt.deltaX * sensitivity, ranges.current.x, plot_width);
466
+ const x2_delta = pixels_to_data_delta(evt.deltaX * sensitivity, ranges.current.x2, plot_width);
398
467
  const y_delta = pixels_to_data_delta(evt.deltaY * sensitivity, ranges.current.y, plot_height);
399
468
  const y2_delta = pixels_to_data_delta(evt.deltaY * sensitivity, ranges.current.y2, plot_height);
400
469
  if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
401
470
  ranges.current.x = pan_range(ranges.current.x, x_delta);
471
+ ranges.current.x2 = pan_range(ranges.current.x2, x2_delta);
402
472
  }
403
473
  else {
404
474
  ranges.current.y = pan_range(ranges.current.y, y_delta);
@@ -415,6 +485,7 @@ function handle_touch_start(evt) {
415
485
  touch_state = {
416
486
  start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
417
487
  initial_x_range: [...ranges.current.x],
488
+ initial_x2_range: [...ranges.current.x2],
418
489
  initial_y_range: [...ranges.current.y],
419
490
  initial_y2_range: [...ranges.current.y2],
420
491
  };
@@ -449,16 +520,23 @@ function handle_touch_move(evt) {
449
520
  // Pinch zoom centered on gesture center
450
521
  // Divide by scale so spread (scale > 1) = smaller span (zoom in)
451
522
  const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0];
523
+ const x2_span = touch_state.initial_x2_range[1] -
524
+ touch_state.initial_x2_range[0];
452
525
  const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0];
453
526
  const y2_span = touch_state.initial_y2_range[1] -
454
527
  touch_state.initial_y2_range[0];
455
528
  const x_center = (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2;
529
+ const x2_center = (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2;
456
530
  const y_center = (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2;
457
531
  const y2_center = (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2;
458
532
  ranges.current.x = [
459
533
  x_center - x_span / scale / 2,
460
534
  x_center + x_span / scale / 2,
461
535
  ];
536
+ ranges.current.x2 = [
537
+ x2_center - x2_span / scale / 2,
538
+ x2_center + x2_span / scale / 2,
539
+ ];
462
540
  ranges.current.y = [
463
541
  y_center - y_span / scale / 2,
464
542
  y_center + y_span / scale / 2,
@@ -471,9 +549,11 @@ function handle_touch_move(evt) {
471
549
  else {
472
550
  // Pan
473
551
  const x_delta = pixels_to_data_delta(-dx, touch_state.initial_x_range, plot_width);
552
+ const x2_delta = pixels_to_data_delta(-dx, touch_state.initial_x2_range, plot_width);
474
553
  const y_delta = pixels_to_data_delta(dy, touch_state.initial_y_range, plot_height);
475
554
  const y2_delta = pixels_to_data_delta(dy, touch_state.initial_y2_range, plot_height);
476
555
  ranges.current.x = pan_range(touch_state.initial_x_range, x_delta);
556
+ ranges.current.x2 = pan_range(touch_state.initial_x2_range, x2_delta);
477
557
  ranges.current.y = pan_range(touch_state.initial_y_range, y_delta);
478
558
  ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta);
479
559
  }
@@ -484,26 +564,30 @@ function handle_touch_end() {
484
564
  function handle_double_click() {
485
565
  // Reset zoom to initial ranges (undo any pan/zoom)
486
566
  ranges.current.x = [...ranges.initial.x];
567
+ ranges.current.x2 = [...ranges.initial.x2];
487
568
  ranges.current.y = [...ranges.initial.y];
488
569
  ranges.current.y2 = [...ranges.initial.y2];
489
570
  // Also reset axis props so future data changes recalculate auto ranges
490
571
  x_axis = { ...x_axis, range: [null, null] };
572
+ x2_axis = { ...x2_axis, range: [null, null] };
491
573
  y_axis = { ...y_axis, range: [null, null] };
492
574
  y2_axis = { ...y2_axis, range: [null, null] };
493
575
  }
494
- function handle_mouse_move(evt, value, count, property, active_y_axis = `y1`, series_idx = 0) {
576
+ function handle_mouse_move(evt, value, count, property, active_y_axis = `y1`, series_idx = 0, active_x_axis = `x1`) {
495
577
  hovered = true;
496
578
  hover_info = {
497
579
  value,
498
580
  count,
499
581
  property,
500
582
  active_y_axis,
583
+ active_x_axis,
501
584
  x: value,
502
585
  y: count,
503
586
  series_idx,
504
587
  metadata: null,
505
588
  label: property,
506
- x_axis,
589
+ x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
590
+ x2_axis,
507
591
  y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
508
592
  y2_axis,
509
593
  };
@@ -527,11 +611,21 @@ $effect(() => {
527
611
  });
528
612
  // State accessors for shared axis change handler
529
613
  const axis_state = {
530
- get_axis: (axis) => (axis === `x` ? x_axis : axis === `y` ? y_axis : y2_axis),
614
+ get_axis: (axis) => {
615
+ if (axis === `x`)
616
+ return x_axis;
617
+ if (axis === `x2`)
618
+ return x2_axis;
619
+ if (axis === `y`)
620
+ return y_axis;
621
+ return y2_axis;
622
+ },
531
623
  set_axis: (axis, config) => {
532
624
  // Spread into existing state to preserve merged type structure
533
625
  if (axis === `x`)
534
626
  x_axis = { ...x_axis, ...config };
627
+ else if (axis === `x2`)
628
+ x2_axis = { ...x2_axis, ...config };
535
629
  else if (axis === `y`)
536
630
  y_axis = { ...y_axis, ...config };
537
631
  else
@@ -569,11 +663,12 @@ $effect(() => {
569
663
  <ReferenceLine
570
664
  ref_line={line}
571
665
  line_idx={line.idx}
572
- x_min={ranges.current.x[0]}
573
- x_max={ranges.current.x[1]}
666
+ x_min={line.x_axis === `x2` ? ranges.current.x2[0] : ranges.current.x[0]}
667
+ x_max={line.x_axis === `x2` ? ranges.current.x2[1] : ranges.current.x[1]}
574
668
  y_min={line.y_axis === `y2` ? ranges.current.y2[0] : ranges.current.y[0]}
575
669
  y_max={line.y_axis === `y2` ? ranges.current.y2[1] : ranges.current.y[1]}
576
670
  x_scale={scales.x}
671
+ x2_scale={scales.x2}
577
672
  y_scale={scales.y}
578
673
  y2_scale={scales.y2}
579
674
  {clip_path_id}
@@ -625,6 +720,9 @@ $effect(() => {
625
720
  <svg
626
721
  bind:this={svg_element}
627
722
  role="application"
723
+ aria-label={rest[`aria-label`] ??
724
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
725
+ `Histogram`)}
628
726
  tabindex="0"
629
727
  onfocusin={() => (is_focused = true)}
630
728
  onfocusout={() => (is_focused = false)}
@@ -670,79 +768,33 @@ $effect(() => {
670
768
  <!-- Reference lines: below grid (must render first to appear behind grid) -->
671
769
  {@render ref_lines_layer(ref_lines_by_z.below_grid)}
672
770
 
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}
771
+ <ZoomRect start={drag_state.start} current={drag_state.current} />
772
+
773
+ <ZeroLines
774
+ {display}
775
+ x_scale_fn={scales.x}
776
+ x2_scale_fn={scales.x2}
777
+ y_scale_fn={scales.y}
778
+ y2_scale_fn={scales.y2}
779
+ x_range={ranges.current.x}
780
+ x2_range={ranges.current.x2}
781
+ y_range={ranges.current.y}
782
+ y2_range={ranges.current.y2}
783
+ x_scale_type={final_x_axis.scale_type}
784
+ x2_scale_type={final_x2_axis.scale_type}
785
+ y_scale_type={final_y_axis.scale_type}
786
+ y2_scale_type={final_y2_axis.scale_type}
787
+ has_x2={x2_series.length > 0}
788
+ has_y2={y2_series.length > 0}
789
+ {width}
790
+ {height}
791
+ {pad}
792
+ />
683
793
 
684
794
  <!-- Reference lines: below lines -->
685
795
  {@render ref_lines_layer(ref_lines_by_z.below_lines)}
686
796
 
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) -->
797
+ <!-- Reference lines: below points -->
746
798
  {@render ref_lines_layer(ref_lines_by_z.below_points)}
747
799
 
748
800
  <!-- X-axis -->
@@ -761,7 +813,7 @@ $effect(() => {
761
813
  {@const inside = final_x_axis.tick?.label?.inside ?? false}
762
814
  {@const shift_x = final_x_axis.tick?.label?.shift?.x ?? 0}
763
815
  {@const shift_y = final_x_axis.tick?.label?.shift?.y ?? 0}
764
- {@const base_y = inside ? -8 : 18}
816
+ {@const base_y = inside ? -8 : 8}
765
817
  {@const text_y = base_y + shift_y}
766
818
  {@const dominant_baseline = inside ? `auto` : `hanging`}
767
819
  <g class="tick" transform="translate({tick_x}, {height - pad.b})">
@@ -793,31 +845,87 @@ $effect(() => {
793
845
  </g>
794
846
  {/each}
795
847
  {#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>
848
+ {@const { label_shift, label = ``, options, selected_key, color } = final_x_axis}
849
+ <AxisLabel
850
+ x={(pad.l + width - pad.r) / 2 + (label_shift?.x ?? 0)}
851
+ y={height - 10 + (label_shift?.y ?? 0)}
852
+ {label}
853
+ {options}
854
+ {selected_key}
855
+ loading={axis_loading === `x`}
856
+ axis_type="x"
857
+ {color}
858
+ on_select={(key) => handle_axis_change(`x`, key)}
859
+ />
818
860
  {/if}
819
861
  </g>
820
862
 
863
+ <!-- X2-axis (Top) -->
864
+ {#if x2_series.length > 0}
865
+ <g class="x2-axis">
866
+ <line
867
+ x1={pad.l}
868
+ x2={width - pad.r}
869
+ y1={pad.t}
870
+ y2={pad.t}
871
+ stroke={final_x2_axis.color || `var(--border-color, gray)`}
872
+ stroke-width="1"
873
+ />
874
+ {#each ticks.x2 as tick (tick)}
875
+ {@const tick_x = scales.x2(tick as number)}
876
+ {@const custom_label = get_tick_label(tick as number, final_x2_axis.ticks)}
877
+ {@const inside = final_x2_axis.tick?.label?.inside ?? false}
878
+ {@const shift_x = final_x2_axis.tick?.label?.shift?.x ?? 0}
879
+ {@const shift_y = final_x2_axis.tick?.label?.shift?.y ?? 0}
880
+ {@const base_y = inside ? 8 : -20}
881
+ {@const text_y = base_y + shift_y}
882
+ {@const dominant_baseline = inside ? `hanging` : `auto`}
883
+ <g class="tick" transform="translate({tick_x}, {pad.t})">
884
+ {#if display.x2_grid}
885
+ <line
886
+ y1="0"
887
+ y2={height - pad.b - pad.t}
888
+ stroke="var(--border-color, gray)"
889
+ stroke-dasharray="4"
890
+ stroke-width="1"
891
+ {...final_x2_axis.grid_style ?? {}}
892
+ />
893
+ {/if}
894
+ <line
895
+ y1={inside ? 0 : -5}
896
+ y2={inside ? 5 : 0}
897
+ stroke={final_x2_axis.color || `var(--border-color, gray)`}
898
+ stroke-width="1"
899
+ />
900
+ <text
901
+ x={shift_x}
902
+ y={text_y}
903
+ text-anchor="middle"
904
+ dominant-baseline={dominant_baseline}
905
+ fill={final_x2_axis.color || `var(--text-color)`}
906
+ >
907
+ {custom_label ?? format_value(tick, final_x2_axis.format)}
908
+ </text>
909
+ </g>
910
+ {/each}
911
+ {#if final_x2_axis.label || x2_axis.options?.length}
912
+ {@const { label_shift, label = ``, options, selected_key, color } =
913
+ final_x2_axis}
914
+ <AxisLabel
915
+ x={(pad.l + width - pad.r) / 2 + (label_shift?.x ?? 0)}
916
+ y={Math.max(12, pad.t - (label_shift?.y ?? 40))}
917
+ {label}
918
+ {options}
919
+ {selected_key}
920
+ loading={axis_loading === `x2`}
921
+ axis_type="x2"
922
+ {color}
923
+ on_select={(key) => handle_axis_change(`x2`, key)}
924
+ />
925
+ {/if}
926
+ </g>
927
+ {/if}
928
+
821
929
  <!-- Y-axis -->
822
930
  <g class="y-axis">
823
931
  <line
@@ -866,41 +974,25 @@ $effect(() => {
866
974
  </g>
867
975
  {/each}
868
976
  {#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>
977
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
978
+ final_y_axis}
979
+ {@const y_inside = tick?.label?.inside ?? false}
980
+ <AxisLabel
981
+ x={Math.max(
982
+ 12,
983
+ pad.l - (y_inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
984
+ ) +
985
+ (label_shift?.x ?? 0)}
986
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
987
+ rotate
988
+ {label}
989
+ {options}
990
+ {selected_key}
991
+ loading={axis_loading === `y`}
992
+ axis_type="y"
993
+ {color}
994
+ on_select={(key) => handle_axis_change(`y`, key)}
995
+ />
904
996
  {/if}
905
997
  </g>
906
998
 
@@ -952,80 +1044,86 @@ $effect(() => {
952
1044
  </g>
953
1045
  {/each}
954
1046
  {#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>
1047
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
1048
+ final_y2_axis}
1049
+ {@const inside = tick?.label?.inside ?? false}
1050
+ {@const tick_shift = inside ? 0 : (tick?.label?.shift?.x ?? 0) + 8}
1051
+ {@const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max}
1052
+ <AxisLabel
1053
+ x={width - pad.r + tick_shift + tick_width_contribution +
1054
+ LABEL_GAP_DEFAULT + (label_shift?.x ?? 0)}
1055
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
1056
+ rotate
1057
+ {label}
1058
+ {options}
1059
+ {selected_key}
1060
+ loading={axis_loading === `y2`}
1061
+ axis_type="y2"
1062
+ {color}
1063
+ on_select={(key) => handle_axis_change(`y2`, key)}
1064
+ />
994
1065
  {/if}
995
1066
  </g>
996
1067
  {/if}
997
1068
 
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}
1069
+ <!-- Histogram bars (rendered after axes so bars appear above grid lines) -->
1070
+ {#each histogram_data as
1071
+ { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis },
1072
+ series_idx
1073
+ (id ?? series_idx)
1074
+ }
1075
+ <g class="histogram-series" data-series-idx={series_idx}>
1076
+ {#each bins as bin, bin_idx (bin_idx)}
1077
+ {@const bar_x = x_scale(bin.x0!)}
1078
+ {@const bar_width = Math.max(1, Math.abs(x_scale(bin.x1!) - bar_x))}
1079
+ {@const bar_height = Math.max(0, (height - pad.b) - y_scale(bin.length))}
1080
+ {@const bar_y = y_scale(bin.length)}
1081
+ {@const value = (bin.x0! + bin.x1!) / 2}
1082
+ {#if bar_height > 0}
1083
+ <path
1084
+ d={bar_path(
1085
+ bar_x,
1086
+ bar_y,
1087
+ bar_width,
1088
+ bar_height,
1089
+ Math.min(final_bar.border_radius ?? 0, bar_width / 2, bar_height / 2),
1090
+ )}
1091
+ fill={color}
1092
+ opacity={final_bar.opacity}
1093
+ stroke={final_bar.stroke_color}
1094
+ stroke-opacity={final_bar.stroke_opacity}
1095
+ stroke-width={final_bar.stroke_width}
1096
+ role="button"
1097
+ tabindex="0"
1098
+ onmousemove={(evt) =>
1099
+ handle_mouse_move(
1100
+ evt,
1101
+ value,
1102
+ bin.length,
1103
+ label,
1104
+ (y_axis ?? `y1`) as `y1` | `y2`,
1105
+ series_idx,
1106
+ (srs_x_axis ?? `x1`) as `x1` | `x2`,
1107
+ )}
1108
+ onmouseleave={() => {
1109
+ hover_info = null
1110
+ change(null)
1111
+ on_bar_hover?.(null)
1112
+ }}
1113
+ onclick={(event) =>
1114
+ on_bar_click?.({ value, count: bin.length, property: label, event })}
1115
+ onkeydown={(event: KeyboardEvent) => {
1116
+ if ([`Enter`, ` `].includes(event.key)) {
1117
+ event.preventDefault()
1118
+ on_bar_click?.({ value, count: bin.length, property: label, event })
1119
+ }
1120
+ }}
1121
+ style:cursor={on_bar_click ? `pointer` : undefined}
1122
+ />
1123
+ {/if}
1124
+ {/each}
1125
+ </g>
1126
+ {/each}
1029
1127
 
1030
1128
  <!-- Reference lines: above all -->
1031
1129
  {@render ref_lines_layer(ref_lines_by_z.above_all)}
@@ -1033,8 +1131,8 @@ $effect(() => {
1033
1131
 
1034
1132
  <!-- Tooltip (outside SVG for proper HTML rendering) -->
1035
1133
  {#if hover_info}
1036
- {@const { value, count, property, active_y_axis } = hover_info}
1037
- {@const tooltip_x = scales.x(value)}
1134
+ {@const { value, count, property, active_y_axis, active_x_axis } = hover_info}
1135
+ {@const tooltip_x = (active_x_axis === `x2` ? scales.x2 : scales.x)(value)}
1038
1136
  {@const tooltip_y = (active_y_axis === `y2` ? scales.y2 : scales.y)(count)}
1039
1137
  {@const tooltip_pos = constrain_tooltip_position(
1040
1138
  tooltip_x,
@@ -1045,7 +1143,6 @@ $effect(() => {
1045
1143
  height,
1046
1144
  { offset_x: 5, offset_y: -10 },
1047
1145
  )}
1048
- {@const active_y_config = active_y_axis === `y2` ? final_y2_axis : final_y_axis}
1049
1146
  <PlotTooltip
1050
1147
  x={tooltip_pos.x}
1051
1148
  y={tooltip_pos.y}
@@ -1055,8 +1152,8 @@ $effect(() => {
1055
1152
  {#if tooltip}
1056
1153
  {@render tooltip({ ...hover_info, fullscreen })}
1057
1154
  {: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>
1155
+ <div>Value: {format_value(value, hover_info.x_axis.format || `.3~s`)}</div>
1156
+ <div>Count: {format_value(count, hover_info.y_axis.format || `.3~s`)}</div>
1060
1157
  {#if mode === `overlay`}<div>{property}</div>{/if}
1061
1158
  {/if}
1062
1159
  </PlotTooltip>
@@ -1066,7 +1163,7 @@ $effect(() => {
1066
1163
  <HistogramControls
1067
1164
  toggle_props={{
1068
1165
  ...controls_toggle_props,
1069
- style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 36px); top: 4px; ${
1166
+ style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); ${
1070
1167
  controls_toggle_props?.style ?? ``
1071
1168
  }`,
1072
1169
  }}
@@ -1080,12 +1177,16 @@ $effect(() => {
1080
1177
  bind:display
1081
1178
  bind:bar
1082
1179
  bind:x_axis
1180
+ bind:x2_axis
1083
1181
  bind:y_axis
1084
1182
  bind:y2_axis
1085
1183
  auto_x_range={auto_ranges.x}
1184
+ auto_x2_range={auto_ranges.x2}
1086
1185
  auto_y_range={auto_ranges.y}
1087
1186
  auto_y2_range={auto_ranges.y2}
1088
1187
  {series}
1188
+ has_x2_points={x2_series.length > 0}
1189
+ has_y2_points={y2_series.length > 0}
1089
1190
  children={controls_extra}
1090
1191
  />
1091
1192
  {/if}
@@ -1178,7 +1279,7 @@ $effect(() => {
1178
1279
  font-weight: var(--histogram-font-weight);
1179
1280
  font-size: var(--histogram-font-size);
1180
1281
  }
1181
- g:is(.x-axis, .y-axis, .y2-axis) .tick text {
1282
+ g:is(.x-axis, .x2-axis, .y-axis, .y2-axis) .tick text {
1182
1283
  font-size: var(--tick-font-size, 0.8em); /* shrink tick labels */
1183
1284
  }
1184
1285
  .histogram-series path {
@@ -1187,15 +1288,4 @@ $effect(() => {
1187
1288
  .histogram-series path:hover {
1188
1289
  opacity: 1 !important;
1189
1290
  }
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
1291
  </style>