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
package/dist/plot/BarPlot.svelte
CHANGED
|
@@ -1,37 +1,227 @@
|
|
|
1
1
|
<script
|
|
2
2
|
lang="ts"
|
|
3
3
|
generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
|
|
4
|
-
>
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
4
|
+
>
|
|
5
|
+
import type { D3ColorSchemeName, D3InterpolateName } from '../colors'
|
|
6
|
+
import { format_value } from '../labels'
|
|
7
|
+
import { sanitize_html } from '../sanitize'
|
|
8
|
+
import { FullscreenToggle, set_fullscreen_bg } from '../layout'
|
|
9
|
+
import type {
|
|
10
|
+
AxisLoadError,
|
|
11
|
+
BarHandlerProps,
|
|
12
|
+
BarMode,
|
|
13
|
+
BarSeries,
|
|
14
|
+
BarStyle,
|
|
15
|
+
BasePlotProps,
|
|
16
|
+
DataLoaderFn,
|
|
17
|
+
InitialRanges,
|
|
18
|
+
InternalPoint,
|
|
19
|
+
LegendConfig,
|
|
20
|
+
LegendItem,
|
|
21
|
+
LineStyle,
|
|
22
|
+
Orientation,
|
|
23
|
+
PanConfig,
|
|
24
|
+
PlotConfig,
|
|
25
|
+
RefLine,
|
|
26
|
+
RefLineEvent,
|
|
27
|
+
ScaleType,
|
|
28
|
+
TweenedOptions,
|
|
29
|
+
UserContentProps,
|
|
30
|
+
XyObj,
|
|
31
|
+
} from './'
|
|
32
|
+
import {
|
|
33
|
+
AxisLabel,
|
|
34
|
+
BarPlotControls,
|
|
35
|
+
compute_element_placement,
|
|
36
|
+
PlotLegend,
|
|
37
|
+
ReferenceLine,
|
|
38
|
+
ScatterPoint,
|
|
39
|
+
} from './'
|
|
40
|
+
import type { AxisChangeState } from './axis-utils'
|
|
41
|
+
import { create_axis_change_handler } from './axis-utils'
|
|
42
|
+
import { process_prop } from './data-transform'
|
|
43
|
+
import {
|
|
44
|
+
create_dimension_tracker,
|
|
45
|
+
create_hover_lock,
|
|
46
|
+
} from './hover-lock.svelte'
|
|
47
|
+
import {
|
|
48
|
+
get_relative_coords,
|
|
49
|
+
pan_range,
|
|
50
|
+
PINCH_ZOOM_THRESHOLD,
|
|
51
|
+
pixels_to_data_delta,
|
|
52
|
+
} from './interactions'
|
|
53
|
+
import type { IndexedRefLine } from './reference-line'
|
|
54
|
+
import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
|
|
55
|
+
import {
|
|
56
|
+
create_color_scale,
|
|
57
|
+
create_scale,
|
|
58
|
+
create_size_scale,
|
|
59
|
+
generate_ticks,
|
|
60
|
+
get_nice_data_range,
|
|
61
|
+
get_tick_label,
|
|
62
|
+
} from './scales'
|
|
63
|
+
import {
|
|
64
|
+
DEFAULT_GRID_STYLE,
|
|
65
|
+
DEFAULT_MARKERS,
|
|
66
|
+
get_scale_type_name,
|
|
67
|
+
} from './types'
|
|
68
|
+
import { DEFAULTS } from '../settings'
|
|
69
|
+
import { extent } from 'd3-array'
|
|
70
|
+
import type { Snippet } from 'svelte'
|
|
71
|
+
import { untrack } from 'svelte'
|
|
72
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
73
|
+
import { Tween } from 'svelte/motion'
|
|
74
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
75
|
+
import type { Vec2 } from '../math'
|
|
76
|
+
import {
|
|
77
|
+
calc_auto_padding,
|
|
78
|
+
constrain_tooltip_position,
|
|
79
|
+
filter_padding,
|
|
80
|
+
LABEL_GAP_DEFAULT,
|
|
81
|
+
measure_max_tick_width,
|
|
82
|
+
} from './layout'
|
|
83
|
+
import PlotTooltip from './PlotTooltip.svelte'
|
|
84
|
+
import { bar_path } from './svg'
|
|
85
|
+
import ZeroLines from './ZeroLines.svelte'
|
|
86
|
+
import ZoomRect from './ZoomRect.svelte'
|
|
87
|
+
|
|
88
|
+
// Handler props for line marker events (extends BarHandlerProps with point-specific data)
|
|
89
|
+
interface LineMarkerHandlerProps extends BarHandlerProps<Metadata> {
|
|
90
|
+
point: InternalPoint<Metadata>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extended point type with computed screen coordinates (used internally for rendering)
|
|
94
|
+
type LineSeriesPoint = InternalPoint<Metadata> & {
|
|
95
|
+
x: number // Screen x coordinate
|
|
96
|
+
y: number // Screen y coordinate
|
|
97
|
+
data_x: number // Original data x value
|
|
98
|
+
data_y: number // Original data y value
|
|
99
|
+
idx: number // Index in series
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let {
|
|
103
|
+
series = $bindable([]),
|
|
104
|
+
orientation = $bindable(`vertical`),
|
|
105
|
+
mode = $bindable(`overlay`),
|
|
106
|
+
x_axis = $bindable({}),
|
|
107
|
+
x2_axis = $bindable({}),
|
|
108
|
+
y_axis = $bindable({}),
|
|
109
|
+
y2_axis = $bindable({}),
|
|
110
|
+
display = $bindable(DEFAULTS.bar.display),
|
|
111
|
+
x_range = [null, null],
|
|
112
|
+
x2_range = [null, null],
|
|
113
|
+
y_range = [null, null],
|
|
114
|
+
y2_range = [null, null],
|
|
115
|
+
range_padding = 0.05,
|
|
116
|
+
padding = { t: 20, b: 60, l: 60, r: 20 },
|
|
117
|
+
legend = {},
|
|
118
|
+
show_legend,
|
|
119
|
+
bar = {},
|
|
120
|
+
line = {},
|
|
121
|
+
tooltip,
|
|
122
|
+
user_content,
|
|
123
|
+
hovered = $bindable(false),
|
|
124
|
+
change = () => {},
|
|
125
|
+
on_bar_click,
|
|
126
|
+
on_bar_hover,
|
|
127
|
+
// Line marker props (matching ScatterPlot)
|
|
128
|
+
color_scale = {
|
|
129
|
+
type: `linear`,
|
|
130
|
+
scheme: `interpolateViridis`,
|
|
131
|
+
value_range: undefined,
|
|
132
|
+
},
|
|
133
|
+
size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
|
|
134
|
+
point_tween,
|
|
135
|
+
on_point_click,
|
|
136
|
+
on_point_hover,
|
|
137
|
+
ref_lines = $bindable([]),
|
|
138
|
+
on_ref_line_click,
|
|
139
|
+
on_ref_line_hover,
|
|
140
|
+
show_controls = $bindable(true),
|
|
141
|
+
controls_open = $bindable(false),
|
|
142
|
+
controls_toggle_props,
|
|
143
|
+
controls_pane_props,
|
|
144
|
+
fullscreen = $bindable(false),
|
|
145
|
+
fullscreen_toggle = true,
|
|
146
|
+
children,
|
|
147
|
+
header_controls,
|
|
148
|
+
controls_extra,
|
|
149
|
+
data_loader,
|
|
150
|
+
on_axis_change,
|
|
151
|
+
on_error,
|
|
152
|
+
pan = {},
|
|
153
|
+
...rest
|
|
154
|
+
}: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
|
|
155
|
+
series?: BarSeries<Metadata>[]
|
|
156
|
+
// Component-specific props
|
|
157
|
+
orientation?: Orientation
|
|
158
|
+
mode?: BarMode
|
|
159
|
+
legend?: LegendConfig | null
|
|
160
|
+
show_legend?: boolean
|
|
161
|
+
bar?: BarStyle
|
|
162
|
+
line?: LineStyle
|
|
163
|
+
tooltip?: Snippet<[BarHandlerProps<Metadata>]>
|
|
164
|
+
user_content?: Snippet<[UserContentProps]>
|
|
165
|
+
header_controls?: Snippet<
|
|
166
|
+
[{ height: number; width: number; fullscreen: boolean }]
|
|
167
|
+
>
|
|
168
|
+
controls_extra?: Snippet<
|
|
169
|
+
[{ orientation: Orientation; mode: BarMode } & Required<PlotConfig>]
|
|
170
|
+
>
|
|
171
|
+
change?: (data: BarHandlerProps<Metadata> | null) => void
|
|
172
|
+
on_bar_click?: (
|
|
173
|
+
data: BarHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
|
|
174
|
+
) => void
|
|
175
|
+
on_bar_hover?: (
|
|
176
|
+
data:
|
|
177
|
+
| (BarHandlerProps<Metadata> & {
|
|
178
|
+
event: MouseEvent | FocusEvent | KeyboardEvent
|
|
179
|
+
})
|
|
180
|
+
| null,
|
|
181
|
+
) => void
|
|
182
|
+
// Line marker props (matching ScatterPlot)
|
|
183
|
+
// Note: For line series with markers, BOTH on_bar_* AND on_point_* events fire.
|
|
184
|
+
// Use on_point_* for marker-specific data (includes `point` with InternalPoint details)
|
|
185
|
+
// or on_bar_* for backward compatibility with bar-style event handling.
|
|
186
|
+
color_scale?: {
|
|
187
|
+
type?: ScaleType
|
|
188
|
+
scheme?: D3ColorSchemeName | D3InterpolateName
|
|
189
|
+
value_range?: [number, number]
|
|
190
|
+
} | D3InterpolateName
|
|
191
|
+
size_scale?: {
|
|
192
|
+
type?: ScaleType
|
|
193
|
+
radius_range?: [number, number]
|
|
194
|
+
value_range?: [number, number]
|
|
195
|
+
}
|
|
196
|
+
point_tween?: TweenedOptions<XyObj>
|
|
197
|
+
on_point_click?: (
|
|
198
|
+
data: LineMarkerHandlerProps & { event: MouseEvent | KeyboardEvent },
|
|
199
|
+
) => void
|
|
200
|
+
on_point_hover?: (
|
|
201
|
+
data:
|
|
202
|
+
| (LineMarkerHandlerProps & {
|
|
203
|
+
event: MouseEvent | FocusEvent | KeyboardEvent
|
|
204
|
+
})
|
|
205
|
+
| null,
|
|
206
|
+
) => void
|
|
207
|
+
ref_lines?: RefLine[]
|
|
208
|
+
on_ref_line_click?: (event: RefLineEvent) => void
|
|
209
|
+
on_ref_line_hover?: (event: RefLineEvent | null) => void
|
|
210
|
+
// Interactive axis props
|
|
211
|
+
data_loader?: DataLoaderFn<Metadata, BarSeries<Metadata>>
|
|
212
|
+
on_axis_change?: (
|
|
213
|
+
axis: `x` | `x2` | `y` | `y2`,
|
|
214
|
+
key: string,
|
|
215
|
+
new_series: BarSeries<Metadata>[],
|
|
216
|
+
) => void
|
|
217
|
+
on_error?: (error: AxisLoadError) => void
|
|
218
|
+
pan?: PanConfig
|
|
219
|
+
} = $props()
|
|
220
|
+
|
|
221
|
+
// Initialize bar, line, y2_axis with defaults - using $derived for reactivity
|
|
222
|
+
let bar_state = $derived({ ...DEFAULTS.bar.bar, ...bar })
|
|
223
|
+
let line_state = $derived({ ...DEFAULTS.bar.line, ...line })
|
|
224
|
+
y2_axis = {
|
|
35
225
|
format: ``,
|
|
36
226
|
scale_type: `linear`,
|
|
37
227
|
ticks: 5,
|
|
@@ -39,8 +229,8 @@ y2_axis = {
|
|
|
39
229
|
tick: { label: { shift: { x: 0, y: 0 } } }, // base offset handled in rendering
|
|
40
230
|
range: [null, null],
|
|
41
231
|
...y2_axis,
|
|
42
|
-
}
|
|
43
|
-
x2_axis = {
|
|
232
|
+
}
|
|
233
|
+
x2_axis = {
|
|
44
234
|
format: ``,
|
|
45
235
|
scale_type: `linear`,
|
|
46
236
|
ticks: 5,
|
|
@@ -48,815 +238,1073 @@ x2_axis = {
|
|
|
48
238
|
tick: { label: { shift: { x: 0, y: 0 } } },
|
|
49
239
|
range: [null, null],
|
|
50
240
|
...x2_axis,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
let
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let [width, height] = $state([0, 0])
|
|
244
|
+
let wrapper: HTMLDivElement | undefined = $state()
|
|
245
|
+
let svg_element: SVGElement | null = $state(null)
|
|
246
|
+
let clip_path_id = `chart-clip-${crypto?.randomUUID?.()}`
|
|
247
|
+
|
|
248
|
+
// Reference line hover state
|
|
249
|
+
let hovered_ref_line_idx = $state<number | null>(null)
|
|
250
|
+
|
|
251
|
+
// Interactive axis loading state
|
|
252
|
+
let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
|
|
253
|
+
|
|
254
|
+
// Compute ref_lines with index and group by z-index (using shared utilities)
|
|
255
|
+
let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
|
|
256
|
+
let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
|
|
257
|
+
|
|
258
|
+
// === Categorical Normalization ===
|
|
259
|
+
// Internal type with guaranteed numeric x (for downstream scale/rendering code)
|
|
260
|
+
type NumericBarSeries = Omit<BarSeries<Metadata>, `x`> & { x: readonly number[] }
|
|
261
|
+
|
|
262
|
+
let is_categorical = $derived(
|
|
263
|
+
series.some((srs) => srs.x.some((val) => typeof val === `string`)),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
let category_list = $derived.by(() => {
|
|
267
|
+
if (!is_categorical) return [] as string[]
|
|
268
|
+
if (x_axis.categories?.length) return [...x_axis.categories]
|
|
269
|
+
return [...new Set(series.flatMap((srs) => srs.x.map(String)))]
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
let category_indices = $derived(
|
|
273
|
+
category_list.length ? category_list.map((_, idx) => idx) : null,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
let internal_series = $derived.by<NumericBarSeries[]>(() => {
|
|
73
277
|
// safe: when !category_indices, all x values are numeric (is_categorical is false)
|
|
74
|
-
if (!category_indices)
|
|
75
|
-
return series;
|
|
278
|
+
if (!category_indices) return series as unknown as NumericBarSeries[]
|
|
76
279
|
return series.map((srs) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
280
|
+
const orig_map = new Map(srs.x.map((val, idx) => [String(val), idx]))
|
|
281
|
+
if (orig_map.size < srs.x.length) {
|
|
282
|
+
console.warn(
|
|
283
|
+
`BarPlot: series "${
|
|
284
|
+
srs.label ?? `?`
|
|
285
|
+
}" has duplicate x values — last occurrence wins`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
// Resolve original index for each category (undefined if series lacks it)
|
|
289
|
+
const orig_indices = category_list.map((cat) => orig_map.get(cat))
|
|
290
|
+
const remap = <T>(arr: readonly T[] | null | undefined, fallback: T): T[] =>
|
|
291
|
+
orig_indices.map((oi) => oi != null ? (arr?.[oi] ?? fallback) : fallback)
|
|
292
|
+
const bw_arr = Array.isArray(srs.bar_width) ? srs.bar_width : null
|
|
293
|
+
const meta_arr = Array.isArray(srs.metadata) ? srs.metadata : null
|
|
294
|
+
return {
|
|
295
|
+
...srs,
|
|
296
|
+
x: category_indices,
|
|
297
|
+
y: remap(srs.y, srs.render_mode === `line` ? NaN : 0),
|
|
298
|
+
labels: remap(srs.labels, null),
|
|
299
|
+
metadata: orig_indices.map((oi) =>
|
|
300
|
+
oi != null ? (meta_arr ? meta_arr[oi] : srs.metadata) : undefined
|
|
301
|
+
) as Metadata[],
|
|
302
|
+
...(bw_arr ? { bar_width: remap(bw_arr, 0.5) } : {}),
|
|
303
|
+
...(srs.color_values ? { color_values: remap(srs.color_values, null) } : {}),
|
|
304
|
+
...(srs.size_values ? { size_values: remap(srs.size_values, null) } : {}),
|
|
305
|
+
} as NumericBarSeries
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Compute auto ranges from visible series
|
|
310
|
+
let visible_series = $derived(
|
|
311
|
+
internal_series.filter((srs) => srs?.visible ?? true),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// Separate series by y-axis
|
|
315
|
+
let y1_series = $derived(
|
|
316
|
+
visible_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`),
|
|
317
|
+
)
|
|
318
|
+
let y2_series = $derived(
|
|
319
|
+
visible_series.filter((srs) => srs.y_axis === `y2`),
|
|
320
|
+
)
|
|
321
|
+
let x2_series = $derived(
|
|
322
|
+
visible_series.filter((srs) => srs.x_axis === `x2`),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
let auto_ranges = $derived.by(() => {
|
|
105
326
|
// Calculate separate ranges for y1 and y2 axes
|
|
106
|
-
const calc_y_range = (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
327
|
+
const calc_y_range = (
|
|
328
|
+
series_list: typeof visible_series,
|
|
329
|
+
y_limit: typeof y_range,
|
|
330
|
+
scale_type: ScaleType,
|
|
331
|
+
) => {
|
|
332
|
+
let points = series_list.flatMap((srs) =>
|
|
333
|
+
srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// In stacked mode, calculate stacked totals for accurate range (only for bars on the same axis)
|
|
337
|
+
if (mode === `stacked`) {
|
|
338
|
+
const stacked_totals = new SvelteMap<number, { pos: number; neg: number }>()
|
|
339
|
+
|
|
340
|
+
// Only include visible bar series (not lines) in stacking
|
|
341
|
+
series_list
|
|
342
|
+
.filter((srs) => srs.render_mode !== `line`)
|
|
343
|
+
.forEach((srs) =>
|
|
344
|
+
srs.x.forEach((x_val, idx) => {
|
|
345
|
+
const y_val = srs.y[idx] ?? 0
|
|
346
|
+
const totals = stacked_totals.get(x_val) ?? { pos: 0, neg: 0 }
|
|
347
|
+
if (y_val >= 0) totals.pos += y_val
|
|
348
|
+
else totals.neg += y_val
|
|
349
|
+
stacked_totals.set(x_val, totals)
|
|
350
|
+
})
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Replace points with stacked totals + line series (which don't stack)
|
|
354
|
+
points = [
|
|
355
|
+
...Array.from(stacked_totals).flatMap(([x_val, { pos, neg }]) => [
|
|
356
|
+
...(pos > 0 ? [{ x: x_val, y: pos }] : []),
|
|
357
|
+
...(neg < 0 ? [{ x: x_val, y: neg }] : []),
|
|
358
|
+
]),
|
|
359
|
+
...series_list
|
|
360
|
+
.filter((srs) => srs.render_mode === `line`)
|
|
361
|
+
.flatMap((srs) =>
|
|
362
|
+
srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
|
|
363
|
+
),
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!points.length) return [0, 1]
|
|
368
|
+
|
|
369
|
+
let y_range = get_nice_data_range(
|
|
370
|
+
points,
|
|
371
|
+
(pt) => pt.y,
|
|
372
|
+
y_limit,
|
|
373
|
+
scale_type,
|
|
374
|
+
range_padding,
|
|
375
|
+
false,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// For bar plots, ensure the value axis starts at 0 unless there are negative values
|
|
379
|
+
// Only apply zero-clamping for linear and arcsinh scales (not log)
|
|
380
|
+
const type_name = get_scale_type_name(scale_type)
|
|
381
|
+
if (type_name === `linear` || type_name === `arcsinh`) {
|
|
382
|
+
const has_negative = points.some((pt) => pt.y < 0)
|
|
383
|
+
const has_positive = points.some((pt) => pt.y > 0)
|
|
384
|
+
|
|
385
|
+
// Only adjust if no explicit y_range is set
|
|
386
|
+
if (y_limit?.[0] == null && y_limit?.[1] == null) {
|
|
387
|
+
if (has_positive && !has_negative) y_range = [0, y_range[1]]
|
|
388
|
+
else if (has_negative && !has_positive) y_range = [y_range[0], 0]
|
|
150
389
|
}
|
|
151
|
-
|
|
152
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return y_range
|
|
393
|
+
}
|
|
394
|
+
|
|
153
395
|
// Get x values split by axis for range calculation
|
|
154
396
|
// For categorical data, use fixed range centered on integer indices
|
|
155
|
-
let x_auto_range
|
|
397
|
+
let x_auto_range: number[]
|
|
156
398
|
if (category_list.length) {
|
|
157
|
-
|
|
399
|
+
x_auto_range = [-0.5, category_list.length - 0.5]
|
|
400
|
+
} else {
|
|
401
|
+
const x1_x_points = visible_series
|
|
402
|
+
.filter((srs) => (srs.x_axis ?? `x1`) === `x1`)
|
|
403
|
+
.flatMap((srs) => srs.x.map((x_val) => ({ x: x_val, y: 0 })))
|
|
404
|
+
x_auto_range = x1_x_points.length
|
|
405
|
+
? get_nice_data_range(
|
|
406
|
+
x1_x_points,
|
|
407
|
+
(pt) => pt.x,
|
|
408
|
+
x_range,
|
|
409
|
+
x_axis.scale_type ?? `linear`,
|
|
410
|
+
range_padding,
|
|
411
|
+
x_axis.format?.startsWith(`%`) || false,
|
|
412
|
+
)
|
|
413
|
+
: [0, 1]
|
|
158
414
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
? get_nice_data_range(x1_x_points, (pt) => pt.x, x_range, x_axis.scale_type ?? `linear`, range_padding, x_axis.format?.startsWith(`%`) || false)
|
|
165
|
-
: [0, 1];
|
|
166
|
-
}
|
|
167
|
-
const x2_x_points = x2_series.flatMap((srs) => srs.x.map((x_val) => ({ x: x_val, y: 0 })));
|
|
168
|
-
const x2_scale_type = x2_axis.scale_type ?? `linear`;
|
|
415
|
+
|
|
416
|
+
const x2_x_points = x2_series.flatMap((srs) =>
|
|
417
|
+
srs.x.map((x_val) => ({ x: x_val, y: 0 }))
|
|
418
|
+
)
|
|
419
|
+
const x2_scale_type = x2_axis.scale_type ?? `linear`
|
|
169
420
|
const x2_auto_range = x2_x_points.length
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
421
|
+
? get_nice_data_range(
|
|
422
|
+
x2_x_points,
|
|
423
|
+
(pt) => pt.x,
|
|
424
|
+
x2_range,
|
|
425
|
+
x2_scale_type,
|
|
426
|
+
range_padding,
|
|
427
|
+
x2_axis.format?.startsWith(`%`) || false,
|
|
428
|
+
)
|
|
429
|
+
: [0, 1]
|
|
430
|
+
|
|
431
|
+
const y1_range = calc_y_range(y1_series, y_range, y_axis.scale_type ?? `linear`)
|
|
432
|
+
const y2_auto_range = calc_y_range(
|
|
433
|
+
y2_series,
|
|
434
|
+
y2_range,
|
|
435
|
+
y2_axis.scale_type ?? `linear`,
|
|
436
|
+
)
|
|
437
|
+
|
|
174
438
|
// Map data ranges to axis ranges depending on orientation
|
|
175
439
|
return orientation === `horizontal`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
440
|
+
? ({ x: y1_range, x2: x2_auto_range, y: x_auto_range, y2: y2_auto_range })
|
|
441
|
+
: ({ x: x_auto_range, x2: x2_auto_range, y: y1_range, y2: y2_auto_range })
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// Initialize and current ranges
|
|
445
|
+
let ranges = $state<{
|
|
446
|
+
initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
447
|
+
current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
448
|
+
}>({
|
|
181
449
|
initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
182
450
|
current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
183
|
-
})
|
|
184
|
-
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
$effect(() => { // handle x_axis.range / x2_axis.range / y_axis.range / y2_axis.range changes
|
|
185
454
|
const new_x = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
]
|
|
455
|
+
x_axis.range?.[0] ?? auto_ranges.x[0],
|
|
456
|
+
x_axis.range?.[1] ?? auto_ranges.x[1],
|
|
457
|
+
] as Vec2
|
|
189
458
|
const new_x2 = [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
]
|
|
459
|
+
x2_axis.range?.[0] ?? auto_ranges.x2[0],
|
|
460
|
+
x2_axis.range?.[1] ?? auto_ranges.x2[1],
|
|
461
|
+
] as Vec2
|
|
193
462
|
const new_y = [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
]
|
|
463
|
+
y_axis.range?.[0] ?? auto_ranges.y[0],
|
|
464
|
+
y_axis.range?.[1] ?? auto_ranges.y[1],
|
|
465
|
+
] as Vec2
|
|
197
466
|
const new_y2 = [
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]
|
|
467
|
+
y2_axis.range?.[0] ?? auto_ranges.y2[0],
|
|
468
|
+
y2_axis.range?.[1] ?? auto_ranges.y2[1],
|
|
469
|
+
] as Vec2
|
|
201
470
|
// Only update if the initial (data-driven) ranges changed, not when user pans
|
|
202
471
|
// Comparing against initial preserves user's pan/zoom state
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
472
|
+
if (
|
|
473
|
+
ranges.initial.x[0] !== new_x[0] ||
|
|
474
|
+
ranges.initial.x[1] !== new_x[1] ||
|
|
475
|
+
ranges.initial.x2[0] !== new_x2[0] ||
|
|
476
|
+
ranges.initial.x2[1] !== new_x2[1] ||
|
|
477
|
+
ranges.initial.y[0] !== new_y[0] ||
|
|
478
|
+
ranges.initial.y[1] !== new_y[1] ||
|
|
479
|
+
ranges.initial.y2[0] !== new_y2[0] ||
|
|
480
|
+
ranges.initial.y2[1] !== new_y2[1]
|
|
481
|
+
) {
|
|
482
|
+
ranges = {
|
|
483
|
+
initial: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
|
|
484
|
+
current: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
|
|
485
|
+
}
|
|
215
486
|
}
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// Layout: dynamic padding based on tick label widths
|
|
490
|
+
const default_padding = { t: 20, b: 60, l: 60, r: 20 }
|
|
491
|
+
let pad = $derived(filter_padding(padding, default_padding))
|
|
492
|
+
|
|
493
|
+
// Update padding when format or ticks change
|
|
494
|
+
$effect(() => {
|
|
222
495
|
const new_pad = width && height && ticks.y.length
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
496
|
+
? calc_auto_padding({
|
|
497
|
+
padding,
|
|
498
|
+
default_padding,
|
|
499
|
+
x2_axis: { ...x2_axis, tick_values: ticks.x2 },
|
|
500
|
+
y_axis: { ...y_axis, tick_values: ticks.y },
|
|
501
|
+
y2_axis: { ...y2_axis, tick_values: ticks.y2 },
|
|
502
|
+
})
|
|
503
|
+
: filter_padding(padding, default_padding)
|
|
231
504
|
// Expand right padding if y2 ticks are shown (only for vertical orientation)
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
505
|
+
if (
|
|
506
|
+
width && height && y2_series.length && ticks.y2.length &&
|
|
507
|
+
orientation === `vertical`
|
|
508
|
+
) {
|
|
509
|
+
// Need space for: tick shift + tick width + gap (30px) + label space (20px if present)
|
|
510
|
+
// When ticks are inside, they don't contribute to padding
|
|
511
|
+
const inside = y2_axis.tick?.label?.inside ?? false
|
|
512
|
+
const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
|
|
513
|
+
const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
|
|
514
|
+
const label_space = y2_axis.label ? 20 : 0
|
|
515
|
+
new_pad.r = Math.max(
|
|
516
|
+
new_pad.r,
|
|
517
|
+
tick_shift + tick_width_contribution + 30 + label_space,
|
|
518
|
+
)
|
|
241
519
|
}
|
|
242
520
|
// Expand top padding if x2 ticks are shown (only for vertical orientation)
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
521
|
+
if (
|
|
522
|
+
width && height && x2_series.length && ticks.x2.length &&
|
|
523
|
+
orientation === `vertical`
|
|
524
|
+
) {
|
|
525
|
+
const inside = x2_axis.tick?.label?.inside ?? false
|
|
526
|
+
const tick_shift = inside ? 0 : Math.abs(x2_axis.tick?.label?.shift?.y ?? 0) + 5
|
|
527
|
+
const tick_height = inside ? 0 : 16
|
|
528
|
+
const label_space = x2_axis.label ? 20 : 0
|
|
529
|
+
new_pad.t = Math.max(new_pad.t, tick_shift + tick_height + 30 + label_space)
|
|
250
530
|
}
|
|
531
|
+
|
|
251
532
|
// Only update if padding actually changed (prevents infinite loop)
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
533
|
+
if (
|
|
534
|
+
pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
|
|
535
|
+
pad.r !== new_pad.r
|
|
536
|
+
) pad = new_pad
|
|
537
|
+
})
|
|
538
|
+
const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
|
|
539
|
+
const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
|
|
540
|
+
|
|
541
|
+
// Scales
|
|
542
|
+
let scales = $derived({
|
|
260
543
|
x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [
|
|
261
|
-
|
|
262
|
-
|
|
544
|
+
pad.l,
|
|
545
|
+
width - pad.r,
|
|
263
546
|
]),
|
|
264
547
|
x2: create_scale(x2_axis.scale_type ?? `linear`, ranges.current.x2, [
|
|
265
|
-
|
|
266
|
-
|
|
548
|
+
pad.l,
|
|
549
|
+
width - pad.r,
|
|
267
550
|
]),
|
|
268
551
|
y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [
|
|
269
|
-
|
|
270
|
-
|
|
552
|
+
height - pad.b,
|
|
553
|
+
pad.t,
|
|
271
554
|
]),
|
|
272
555
|
y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [
|
|
273
|
-
|
|
274
|
-
|
|
556
|
+
height - pad.b,
|
|
557
|
+
pad.t,
|
|
275
558
|
]),
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
let
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
let
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// Compute plot center for point tweening origin
|
|
562
|
+
let plot_center_x = $derived(pad.l + (width - pad.r - pad.l) / 2)
|
|
563
|
+
let plot_center_y = $derived(pad.t + (height - pad.b - pad.t) / 2)
|
|
564
|
+
|
|
565
|
+
// Compute color values from line series for color scaling (filter to numbers only)
|
|
566
|
+
let all_color_values = $derived(
|
|
567
|
+
visible_series
|
|
568
|
+
.filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
|
|
569
|
+
.flatMap((srs: BarSeries<Metadata>) =>
|
|
570
|
+
(srs.color_values ?? []).filter(
|
|
571
|
+
(val): val is number => typeof val === `number`,
|
|
572
|
+
)
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
// Create auto color range (safely handle empty arrays or undefined extent results)
|
|
577
|
+
let auto_color_range: [number, number] = $derived.by(() => {
|
|
578
|
+
if (all_color_values.length === 0) return [0, 1]
|
|
579
|
+
const [min_val, max_val] = extent(all_color_values)
|
|
580
|
+
return [min_val ?? 0, max_val ?? 1]
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// All size values from line series (for size scale, filter to numbers only)
|
|
584
|
+
let all_size_values = $derived(
|
|
585
|
+
visible_series
|
|
586
|
+
.filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
|
|
587
|
+
.flatMap((srs: BarSeries<Metadata>) =>
|
|
588
|
+
[...(srs.size_values ?? [])].filter(
|
|
589
|
+
(val): val is number => typeof val === `number`,
|
|
590
|
+
)
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
// Color scale function (using shared utility)
|
|
595
|
+
let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range))
|
|
596
|
+
|
|
597
|
+
// Size scale function (using shared utility)
|
|
598
|
+
let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
|
|
599
|
+
|
|
600
|
+
// Auto-generate tick labels for categorical data (unless user provides explicit ticks)
|
|
601
|
+
// In vertical mode categories are on x-axis; in horizontal mode on y-axis
|
|
602
|
+
let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
|
|
603
|
+
let effective_cat_ticks = $derived.by(() => {
|
|
604
|
+
if (!category_list.length) return undefined
|
|
305
605
|
// Only respect user ticks when they're a Record (custom label mapping),
|
|
306
606
|
// not a number (tick count) or array (tick positions)
|
|
307
|
-
const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
607
|
+
const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
|
|
608
|
+
if (
|
|
609
|
+
user_ticks != null && typeof user_ticks === `object` &&
|
|
610
|
+
!Array.isArray(user_ticks)
|
|
611
|
+
) return user_ticks
|
|
612
|
+
return Object.fromEntries(
|
|
613
|
+
category_list.map((cat, idx) => [idx, cat]),
|
|
614
|
+
) as Record<number, string>
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
// Ticks
|
|
618
|
+
let ticks = $derived({
|
|
315
619
|
x: width && height
|
|
316
|
-
|
|
317
|
-
|
|
620
|
+
? (category_indices && cat_axis === `x` ? category_indices : generate_ticks(
|
|
621
|
+
ranges.current.x,
|
|
622
|
+
x_axis.scale_type ?? `linear`,
|
|
623
|
+
x_axis.ticks,
|
|
624
|
+
scales.x,
|
|
625
|
+
{ default_count: 8 },
|
|
626
|
+
))
|
|
627
|
+
: [],
|
|
318
628
|
y: width && height
|
|
319
|
-
|
|
320
|
-
|
|
629
|
+
? (category_indices && cat_axis === `y` ? category_indices : generate_ticks(
|
|
630
|
+
ranges.current.y,
|
|
631
|
+
y_axis.scale_type ?? `linear`,
|
|
632
|
+
y_axis.ticks,
|
|
633
|
+
scales.y,
|
|
634
|
+
{ default_count: 6 },
|
|
635
|
+
))
|
|
636
|
+
: [],
|
|
321
637
|
y2: width && height && y2_series.length > 0 && orientation === `vertical`
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
638
|
+
? generate_ticks(
|
|
639
|
+
ranges.current.y2,
|
|
640
|
+
y2_axis.scale_type ?? `linear`,
|
|
641
|
+
y2_axis.ticks,
|
|
642
|
+
scales.y2,
|
|
643
|
+
{
|
|
644
|
+
default_count: 6,
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
: [],
|
|
326
648
|
x2: width && height && x2_series.length > 0 && orientation === `vertical`
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
649
|
+
? generate_ticks(
|
|
650
|
+
ranges.current.x2,
|
|
651
|
+
x2_axis.scale_type ?? `linear`,
|
|
652
|
+
x2_axis.ticks,
|
|
653
|
+
scales.x2,
|
|
654
|
+
{
|
|
655
|
+
default_count: 8,
|
|
656
|
+
},
|
|
657
|
+
)
|
|
658
|
+
: [],
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
// Cache measured tick-label widths so expensive canvas text measurement
|
|
662
|
+
// only runs when ticks/format change, not on every template rerender.
|
|
663
|
+
let tick_label_widths = $derived({
|
|
335
664
|
y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
|
|
336
665
|
y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
|
|
337
666
|
x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
// Zoom drag state
|
|
670
|
+
let drag_state = $state<{
|
|
671
|
+
start: { x: number; y: number } | null
|
|
672
|
+
current: { x: number; y: number } | null
|
|
673
|
+
bounds: DOMRect | null
|
|
674
|
+
}>({ start: null, current: null, bounds: null })
|
|
675
|
+
|
|
676
|
+
// Pan state
|
|
677
|
+
let is_focused = $state(false)
|
|
678
|
+
let shift_held = $state(false)
|
|
679
|
+
let pan_drag_state = $state<
|
|
680
|
+
InitialRanges & { start: { x: number; y: number } } | null
|
|
681
|
+
>(null)
|
|
682
|
+
let touch_state = $state<
|
|
683
|
+
InitialRanges & { start_touches: { x: number; y: number }[] } | null
|
|
684
|
+
>(null)
|
|
685
|
+
const on_window_mouse_move = (evt: MouseEvent) => {
|
|
686
|
+
if (!drag_state.start || !drag_state.bounds) return
|
|
349
687
|
drag_state.current = {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const on_window_mouse_up = () => {
|
|
688
|
+
x: evt.clientX - drag_state.bounds.left,
|
|
689
|
+
y: evt.clientY - drag_state.bounds.top,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const on_window_mouse_up = () => {
|
|
355
693
|
if (drag_state.start && drag_state.current) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
[x2r1, x2r2
|
|
388
|
-
|
|
389
|
-
// Update axis ranges to trigger reactivity and prevent effect from overriding
|
|
390
|
-
x_axis = { ...x_axis, range: [Math.min(xr1, xr2), Math.max(xr1, xr2)] };
|
|
391
|
-
if (x2_series.length > 0 && Number.isFinite(x2r1) && Number.isFinite(x2r2)) {
|
|
392
|
-
x2_axis = {
|
|
393
|
-
...x2_axis,
|
|
394
|
-
range: [Math.min(x2r1, x2r2), Math.max(x2r1, x2r2)],
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
y_axis = { ...y_axis, range: [Math.min(y1, y2), Math.max(y1, y2)] };
|
|
398
|
-
y2_axis = { ...y2_axis, range: [Math.min(y2_1, y2_2), Math.max(y2_1, y2_2)] };
|
|
694
|
+
const x1_raw = scales.x.invert(drag_state.start.x) as number | Date
|
|
695
|
+
const x2_raw = scales.x.invert(drag_state.current.x) as number | Date
|
|
696
|
+
const y1 = scales.y.invert(drag_state.start.y)
|
|
697
|
+
const y2 = scales.y.invert(drag_state.current.y)
|
|
698
|
+
const y2_1 = scales.y2.invert(drag_state.start.y)
|
|
699
|
+
const y2_2 = scales.y2.invert(drag_state.current.y)
|
|
700
|
+
const x2a_1_raw = scales.x2.invert(drag_state.start.x) as number | Date
|
|
701
|
+
const x2a_2_raw = scales.x2.invert(drag_state.current.x) as number | Date
|
|
702
|
+
const dx = Math.abs(drag_state.start.x - drag_state.current.x)
|
|
703
|
+
const dy = Math.abs(drag_state.start.y - drag_state.current.y)
|
|
704
|
+
|
|
705
|
+
let xr1: number, xr2: number
|
|
706
|
+
if (x1_raw instanceof Date && x2_raw instanceof Date) {
|
|
707
|
+
;[xr1, xr2] = [x1_raw.getTime(), x2_raw.getTime()]
|
|
708
|
+
} else if (typeof x1_raw === `number` && typeof x2_raw === `number`) {
|
|
709
|
+
;[xr1, xr2] = [x1_raw, x2_raw]
|
|
710
|
+
} else [xr1, xr2] = [NaN, NaN] // bail: mixed types
|
|
711
|
+
|
|
712
|
+
let x2r1: number, x2r2: number
|
|
713
|
+
if (x2a_1_raw instanceof Date && x2a_2_raw instanceof Date) {
|
|
714
|
+
;[x2r1, x2r2] = [x2a_1_raw.getTime(), x2a_2_raw.getTime()]
|
|
715
|
+
} else if (typeof x2a_1_raw === `number` && typeof x2a_2_raw === `number`) {
|
|
716
|
+
;[x2r1, x2r2] = [x2a_1_raw, x2a_2_raw]
|
|
717
|
+
} else [x2r1, x2r2] = [NaN, NaN]
|
|
718
|
+
|
|
719
|
+
if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
|
|
720
|
+
// Update axis ranges to trigger reactivity and prevent effect from overriding
|
|
721
|
+
x_axis = { ...x_axis, range: [Math.min(xr1, xr2), Math.max(xr1, xr2)] }
|
|
722
|
+
if (x2_series.length > 0 && Number.isFinite(x2r1) && Number.isFinite(x2r2)) {
|
|
723
|
+
x2_axis = {
|
|
724
|
+
...x2_axis,
|
|
725
|
+
range: [Math.min(x2r1, x2r2), Math.max(x2r1, x2r2)],
|
|
726
|
+
}
|
|
399
727
|
}
|
|
728
|
+
y_axis = { ...y_axis, range: [Math.min(y1, y2), Math.max(y1, y2)] }
|
|
729
|
+
y2_axis = { ...y2_axis, range: [Math.min(y2_1, y2_2), Math.max(y2_1, y2_2)] }
|
|
730
|
+
}
|
|
400
731
|
}
|
|
401
|
-
drag_state = { start: null, current: null, bounds: null }
|
|
402
|
-
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
403
|
-
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
404
|
-
document.body.style.cursor = `default
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const dx = evt.clientX - pan_drag_state.start.x
|
|
411
|
-
const dy = evt.clientY - pan_drag_state.start.y
|
|
732
|
+
drag_state = { start: null, current: null, bounds: null }
|
|
733
|
+
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
734
|
+
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
735
|
+
document.body.style.cursor = `default`
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Pan drag handlers
|
|
739
|
+
const on_pan_move = (evt: MouseEvent) => {
|
|
740
|
+
if (!pan_drag_state) return
|
|
741
|
+
const dx = evt.clientX - pan_drag_state.start.x
|
|
742
|
+
const dy = evt.clientY - pan_drag_state.start.y
|
|
743
|
+
|
|
412
744
|
// Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
|
|
413
|
-
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
745
|
+
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
746
|
+
|
|
747
|
+
const x_delta = pixels_to_data_delta(
|
|
748
|
+
-dx * sensitivity,
|
|
749
|
+
pan_drag_state.initial_x_range,
|
|
750
|
+
chart_width,
|
|
751
|
+
)
|
|
752
|
+
const x2_delta = pixels_to_data_delta(
|
|
753
|
+
-dx * sensitivity,
|
|
754
|
+
pan_drag_state.initial_x2_range,
|
|
755
|
+
chart_width,
|
|
756
|
+
)
|
|
757
|
+
const y_delta = pixels_to_data_delta(
|
|
758
|
+
dy * sensitivity,
|
|
759
|
+
pan_drag_state.initial_y_range,
|
|
760
|
+
chart_height,
|
|
761
|
+
)
|
|
762
|
+
const y2_delta = pixels_to_data_delta(
|
|
763
|
+
dy * sensitivity,
|
|
764
|
+
pan_drag_state.initial_y2_range,
|
|
765
|
+
chart_height,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta)
|
|
769
|
+
ranges.current.x2 = pan_range(pan_drag_state.initial_x2_range, x2_delta)
|
|
770
|
+
ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta)
|
|
771
|
+
ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const on_pan_end = () => {
|
|
775
|
+
pan_drag_state = null
|
|
776
|
+
document.body.style.cursor = ``
|
|
777
|
+
window.removeEventListener(`mousemove`, on_pan_move)
|
|
778
|
+
window.removeEventListener(`mouseup`, on_pan_end)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function handle_mouse_down(evt: MouseEvent) {
|
|
782
|
+
const coords = get_relative_coords(evt)
|
|
783
|
+
if (!coords || !svg_element) return
|
|
784
|
+
|
|
433
785
|
// Check if pan is enabled and shift is held for pan mode
|
|
434
|
-
const pan_enabled = pan?.enabled !== false
|
|
786
|
+
const pan_enabled = pan?.enabled !== false
|
|
435
787
|
if (pan_enabled && evt.shiftKey) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
788
|
+
evt.preventDefault()
|
|
789
|
+
pan_drag_state = {
|
|
790
|
+
start: { x: evt.clientX, y: evt.clientY },
|
|
791
|
+
initial_x_range: [...ranges.current.x] as [number, number],
|
|
792
|
+
initial_x2_range: [...ranges.current.x2] as [number, number],
|
|
793
|
+
initial_y_range: [...ranges.current.y] as [number, number],
|
|
794
|
+
initial_y2_range: [...ranges.current.y2] as [number, number],
|
|
795
|
+
}
|
|
796
|
+
document.body.style.cursor = `grabbing`
|
|
797
|
+
window.addEventListener(`mousemove`, on_pan_move)
|
|
798
|
+
window.addEventListener(`mouseup`, on_pan_end)
|
|
799
|
+
return
|
|
448
800
|
}
|
|
801
|
+
|
|
449
802
|
drag_state = {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
455
|
-
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
456
|
-
evt.preventDefault()
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
803
|
+
start: coords,
|
|
804
|
+
current: coords,
|
|
805
|
+
bounds: svg_element.getBoundingClientRect(),
|
|
806
|
+
}
|
|
807
|
+
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
808
|
+
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
809
|
+
evt.preventDefault()
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Wheel handler for pan (requires focus and shift)
|
|
813
|
+
function handle_wheel(evt: WheelEvent) {
|
|
814
|
+
const pan_enabled = pan?.enabled !== false
|
|
461
815
|
// Only capture wheel when focused AND Shift is held
|
|
462
816
|
// Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
|
|
463
|
-
if (!pan_enabled || !is_focused || !shift_held)
|
|
464
|
-
|
|
465
|
-
evt.preventDefault()
|
|
466
|
-
|
|
817
|
+
if (!pan_enabled || !is_focused || !shift_held) return
|
|
818
|
+
|
|
819
|
+
evt.preventDefault()
|
|
820
|
+
|
|
821
|
+
const sensitivity = pan?.wheel_sensitivity ?? 1
|
|
822
|
+
|
|
467
823
|
// Determine pan direction based on wheel delta
|
|
468
|
-
const x_delta = pixels_to_data_delta(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
824
|
+
const x_delta = pixels_to_data_delta(
|
|
825
|
+
evt.deltaX * sensitivity,
|
|
826
|
+
ranges.current.x,
|
|
827
|
+
chart_width,
|
|
828
|
+
)
|
|
829
|
+
const x2_delta = pixels_to_data_delta(
|
|
830
|
+
evt.deltaX * sensitivity,
|
|
831
|
+
ranges.current.x2,
|
|
832
|
+
chart_width,
|
|
833
|
+
)
|
|
834
|
+
const y_delta = pixels_to_data_delta(
|
|
835
|
+
evt.deltaY * sensitivity,
|
|
836
|
+
ranges.current.y,
|
|
837
|
+
chart_height,
|
|
838
|
+
)
|
|
839
|
+
const y2_delta = pixels_to_data_delta(
|
|
840
|
+
evt.deltaY * sensitivity,
|
|
841
|
+
ranges.current.y2,
|
|
842
|
+
chart_height,
|
|
843
|
+
)
|
|
844
|
+
|
|
472
845
|
if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
|
|
473
|
-
|
|
474
|
-
|
|
846
|
+
ranges.current.x = pan_range(ranges.current.x, x_delta)
|
|
847
|
+
ranges.current.x2 = pan_range(ranges.current.x2, x2_delta)
|
|
848
|
+
} else {
|
|
849
|
+
ranges.current.y = pan_range(ranges.current.y, y_delta)
|
|
850
|
+
ranges.current.y2 = pan_range(ranges.current.y2, y2_delta)
|
|
475
851
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return;
|
|
486
|
-
evt.preventDefault();
|
|
487
|
-
const touches = Array.from(evt.touches);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Touch handlers for pinch-zoom and two-finger pan
|
|
855
|
+
function handle_touch_start(evt: TouchEvent) {
|
|
856
|
+
const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
|
|
857
|
+
if (!touch_enabled || evt.touches.length !== 2) return
|
|
858
|
+
|
|
859
|
+
evt.preventDefault()
|
|
860
|
+
const touches = Array.from(evt.touches)
|
|
488
861
|
touch_state = {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
evt.preventDefault()
|
|
500
|
-
|
|
501
|
-
const [
|
|
862
|
+
start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
|
|
863
|
+
initial_x_range: [...ranges.current.x] as [number, number],
|
|
864
|
+
initial_x2_range: [...ranges.current.x2] as [number, number],
|
|
865
|
+
initial_y_range: [...ranges.current.y] as [number, number],
|
|
866
|
+
initial_y2_range: [...ranges.current.y2] as [number, number],
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function handle_touch_move(evt: TouchEvent) {
|
|
871
|
+
if (!touch_state || evt.touches.length !== 2) return
|
|
872
|
+
evt.preventDefault()
|
|
873
|
+
|
|
874
|
+
const [t1, t2] = Array.from(evt.touches)
|
|
875
|
+
const [s1, s2] = touch_state.start_touches
|
|
876
|
+
|
|
502
877
|
// Calculate center movement for pan
|
|
503
|
-
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
878
|
+
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
504
879
|
const curr_center = {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
const dx = curr_center.x - start_center.x
|
|
509
|
-
const dy = curr_center.y - start_center.y
|
|
880
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
881
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
882
|
+
}
|
|
883
|
+
const dx = curr_center.x - start_center.x
|
|
884
|
+
const dy = curr_center.y - start_center.y
|
|
885
|
+
|
|
510
886
|
// Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
|
|
511
|
-
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
887
|
+
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
512
888
|
// Guard against zero-distance pinch to avoid Infinity scale
|
|
513
|
-
if (start_dist < Number.EPSILON)
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
889
|
+
if (start_dist < Number.EPSILON) return
|
|
890
|
+
const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
|
891
|
+
const scale = curr_dist / start_dist
|
|
892
|
+
|
|
517
893
|
// If scale changed significantly, treat as pinch-zoom
|
|
518
894
|
// Also guard against scale being too small to avoid division by zero
|
|
519
895
|
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
|
-
|
|
896
|
+
// Pinch zoom centered on gesture center
|
|
897
|
+
// Divide by scale so spread (scale > 1) = smaller span (zoom in)
|
|
898
|
+
const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
|
|
899
|
+
const x2_span = touch_state.initial_x2_range[1] -
|
|
900
|
+
touch_state.initial_x2_range[0]
|
|
901
|
+
const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
|
|
902
|
+
const y2_span = touch_state.initial_y2_range[1] -
|
|
903
|
+
touch_state.initial_y2_range[0]
|
|
904
|
+
const x_center =
|
|
905
|
+
(touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
|
|
906
|
+
const x2_center =
|
|
907
|
+
(touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
|
|
908
|
+
const y_center =
|
|
909
|
+
(touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
|
|
910
|
+
const y2_center =
|
|
911
|
+
(touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
|
|
912
|
+
|
|
913
|
+
ranges.current.x = [
|
|
914
|
+
x_center - x_span / scale / 2,
|
|
915
|
+
x_center + x_span / scale / 2,
|
|
916
|
+
]
|
|
917
|
+
ranges.current.x2 = [
|
|
918
|
+
x2_center - x2_span / scale / 2,
|
|
919
|
+
x2_center + x2_span / scale / 2,
|
|
920
|
+
]
|
|
921
|
+
ranges.current.y = [
|
|
922
|
+
y_center - y_span / scale / 2,
|
|
923
|
+
y_center + y_span / scale / 2,
|
|
924
|
+
]
|
|
925
|
+
ranges.current.y2 = [
|
|
926
|
+
y2_center - y2_span / scale / 2,
|
|
927
|
+
y2_center + y2_span / scale / 2,
|
|
928
|
+
]
|
|
929
|
+
} else {
|
|
930
|
+
// Pan
|
|
931
|
+
const x_delta = pixels_to_data_delta(
|
|
932
|
+
-dx,
|
|
933
|
+
touch_state.initial_x_range,
|
|
934
|
+
chart_width,
|
|
935
|
+
)
|
|
936
|
+
const x2_delta = pixels_to_data_delta(
|
|
937
|
+
-dx,
|
|
938
|
+
touch_state.initial_x2_range,
|
|
939
|
+
chart_width,
|
|
940
|
+
)
|
|
941
|
+
const y_delta = pixels_to_data_delta(
|
|
942
|
+
dy,
|
|
943
|
+
touch_state.initial_y_range,
|
|
944
|
+
chart_height,
|
|
945
|
+
)
|
|
946
|
+
const y2_delta = pixels_to_data_delta(
|
|
947
|
+
dy,
|
|
948
|
+
touch_state.initial_y2_range,
|
|
949
|
+
chart_height,
|
|
950
|
+
)
|
|
951
|
+
ranges.current.x = pan_range(touch_state.initial_x_range, x_delta)
|
|
952
|
+
ranges.current.x2 = pan_range(touch_state.initial_x2_range, x2_delta)
|
|
953
|
+
ranges.current.y = pan_range(touch_state.initial_y_range, y_delta)
|
|
954
|
+
ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta)
|
|
559
955
|
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function handle_touch_end() {
|
|
959
|
+
touch_state = null
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Legend data and handlers
|
|
963
|
+
let legend_data = $derived.by<LegendItem[]>(() =>
|
|
964
|
+
series.map((srs: BarSeries<Metadata>, idx: number) => {
|
|
965
|
+
const is_line = srs.render_mode === `line`
|
|
966
|
+
const series_markers = srs.markers ?? DEFAULT_MARKERS
|
|
967
|
+
const has_line = series_markers === `line` || series_markers === `line+points`
|
|
968
|
+
const has_points = series_markers === `points` ||
|
|
969
|
+
series_markers === `line+points`
|
|
970
|
+
const series_color = srs.color ?? (is_line ? line_state.color : bar_state.color)
|
|
971
|
+
|
|
972
|
+
// Get point style for symbol color (handle array or single object)
|
|
973
|
+
const first_point_style = Array.isArray(srs.point_style)
|
|
574
974
|
? srs.point_style[0]
|
|
575
|
-
: srs.point_style
|
|
576
|
-
|
|
577
|
-
|
|
975
|
+
: srs.point_style
|
|
976
|
+
const first_color_value = srs.color_values?.[0]
|
|
977
|
+
const point_color = first_color_value != null
|
|
578
978
|
? color_scale_fn(first_color_value)
|
|
579
|
-
: first_point_style?.fill ?? series_color
|
|
580
|
-
|
|
979
|
+
: first_point_style?.fill ?? series_color
|
|
980
|
+
|
|
981
|
+
if (is_line) {
|
|
581
982
|
// Line series: show line and/or symbol based on markers
|
|
582
983
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
984
|
+
series_idx: idx,
|
|
985
|
+
label: srs.label ?? `Series ${idx + 1}`,
|
|
986
|
+
visible: srs.visible ?? true,
|
|
987
|
+
legend_group: srs.legend_group,
|
|
988
|
+
display_style: {
|
|
989
|
+
...(has_line
|
|
990
|
+
? {
|
|
991
|
+
line_color: series_color,
|
|
992
|
+
line_dash: srs.line_style?.line_dash,
|
|
993
|
+
}
|
|
994
|
+
: {}),
|
|
995
|
+
...(has_points
|
|
996
|
+
? {
|
|
997
|
+
symbol_type: first_point_style?.symbol_type ??
|
|
998
|
+
DEFAULTS.scatter.symbol_type,
|
|
999
|
+
symbol_color: point_color,
|
|
1000
|
+
}
|
|
1001
|
+
: {}),
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Bar series: show square symbol
|
|
1006
|
+
return {
|
|
606
1007
|
series_idx: idx,
|
|
607
1008
|
label: srs.label ?? `Series ${idx + 1}`,
|
|
608
1009
|
visible: srs.visible ?? true,
|
|
609
1010
|
legend_group: srs.legend_group,
|
|
610
1011
|
display_style: {
|
|
611
|
-
|
|
612
|
-
|
|
1012
|
+
symbol_type: `Square` as const,
|
|
1013
|
+
symbol_color: series_color,
|
|
613
1014
|
},
|
|
614
|
-
|
|
615
|
-
})
|
|
616
|
-
|
|
1015
|
+
}
|
|
1016
|
+
})
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
function toggle_series_visibility(series_idx: number) {
|
|
617
1020
|
if (series_idx >= 0 && series_idx < series.length) {
|
|
618
|
-
|
|
1021
|
+
series = series.map((srs, idx) =>
|
|
1022
|
+
idx === series_idx ? { ...srs, visible: !(srs.visible ?? true) } : srs
|
|
1023
|
+
)
|
|
619
1024
|
}
|
|
620
|
-
}
|
|
621
|
-
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function toggle_group_visibility(_group_name: string, series_indices: number[]) {
|
|
622
1028
|
// Filter to valid indices upfront (consistent with shared toggle_group_visibility)
|
|
623
|
-
const valid_indices = series_indices.filter((idx) =>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1029
|
+
const valid_indices = series_indices.filter((idx) =>
|
|
1030
|
+
idx >= 0 && idx < series.length
|
|
1031
|
+
)
|
|
1032
|
+
if (valid_indices.length === 0) return
|
|
1033
|
+
|
|
1034
|
+
const idx_set = new Set(valid_indices)
|
|
627
1035
|
// Check if all series in the group are currently visible
|
|
628
|
-
const all_visible = valid_indices.every((idx) => series[idx].visible ?? true)
|
|
1036
|
+
const all_visible = valid_indices.every((idx) => series[idx].visible ?? true)
|
|
629
1037
|
// Toggle: if all visible, hide all; otherwise show all
|
|
630
|
-
const new_visibility = !all_visible
|
|
631
|
-
series = series.map((srs, idx) =>
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1038
|
+
const new_visibility = !all_visible
|
|
1039
|
+
series = series.map((srs, idx) =>
|
|
1040
|
+
idx_set.has(idx) ? { ...srs, visible: new_visibility } : srs
|
|
1041
|
+
)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Collect bar and line positions for legend placement
|
|
1045
|
+
let bar_points_for_placement = $derived.by(() => {
|
|
1046
|
+
if (!width || !height || !visible_series.length) return []
|
|
1047
|
+
|
|
637
1048
|
return internal_series.flatMap((srs, series_idx) => {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return { x: bar_x, y: bar_y };
|
|
1049
|
+
if (!(srs?.visible ?? true)) return []
|
|
1050
|
+
const is_line = srs.render_mode === `line`
|
|
1051
|
+
const series_offsets = stacked_offsets[series_idx] ?? []
|
|
1052
|
+
const use_y2 = srs.y_axis === `y2`
|
|
1053
|
+
const y_scale = use_y2 ? scales.y2 : scales.y
|
|
1054
|
+
const use_x2_pl = srs.x_axis === `x2`
|
|
1055
|
+
const x_scale_pl = use_x2_pl ? scales.x2 : scales.x
|
|
1056
|
+
return srs.x
|
|
1057
|
+
.map((x_val, bar_idx) => {
|
|
1058
|
+
const y_val = srs.y[bar_idx]
|
|
1059
|
+
const base = !is_line && mode === `stacked`
|
|
1060
|
+
? (series_offsets[bar_idx] ?? 0)
|
|
1061
|
+
: 0
|
|
1062
|
+
const [bar_x, bar_y] = orientation === `vertical`
|
|
1063
|
+
? [x_scale_pl(x_val), y_scale(base + y_val)]
|
|
1064
|
+
: [x_scale_pl(base + y_val), scales.y(x_val)]
|
|
1065
|
+
return { x: bar_x, y: bar_y }
|
|
656
1066
|
})
|
|
657
|
-
|
|
658
|
-
})
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1067
|
+
.filter(({ x, y }) => isFinite(x) && isFinite(y))
|
|
1068
|
+
})
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
// Legend placement stability state
|
|
1072
|
+
let legend_element = $state<HTMLDivElement | undefined>()
|
|
1073
|
+
const legend_hover = create_hover_lock()
|
|
1074
|
+
const dim_tracker = create_dimension_tracker()
|
|
1075
|
+
let has_initial_legend_placement = $state(false)
|
|
1076
|
+
|
|
1077
|
+
// Clear pending hover lock timeout on unmount
|
|
1078
|
+
$effect(() => () => legend_hover.cleanup())
|
|
1079
|
+
|
|
1080
|
+
// Calculate best legend placement using continuous grid sampling
|
|
1081
|
+
let legend_placement = $derived.by(() => {
|
|
1082
|
+
const should_show = show_legend !== undefined ? show_legend : series.length > 1
|
|
1083
|
+
if (!should_show || !width || !height) return null
|
|
1084
|
+
|
|
672
1085
|
// Use measured size if available, otherwise estimate
|
|
673
1086
|
const legend_size = legend_element
|
|
674
|
-
|
|
675
|
-
|
|
1087
|
+
? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
|
|
1088
|
+
: { width: 120, height: 60 }
|
|
1089
|
+
|
|
676
1090
|
const result = compute_element_placement({
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1091
|
+
plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
|
|
1092
|
+
element_size: legend_size,
|
|
1093
|
+
axis_clearance: legend?.axis_clearance,
|
|
1094
|
+
exclude_rects: [],
|
|
1095
|
+
points: bar_points_for_placement,
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
return result
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
// Tweened legend coordinates for smooth animation - create once, update target via effect
|
|
1102
|
+
// untrack() explicitly captures initial tween config (intentional - config set once at mount)
|
|
1103
|
+
const tweened_legend_coords = new Tween(
|
|
1104
|
+
{ x: 0, y: 0 },
|
|
1105
|
+
untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
// Update legend position with stability checks
|
|
1109
|
+
$effect(() => {
|
|
1110
|
+
if (!width || !height || !legend_placement) return
|
|
1111
|
+
|
|
692
1112
|
// Track dimensions for resize detection
|
|
693
|
-
const dims_changed = dim_tracker.has_changed(width, height)
|
|
694
|
-
if (dims_changed)
|
|
695
|
-
|
|
696
|
-
const is_responsive = legend?.responsive ?? false
|
|
1113
|
+
const dims_changed = dim_tracker.has_changed(width, height)
|
|
1114
|
+
if (dims_changed) dim_tracker.update(width, height)
|
|
1115
|
+
|
|
1116
|
+
const is_responsive = legend?.responsive ?? false
|
|
697
1117
|
// Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
|
|
698
1118
|
const should_update = dims_changed || (!legend_hover.is_locked.current &&
|
|
699
|
-
|
|
1119
|
+
(is_responsive || !has_initial_legend_placement))
|
|
1120
|
+
|
|
700
1121
|
if (should_update) {
|
|
701
|
-
|
|
1122
|
+
tweened_legend_coords.set(
|
|
1123
|
+
{ x: legend_placement.x, y: legend_placement.y },
|
|
702
1124
|
// Skip animation on initial placement to avoid jump from (0, 0)
|
|
703
|
-
has_initial_legend_placement ? undefined : { duration: 0 }
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1125
|
+
has_initial_legend_placement ? undefined : { duration: 0 },
|
|
1126
|
+
)
|
|
1127
|
+
// Only lock position after we have actual measured size
|
|
1128
|
+
if (legend_element) {
|
|
1129
|
+
has_initial_legend_placement = true
|
|
1130
|
+
}
|
|
708
1131
|
}
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// Tooltip state
|
|
1135
|
+
let hover_info = $state<BarHandlerProps<Metadata> | null>(null)
|
|
1136
|
+
let tooltip_el = $state<HTMLDivElement | undefined>()
|
|
1137
|
+
|
|
1138
|
+
function get_bar_data(
|
|
1139
|
+
series_idx: number,
|
|
1140
|
+
bar_idx: number,
|
|
1141
|
+
color: string,
|
|
1142
|
+
): BarHandlerProps<Metadata> {
|
|
1143
|
+
const srs = internal_series[series_idx]
|
|
1144
|
+
const [x, y] = [srs.x[bar_idx], srs.y[bar_idx]]
|
|
1145
|
+
const [orient_x, orient_y] = orientation === `horizontal` ? [y, x] : [x, y]
|
|
717
1146
|
const metadata = Array.isArray(srs.metadata)
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const label = srs.labels?.[bar_idx] ?? null
|
|
721
|
-
const active_y_axis = srs.y_axis ?? `y1
|
|
722
|
-
const active_x_axis = srs.x_axis ?? `x1
|
|
723
|
-
const category_label = category_list[x]
|
|
1147
|
+
? srs.metadata[bar_idx]
|
|
1148
|
+
: srs.metadata
|
|
1149
|
+
const label = srs.labels?.[bar_idx] ?? null
|
|
1150
|
+
const active_y_axis = srs.y_axis ?? `y1`
|
|
1151
|
+
const active_x_axis = srs.x_axis ?? `x1`
|
|
1152
|
+
const category_label = category_list[x]
|
|
724
1153
|
const coords = {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
1154
|
+
x,
|
|
1155
|
+
y,
|
|
1156
|
+
orient_x,
|
|
1157
|
+
orient_y,
|
|
1158
|
+
x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
|
|
1159
|
+
x2_axis,
|
|
1160
|
+
y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
|
|
1161
|
+
y2_axis,
|
|
1162
|
+
}
|
|
734
1163
|
return {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1164
|
+
...coords,
|
|
1165
|
+
metadata,
|
|
1166
|
+
color,
|
|
1167
|
+
label,
|
|
1168
|
+
series_idx,
|
|
1169
|
+
bar_idx,
|
|
1170
|
+
active_y_axis,
|
|
1171
|
+
active_x_axis,
|
|
1172
|
+
category_label,
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Find the point closest to the cursor on a polyline overlay (O(n) scan).
|
|
1177
|
+
function find_closest_point(
|
|
1178
|
+
evt: MouseEvent,
|
|
1179
|
+
points: LineSeriesPoint[],
|
|
1180
|
+
): LineSeriesPoint | null {
|
|
1181
|
+
const svg_el = (evt.target as Element).closest(`svg`)
|
|
1182
|
+
if (!svg_el) return null
|
|
1183
|
+
const rect = svg_el.getBoundingClientRect()
|
|
1184
|
+
const mx = evt.clientX - rect.left
|
|
1185
|
+
const my = evt.clientY - rect.top
|
|
1186
|
+
let best: LineSeriesPoint | null = null
|
|
1187
|
+
let best_dist = Infinity
|
|
756
1188
|
for (const pt of points) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1189
|
+
const dist = (pt.x - mx) ** 2 + (pt.y - my) ** 2
|
|
1190
|
+
if (dist < best_dist) {
|
|
1191
|
+
best_dist = dist
|
|
1192
|
+
best = pt
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return best
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const line_point_fill = (pt: LineSeriesPoint, series_color: string): string =>
|
|
1199
|
+
pt.color_value != null
|
|
1200
|
+
? color_scale_fn(pt.color_value)
|
|
1201
|
+
: pt.point_style?.fill ?? series_color
|
|
1202
|
+
|
|
1203
|
+
const handle_bar_hover =
|
|
1204
|
+
(series_idx: number, bar_idx: number, color: string) => (event: MouseEvent) => {
|
|
1205
|
+
hovered = true
|
|
1206
|
+
hover_info = get_bar_data(series_idx, bar_idx, color)
|
|
1207
|
+
change(hover_info)
|
|
1208
|
+
on_bar_hover?.({ ...hover_info, event })
|
|
762
1209
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
let stacked_offsets = $derived.by(() => {
|
|
776
|
-
if (mode !== `stacked`)
|
|
777
|
-
return [];
|
|
778
|
-
const max_len = Math.max(0, ...internal_series.map((srs) => srs.y.length));
|
|
779
|
-
const offsets = internal_series.map(() => Array.from({ length: max_len }, () => 0));
|
|
1210
|
+
|
|
1211
|
+
// Stack offsets (only for bar series in stacked mode, grouped by y-axis)
|
|
1212
|
+
let stacked_offsets = $derived.by(() => {
|
|
1213
|
+
if (mode !== `stacked`) return [] as number[][]
|
|
1214
|
+
const max_len = Math.max(
|
|
1215
|
+
0,
|
|
1216
|
+
...internal_series.map((srs) => srs.y.length),
|
|
1217
|
+
)
|
|
1218
|
+
const offsets = internal_series.map(() =>
|
|
1219
|
+
Array.from({ length: max_len }, () => 0)
|
|
1220
|
+
)
|
|
1221
|
+
|
|
780
1222
|
// Separate accumulators for y1 and y2 axes
|
|
781
|
-
const y1_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
782
|
-
const y1_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
783
|
-
const y2_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
784
|
-
const y2_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
1223
|
+
const y1_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
1224
|
+
const y1_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
1225
|
+
const y2_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
1226
|
+
const y2_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
1227
|
+
|
|
785
1228
|
internal_series.forEach((srs, series_idx) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1229
|
+
if (!(srs?.visible ?? true) || srs.render_mode === `line`) return
|
|
1230
|
+
|
|
1231
|
+
const use_y2 = srs.y_axis === `y2`
|
|
1232
|
+
const pos_acc = use_y2 ? y2_pos_acc : y1_pos_acc
|
|
1233
|
+
const neg_acc = use_y2 ? y2_neg_acc : y1_neg_acc
|
|
1234
|
+
|
|
1235
|
+
for (let bar_idx = 0; bar_idx < max_len; bar_idx++) {
|
|
1236
|
+
const y_val = srs.y[bar_idx] ?? 0
|
|
1237
|
+
const acc = y_val >= 0 ? pos_acc : neg_acc
|
|
1238
|
+
offsets[series_idx][bar_idx] = acc[bar_idx]
|
|
1239
|
+
acc[bar_idx] += y_val
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
return offsets
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
// Calculate group positions for grouped mode (side-by-side bars)
|
|
1246
|
+
let group_info = $derived.by(() => {
|
|
1247
|
+
if (mode !== `grouped`) return { bar_series_count: 0, bar_series_indices: [] }
|
|
804
1248
|
const bar_series_indices = internal_series
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1249
|
+
.map((srs, idx) =>
|
|
1250
|
+
(srs?.visible ?? true) && srs.render_mode !== `line` ? idx : -1
|
|
1251
|
+
)
|
|
1252
|
+
.filter((idx) => idx >= 0)
|
|
1253
|
+
return { bar_series_count: bar_series_indices.length, bar_series_indices }
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
// Set theme-aware background when entering fullscreen
|
|
1257
|
+
$effect(() => {
|
|
1258
|
+
set_fullscreen_bg(wrapper, fullscreen, `--barplot-fullscreen-bg`)
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
// State accessors for shared axis change handler
|
|
1262
|
+
const axis_state: AxisChangeState<BarSeries<Metadata>> = {
|
|
815
1263
|
get_axis: (axis) => {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (axis === `y`)
|
|
821
|
-
return y_axis;
|
|
822
|
-
return y2_axis;
|
|
1264
|
+
if (axis === `x`) return x_axis
|
|
1265
|
+
if (axis === `x2`) return x2_axis
|
|
1266
|
+
if (axis === `y`) return y_axis
|
|
1267
|
+
return y2_axis
|
|
823
1268
|
},
|
|
824
1269
|
set_axis: (axis, config) => {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
else if (axis === `y`)
|
|
831
|
-
y_axis = { ...y_axis, ...config };
|
|
832
|
-
else
|
|
833
|
-
y2_axis = { ...y2_axis, ...config };
|
|
1270
|
+
// Spread into existing state to preserve merged type structure
|
|
1271
|
+
if (axis === `x`) x_axis = { ...x_axis, ...config }
|
|
1272
|
+
else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
|
|
1273
|
+
else if (axis === `y`) y_axis = { ...y_axis, ...config }
|
|
1274
|
+
else y2_axis = { ...y2_axis, ...config }
|
|
834
1275
|
},
|
|
835
1276
|
get_series: () => series,
|
|
836
1277
|
set_series: (new_series) => (series = new_series),
|
|
837
1278
|
get_loading: () => axis_loading,
|
|
838
1279
|
set_loading: (axis) => (axis_loading = axis),
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Create shared handler bound to this component's state
|
|
1283
|
+
// Using $derived so handler updates when callback props change
|
|
1284
|
+
const handle_axis_change = $derived(create_axis_change_handler(
|
|
1285
|
+
axis_state,
|
|
1286
|
+
data_loader,
|
|
1287
|
+
on_axis_change,
|
|
1288
|
+
on_error,
|
|
1289
|
+
))
|
|
1290
|
+
|
|
1291
|
+
let auto_load_attempted = false // prevent infinite retries on failure
|
|
1292
|
+
|
|
1293
|
+
// Auto-load data if series is empty but options exist (runs once)
|
|
1294
|
+
$effect(() => {
|
|
846
1295
|
if (series.length === 0 && data_loader && !auto_load_attempted) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
1296
|
+
// Check x-axis first, then y-axis
|
|
1297
|
+
if (x_axis.options?.length) {
|
|
1298
|
+
auto_load_attempted = true
|
|
1299
|
+
const first_key = x_axis.selected_key ?? x_axis.options[0].key
|
|
1300
|
+
handle_axis_change(`x`, first_key).catch(() => {})
|
|
1301
|
+
} else if (y_axis.options?.length) {
|
|
1302
|
+
auto_load_attempted = true
|
|
1303
|
+
const first_key = y_axis.selected_key ?? y_axis.options[0].key
|
|
1304
|
+
handle_axis_change(`y`, first_key).catch(() => {})
|
|
1305
|
+
}
|
|
858
1306
|
}
|
|
859
|
-
})
|
|
1307
|
+
})
|
|
860
1308
|
</script>
|
|
861
1309
|
|
|
862
1310
|
{#snippet ref_lines_layer(lines: IndexedRefLine[])}
|
|
@@ -1680,13 +2128,13 @@ $effect(() => {
|
|
|
1680
2128
|
<div><strong>{series_label}</strong></div>
|
|
1681
2129
|
{/if}
|
|
1682
2130
|
<div>
|
|
1683
|
-
{@html hover_info.x_axis.label || `x`}: {
|
|
2131
|
+
{@html sanitize_html(hover_info.x_axis.label || `x`)}: {
|
|
1684
2132
|
(cat_axis === `x` ? hover_info.category_label : undefined) ??
|
|
1685
2133
|
format_value(hover_info.orient_x, hover_info.x_axis.format || `.3~s`)
|
|
1686
2134
|
}
|
|
1687
2135
|
</div>
|
|
1688
2136
|
<div>
|
|
1689
|
-
{@html hover_info.y_axis.label || `y`}: {
|
|
2137
|
+
{@html sanitize_html(hover_info.y_axis.label || `y`)}: {
|
|
1690
2138
|
(cat_axis === `y` ? hover_info.category_label : undefined) ??
|
|
1691
2139
|
format_value(hover_info.orient_y, hover_info.y_axis.format || `.3~s`)
|
|
1692
2140
|
}
|