matterviz 0.3.2 → 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 (280) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  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 +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/feedback/ClickFeedback.svelte +16 -5
  76. package/dist/feedback/DragOverlay.svelte +10 -2
  77. package/dist/feedback/Spinner.svelte +4 -2
  78. package/dist/feedback/StatusMessage.svelte +8 -2
  79. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  80. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  81. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  82. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  84. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  86. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  87. package/dist/fermi-surface/compute.js +16 -20
  88. package/dist/fermi-surface/parse.js +24 -14
  89. package/dist/fermi-surface/symmetry.js +2 -7
  90. package/dist/fermi-surface/types.d.ts +3 -5
  91. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  93. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  95. package/dist/icons.js +47 -0
  96. package/dist/index.d.ts +2 -1
  97. package/dist/index.js +2 -1
  98. package/dist/io/decompress.js +1 -1
  99. package/dist/io/export.d.ts +3 -0
  100. package/dist/io/export.js +129 -143
  101. package/dist/io/is-binary.js +2 -3
  102. package/dist/io/url-drop.js +1 -2
  103. package/dist/isosurface/Isosurface.svelte +202 -148
  104. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  105. package/dist/isosurface/parse.js +34 -29
  106. package/dist/isosurface/slice.js +5 -10
  107. package/dist/isosurface/types.d.ts +2 -1
  108. package/dist/isosurface/types.js +61 -12
  109. package/dist/labels.js +11 -8
  110. package/dist/layout/FullscreenToggle.svelte +11 -2
  111. package/dist/layout/InfoCard.svelte +38 -6
  112. package/dist/layout/InfoTag.svelte +63 -32
  113. package/dist/layout/PropertyFilter.svelte +82 -37
  114. package/dist/layout/SettingsSection.svelte +85 -55
  115. package/dist/layout/SubpageGrid.svelte +10 -2
  116. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  117. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  118. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  119. package/dist/layout/json-tree/utils.js +4 -2
  120. package/dist/marching-cubes.js +25 -2
  121. package/dist/math.d.ts +13 -17
  122. package/dist/math.js +133 -67
  123. package/dist/overlays/ContextMenu.svelte +65 -40
  124. package/dist/overlays/DraggablePane.svelte +211 -139
  125. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  126. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  127. package/dist/periodic-table/PropertySelect.svelte +25 -7
  128. package/dist/periodic-table/TableInset.svelte +8 -3
  129. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  134. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  136. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  137. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  138. package/dist/phase-diagram/build-diagram.js +9 -9
  139. package/dist/phase-diagram/colors.js +1 -3
  140. package/dist/phase-diagram/parse.js +10 -9
  141. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  142. package/dist/phase-diagram/utils.d.ts +1 -0
  143. package/dist/phase-diagram/utils.js +80 -25
  144. package/dist/plot/AxisLabel.svelte +28 -3
  145. package/dist/plot/BarPlot.svelte +1182 -734
  146. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  147. package/dist/plot/BarPlotControls.svelte +31 -5
  148. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  149. package/dist/plot/ColorBar.svelte +479 -329
  150. package/dist/plot/ColorScaleSelect.svelte +27 -6
  151. package/dist/plot/ElementScatter.svelte +36 -15
  152. package/dist/plot/FillArea.svelte +152 -95
  153. package/dist/plot/Histogram.svelte +934 -571
  154. package/dist/plot/Histogram.svelte.d.ts +1 -1
  155. package/dist/plot/HistogramControls.svelte +53 -9
  156. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  157. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  158. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  159. package/dist/plot/Line.svelte +63 -28
  160. package/dist/plot/PlotControls.svelte +157 -114
  161. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  162. package/dist/plot/PlotLegend.svelte +174 -91
  163. package/dist/plot/PlotTooltip.svelte +45 -6
  164. package/dist/plot/PortalSelect.svelte +175 -147
  165. package/dist/plot/ReferenceLine.svelte +76 -22
  166. package/dist/plot/ReferenceLine3D.svelte +132 -107
  167. package/dist/plot/ReferencePlane.svelte +146 -121
  168. package/dist/plot/ScatterPlot.svelte +1681 -1091
  169. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  170. package/dist/plot/ScatterPlot3D.svelte +256 -131
  171. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  172. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  173. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  174. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  175. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  176. package/dist/plot/ScatterPlotControls.svelte +65 -25
  177. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  178. package/dist/plot/ScatterPoint.svelte +98 -26
  179. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  180. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  181. package/dist/plot/Surface3D.svelte +159 -108
  182. package/dist/plot/ZeroLines.svelte +55 -3
  183. package/dist/plot/ZoomRect.svelte +4 -2
  184. package/dist/plot/axis-utils.js +1 -3
  185. package/dist/plot/data-cleaning.js +12 -28
  186. package/dist/plot/data-transform.js +2 -1
  187. package/dist/plot/fill-utils.js +2 -0
  188. package/dist/plot/layout.d.ts +4 -1
  189. package/dist/plot/layout.js +33 -14
  190. package/dist/plot/reference-line.d.ts +2 -2
  191. package/dist/plot/reference-line.js +7 -5
  192. package/dist/plot/scales.js +24 -36
  193. package/dist/plot/types.d.ts +11 -23
  194. package/dist/plot/types.js +6 -11
  195. package/dist/plot/utils/label-placement.d.ts +32 -15
  196. package/dist/plot/utils/label-placement.js +227 -66
  197. package/dist/plot/utils/series-visibility.js +2 -3
  198. package/dist/rdf/RdfPlot.svelte +143 -91
  199. package/dist/rdf/calc-rdf.js +4 -5
  200. package/dist/sanitize.d.ts +4 -0
  201. package/dist/sanitize.js +107 -0
  202. package/dist/settings.d.ts +18 -6
  203. package/dist/settings.js +46 -16
  204. package/dist/spectral/Bands.svelte +632 -453
  205. package/dist/spectral/BandsAndDos.svelte +90 -49
  206. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  207. package/dist/spectral/Dos.svelte +389 -258
  208. package/dist/spectral/helpers.js +55 -43
  209. package/dist/state.svelte.d.ts +1 -1
  210. package/dist/state.svelte.js +3 -2
  211. package/dist/structure/Arrow.svelte +59 -20
  212. package/dist/structure/AtomLegend.svelte +215 -134
  213. package/dist/structure/Bond.svelte +73 -47
  214. package/dist/structure/CanvasTooltip.svelte +10 -2
  215. package/dist/structure/CellSelect.svelte +72 -45
  216. package/dist/structure/Cylinder.svelte +33 -17
  217. package/dist/structure/Lattice.svelte +88 -33
  218. package/dist/structure/Structure.svelte +1063 -797
  219. package/dist/structure/Structure.svelte.d.ts +1 -1
  220. package/dist/structure/StructureControls.svelte +349 -118
  221. package/dist/structure/StructureExportPane.svelte +124 -89
  222. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  223. package/dist/structure/StructureInfoPane.svelte +304 -237
  224. package/dist/structure/StructureScene.svelte +879 -443
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  226. package/dist/structure/atom-properties.js +8 -8
  227. package/dist/structure/bonding.js +6 -7
  228. package/dist/structure/export.js +14 -29
  229. package/dist/structure/ferrox-wasm.js +1 -1
  230. package/dist/structure/index.d.ts +13 -3
  231. package/dist/structure/index.js +83 -23
  232. package/dist/structure/measure.d.ts +2 -2
  233. package/dist/structure/measure.js +4 -44
  234. package/dist/structure/parse.js +113 -141
  235. package/dist/structure/partial-occupancy.js +7 -10
  236. package/dist/structure/pbc.d.ts +1 -0
  237. package/dist/structure/pbc.js +16 -6
  238. package/dist/structure/supercell.d.ts +2 -2
  239. package/dist/structure/supercell.js +12 -22
  240. package/dist/structure/validation.js +1 -2
  241. package/dist/symmetry/SymmetryStats.svelte +84 -41
  242. package/dist/symmetry/WyckoffTable.svelte +26 -6
  243. package/dist/symmetry/cell-transform.js +5 -3
  244. package/dist/symmetry/index.js +8 -7
  245. package/dist/symmetry/spacegroups.js +148 -148
  246. package/dist/table/HeatmapTable.svelte +790 -554
  247. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  248. package/dist/table/ToggleMenu.svelte +125 -92
  249. package/dist/table/index.js +2 -4
  250. package/dist/theme/ThemeControl.svelte +21 -12
  251. package/dist/time.js +4 -1
  252. package/dist/tooltip/TooltipContent.svelte +33 -8
  253. package/dist/trajectory/Trajectory.svelte +758 -558
  254. package/dist/trajectory/TrajectoryError.svelte +14 -3
  255. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  256. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  257. package/dist/trajectory/extract.js +10 -26
  258. package/dist/trajectory/format-detect.js +5 -5
  259. package/dist/trajectory/frame-reader.d.ts +1 -1
  260. package/dist/trajectory/frame-reader.js +5 -12
  261. package/dist/trajectory/helpers.d.ts +0 -1
  262. package/dist/trajectory/helpers.js +2 -17
  263. package/dist/trajectory/index.js +14 -12
  264. package/dist/trajectory/parse/ase.js +5 -4
  265. package/dist/trajectory/parse/hdf5.js +26 -18
  266. package/dist/trajectory/parse/index.js +13 -18
  267. package/dist/trajectory/parse/lammps.js +17 -7
  268. package/dist/trajectory/parse/vasp.js +5 -2
  269. package/dist/trajectory/parse/xyz.js +8 -7
  270. package/dist/trajectory/plotting.js +13 -8
  271. package/dist/utils.d.ts +1 -0
  272. package/dist/utils.js +13 -0
  273. package/dist/xrd/XrdPlot.svelte +337 -247
  274. package/dist/xrd/broadening.js +14 -9
  275. package/dist/xrd/calc-xrd.js +12 -18
  276. package/dist/xrd/parse.d.ts +1 -1
  277. package/dist/xrd/parse.js +17 -17
  278. package/package.json +99 -103
  279. package/readme.md +1 -1
  280. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,661 +1,1024 @@
