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.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,661 +1,1024 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
:
|
|
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 = (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const new_x2 = final_x2_axis.range
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const new_y = final_y_axis.range
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const new_y2 = final_y2_axis.range
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
370
|
+
new_x[1] !== ranges.initial.x[1]
|
|
160
371
|
const x2_changed = new_x2[0] !== ranges.initial.x2[0] ||
|
|
161
|
-
|
|
372
|
+
new_x2[1] !== ranges.initial.x2[1]
|
|
162
373
|
const y_changed = new_y[0] !== ranges.initial.y[0] ||
|
|
163
|
-
|
|
374
|
+
new_y[1] !== ranges.initial.y[1]
|
|
164
375
|
const y2_changed = new_y2[0] !== ranges.initial.y2[0] ||
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (x2_changed)
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
470
|
+
.domain([ranges.current.x[0], ranges.current.x[1]])
|
|
471
|
+
.thresholds(bins)
|
|
230
472
|
const x2_hist_generator = x2_series.length > 0
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})
|
|
255
|
-
})
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
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
|
-
|
|
310
|
-
|
|
581
|
+
? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
|
|
582
|
+
: { width: 120, height: 60 }
|
|
583
|
+
|
|
311
584
|
const result = compute_element_placement({
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
613
|
+
(is_responsive || !has_initial_legend_placement))
|
|
614
|
+
|
|
335
615
|
if (should_update) {
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
window.removeEventListener(`
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
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
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
449
|
-
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
450
|
-
evt.preventDefault()
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
evt.preventDefault()
|
|
497
|
-
|
|
498
|
-
const [
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
]
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
change({ value, count, property })
|
|
595
|
-
on_bar_hover?.({ value, count, property, event: evt })
|
|
596
|
-
}
|
|
597
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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[])}
|