1
- <script lang="ts">import { format_value } from '../labels';
2
- import { FullscreenToggle, set_fullscreen_bg } from '../layout';
3
- import { AxisLabel, compute_element_placement, HistogramControls, PlotLegend, ReferenceLine, } from './';
4
- import { 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_max_tick_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 ZeroLines from './ZeroLines.svelte';
14
- import ZoomRect from './ZoomRect.svelte';
15
- import { DEFAULTS } from '../settings';
16
- import { bin, max } from 'd3-array';
17
- import { untrack } from 'svelte';
18
- import { Tween } from 'svelte/motion';
19
- import PlotTooltip from './PlotTooltip.svelte';
20
- import { bar_path } from './svg';
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();
22
- // Local state for controls (initialized from props, owned by this component)
23
- // Include key AXIS_DEFAULTS props (range, ticks, scale_type) that PlotControls needs
24
- // Using $state because these have bindings in HistogramControls/PlotControls
25
- // untrack() explicitly captures initial prop values (intentional - props provide initial config)
26
- const { format: _, ...axis_state_defaults } = AXIS_DEFAULTS; // Exclude format (has component-specific default)
27
- let bar = $state(untrack(() => ({ ...DEFAULTS.histogram.bar, ...bar_init })));
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(() => ({
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(() => ({
31
164
  ...axis_state_defaults,
32
165
  label_shift: { x: 0, y: 40 },
33
166
  ...x2_axis_init,
34
- })));
35
- let y_axis = $state(untrack(() => ({ ...axis_state_defaults, ...y_axis_init })));
36
- // y2-axis needs different default label_shift for right-side positioning
37
- let y2_axis = $state(untrack(() => ({
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(() => ({
38
171
  ...axis_state_defaults,
39
172
  label_shift: { x: 0, y: 60 },
40
173
  ...y2_axis_init,
41
- })));
42
- let display = $state(untrack(() => ({ ...DEFAULTS.histogram.display, ...display_init })));
43
- // Merge component-specific defaults with local state (format comes from here, not AXIS_DEFAULTS)
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 });
46
- const final_y_axis = $derived({ label: `Count`, format: `d`, ...y_axis });
47
- const final_bar = $derived({ ...DEFAULTS.histogram.bar, ...bar });
48
- const final_y2_axis = $derived({ label: `Count`, format: `d`, ...y2_axis });
49
- // Core state
50
- let [width, height] = $state([0, 0]);
51
- let wrapper = $state();
52
- let svg_element = $state(null);
53
- let clip_path_id = `histogram-clip-${crypto?.randomUUID?.()}`;
54
- let hover_info = $state(null);
55
- // Reference line hover state
56
- let hovered_ref_line_idx = $state(null);
57
- // Interactive axis loading state
58
- let axis_loading = $state(null);
59
- // Compute ref_lines with index and group by z-index (using shared utilities)
60
- let indexed_ref_lines = $derived(index_ref_lines(ref_lines));
61
- let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines));
62
- let tooltip_el = $state();
63
- let drag_state = $state({ start: null, current: null, bounds: null });
64
- // Pan state
65
- let is_focused = $state(false);
66
- let shift_held = $state(false);
67
- let pan_drag_state = $state(null);
68
- let touch_state = $state(null);
69
- // Legend placement stability state
70
- let legend_element = $state();
71
- const legend_hover = create_hover_lock();
72
- const dim_tracker = create_dimension_tracker();
73
- let has_initial_legend_placement = $state(false);
74
- // Clear pending hover lock timeout on unmount
75
- $effect(() => () => legend_hover.cleanup());
76
- // Derived data
77
- let selected_series = $derived(mode === `single` && selected_property
78
- ? series.filter((srs) => (srs.visible ?? true) && srs.label === selected_property)
79
- : series.filter((srs) => srs.visible ?? true));
80
- // Separate series by y-axis
81
- let y1_series = $derived(selected_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`));
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`));
84
- let auto_ranges = $derived.by(() => {
85
- const all_values = selected_series.flatMap((srs) => srs.y);
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);
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)
88
260
  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];
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
+
91
271
  // Calculate y-range for a specific set of series
92
- const calc_y_range = (series_list, y_limit, scale_type) => {
93
- const type_name = get_scale_type_name(scale_type);
94
- if (!series_list.length) {
95
- const fallback = type_name === `log` ? 1 : 0;
96
- return [fallback, 1];
97
- }
98
- const hist = bin().domain([auto_x[0], auto_x[1]]).thresholds(bins);
99
- const max_count = Math.max(0, ...series_list.map((srs) => max(hist(srs.y), (data) => data.length) || 0));
100
- // If there's effectively no data, avoid log-range issues (counts can't be <= 0 on log)
101
- if (max_count <= 0) {
102
- const fallback = type_name === `log` ? 1 : 0;
103
- return [fallback, 1];
104
- }
105
- 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);
106
- // For log scale, minimum must be >= 1 (count can't be 0 on log)
107
- // For linear/arcsinh, start from 0
108
- const y_min = type_name === `log` ? Math.max(1, y0) : Math.max(0, y0);
109
- return [y_min, y1];
110
- };
111
- const y1_range = calc_y_range(y1_series, y_range, final_y_axis.scale_type ?? `linear`);
112
- const y2_auto_range = calc_y_range(y2_series, y2_range, final_y2_axis.scale_type ?? `linear`);
113
- return { x: auto_x, x2: auto_x2, y: y1_range, y2: y2_auto_range };
114
- });
115
- // Initialize ranges
116
- 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({
117
326
  initial: {
118
- x: [0, 1],
119
- x2: [0, 1],
120
- y: [0, 1],
121
- 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,
122
331
  },
123
332
  current: {
124
- x: [0, 1],
125
- x2: [0, 1],
126
- y: [0, 1],
127
- 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,
128
337
  },
129
- });
130
- $effect(() => {
338
+ })
339
+
340
+ $effect(() => {
131
341
  // Support one-sided range pinning: merge user range with auto range for null values
132
- const new_x = final_x_axis.range
133
- ? [
134
- final_x_axis.range[0] ?? auto_ranges.x[0],
135
- final_x_axis.range[1] ?? auto_ranges.x[1],
136
- ]
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;
144
- const new_y = final_y_axis.range
145
- ? [
146
- final_y_axis.range[0] ?? auto_ranges.y[0],
147
- final_y_axis.range[1] ?? auto_ranges.y[1],
148
- ]
149
- : auto_ranges.y;
150
- const new_y2 = final_y2_axis.range
151
- ? [
152
- final_y2_axis.range[0] ?? auto_ranges.y2[0],
153
- final_y2_axis.range[1] ?? auto_ranges.y2[1],
154
- ]
155
- : 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
+
156
367
  // Only update if the initial (data-driven) ranges changed, not when user pans
157
368
  // Comparing against initial preserves user's pan/zoom state
158
369
  const x_changed = new_x[0] !== ranges.initial.x[0] ||
159
- new_x[1] !== ranges.initial.x[1];
370
+ new_x[1] !== ranges.initial.x[1]
160
371
  const x2_changed = new_x2[0] !== ranges.initial.x2[0] ||
161
- new_x2[1] !== ranges.initial.x2[1];
372
+ new_x2[1] !== ranges.initial.x2[1]
162
373
  const y_changed = new_y[0] !== ranges.initial.y[0] ||
163
- new_y[1] !== ranges.initial.y[1];
374
+ new_y[1] !== ranges.initial.y[1]
164
375
  const y2_changed = new_y2[0] !== ranges.initial.y2[0] ||
165
- new_y2[1] !== ranges.initial.y2[1];
166
- if (x_changed)
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];
170
- if (y_changed)
171
- [ranges.initial.y, ranges.current.y] = [new_y, new_y];
172
- if (y2_changed)
173
- [ranges.initial.y2, ranges.current.y2] = [new_y2, new_y2];
174
- });
175
- // Layout: dynamic padding based on tick label widths
176
- const default_padding = { t: 20, b: 60, l: 60, r: 20 };
177
- let pad = $derived(filter_padding(padding, default_padding));
178
- // Update padding based on tick label widths (untrack breaks circular dependency)
179
- $effect(() => {
180
- const current_ticks_x2 = untrack(() => ticks.x2);
181
- const current_ticks_y = untrack(() => ticks.y);
182
- 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
+
183
394
  const new_pad = width && height && current_ticks_y.length
184
- ? calc_auto_padding({
185
- padding,
186
- default_padding,
187
- x2_axis: { ...final_x2_axis, tick_values: current_ticks_x2 },
188
- y_axis: { ...final_y_axis, tick_values: current_ticks_y },
189
- y2_axis: { ...final_y2_axis, tick_values: current_ticks_y2 },
190
- })
191
- : 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
+
192
404
  // Add y2 axis label space (calc_auto_padding only accounts for tick labels)
193
- if (width && height && y2_series.length && current_ticks_y2.length &&
194
- final_y2_axis.label) {
195
- const inside = final_y2_axis.tick?.label?.inside ?? false;
196
- // When ticks are inside, they don't contribute to padding
197
- const tick_shift = inside ? 0 : (final_y2_axis.tick?.label?.shift?.x ?? 0) + 8;
198
- const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max;
199
- const label_thickness = Math.round(12 * 1.2);
200
- 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
+ )
201
418
  }
419
+
202
420
  // 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);
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
+ )
211
434
  }
435
+
212
436
  // Only update if padding actually changed
213
- if (pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
214
- pad.r !== new_pad.r)
215
- pad = new_pad;
216
- });
217
- // Scales and data
218
- let scales = $derived({
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]),
221
- y: create_scale(final_y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
222
- y2: create_scale(final_y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
223
- });
224
- let histogram_data = $derived.by(() => {
225
- if (!selected_series.length || !width || !height)
226
- 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 []
227
469
  const hist_generator = bin()
228
- .domain([ranges.current.x[0], ranges.current.x[1]])
229
- .thresholds(bins);
470
+ .domain([ranges.current.x[0], ranges.current.x[1]])
471
+ .thresholds(bins)
230
472
  const x2_hist_generator = x2_series.length > 0
231
- ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
232
- : null;
473
+ ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
474
+ : null
233
475
  return selected_series.map((series_data, series_idx) => {
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);
239
- const use_y2 = series_data.y_axis === `y2`;
240
- return {
241
- id: series_data.id ?? series_idx,
242
- series_idx,
243
- label: series_data.label || `Series ${series_idx + 1}`,
244
- color: selected_series.length === 1
245
- ? final_bar.color
246
- : extract_series_color(series_data),
247
- bins: bins_arr,
248
- max_count: max(bins_arr, (data) => data.length) || 0,
249
- x_axis: series_data.x_axis,
250
- y_axis: series_data.y_axis,
251
- x_scale: use_x2 ? scales.x2 : scales.x,
252
- y_scale: use_y2 ? scales.y2 : scales.y,
253
- };
254
- });
255
- });
256
- 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({
257
500
  x: width && height
258
- ? generate_ticks(ranges.current.x, final_x_axis.scale_type ?? `linear`, final_x_axis.ticks, scales.x, { default_count: 8 })
259
- : [],
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
+ : [],
260
509
  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
- : [],
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
+ : [],
263
518
  y: width && height
264
- ? generate_ticks(ranges.current.y, final_y_axis.scale_type ?? `linear`, final_y_axis.ticks, scales.y, { default_count: 6 })
265
- : [],
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
+ : [],
266
527
  y2: width && height && y2_series.length > 0
267
- ? generate_ticks(ranges.current.y2, final_y2_axis.scale_type ?? `linear`, final_y2_axis.ticks, scales.y2, { default_count: 6 })
268
- : [],
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({
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({
273
541
  x2_max: measure_max_tick_width(ticks.x2, final_x2_axis.format ?? ``),
274
542
  y_max: measure_max_tick_width(ticks.y, final_y_axis.format ?? ``),
275
543
  y2_max: measure_max_tick_width(ticks.y2, final_y2_axis.format ?? ``),
276
- });
277
- let legend_data = $derived(prepare_legend_data(series));
278
- // Collect histogram bar positions for legend placement
279
- let hist_points_for_placement = $derived.by(() => {
280
- if (!width || !height || !histogram_data.length)
281
- return [];
282
- const points = [];
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
+
283
554
  for (const { bins, x_scale, y_scale } of histogram_data) {
284
- for (const bin of bins) {
285
- if (bin.length > 0) {
286
- const bar_x = x_scale((bin.x0 + bin.x1) / 2);
287
- const bar_y = y_scale(bin.length);
288
- if (isFinite(bar_x) && isFinite(bar_y)) {
289
- // Add multiple points for taller bars to increase their weight
290
- // Cap to prevent O(N·count/10) blow-ups for large counts
291
- const weight = Math.min(20, Math.ceil(bin.length / 10));
292
- for (let idx = 0; idx < weight; idx++)
293
- points.push({ x: bar_x, y: bar_y });
294
- }
295
- }
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
+ }
296
565
  }
566
+ }
297
567
  }
298
- return points;
299
- });
300
- // Calculate best legend placement using continuous grid sampling
301
- let legend_placement = $derived.by(() => {
302
- const should_place = show_legend && legend != null && series.length > 1;
303
- if (!should_place || !width || !height)
304
- return null;
305
- const plot_width = width - pad.l - pad.r;
306
- 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
+
307
579
  // Use measured size if available, otherwise estimate
308
580
  const legend_size = legend_element
309
- ? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
310
- : { width: 120, height: 60 };
581
+ ? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
582
+ : { width: 120, height: 60 }
583
+
311
584
  const result = compute_element_placement({
312
- plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
313
- element_size: legend_size,
314
- axis_clearance: legend?.axis_clearance,
315
- exclude_rects: [],
316
- points: hist_points_for_placement,
317
- });
318
- return result;
319
- });
320
- // Tweened legend coordinates for smooth animation - create once, update target via effect
321
- // untrack() explicitly captures initial tween config (intentional - config set once at mount)
322
- const tweened_legend_coords = new Tween({ x: 0, y: 0 }, untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })));
323
- // Update legend position with stability checks
324
- $effect(() => {
325
- if (!width || !height || !legend_placement)
326
- 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
+
327
606
  // Track dimensions for resize detection
328
- const dims_changed = dim_tracker.has_changed(width, height);
329
- if (dims_changed)
330
- 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
+
331
610
  // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
332
- const is_responsive = legend?.responsive ?? false;
611
+ const is_responsive = legend?.responsive ?? false
333
612
  const should_update = dims_changed || (!legend_hover.is_locked.current &&
334
- (is_responsive || !has_initial_legend_placement));
613
+ (is_responsive || !has_initial_legend_placement))
614
+
335
615
  if (should_update) {
336
- 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 },
337
618
  // Skip animation on initial placement to avoid jump from (0, 0)
338
- has_initial_legend_placement ? undefined : { duration: 0 });
339
- // Only lock position after we have actual measured size
340
- if (legend_element) {
341
- has_initial_legend_placement = true;
342
- }
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
+ }
343
625
  }
344
- });
345
- // Event handlers
346
- const handle_zoom = () => {
347
- if (!drag_state.start || !drag_state.current)
348
- return;
349
- const start_x = scales.x.invert(drag_state.start.x);
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);
353
- const start_y = scales.y.invert(drag_state.start.y);
354
- const end_y = scales.y.invert(drag_state.current.y);
355
- const start_y2 = scales.y2.invert(drag_state.start.y);
356
- 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
+
357
640
  if (typeof start_x === `number` && typeof end_x === `number`) {
358
- const dx = Math.abs(drag_state.start.x - drag_state.current.x);
359
- const dy = Math.abs(drag_state.start.y - drag_state.current.y);
360
- if (dx > 5 && dy > 5) {
361
- // Update axis ranges to trigger reactivity and prevent effect from overriding
362
- x_axis = {
363
- ...x_axis,
364
- range: [Math.min(start_x, end_x), Math.max(start_x, end_x)],
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
- }
372
- y_axis = {
373
- ...y_axis,
374
- range: [Math.min(start_y, end_y), Math.max(start_y, end_y)],
375
- };
376
- y2_axis = {
377
- ...y2_axis,
378
- range: [Math.min(start_y2, end_y2), Math.max(start_y2, end_y2)],
379
- };
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)],
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)],
380
658
  }
659
+ y2_axis = {
660
+ ...y2_axis,
661
+ range: [Math.min(start_y2, end_y2), Math.max(start_y2, end_y2)],
662
+ }
663
+ }
381
664
  }
382
- };
383
- const on_window_mouse_move = (evt) => {
384
- if (!drag_state.start || !drag_state.bounds)
385
- return;
665
+ }
666
+
667
+ const on_window_mouse_move = (evt: MouseEvent) => {
668
+ if (!drag_state.start || !drag_state.bounds) return
386
669
  drag_state.current = {
387
- x: evt.clientX - drag_state.bounds.left,
388
- y: evt.clientY - drag_state.bounds.top,
389
- };
390
- };
391
- const on_window_mouse_up = () => {
392
- handle_zoom();
393
- drag_state = { start: null, current: null, bounds: null };
394
- window.removeEventListener(`mousemove`, on_window_mouse_move);
395
- window.removeEventListener(`mouseup`, on_window_mouse_up);
396
- document.body.style.cursor = `default`;
397
- };
398
- // Pan drag handlers
399
- const on_pan_move = (evt) => {
400
- if (!pan_drag_state)
401
- return;
402
- const dx = evt.clientX - pan_drag_state.start.x;
403
- 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
+
404
689
  // Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
405
- const plot_width = width - pad.l - pad.r;
406
- const plot_height = height - pad.t - pad.b;
407
- const sensitivity = pan?.drag_sensitivity ?? 1;
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);
410
- const y_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y_range, plot_height);
411
- const y2_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y2_range, plot_height);
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);
414
- ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta);
415
- ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta);
416
- };
417
- const on_pan_end = () => {
418
- pan_drag_state = null;
419
- document.body.style.cursor = ``;
420
- window.removeEventListener(`mousemove`, on_pan_move);
421
- window.removeEventListener(`mouseup`, on_pan_end);
422
- };
423
- function handle_mouse_down(evt) {
424
- const coords = get_relative_coords(evt);
425
- if (!coords || !svg_element)
426
- 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
+
427
732
  // Check if pan is enabled and shift is held for pan mode
428
- const pan_enabled = pan?.enabled !== false;
733
+ const pan_enabled = pan?.enabled !== false
429
734
  if (pan_enabled && evt.shiftKey) {
430
- evt.preventDefault();
431
- pan_drag_state = {
432
- start: { x: evt.clientX, y: evt.clientY },
433
- initial_x_range: [...ranges.current.x],
434
- initial_x2_range: [...ranges.current.x2],
435
- initial_y_range: [...ranges.current.y],
436
- initial_y2_range: [...ranges.current.y2],
437
- };
438
- document.body.style.cursor = `grabbing`;
439
- window.addEventListener(`mousemove`, on_pan_move);
440
- window.addEventListener(`mouseup`, on_pan_end);
441
- 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
442
747
  }
748
+
443
749
  drag_state = {
444
- start: coords,
445
- current: coords,
446
- bounds: svg_element.getBoundingClientRect(),
447
- };
448
- window.addEventListener(`mousemove`, on_window_mouse_move);
449
- window.addEventListener(`mouseup`, on_window_mouse_up);
450
- evt.preventDefault();
451
- }
452
- // Wheel handler for pan (requires focus and shift)
453
- function handle_wheel(evt) {
454
- 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
455
762
  // Only capture wheel when focused AND Shift is held
456
763
  // Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
457
- if (!pan_enabled || !is_focused || !shift_held)
458
- return;
459
- evt.preventDefault();
764
+ if (!pan_enabled || !is_focused || !shift_held) return
765
+
766
+ evt.preventDefault()
767
+
460
768
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
461
- const plot_width = Math.max(1, width - pad.l - pad.r);
462
- const plot_height = Math.max(1, height - pad.t - pad.b);
463
- 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
+
464
773
  // Determine pan direction based on wheel delta
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);
467
- const y_delta = pixels_to_data_delta(evt.deltaY * sensitivity, ranges.current.y, plot_height);
468
- 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
+
469
795
  if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
470
- ranges.current.x = pan_range(ranges.current.x, x_delta);
471
- ranges.current.x2 = pan_range(ranges.current.x2, x2_delta);
472
- }
473
- else {
474
- ranges.current.y = pan_range(ranges.current.y, y_delta);
475
- ranges.current.y2 = pan_range(ranges.current.y2, y2_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)
476
801
  }
477
- }
478
- // Touch handlers for pinch-zoom and two-finger pan
479
- function handle_touch_start(evt) {
480
- const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false;
481
- if (!touch_enabled || evt.touches.length !== 2)
482
- return;
483
- evt.preventDefault();
484
- 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)
485
811
  touch_state = {
486
- start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
487
- initial_x_range: [...ranges.current.x],
488
- initial_x2_range: [...ranges.current.x2],
489
- initial_y_range: [...ranges.current.y],
490
- initial_y2_range: [...ranges.current.y2],
491
- };
492
- }
493
- function handle_touch_move(evt) {
494
- if (!touch_state || evt.touches.length !== 2)
495
- return;
496
- evt.preventDefault();
497
- const [t1, t2] = Array.from(evt.touches);
498
- 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
+
499
827
  // Calculate center movement for pan
500
- 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 }
501
829
  const curr_center = {
502
- x: (t1.clientX + t2.clientX) / 2,
503
- y: (t1.clientY + t2.clientY) / 2,
504
- };
505
- const dx = curr_center.x - start_center.x;
506
- 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
+
507
836
  // Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
508
- 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)
509
838
  // Guard against zero-distance pinch to avoid Infinity scale
510
- if (start_dist < Number.EPSILON)
511
- return;
512
- const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
513
- 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
+
514
843
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
515
- const plot_width = Math.max(1, width - pad.l - pad.r);
516
- 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
+
517
847
  // If scale changed significantly, treat as pinch-zoom
518
848
  // Also guard against scale being too small to avoid division by zero
519
849
  if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
520
- // Pinch zoom centered on gesture center
521
- // Divide by scale so spread (scale > 1) = smaller span (zoom in)
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];
525
- const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0];
526
- const y2_span = touch_state.initial_y2_range[1] -
527
- touch_state.initial_y2_range[0];
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;
530
- const y_center = (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2;
531
- const y2_center = (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2;
532
- ranges.current.x = [
533
- x_center - x_span / scale / 2,
534
- x_center + x_span / scale / 2,
535
- ];
536
- ranges.current.x2 = [
537
- x2_center - x2_span / scale / 2,
538
- x2_center + x2_span / scale / 2,
539
- ];
540
- ranges.current.y = [
541
- y_center - y_span / scale / 2,
542
- y_center + y_span / scale / 2,
543
- ];
544
- ranges.current.y2 = [
545
- y2_center - y2_span / scale / 2,
546
- y2_center + y2_span / scale / 2,
547
- ];
548
- }
549
- else {
550
- // Pan
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);
553
- const y_delta = pixels_to_data_delta(dy, touch_state.initial_y_range, plot_height);
554
- const y2_delta = pixels_to_data_delta(dy, touch_state.initial_y2_range, plot_height);
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);
557
- ranges.current.y = pan_range(touch_state.initial_y_range, y_delta);
558
- 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)
559
909
  }
560
- }
561
- function handle_touch_end() {
562
- touch_state = null;
563
- }
564
- function handle_double_click() {
910
+ }
911
+
912
+ function handle_touch_end() {
913
+ touch_state = null
914
+ }
915
+
916
+ function handle_double_click() {
565
917
  // Reset zoom to initial ranges (undo any pan/zoom)
566
- ranges.current.x = [...ranges.initial.x];
567
- ranges.current.x2 = [...ranges.initial.x2];
568
- ranges.current.y = [...ranges.initial.y];
569
- 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]
570
922
  // Also reset axis props so future data changes recalculate auto ranges
571
- x_axis = { ...x_axis, range: [null, null] };
572
- x2_axis = { ...x2_axis, range: [null, null] };
573
- y_axis = { ...y_axis, range: [null, null] };
574
- y2_axis = { ...y2_axis, range: [null, null] };
575
- }
576
- function handle_mouse_move(evt, value, count, property, active_y_axis = `y1`, series_idx = 0, active_x_axis = `x1`) {
577
- 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
578
939
  hover_info = {
579
- value,
580
- count,
581
- property,
582
- active_y_axis,
583
- active_x_axis,
584
- x: value,
585
- y: count,
586
- series_idx,
587
- metadata: null,
588
- label: property,
589
- x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
590
- x2_axis,
591
- y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
592
- y2_axis,
593
- };
594
- change({ value, count, property });
595
- on_bar_hover?.({ value, count, property, event: evt });
596
- }
597
- 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) {
598
960
  if (series_idx >= 0 && series_idx < series.length) {
599
- // Toggle series visibility
600
- series = series.map((srs, idx) => {
601
- if (idx === series_idx)
602
- return { ...srs, visible: !(srs.visible ?? true) };
603
- return srs;
604
- });
605
- (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)
606
967
  }
607
- }
608
- // Set theme-aware background when entering fullscreen
609
- $effect(() => {
610
- set_fullscreen_bg(wrapper, fullscreen, `--histogram-fullscreen-bg`);
611
- });
612
- // State accessors for shared axis change handler
613
- const axis_state = {
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> = {
614
977
  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;
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
622
982
  },
623
983
  set_axis: (axis, config) => {
624
- // Spread into existing state to preserve merged type structure
625
- if (axis === `x`)
626
- x_axis = { ...x_axis, ...config };
627
- else if (axis === `x2`)
628
- x2_axis = { ...x2_axis, ...config };
629
- else if (axis === `y`)
630
- y_axis = { ...y_axis, ...config };
631
- else
632
- 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 }
633
989
  },
634
990
  get_series: () => series,
635
991
  set_series: (new_series) => (series = new_series),
636
992
  get_loading: () => axis_loading,
637
993
  set_loading: (axis) => (axis_loading = axis),
638
- };
639
- // Create shared handler bound to this component's state
640
- // Using $derived so handler updates when callback props change
641
- const handle_axis_change = $derived(create_axis_change_handler(axis_state, data_loader, on_axis_change, on_error));
642
- let auto_load_attempted = false; // prevent infinite retries on failure
643
- // Auto-load data if series is empty but options exist (runs once)
644
- $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(() => {
645
1009
  if (series.length === 0 && data_loader && !auto_load_attempted) {
646
- // Check x-axis first, then y-axis
647
- if (x_axis.options?.length) {
648
- auto_load_attempted = true;
649
- const first_key = x_axis.selected_key ?? x_axis.options[0].key;
650
- handle_axis_change(`x`, first_key).catch(() => { });
651
- }
652
- else if (y_axis.options?.length) {
653
- auto_load_attempted = true;
654
- const first_key = y_axis.selected_key ?? y_axis.options[0].key;
655
- handle_axis_change(`y`, first_key).catch(() => { });
656
- }
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
+ }
657
1020
  }
658
- });
1021
+ })
659
1022
  </script>
660
1023
 
661
1024
  {#snippet ref_lines_layer(lines: IndexedRefLine[])}