matterviz 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +154 -96
- package/dist/Icon.svelte +20 -14
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -178
- package/dist/brillouin/BrillouinZone.svelte +299 -198
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
- 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 +327 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- 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.d.ts +10 -0
- package/dist/chempot-diagram/color.js +32 -0
- package/dist/chempot-diagram/compute.d.ts +48 -0
- package/dist/chempot-diagram/compute.js +812 -0
- package/dist/chempot-diagram/index.d.ts +6 -0
- package/dist/chempot-diagram/index.js +6 -0
- package/dist/chempot-diagram/pointer.d.ts +16 -0
- package/dist/chempot-diagram/pointer.js +40 -0
- package/dist/chempot-diagram/temperature.d.ts +15 -0
- package/dist/chempot-diagram/temperature.js +36 -0
- package/dist/chempot-diagram/types.d.ts +86 -0
- package/dist/chempot-diagram/types.js +28 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +9 -3
- package/dist/composition/BarChart.svelte +141 -77
- package/dist/composition/BubbleChart.svelte +107 -52
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +973 -353
- package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
- package/dist/composition/PieChart.svelte +199 -99
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- 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 -38
- package/dist/convex-hull/ConvexHull2D.svelte +551 -393
- package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
- package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +821 -249
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +41 -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.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +40 -0
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +10 -1
- package/dist/convex-hull/helpers.js +79 -38
- package/dist/convex-hull/index.d.ts +1 -0
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +163 -69
- package/dist/convex-hull/types.d.ts +12 -12
- package/dist/convex-hull/types.js +0 -12
- package/dist/coordination/CoordinationBarPlot.svelte +232 -176
- package/dist/element/BohrAtom.svelte +56 -13
- 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/element/data.js +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/types.d.ts +1 -0
- 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 +336 -239
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
- 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 +37 -33
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
- package/dist/heatmap-matrix/index.d.ts +53 -0
- package/dist/heatmap-matrix/index.js +100 -0
- package/dist/heatmap-matrix/shared.d.ts +2 -0
- package/dist/heatmap-matrix/shared.js +4 -0
- package/dist/icons.d.ts +111 -0
- package/dist/icons.js +158 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +138 -140
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +43 -0
- package/dist/io/index.d.ts +2 -2
- package/dist/io/index.js +2 -112
- package/dist/io/is-binary.js +2 -3
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +117 -0
- package/dist/isosurface/Isosurface.svelte +220 -110
- package/dist/isosurface/IsosurfaceControls.svelte +65 -28
- package/dist/isosurface/parse.js +104 -56
- package/dist/isosurface/slice.d.ts +2 -1
- package/dist/isosurface/slice.js +8 -13
- package/dist/isosurface/types.d.ts +14 -1
- package/dist/isosurface/types.js +152 -5
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +12 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +125 -94
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +82 -0
- package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +266 -223
- package/dist/layout/json-tree/JsonTree.svelte +516 -429
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +281 -173
- package/dist/layout/json-tree/types.d.ts +10 -2
- package/dist/layout/json-tree/utils.d.ts +2 -0
- package/dist/layout/json-tree/utils.js +37 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +20 -17
- package/dist/math.js +474 -57
- package/dist/overlays/ContextMenu.svelte +66 -40
- package/dist/overlays/DraggablePane.svelte +331 -154
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- 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 +559 -267
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- 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/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +869 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +8 -4
- package/dist/phase-diagram/utils.js +219 -74
- package/dist/plot/AxisLabel.svelte +51 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +1461 -768
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +33 -6
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +533 -383
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ColorScaleSelect.svelte +28 -7
- package/dist/plot/ElementScatter.svelte +38 -16
- package/dist/plot/FillArea.svelte +152 -92
- package/dist/plot/Histogram.svelte +1162 -709
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +81 -18
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- 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 +221 -96
- 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 -146
- package/dist/plot/ReferenceLine.svelte +77 -22
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -123
- package/dist/plot/ScatterPlot.svelte +1880 -1156
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +300 -297
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -406
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +150 -70
- 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 +96 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +23 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- 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/index.d.ts +6 -2
- package/dist/plot/index.js +6 -2
- package/dist/plot/interactions.d.ts +8 -10
- package/dist/plot/interactions.js +2 -3
- package/dist/plot/layout.d.ts +11 -2
- package/dist/plot/layout.js +44 -17
- package/dist/plot/reference-line.d.ts +5 -22
- package/dist/plot/reference-line.js +12 -84
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +53 -40
- package/dist/plot/types.js +12 -7
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -63
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +173 -132
- 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 +21 -6
- package/dist/settings.js +63 -19
- package/dist/spectral/Bands.svelte +963 -412
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- 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.d.ts +23 -1
- package/dist/spectral/helpers.js +119 -51
- package/dist/spectral/types.d.ts +2 -0
- 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 +231 -129
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +148 -51
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1077 -821
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +373 -139
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -231
- package/dist/structure/StructureScene.svelte +919 -445
- package/dist/structure/StructureScene.svelte.d.ts +16 -7
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +42 -29
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +22 -34
- package/dist/structure/ferrox-wasm-types.d.ts +3 -2
- package/dist/structure/ferrox-wasm-types.js +0 -3
- package/dist/structure/ferrox-wasm.d.ts +3 -2
- package/dist/structure/ferrox-wasm.js +2 -3
- package/dist/structure/index.d.ts +16 -0
- package/dist/structure/index.js +88 -6
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +130 -155
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +99 -0
- 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 +5 -3
- package/dist/symmetry/SymmetryStats.svelte +94 -37
- package/dist/symmetry/WyckoffTable.svelte +42 -14
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +87 -21
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +1112 -516
- package/dist/table/HeatmapTable.svelte.d.ts +12 -1
- package/dist/table/ToggleMenu.svelte +125 -90
- package/dist/table/index.d.ts +2 -0
- 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 +889 -687
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +13 -31
- package/dist/trajectory/format-detect.d.ts +9 -0
- package/dist/trajectory/format-detect.js +76 -0
- package/dist/trajectory/frame-reader.d.ts +17 -0
- package/dist/trajectory/frame-reader.js +332 -0
- package/dist/trajectory/helpers.d.ts +14 -0
- package/dist/trajectory/helpers.js +172 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +23 -14
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +77 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +129 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +299 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +179 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +68 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +110 -0
- package/dist/trajectory/plotting.js +13 -8
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +17 -0
- package/dist/xrd/XrdPlot.svelte +337 -245
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -19
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +103 -101
- package/readme.md +4 -4
- package/dist/trajectory/parse.d.ts +0 -42
- package/dist/trajectory/parse.js +0 -1267
- /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,1161 +1,1869 @@
|
|
|
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
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
>
|
|
5
|
+
import type { D3ColorSchemeName, D3InterpolateName } from '../colors'
|
|
6
|
+
import type { D3SymbolName } from '../labels'
|
|
7
|
+
import { format_value, symbol_names } from '../labels'
|
|
8
|
+
import { sanitize_html } from '../sanitize'
|
|
9
|
+
import { FullscreenToggle, set_fullscreen_bg } from '../layout'
|
|
10
|
+
import type { Vec2 } from '../math'
|
|
11
|
+
import type {
|
|
12
|
+
AxisLoadError,
|
|
13
|
+
BasePlotProps,
|
|
14
|
+
ControlsConfig,
|
|
15
|
+
DataLoaderFn,
|
|
16
|
+
DataSeries,
|
|
17
|
+
ErrorBand,
|
|
18
|
+
FillHandlerEvent,
|
|
19
|
+
FillRegion,
|
|
20
|
+
HoverConfig,
|
|
21
|
+
InitialRanges,
|
|
22
|
+
InternalPoint,
|
|
23
|
+
LabelPlacementConfig,
|
|
24
|
+
LegendConfig,
|
|
25
|
+
PanConfig,
|
|
26
|
+
PlotConfig,
|
|
27
|
+
Point,
|
|
28
|
+
RefLine,
|
|
29
|
+
RefLineEvent,
|
|
30
|
+
ScaleType,
|
|
31
|
+
ScatterHandlerEvent,
|
|
32
|
+
ScatterHandlerProps,
|
|
33
|
+
Sides,
|
|
34
|
+
StyleOverrides,
|
|
35
|
+
TweenedOptions,
|
|
36
|
+
UserContentProps,
|
|
37
|
+
XyObj,
|
|
38
|
+
} from './'
|
|
39
|
+
import {
|
|
40
|
+
AxisLabel,
|
|
41
|
+
ColorBar,
|
|
42
|
+
compute_element_placement,
|
|
43
|
+
FillArea,
|
|
44
|
+
get_tick_label,
|
|
45
|
+
Line,
|
|
46
|
+
PlotLegend,
|
|
47
|
+
PlotTooltip,
|
|
48
|
+
ReferenceLine,
|
|
49
|
+
ScatterPlotControls,
|
|
50
|
+
ScatterPoint,
|
|
51
|
+
ZeroLines,
|
|
52
|
+
ZoomRect,
|
|
53
|
+
} from './'
|
|
54
|
+
import type { AxisChangeState } from './axis-utils'
|
|
55
|
+
import { create_axis_change_handler } from './axis-utils'
|
|
56
|
+
import {
|
|
57
|
+
get_series_color,
|
|
58
|
+
get_series_symbol,
|
|
59
|
+
process_prop,
|
|
60
|
+
} from './data-transform'
|
|
61
|
+
import { AXIS_DEFAULTS } from './defaults'
|
|
62
|
+
import {
|
|
63
|
+
create_dimension_tracker,
|
|
64
|
+
create_hover_lock,
|
|
65
|
+
} from './hover-lock.svelte'
|
|
66
|
+
import {
|
|
67
|
+
DEFAULT_GRID_STYLE,
|
|
68
|
+
DEFAULT_MARKERS,
|
|
69
|
+
get_scale_type_name,
|
|
70
|
+
is_time_scale,
|
|
71
|
+
} from './types'
|
|
72
|
+
import { compute_label_positions } from './utils/label-placement'
|
|
73
|
+
import {
|
|
74
|
+
handle_legend_double_click,
|
|
75
|
+
toggle_group_visibility,
|
|
76
|
+
toggle_series_visibility,
|
|
77
|
+
} from './utils/series-visibility'
|
|
78
|
+
import { DEFAULTS } from '../settings'
|
|
79
|
+
import { extent } from 'd3-array'
|
|
80
|
+
import { scaleTime } from 'd3-scale'
|
|
81
|
+
import type { ComponentProps, Snippet } from 'svelte'
|
|
82
|
+
import { untrack } from 'svelte'
|
|
83
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
84
|
+
import { Tween } from 'svelte/motion'
|
|
85
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
86
|
+
import type { FillPathPoint } from './fill-utils'
|
|
87
|
+
import {
|
|
88
|
+
apply_range_constraints,
|
|
89
|
+
apply_where_condition,
|
|
90
|
+
clamp_for_log_scale,
|
|
91
|
+
convert_error_band_to_fill_region,
|
|
92
|
+
generate_fill_path,
|
|
93
|
+
is_fill_gradient,
|
|
94
|
+
resolve_boundary,
|
|
95
|
+
} from './fill-utils'
|
|
96
|
+
import {
|
|
97
|
+
expand_range_if_needed,
|
|
98
|
+
get_relative_coords,
|
|
99
|
+
normalize_y2_sync,
|
|
100
|
+
pan_range,
|
|
101
|
+
PINCH_ZOOM_THRESHOLD,
|
|
102
|
+
pixels_to_data_delta,
|
|
103
|
+
sync_y2_range,
|
|
104
|
+
} from './interactions'
|
|
105
|
+
import type { Rect } from './layout'
|
|
106
|
+
import {
|
|
107
|
+
calc_auto_padding,
|
|
108
|
+
constrain_tooltip_position,
|
|
109
|
+
filter_padding,
|
|
110
|
+
LABEL_GAP_DEFAULT,
|
|
111
|
+
measure_max_tick_width,
|
|
112
|
+
} from './layout'
|
|
113
|
+
import type { IndexedRefLine } from './reference-line'
|
|
114
|
+
import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
|
|
115
|
+
import {
|
|
116
|
+
create_color_scale,
|
|
117
|
+
create_scale,
|
|
118
|
+
create_size_scale,
|
|
119
|
+
generate_ticks,
|
|
120
|
+
get_nice_data_range,
|
|
121
|
+
} from './scales'
|
|
122
|
+
|
|
123
|
+
let {
|
|
124
|
+
series = $bindable([]),
|
|
125
|
+
x_axis = $bindable({}),
|
|
126
|
+
x2_axis = $bindable({}),
|
|
127
|
+
y_axis = $bindable({}),
|
|
128
|
+
y2_axis = $bindable({}),
|
|
129
|
+
display = $bindable(DEFAULTS.scatter.display),
|
|
130
|
+
styles: styles_init = {},
|
|
131
|
+
controls: controls_init = {},
|
|
132
|
+
padding = {},
|
|
133
|
+
range_padding = 0.05,
|
|
134
|
+
current_x_value = null,
|
|
135
|
+
tooltip_point = $bindable(null),
|
|
136
|
+
selected_point = null,
|
|
137
|
+
hovered = $bindable(false),
|
|
138
|
+
tooltip,
|
|
139
|
+
user_content,
|
|
140
|
+
change = () => {},
|
|
141
|
+
color_scale = {
|
|
142
|
+
type: `linear`,
|
|
143
|
+
scheme: `interpolateViridis`,
|
|
144
|
+
value_range: undefined,
|
|
145
|
+
},
|
|
146
|
+
color_bar = {},
|
|
147
|
+
size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
|
|
148
|
+
label_placement_config = {},
|
|
149
|
+
hover_config = {},
|
|
150
|
+
legend = {},
|
|
151
|
+
point_tween,
|
|
152
|
+
line_tween,
|
|
153
|
+
point_events,
|
|
154
|
+
on_point_click,
|
|
155
|
+
on_point_hover,
|
|
156
|
+
fill_regions = $bindable([]),
|
|
157
|
+
error_bands = [],
|
|
158
|
+
on_fill_click,
|
|
159
|
+
on_fill_hover,
|
|
160
|
+
ref_lines = $bindable([]),
|
|
161
|
+
on_ref_line_click,
|
|
162
|
+
on_ref_line_hover,
|
|
163
|
+
selected_series_idx = $bindable(0),
|
|
164
|
+
wrapper = $bindable(),
|
|
165
|
+
fullscreen = $bindable(false),
|
|
166
|
+
fullscreen_toggle = true,
|
|
167
|
+
children,
|
|
168
|
+
header_controls,
|
|
169
|
+
controls_extra,
|
|
170
|
+
data_loader,
|
|
171
|
+
on_axis_change,
|
|
172
|
+
on_error,
|
|
173
|
+
pan = {},
|
|
174
|
+
...rest
|
|
175
|
+
}: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, `change`> & PlotConfig & {
|
|
176
|
+
series?: DataSeries<Metadata>[]
|
|
177
|
+
styles?: StyleOverrides
|
|
178
|
+
controls?: ControlsConfig
|
|
179
|
+
current_x_value?: number | null
|
|
180
|
+
tooltip_point?: InternalPoint<Metadata> | null
|
|
181
|
+
selected_point?: { series_idx: number; point_idx: number } | null
|
|
182
|
+
tooltip?: Snippet<[ScatterHandlerProps<Metadata>]>
|
|
183
|
+
user_content?: Snippet<[UserContentProps]>
|
|
184
|
+
header_controls?: Snippet<
|
|
185
|
+
[{ height: number; width: number; fullscreen: boolean }]
|
|
186
|
+
>
|
|
187
|
+
controls_extra?: Snippet<
|
|
188
|
+
[
|
|
189
|
+
& { styles: StyleOverrides; selected_series_idx: number }
|
|
190
|
+
& Required<PlotConfig>,
|
|
191
|
+
]
|
|
192
|
+
>
|
|
193
|
+
change?: (
|
|
194
|
+
data: (Point<Metadata> & { series: DataSeries<Metadata> }) | null,
|
|
195
|
+
) => void
|
|
196
|
+
color_scale?: {
|
|
197
|
+
type?: ScaleType
|
|
198
|
+
scheme?: D3ColorSchemeName | D3InterpolateName
|
|
199
|
+
value_range?: [number, number]
|
|
200
|
+
} | D3InterpolateName
|
|
201
|
+
size_scale?: {
|
|
202
|
+
type?: ScaleType
|
|
203
|
+
radius_range?: [number, number]
|
|
204
|
+
value_range?: [number, number]
|
|
205
|
+
}
|
|
206
|
+
color_bar?:
|
|
207
|
+
| (ComponentProps<typeof ColorBar> & {
|
|
208
|
+
margin?: number | Sides
|
|
209
|
+
tween?: TweenedOptions<XyObj>
|
|
210
|
+
responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
|
|
211
|
+
})
|
|
212
|
+
| null
|
|
213
|
+
label_placement_config?: Partial<LabelPlacementConfig>
|
|
214
|
+
hover_config?: Partial<HoverConfig>
|
|
215
|
+
legend?: LegendConfig | null
|
|
216
|
+
point_tween?: TweenedOptions<XyObj>
|
|
217
|
+
line_tween?: TweenedOptions<string>
|
|
218
|
+
point_events?: Record<
|
|
219
|
+
string,
|
|
220
|
+
(payload: { point: InternalPoint<Metadata>; event: Event }) => void
|
|
221
|
+
>
|
|
222
|
+
on_point_click?: (data: ScatterHandlerEvent<Metadata>) => void
|
|
223
|
+
on_point_hover?: (data: ScatterHandlerEvent<Metadata> | null) => void
|
|
224
|
+
fill_regions?: FillRegion[] // Bindable for legend toggle support
|
|
225
|
+
error_bands?: ErrorBand[]
|
|
226
|
+
on_fill_click?: (event: FillHandlerEvent) => void
|
|
227
|
+
on_fill_hover?: (event: FillHandlerEvent | null) => void
|
|
228
|
+
ref_lines?: RefLine[] // Bindable for legend toggle support
|
|
229
|
+
on_ref_line_click?: (event: RefLineEvent) => void
|
|
230
|
+
on_ref_line_hover?: (event: RefLineEvent | null) => void
|
|
231
|
+
selected_series_idx?: number
|
|
232
|
+
wrapper?: HTMLDivElement
|
|
233
|
+
// Interactive axis props
|
|
234
|
+
data_loader?: DataLoaderFn<Metadata>
|
|
235
|
+
on_axis_change?: (
|
|
236
|
+
axis: `x` | `x2` | `y` | `y2`,
|
|
237
|
+
key: string,
|
|
238
|
+
new_series: DataSeries<Metadata>[],
|
|
239
|
+
) => void
|
|
240
|
+
on_error?: (error: AxisLoadError) => void
|
|
241
|
+
pan?: PanConfig
|
|
242
|
+
} = $props()
|
|
243
|
+
|
|
244
|
+
// Merged axis/display values with defaults (use $derived to avoid breaking $bindable)
|
|
245
|
+
const final_x_axis = $derived({
|
|
32
246
|
...AXIS_DEFAULTS,
|
|
33
247
|
label_shift: { x: 0, y: -40 }, // x-axis needs different label position
|
|
34
248
|
...(x_axis ?? {}),
|
|
35
|
-
})
|
|
36
|
-
const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...(y_axis ?? {}) })
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
249
|
+
})
|
|
250
|
+
const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...(y_axis ?? {}) })
|
|
251
|
+
const final_x2_axis = $derived({
|
|
252
|
+
...AXIS_DEFAULTS,
|
|
253
|
+
label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
|
|
254
|
+
...(x2_axis ?? {}),
|
|
255
|
+
})
|
|
256
|
+
const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...(y2_axis ?? {}) })
|
|
257
|
+
// Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
|
|
258
|
+
let is_time_x = $derived(
|
|
259
|
+
is_time_scale(final_x_axis.scale_type, final_x_axis.format),
|
|
260
|
+
)
|
|
261
|
+
let is_time_x2 = $derived(
|
|
262
|
+
is_time_scale(final_x2_axis.scale_type, final_x2_axis.format),
|
|
263
|
+
)
|
|
264
|
+
const final_display = $derived({ ...DEFAULTS.scatter.display, ...(display ?? {}) })
|
|
265
|
+
// Local state for styles (initialized from prop, owned by this component for controls)
|
|
266
|
+
// Using $state because styles has bindings in ScatterPlotControls
|
|
267
|
+
// untrack() explicitly captures initial prop value (intentional - props provide initial config)
|
|
268
|
+
let styles = $state(untrack(() => ({
|
|
43
269
|
show_points: DEFAULTS.scatter.show_points,
|
|
44
270
|
show_lines: DEFAULTS.scatter.show_lines,
|
|
45
271
|
point: { ...DEFAULTS.scatter.point, ...(styles_init?.point ?? {}) },
|
|
46
272
|
line: { ...DEFAULTS.scatter.line, ...(styles_init?.line ?? {}) },
|
|
47
273
|
...(styles_init ?? {}),
|
|
48
|
-
})))
|
|
49
|
-
let controls = $derived({ show: true, open: false, ...controls_init })
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
$
|
|
89
|
-
|
|
274
|
+
})))
|
|
275
|
+
let controls = $derived({ show: true, open: false, ...controls_init })
|
|
276
|
+
|
|
277
|
+
let [width, height] = $state([0, 0])
|
|
278
|
+
let svg_element: SVGElement | null = $state(null) // Bind the SVG element
|
|
279
|
+
let svg_bounding_box: DOMRect | null = $state(null) // Store SVG bounds during drag
|
|
280
|
+
|
|
281
|
+
// Track which specific control properties user has modified
|
|
282
|
+
let touched = new SvelteSet<string>()
|
|
283
|
+
|
|
284
|
+
// Unique component ID to avoid clipPath conflicts between multiple instances
|
|
285
|
+
let component_id = $state(`scatter-${crypto.randomUUID()}`)
|
|
286
|
+
let clip_path_id = $derived(`plot-area-clip-${component_id}`)
|
|
287
|
+
|
|
288
|
+
// Assign stable IDs to series for keying
|
|
289
|
+
let series_with_ids = $derived(
|
|
290
|
+
series.map((srs: DataSeries<Metadata>, idx: number) => {
|
|
291
|
+
if (!srs || typeof srs !== `object`) return srs
|
|
292
|
+
// Use series.id if provided, otherwise fall back to index
|
|
293
|
+
// prevents re-mounts when series are reordered if stable IDs are provided
|
|
294
|
+
return { ...srs, _id: srs.id ?? idx }
|
|
295
|
+
}),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// State for rectangle zoom selection
|
|
299
|
+
let drag_start_coords = $state<XyObj | null>(null)
|
|
300
|
+
let drag_current_coords = $state<XyObj | null>(null)
|
|
301
|
+
|
|
302
|
+
// Zoom/pan state - track both initial (data-driven) and current (after pan/zoom) ranges
|
|
303
|
+
let initial_x_range = $state<[number, number]>([0, 1])
|
|
304
|
+
let initial_x2_range = $state<[number, number]>([0, 1])
|
|
305
|
+
let initial_y_range = $state<[number, number]>([0, 1])
|
|
306
|
+
let initial_y2_range = $state<[number, number]>([0, 1])
|
|
307
|
+
let zoom_x_range = $state<[number, number]>([0, 1])
|
|
308
|
+
let zoom_x2_range = $state<[number, number]>([0, 1])
|
|
309
|
+
let zoom_y_range = $state<[number, number]>([0, 1])
|
|
310
|
+
let zoom_y2_range = $state<[number, number]>([0, 1])
|
|
311
|
+
let previous_series_visibility: boolean[] | null = $state(null)
|
|
312
|
+
|
|
313
|
+
// Y2 axis sync configuration
|
|
314
|
+
let y2_sync_config = $derived(normalize_y2_sync(y2_axis?.sync))
|
|
315
|
+
// Track previous sync mode to detect changes (updated in $effect.pre to avoid race conditions)
|
|
316
|
+
let prev_sync_mode = $state<string>(`none`)
|
|
317
|
+
|
|
318
|
+
// Helper to compute synced y2 range or return fallback when sync disabled
|
|
319
|
+
const get_synced_y2 = (y1_range: Vec2, fallback: Vec2): Vec2 =>
|
|
320
|
+
y2_sync_config.mode !== `none`
|
|
321
|
+
? sync_y2_range(y1_range, initial_y2_range, y2_sync_config)
|
|
322
|
+
: fallback
|
|
323
|
+
|
|
324
|
+
// Effect to update y2 range when sync mode changes - use $effect.pre to capture
|
|
325
|
+
// mode change before the main range-update effect runs, ensuring sync is applied
|
|
326
|
+
// immediately when toggled (not delayed until next data change)
|
|
327
|
+
$effect.pre(() => {
|
|
328
|
+
const mode = y2_sync_config.mode
|
|
90
329
|
if (mode !== prev_sync_mode) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
prev_sync_mode = mode;
|
|
330
|
+
// When sync mode becomes enabled (or changes), apply sync immediately
|
|
331
|
+
if (mode !== `none`) {
|
|
332
|
+
zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
|
|
333
|
+
} else {
|
|
334
|
+
// When switching to independent mode, reset Y2 to its data range
|
|
335
|
+
zoom_y2_range = [...initial_y2_range] as [number, number]
|
|
336
|
+
}
|
|
337
|
+
prev_sync_mode = mode
|
|
100
338
|
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
let
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
let
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
$
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// Pan state
|
|
342
|
+
let is_focused = $state(false)
|
|
343
|
+
let shift_held = $state(false)
|
|
344
|
+
let pan_drag_state = $state<
|
|
345
|
+
InitialRanges & { start: { x: number; y: number } } | null
|
|
346
|
+
>(null)
|
|
347
|
+
let touch_state = $state<
|
|
348
|
+
InitialRanges & { start_touches: { x: number; y: number }[] } | null
|
|
349
|
+
>(null)
|
|
350
|
+
|
|
351
|
+
// Fill region hover state
|
|
352
|
+
let hovered_fill_idx = $state<number | null>(null)
|
|
353
|
+
|
|
354
|
+
// Reference line hover state
|
|
355
|
+
let hovered_ref_line_idx = $state<number | null>(null)
|
|
356
|
+
|
|
357
|
+
// Interactive axis loading state
|
|
358
|
+
let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
|
|
359
|
+
|
|
360
|
+
// State to hold the calculated label positions after simulation
|
|
361
|
+
let label_positions = $state<Record<string, XyObj>>({})
|
|
362
|
+
|
|
363
|
+
// State for legend dragging
|
|
364
|
+
let legend_is_dragging = $state(false)
|
|
365
|
+
let legend_drag_offset = $state<{ x: number; y: number }>({ x: 0, y: 0 })
|
|
366
|
+
let legend_manual_position = $state<{ x: number; y: number } | null>(null)
|
|
367
|
+
|
|
368
|
+
// State for legend/colorbar placement stability
|
|
369
|
+
let legend_element = $state<HTMLDivElement | undefined>()
|
|
370
|
+
let colorbar_element = $state<HTMLDivElement | undefined>()
|
|
371
|
+
const legend_hover = create_hover_lock()
|
|
372
|
+
const colorbar_hover = create_hover_lock()
|
|
373
|
+
const dim_tracker = create_dimension_tracker()
|
|
374
|
+
let has_initial_legend_placement = $state(false)
|
|
375
|
+
let has_initial_colorbar_placement = $state(false)
|
|
376
|
+
|
|
377
|
+
// Clear pending hover lock timeouts on unmount
|
|
378
|
+
$effect(() => () => {
|
|
379
|
+
legend_hover.cleanup()
|
|
380
|
+
colorbar_hover.cleanup()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// Tooltip element reference for dynamic sizing
|
|
384
|
+
let tooltip_el = $state<HTMLDivElement | undefined>()
|
|
385
|
+
|
|
386
|
+
// Module-level constants to avoid repeated allocations
|
|
387
|
+
// Create and categorize points in a single pass (instead of 3 separate iterations)
|
|
388
|
+
type SimplePoint = { x: number; y: number }
|
|
389
|
+
let points_by_axis = $derived.by(() => {
|
|
390
|
+
const all: SimplePoint[] = []
|
|
391
|
+
const y1: SimplePoint[] = []
|
|
392
|
+
const y2: SimplePoint[] = []
|
|
393
|
+
const x2: SimplePoint[] = []
|
|
394
|
+
|
|
138
395
|
for (const srs of series_with_ids) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
y1.push(point);
|
|
150
|
-
}
|
|
396
|
+
if (!srs) continue
|
|
397
|
+
const { x: xs, y: ys, visible = true, y_axis = `y1`, x_axis: x_ax = `x1` } =
|
|
398
|
+
srs as DataSeries
|
|
399
|
+
for (let idx = 0; idx < xs.length; idx++) {
|
|
400
|
+
const point = { x: xs[idx], y: ys[idx] }
|
|
401
|
+
all.push(point)
|
|
402
|
+
if (visible) {
|
|
403
|
+
if (y_axis === `y2`) y2.push(point)
|
|
404
|
+
else y1.push(point)
|
|
405
|
+
if (x_ax === `x2`) x2.push(point)
|
|
151
406
|
}
|
|
407
|
+
}
|
|
152
408
|
}
|
|
153
|
-
return { all, y1, y2 }
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
let
|
|
157
|
-
let
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
409
|
+
return { all, y1, y2, x2 }
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
let all_points = $derived(points_by_axis.all)
|
|
413
|
+
let y1_points = $derived(points_by_axis.y1)
|
|
414
|
+
let y2_points = $derived(points_by_axis.y2)
|
|
415
|
+
let x2_points = $derived(points_by_axis.x2)
|
|
416
|
+
|
|
417
|
+
// Layout: dynamic padding based on tick label widths
|
|
418
|
+
const default_padding = { t: 5, b: 50, l: 50, r: 20 }
|
|
419
|
+
let pad = $state(untrack(() => filter_padding(padding, default_padding)))
|
|
420
|
+
|
|
421
|
+
// Update padding when format or ticks change
|
|
422
|
+
$effect(() => {
|
|
423
|
+
const new_pad = width && height &&
|
|
424
|
+
(y_tick_values.length || y2_tick_values.length || x2_tick_values.length)
|
|
425
|
+
? calc_auto_padding({
|
|
426
|
+
padding,
|
|
427
|
+
default_padding,
|
|
428
|
+
x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
|
|
429
|
+
y_axis: { ...final_y_axis, tick_values: y_tick_values },
|
|
430
|
+
y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
|
|
431
|
+
})
|
|
432
|
+
: filter_padding(padding, default_padding)
|
|
433
|
+
|
|
434
|
+
if (
|
|
435
|
+
pad.t !== new_pad.t ||
|
|
436
|
+
pad.b !== new_pad.b ||
|
|
437
|
+
pad.l !== new_pad.l ||
|
|
438
|
+
pad.r !== new_pad.r
|
|
439
|
+
) pad = new_pad
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Reactive clip area dimensions to ensure proper responsiveness
|
|
443
|
+
let clip_area = $derived({
|
|
176
444
|
x: pad.l || 0,
|
|
177
445
|
y: pad.t || 0,
|
|
178
446
|
width: isFinite(width - pad.l - pad.r) ? Math.max(1, width - pad.l - pad.r) : 1,
|
|
179
447
|
height: isFinite(height - pad.t - pad.b)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
448
|
+
? Math.max(1, height - pad.t - pad.b)
|
|
449
|
+
: 1,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Calculate plot area center coordinates
|
|
453
|
+
let plot_center_x = $derived(pad.l + (width - pad.r - pad.l) / 2)
|
|
454
|
+
let plot_center_y = $derived(pad.t + (height - pad.b - pad.t) / 2)
|
|
455
|
+
|
|
456
|
+
// Extract color and size values in single pass (used for scale computations)
|
|
457
|
+
let series_value_arrays = $derived.by(() => {
|
|
458
|
+
const color_values: number[] = []
|
|
459
|
+
const size_values: number[] = []
|
|
190
460
|
for (const srs of series_with_ids) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
for (const val of cvs)
|
|
196
|
-
if (val != null)
|
|
197
|
-
color_values.push(val);
|
|
198
|
-
}
|
|
199
|
-
if (svs) {
|
|
200
|
-
for (const val of svs)
|
|
201
|
-
if (val != null)
|
|
202
|
-
size_values.push(val);
|
|
203
|
-
}
|
|
461
|
+
if (!srs) continue
|
|
462
|
+
const { color_values: cvs, size_values: svs } = srs as DataSeries
|
|
463
|
+
if (cvs) { for (const val of cvs) if (val != null) color_values.push(val) }
|
|
464
|
+
if (svs) { for (const val of svs) if (val != null) size_values.push(val) }
|
|
204
465
|
}
|
|
205
|
-
return { color_values, size_values }
|
|
206
|
-
})
|
|
207
|
-
let all_color_values = $derived(series_value_arrays.color_values)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
466
|
+
return { color_values, size_values }
|
|
467
|
+
})
|
|
468
|
+
let all_color_values = $derived(series_value_arrays.color_values)
|
|
469
|
+
|
|
470
|
+
// Compute auto ranges based on data and limits
|
|
471
|
+
let auto_x_range = $derived(
|
|
472
|
+
get_nice_data_range(
|
|
473
|
+
all_points,
|
|
474
|
+
({ x }) => x,
|
|
475
|
+
final_x_axis.range ?? [null, null],
|
|
476
|
+
final_x_axis.scale_type ?? `linear`,
|
|
477
|
+
range_padding,
|
|
478
|
+
is_time_x,
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
let auto_y_range = $derived(
|
|
483
|
+
get_nice_data_range(
|
|
484
|
+
y1_points,
|
|
485
|
+
({ y }) => y,
|
|
486
|
+
final_y_axis.range ?? [null, null],
|
|
487
|
+
final_y_axis.scale_type ?? `linear`,
|
|
488
|
+
range_padding,
|
|
489
|
+
false,
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
let auto_x2_range = $derived(
|
|
494
|
+
get_nice_data_range(
|
|
495
|
+
x2_points,
|
|
496
|
+
({ x }) => x,
|
|
497
|
+
final_x2_axis.range ?? [null, null],
|
|
498
|
+
final_x2_axis.scale_type ?? `linear`,
|
|
499
|
+
range_padding,
|
|
500
|
+
is_time_x2,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
let auto_y2_range = $derived(
|
|
505
|
+
get_nice_data_range(
|
|
506
|
+
y2_points,
|
|
507
|
+
({ y }) => y,
|
|
508
|
+
final_y2_axis.range ?? [null, null],
|
|
509
|
+
final_y2_axis.scale_type ?? `linear`,
|
|
510
|
+
range_padding,
|
|
511
|
+
false,
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
// Update zoom ranges when auto ranges or explicit ranges change
|
|
516
|
+
// - Explicit ranges (from zoom/pan): apply directly
|
|
517
|
+
// - Auto ranges (from data changes): use lazy expansion to preserve view context
|
|
518
|
+
$effect(() => {
|
|
216
519
|
// Helper to get effective range (explicit ?? auto) and check if explicit
|
|
217
|
-
const get_range = (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
520
|
+
const get_range = (
|
|
521
|
+
axis: { range?: [number | null, number | null] },
|
|
522
|
+
auto: Vec2,
|
|
523
|
+
): { explicit: boolean; range: Vec2 } => {
|
|
524
|
+
const explicit = axis.range?.[0] != null && axis.range?.[1] != null
|
|
525
|
+
const range = [axis.range?.[0] ?? auto[0], axis.range?.[1] ?? auto[1]] as Vec2
|
|
526
|
+
return { explicit, range }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const x = get_range(final_x_axis, auto_x_range)
|
|
530
|
+
const x2 = get_range(final_x2_axis, auto_x2_range)
|
|
531
|
+
const y = get_range(final_y_axis, auto_y_range)
|
|
532
|
+
const y2 = get_range(final_y2_axis, auto_y2_range)
|
|
533
|
+
|
|
227
534
|
// X axis: explicit → direct, auto → lazy expand
|
|
228
535
|
if (x.explicit) {
|
|
229
|
-
|
|
536
|
+
zoom_x_range = x.range
|
|
537
|
+
} else {
|
|
538
|
+
const result = expand_range_if_needed(initial_x_range, x.range)
|
|
539
|
+
if (result.changed) {
|
|
540
|
+
;[initial_x_range, zoom_x_range] = [result.range, result.range]
|
|
541
|
+
}
|
|
230
542
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
543
|
+
|
|
544
|
+
// X2 axis: explicit → direct, auto → lazy expand
|
|
545
|
+
if (x2.explicit) {
|
|
546
|
+
zoom_x2_range = x2.range
|
|
547
|
+
} else {
|
|
548
|
+
const result = expand_range_if_needed(initial_x2_range, x2.range)
|
|
549
|
+
if (result.changed) {
|
|
550
|
+
;[initial_x2_range, zoom_x2_range] = [result.range, result.range]
|
|
551
|
+
}
|
|
237
552
|
}
|
|
553
|
+
|
|
238
554
|
// Y axis: explicit → direct, auto → lazy expand
|
|
239
555
|
if (y.explicit) {
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
[initial_y_range, zoom_y_range] = [result.range, result.range];
|
|
247
|
-
}
|
|
556
|
+
zoom_y_range = y.range
|
|
557
|
+
} else {
|
|
558
|
+
const result = expand_range_if_needed(initial_y_range, y.range)
|
|
559
|
+
if (result.changed) {
|
|
560
|
+
;[initial_y_range, zoom_y_range] = [result.range, result.range]
|
|
561
|
+
}
|
|
248
562
|
}
|
|
563
|
+
|
|
249
564
|
// Y2 axis: explicit → direct, else expand initial range then optionally sync
|
|
250
565
|
if (y2.explicit) {
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
else if (result.changed) {
|
|
262
|
-
zoom_y2_range = result.range;
|
|
263
|
-
}
|
|
566
|
+
zoom_y2_range = y2.range
|
|
567
|
+
} else {
|
|
568
|
+
const result = expand_range_if_needed(initial_y2_range, y2.range)
|
|
569
|
+
if (result.changed) initial_y2_range = result.range
|
|
570
|
+
// Apply sync if enabled, otherwise use expanded range (or keep current if unchanged)
|
|
571
|
+
if (y2_sync_config.mode !== `none`) {
|
|
572
|
+
zoom_y2_range = sync_y2_range(zoom_y_range, initial_y2_range, y2_sync_config)
|
|
573
|
+
} else if (result.changed) {
|
|
574
|
+
zoom_y2_range = result.range
|
|
575
|
+
}
|
|
264
576
|
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
let [
|
|
268
|
-
let [
|
|
269
|
-
|
|
270
|
-
let
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
let [x_min, x_max] = $derived(zoom_x_range)
|
|
580
|
+
let [x2_min, x2_max] = $derived(zoom_x2_range)
|
|
581
|
+
let [y_min, y_max] = $derived(zoom_y_range)
|
|
582
|
+
let [y2_min, y2_max] = $derived(zoom_y2_range)
|
|
583
|
+
|
|
584
|
+
// Create auto color range
|
|
585
|
+
let auto_color_range = $derived(
|
|
586
|
+
// Ensure we only calculate extent on actual numbers, filtering out nulls/undefined
|
|
587
|
+
all_color_values.length > 0
|
|
588
|
+
? extent(
|
|
589
|
+
all_color_values.filter((color_val: number | null): color_val is number =>
|
|
590
|
+
typeof color_val === `number`
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
: [0, 1],
|
|
594
|
+
) as Vec2
|
|
595
|
+
|
|
596
|
+
// Create scale functions
|
|
597
|
+
// For time scales, use scaleTime directly; otherwise use create_scale (supports linear/log/arcsinh)
|
|
598
|
+
let x_scale_fn = $derived(
|
|
599
|
+
is_time_x
|
|
600
|
+
? scaleTime()
|
|
279
601
|
.domain([new Date(x_min), new Date(x_max)])
|
|
280
602
|
.range([pad.l, width - pad.r])
|
|
281
|
-
|
|
603
|
+
: create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [
|
|
282
604
|
pad.l,
|
|
283
605
|
width - pad.r,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
]
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
let
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
606
|
+
]),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
let x2_scale_fn = $derived(
|
|
610
|
+
is_time_x2
|
|
611
|
+
? scaleTime()
|
|
612
|
+
.domain([new Date(x2_min), new Date(x2_max)])
|
|
613
|
+
.range([pad.l, width - pad.r])
|
|
614
|
+
: create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [
|
|
615
|
+
pad.l,
|
|
616
|
+
width - pad.r,
|
|
617
|
+
]),
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
let y_scale_fn = $derived(
|
|
621
|
+
create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [
|
|
622
|
+
height - pad.b,
|
|
623
|
+
pad.t,
|
|
624
|
+
]),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
let y2_scale_fn = $derived(
|
|
628
|
+
create_scale(final_y2_axis.scale_type ?? `linear`, [y2_min, y2_max], [
|
|
629
|
+
height - pad.b,
|
|
630
|
+
pad.t,
|
|
631
|
+
]),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
// All size values from series (for size scale) - extracted in series_value_arrays
|
|
635
|
+
let all_size_values = $derived(series_value_arrays.size_values)
|
|
636
|
+
|
|
637
|
+
// Size scale function (using shared utility)
|
|
638
|
+
let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
|
|
639
|
+
|
|
640
|
+
// Color scale function (using shared utility)
|
|
641
|
+
let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range))
|
|
642
|
+
|
|
643
|
+
// Filter series data to only include points within bounds and augment with internal data
|
|
644
|
+
let filtered_series = $derived(
|
|
645
|
+
series_with_ids
|
|
646
|
+
.map((data_series: DataSeries<Metadata>, series_idx): DataSeries<Metadata> => {
|
|
647
|
+
// Handle null/undefined series first
|
|
648
|
+
if (!data_series) {
|
|
649
|
+
return {
|
|
305
650
|
x: [],
|
|
306
651
|
y: [],
|
|
307
652
|
visible: true,
|
|
308
653
|
filtered_data: [],
|
|
309
654
|
_id: series_idx,
|
|
310
655
|
orig_series_idx: series_idx,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Handle explicitly hidden series
|
|
660
|
+
if (!(data_series.visible ?? true)) {
|
|
661
|
+
return {
|
|
316
662
|
...data_series,
|
|
317
663
|
visible: false,
|
|
318
664
|
filtered_data: [],
|
|
319
665
|
orig_series_idx: series_idx,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const { x: xs, y: ys, color_values, size_values, ...rest } = data_series
|
|
670
|
+
|
|
671
|
+
// Process points internally, adding properties beyond the base Point type
|
|
672
|
+
const processed_points: InternalPoint<Metadata>[] = xs.map(
|
|
673
|
+
(x_val: number, point_idx: number) => ({
|
|
674
|
+
x: x_val,
|
|
675
|
+
y: ys[point_idx],
|
|
676
|
+
color_value: color_values?.[point_idx],
|
|
677
|
+
metadata: process_prop(rest.metadata, point_idx) as Metadata | undefined,
|
|
678
|
+
point_style: process_prop(rest.point_style, point_idx),
|
|
679
|
+
point_hover: process_prop(rest.point_hover, point_idx),
|
|
680
|
+
point_label: process_prop(rest.point_label, point_idx),
|
|
681
|
+
point_offset: process_prop(rest.point_offset, point_idx),
|
|
682
|
+
series_idx,
|
|
683
|
+
point_idx,
|
|
684
|
+
size_value: size_values?.[point_idx],
|
|
685
|
+
}),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
// Filter to points within the plot bounds (handles inverted ranges like [3.5, 1.4])
|
|
689
|
+
const in_range = (val: number | null | undefined, lo: number, hi: number) =>
|
|
690
|
+
val != null && !isNaN(val) && val >= Math.min(lo, hi) &&
|
|
691
|
+
val <= Math.max(lo, hi)
|
|
692
|
+
|
|
693
|
+
// Determine which ranges to use based on series axis properties
|
|
694
|
+
const [series_x_min, series_x_max] = (data_series.x_axis ?? `x1`) === `x2`
|
|
695
|
+
? [x2_min, x2_max]
|
|
696
|
+
: [x_min, x_max]
|
|
697
|
+
const [series_y_min, series_y_max] = (data_series.y_axis ?? `y1`) === `y2`
|
|
698
|
+
? [y2_min, y2_max]
|
|
699
|
+
: [y_min, y_max]
|
|
700
|
+
|
|
701
|
+
const filtered_data_with_extras = processed_points.filter(
|
|
702
|
+
({ x, y }) =>
|
|
703
|
+
in_range(x, series_x_min, series_x_max) &&
|
|
704
|
+
in_range(y, series_y_min, series_y_max),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
// Return structure consistent with DataSeries but acknowledge internal data structure (filtered_data)
|
|
708
|
+
return {
|
|
709
|
+
...data_series,
|
|
710
|
+
visible: true, // Mark series as visible here
|
|
711
|
+
filtered_data: filtered_data_with_extras,
|
|
712
|
+
orig_series_idx: series_idx, // Store original index for auto-cycling colors/symbols
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
// Filter series end up completely empty after point filtering
|
|
716
|
+
.filter((
|
|
717
|
+
srs,
|
|
718
|
+
): srs is DataSeries<Metadata> & { filtered_data: InternalPoint<Metadata>[] } =>
|
|
719
|
+
!!srs.filtered_data && srs.filtered_data.length > 0
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
// Collect all plot points for legend placement calculation
|
|
724
|
+
let plot_points_for_placement = $derived.by(() => {
|
|
725
|
+
if (!width || !height || !filtered_series) return []
|
|
726
|
+
|
|
727
|
+
const points: { x: number; y: number }[] = []
|
|
728
|
+
|
|
360
729
|
for (const series_data of filtered_series) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
730
|
+
if (!series_data?.filtered_data) continue
|
|
731
|
+
const use_x2_scale = series_data.x_axis === `x2`
|
|
732
|
+
for (const point of series_data.filtered_data) {
|
|
733
|
+
const active_x_scale = use_x2_scale ? x2_scale_fn : x_scale_fn
|
|
734
|
+
const active_is_time_x = use_x2_scale ? is_time_x2 : is_time_x
|
|
735
|
+
const point_x_coord = active_is_time_x
|
|
736
|
+
? active_x_scale(new Date(point.x))
|
|
737
|
+
: active_x_scale(point.x)
|
|
738
|
+
const point_y_coord =
|
|
739
|
+
(series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(
|
|
740
|
+
point.y,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if (isFinite(point_x_coord) && isFinite(point_y_coord)) {
|
|
744
|
+
points.push({ x: point_x_coord, y: point_y_coord })
|
|
371
745
|
}
|
|
746
|
+
}
|
|
372
747
|
}
|
|
373
|
-
return points
|
|
374
|
-
})
|
|
375
|
-
|
|
748
|
+
return points
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
// Explicitly define the type for display_style matching PlotLegend expectations
|
|
752
|
+
type LegendDisplayStyle = {
|
|
753
|
+
symbol_type?: D3SymbolName
|
|
754
|
+
symbol_color?: string
|
|
755
|
+
line_color?: string
|
|
756
|
+
line_dash?: string
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Computed fill regions: merge fill_regions and converted error_bands, resolve boundaries
|
|
760
|
+
type ComputedFill = FillRegion & {
|
|
761
|
+
idx: number
|
|
762
|
+
source_type: `fill_region` | `error_band`
|
|
763
|
+
source_idx: number
|
|
764
|
+
path_segments: string[]
|
|
765
|
+
}
|
|
766
|
+
let computed_fills = $derived.by((): ComputedFill[] => {
|
|
376
767
|
// Early exit: skip expensive computation if no fills to render
|
|
377
|
-
const has_fill_regions = fill_regions && fill_regions.length > 0
|
|
378
|
-
const has_error_bands = error_bands && error_bands.length > 0
|
|
379
|
-
if (!has_fill_regions && !has_error_bands)
|
|
380
|
-
|
|
768
|
+
const has_fill_regions = fill_regions && fill_regions.length > 0
|
|
769
|
+
const has_error_bands = error_bands && error_bands.length > 0
|
|
770
|
+
if (!has_fill_regions && !has_error_bands) return []
|
|
771
|
+
|
|
381
772
|
// Merge fill_regions and converted error_bands, tracking source
|
|
382
|
-
const all_regions
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
773
|
+
const all_regions: {
|
|
774
|
+
region: FillRegion | null
|
|
775
|
+
source_type: `fill_region` | `error_band`
|
|
776
|
+
source_idx: number
|
|
777
|
+
}[] = [
|
|
778
|
+
...(fill_regions ?? []).map((region, source_idx) => ({
|
|
779
|
+
region,
|
|
780
|
+
source_type: `fill_region` as const,
|
|
781
|
+
source_idx,
|
|
782
|
+
})),
|
|
783
|
+
...(error_bands ?? []).map((band, source_idx) => ({
|
|
784
|
+
region: convert_error_band_to_fill_region(band, series_with_ids),
|
|
785
|
+
source_type: `error_band` as const,
|
|
786
|
+
source_idx,
|
|
787
|
+
})),
|
|
788
|
+
]
|
|
789
|
+
|
|
394
790
|
// Compute unique x-values once for all fills
|
|
395
791
|
// Optimization: deduplicate first (O(n)), then sort only unique values (O(k log k))
|
|
396
792
|
// This is faster for datasets with many duplicate x-values across series
|
|
397
|
-
const x_set = new SvelteSet()
|
|
793
|
+
const x_set = new SvelteSet<number>()
|
|
398
794
|
for (const data_series of series_with_ids) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
x_set.add(val);
|
|
404
|
-
}
|
|
795
|
+
if (!data_series) continue
|
|
796
|
+
for (const val of data_series.x) {
|
|
797
|
+
if (typeof val === `number` && isFinite(val)) x_set.add(val)
|
|
798
|
+
}
|
|
405
799
|
}
|
|
406
|
-
const unique_x = [...x_set].sort((val_a, val_b) => val_a - val_b)
|
|
407
|
-
|
|
408
|
-
|
|
800
|
+
const unique_x = [...x_set].sort((val_a, val_b) => val_a - val_b)
|
|
801
|
+
|
|
802
|
+
if (unique_x.length === 0) return []
|
|
803
|
+
|
|
409
804
|
return all_regions
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
805
|
+
.filter((
|
|
806
|
+
entry,
|
|
807
|
+
): entry is {
|
|
808
|
+
region: FillRegion
|
|
809
|
+
source_type: `fill_region` | `error_band`
|
|
810
|
+
source_idx: number
|
|
811
|
+
} => entry.region !== null)
|
|
812
|
+
.map(({ region, source_type, source_idx }, idx) => {
|
|
813
|
+
if (region.visible === false) return null
|
|
814
|
+
|
|
414
815
|
// Domain context for boundary resolution
|
|
415
816
|
const domains = {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
817
|
+
y_domain: [y_min, y_max] as Vec2,
|
|
818
|
+
y2_domain: [y2_min, y2_max] as Vec2,
|
|
819
|
+
}
|
|
820
|
+
|
|
419
821
|
// Resolve upper and lower boundaries
|
|
420
|
-
const upper_values = resolve_boundary(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
822
|
+
const upper_values = resolve_boundary(
|
|
823
|
+
region.upper,
|
|
824
|
+
series_with_ids,
|
|
825
|
+
unique_x,
|
|
826
|
+
domains,
|
|
827
|
+
)
|
|
828
|
+
const lower_values = resolve_boundary(
|
|
829
|
+
region.lower,
|
|
830
|
+
series_with_ids,
|
|
831
|
+
unique_x,
|
|
832
|
+
domains,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
if (!upper_values || !lower_values) return null
|
|
836
|
+
|
|
424
837
|
// Apply range constraints
|
|
425
|
-
const range_filtered = apply_range_constraints(
|
|
838
|
+
const range_filtered = apply_range_constraints(
|
|
839
|
+
unique_x,
|
|
840
|
+
lower_values,
|
|
841
|
+
upper_values,
|
|
842
|
+
region,
|
|
843
|
+
)
|
|
844
|
+
|
|
426
845
|
// Clamp for log scale if needed
|
|
427
|
-
const y_scale_type = final_y_axis.scale_type ?? `linear
|
|
428
|
-
const x_scale_type = final_x_axis.scale_type ?? `linear
|
|
429
|
-
const clamped = clamp_for_log_scale(
|
|
846
|
+
const y_scale_type = final_y_axis.scale_type ?? `linear`
|
|
847
|
+
const x_scale_type = final_x_axis.scale_type ?? `linear`
|
|
848
|
+
const clamped = clamp_for_log_scale(
|
|
849
|
+
range_filtered.x,
|
|
850
|
+
range_filtered.y1,
|
|
851
|
+
range_filtered.y2,
|
|
852
|
+
y_scale_type,
|
|
853
|
+
x_scale_type,
|
|
854
|
+
)
|
|
855
|
+
|
|
430
856
|
// Apply where condition (splits into segments)
|
|
431
|
-
const conditioned = apply_where_condition(
|
|
857
|
+
const conditioned = apply_where_condition(
|
|
858
|
+
clamped.x,
|
|
859
|
+
clamped.y1,
|
|
860
|
+
clamped.y2,
|
|
861
|
+
region,
|
|
862
|
+
)
|
|
863
|
+
|
|
432
864
|
// Generate paths for each segment (convert to pixel coordinates)
|
|
433
865
|
const path_segments = conditioned.segments
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const pixel_data = segment.map((point) => ({
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}))
|
|
441
|
-
return generate_fill_path(pixel_data, region.curve ?? `monotoneX`)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
866
|
+
.filter((segment) => segment.length > 1)
|
|
867
|
+
.map((segment) => {
|
|
868
|
+
const pixel_data: FillPathPoint[] = segment.map((point) => ({
|
|
869
|
+
x: x_scale_fn(point.x),
|
|
870
|
+
y1: y_scale_fn(point.y1),
|
|
871
|
+
y2: y_scale_fn(point.y2),
|
|
872
|
+
}))
|
|
873
|
+
return generate_fill_path(pixel_data, region.curve ?? `monotoneX`)
|
|
874
|
+
})
|
|
875
|
+
.filter((path) => path.length > 0)
|
|
876
|
+
|
|
877
|
+
if (path_segments.length === 0) return null
|
|
878
|
+
|
|
879
|
+
return { ...region, idx, source_type, source_idx, path_segments }
|
|
880
|
+
})
|
|
881
|
+
.filter((fill): fill is ComputedFill => fill !== null)
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
// Prepare data needed for the legend component
|
|
885
|
+
let legend_data = $derived.by(() => {
|
|
886
|
+
const items = series_with_ids.map(
|
|
887
|
+
(data_series: DataSeries & { _id?: string | number }, series_idx: number) => {
|
|
888
|
+
const is_visible = data_series?.visible ?? true
|
|
454
889
|
// Prefer top-level label, fallback to metadata label
|
|
455
890
|
const explicit_label = data_series?.label ??
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
891
|
+
(typeof data_series?.metadata === `object` &&
|
|
892
|
+
data_series.metadata !== null &&
|
|
893
|
+
`label` in data_series.metadata &&
|
|
894
|
+
typeof data_series.metadata.label === `string`
|
|
895
|
+
? data_series.metadata.label
|
|
896
|
+
: null)
|
|
462
897
|
// Use explicit label or generate default
|
|
463
|
-
const label = explicit_label ?? `Series ${series_idx + 1}
|
|
464
|
-
const has_explicit_label = explicit_label != null
|
|
898
|
+
const label = explicit_label ?? `Series ${series_idx + 1}`
|
|
899
|
+
const has_explicit_label = explicit_label != null
|
|
900
|
+
|
|
465
901
|
// Use series-specific defaults for auto-differentiation
|
|
466
|
-
const series_default_color = get_series_color(series_idx)
|
|
467
|
-
const series_default_symbol = get_series_symbol(series_idx)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
902
|
+
const series_default_color = get_series_color(series_idx)
|
|
903
|
+
const series_default_symbol = get_series_symbol(series_idx)
|
|
904
|
+
|
|
905
|
+
const display_style: LegendDisplayStyle = {
|
|
906
|
+
symbol_type: series_default_symbol,
|
|
907
|
+
symbol_color: series_default_color,
|
|
908
|
+
line_color: series_default_color,
|
|
909
|
+
}
|
|
910
|
+
const series_markers = data_series?.markers ?? DEFAULT_MARKERS
|
|
911
|
+
|
|
474
912
|
// Check point_style (could be object or array)
|
|
475
913
|
const first_point_style = Array.isArray(data_series?.point_style)
|
|
476
|
-
|
|
477
|
-
|
|
914
|
+
? data_series.point_style[0]
|
|
915
|
+
: data_series?.point_style
|
|
916
|
+
|
|
478
917
|
if (series_markers?.includes(`points`)) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (first_point_style.fill) {
|
|
488
|
-
display_style.symbol_color = first_point_style.fill;
|
|
489
|
-
}
|
|
490
|
-
if (first_point_style.stroke) {
|
|
491
|
-
// Use stroke color if fill is none or transparent
|
|
492
|
-
if (!display_style.symbol_color ||
|
|
493
|
-
display_style.symbol_color === `none` ||
|
|
494
|
-
display_style.symbol_color.startsWith(`rgba(`, 0) // Check if transparent
|
|
495
|
-
)
|
|
496
|
-
display_style.symbol_color = first_point_style.stroke;
|
|
497
|
-
}
|
|
918
|
+
if (first_point_style) {
|
|
919
|
+
// Use explicit symbol_type if provided and valid, otherwise keep series default
|
|
920
|
+
if (
|
|
921
|
+
typeof first_point_style.symbol_type === `string` &&
|
|
922
|
+
symbol_names.includes(first_point_style.symbol_type as D3SymbolName)
|
|
923
|
+
) {
|
|
924
|
+
display_style.symbol_type = first_point_style
|
|
925
|
+
.symbol_type as D3SymbolName
|
|
498
926
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
927
|
+
|
|
928
|
+
// Use explicit fill color if provided
|
|
929
|
+
if (first_point_style.fill) {
|
|
930
|
+
display_style.symbol_color = first_point_style.fill
|
|
931
|
+
}
|
|
932
|
+
if (first_point_style.stroke) {
|
|
933
|
+
// Use stroke color if fill is none or transparent
|
|
934
|
+
if (
|
|
935
|
+
!display_style.symbol_color ||
|
|
936
|
+
display_style.symbol_color === `none` ||
|
|
937
|
+
display_style.symbol_color.startsWith(`rgba(`, 0) // Check if transparent
|
|
938
|
+
) display_style.symbol_color = first_point_style.stroke
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// else: keep series-specific defaults for symbol_type and symbol_color
|
|
942
|
+
} else {
|
|
943
|
+
// If no points marker, explicitly remove marker style for legend
|
|
944
|
+
display_style.symbol_type = undefined
|
|
945
|
+
display_style.symbol_color = undefined
|
|
505
946
|
}
|
|
947
|
+
|
|
506
948
|
// Check line_style
|
|
507
949
|
if (series_markers?.includes(`line`)) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
950
|
+
// Prefer explicit line stroke, then other explicit colors, then series default
|
|
951
|
+
let legend_line_color = data_series?.line_style?.stroke
|
|
952
|
+
if (!legend_line_color) {
|
|
953
|
+
// Try color scale if available
|
|
954
|
+
const first_cv = Array.isArray(data_series?.color_values)
|
|
955
|
+
? data_series?.color_values?.find((color_val: number | null) =>
|
|
956
|
+
color_val != null
|
|
957
|
+
)
|
|
958
|
+
: undefined
|
|
959
|
+
legend_line_color =
|
|
960
|
+
(first_cv != null ? color_scale_fn(first_cv) : undefined) ||
|
|
961
|
+
first_point_style?.fill ||
|
|
962
|
+
first_point_style?.stroke ||
|
|
963
|
+
series_default_color
|
|
964
|
+
}
|
|
965
|
+
display_style.line_color = legend_line_color
|
|
966
|
+
display_style.line_dash = data_series?.line_style?.line_dash
|
|
967
|
+
} else {
|
|
968
|
+
// If no line marker, explicitly remove line style for legend
|
|
969
|
+
display_style.line_dash = undefined
|
|
970
|
+
display_style.line_color = undefined
|
|
528
971
|
}
|
|
972
|
+
|
|
529
973
|
return {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
974
|
+
series_idx,
|
|
975
|
+
label,
|
|
976
|
+
visible: is_visible,
|
|
977
|
+
display_style,
|
|
978
|
+
has_explicit_label,
|
|
979
|
+
legend_group: data_series?.legend_group,
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
)
|
|
983
|
+
|
|
538
984
|
// Deduplicate by label+legend_group - keep first occurrence of each unique combination
|
|
539
|
-
const seen_labels = new SvelteSet()
|
|
540
|
-
const series_items = items.filter(
|
|
985
|
+
const seen_labels = new SvelteSet<string>()
|
|
986
|
+
const series_items = items.filter(
|
|
987
|
+
(
|
|
988
|
+
legend_item: {
|
|
989
|
+
label: string
|
|
990
|
+
series_idx: number
|
|
991
|
+
visible: boolean
|
|
992
|
+
display_style: LegendDisplayStyle
|
|
993
|
+
has_explicit_label: boolean
|
|
994
|
+
legend_group?: string
|
|
995
|
+
},
|
|
996
|
+
) => {
|
|
541
997
|
// Use label+group as unique key (group may be undefined)
|
|
542
|
-
const unique_key = `${legend_item.legend_group ?? ``}::${legend_item.label}
|
|
543
|
-
if (seen_labels.has(unique_key))
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
998
|
+
const unique_key = `${legend_item.legend_group ?? ``}::${legend_item.label}`
|
|
999
|
+
if (seen_labels.has(unique_key)) return false
|
|
1000
|
+
seen_labels.add(unique_key)
|
|
1001
|
+
return true
|
|
1002
|
+
},
|
|
1003
|
+
)
|
|
1004
|
+
|
|
548
1005
|
// Add fill region items to legend (deduplicated using same key format as series)
|
|
549
1006
|
const fill_items = computed_fills
|
|
550
|
-
|
|
551
|
-
|
|
1007
|
+
.filter((fill) => fill.show_in_legend !== false && fill.label)
|
|
1008
|
+
.filter((fill) => {
|
|
552
1009
|
// Use same composite key as series: legend_group::label
|
|
553
|
-
const unique_key = `${fill.legend_group ?? ``}::${fill.label}
|
|
554
|
-
if (seen_labels.has(unique_key))
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
.map((fill) => {
|
|
1010
|
+
const unique_key = `${fill.legend_group ?? ``}::${fill.label ?? ``}`
|
|
1011
|
+
if (seen_labels.has(unique_key)) return false
|
|
1012
|
+
seen_labels.add(unique_key)
|
|
1013
|
+
return true
|
|
1014
|
+
})
|
|
1015
|
+
.map((fill) => {
|
|
560
1016
|
// Pass gradient for swatch rendering, or solid color as fallback
|
|
561
|
-
const fill_gradient = is_fill_gradient(fill.fill) ? fill.fill : undefined
|
|
562
|
-
const fill_color = typeof fill.fill === `string` ? fill.fill : undefined
|
|
1017
|
+
const fill_gradient = is_fill_gradient(fill.fill) ? fill.fill : undefined
|
|
1018
|
+
const fill_color = typeof fill.fill === `string` ? fill.fill : undefined
|
|
1019
|
+
|
|
563
1020
|
return {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1021
|
+
series_idx: -1, // Not a series
|
|
1022
|
+
fill_idx: fill.idx,
|
|
1023
|
+
fill_source_type: fill.source_type,
|
|
1024
|
+
fill_source_idx: fill.source_idx,
|
|
1025
|
+
item_type: `fill` as const,
|
|
1026
|
+
label: fill.label ?? ``,
|
|
1027
|
+
visible: fill.visible !== false,
|
|
1028
|
+
legend_group: fill.legend_group,
|
|
1029
|
+
display_style: {
|
|
1030
|
+
fill_color,
|
|
1031
|
+
fill_opacity: fill.fill_opacity ?? 0.3,
|
|
1032
|
+
edge_color: fill.edge_upper?.color,
|
|
1033
|
+
fill_gradient,
|
|
1034
|
+
},
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
return [...series_items, ...fill_items]
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
// Group fills by z-index for ordered rendering (single pass instead of 4 filters)
|
|
1042
|
+
let fills_by_z = $derived.by(() => {
|
|
1043
|
+
const groups: {
|
|
1044
|
+
below_grid: typeof computed_fills
|
|
1045
|
+
below_lines: typeof computed_fills
|
|
1046
|
+
below_points: typeof computed_fills
|
|
1047
|
+
above_all: typeof computed_fills
|
|
1048
|
+
} = { below_grid: [], below_lines: [], below_points: [], above_all: [] }
|
|
1049
|
+
|
|
585
1050
|
for (const fill of computed_fills) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
else if (fill.z_index === `above-all`)
|
|
591
|
-
groups.above_all.push(fill);
|
|
592
|
-
else
|
|
593
|
-
groups.below_lines.push(fill); // default: no z_index or 'below-lines'
|
|
1051
|
+
if (fill.z_index === `below-grid`) groups.below_grid.push(fill)
|
|
1052
|
+
else if (fill.z_index === `below-points`) groups.below_points.push(fill)
|
|
1053
|
+
else if (fill.z_index === `above-all`) groups.above_all.push(fill)
|
|
1054
|
+
else groups.below_lines.push(fill) // default: no z_index or 'below-lines'
|
|
594
1055
|
}
|
|
595
|
-
return groups
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
let
|
|
600
|
-
|
|
601
|
-
|
|
1056
|
+
return groups
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
// Compute ref_lines with index and group by z-index (using shared utilities)
|
|
1060
|
+
let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
|
|
1061
|
+
let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
|
|
1062
|
+
|
|
1063
|
+
// Calculate best legend placement using continuous grid sampling
|
|
1064
|
+
let legend_placement = $derived.by(() => {
|
|
602
1065
|
const should_place = legend != null &&
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
1066
|
+
(legend_data.length > 1 || Object.keys(legend).length > 0)
|
|
1067
|
+
|
|
1068
|
+
if (!should_place || !width || !height) return null
|
|
1069
|
+
|
|
1070
|
+
const plot_width = width - pad.l - pad.r
|
|
1071
|
+
const plot_height = height - pad.t - pad.b
|
|
1072
|
+
|
|
608
1073
|
// Use measured size if available, otherwise estimate
|
|
609
1074
|
const legend_size = legend_element
|
|
610
|
-
|
|
611
|
-
|
|
1075
|
+
? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
|
|
1076
|
+
: { width: 120, height: 80 }
|
|
1077
|
+
|
|
612
1078
|
const placement_config = {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1079
|
+
plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
|
|
1080
|
+
element_size: legend_size,
|
|
1081
|
+
axis_clearance: legend?.axis_clearance,
|
|
1082
|
+
exclude_rects: [],
|
|
1083
|
+
points: plot_points_for_placement,
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return compute_element_placement(placement_config)
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
// Calculate color bar placement (coordinates with legend to avoid overlap)
|
|
1090
|
+
let color_bar_placement = $derived.by(() => {
|
|
1091
|
+
if (!color_bar || !all_color_values.length || !width || !height) return null
|
|
1092
|
+
|
|
1093
|
+
const plot_width = width - pad.l - pad.r
|
|
1094
|
+
const plot_height = height - pad.t - pad.b
|
|
1095
|
+
|
|
627
1096
|
// Use measured size if available, otherwise estimate based on orientation
|
|
628
|
-
const is_horizontal = color_bar.orientation === `horizontal
|
|
1097
|
+
const is_horizontal = color_bar.orientation === `horizontal`
|
|
629
1098
|
const colorbar_size = colorbar_element
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1099
|
+
? { width: colorbar_element.offsetWidth, height: colorbar_element.offsetHeight }
|
|
1100
|
+
: is_horizontal
|
|
1101
|
+
? { width: 220, height: 40 }
|
|
1102
|
+
: { width: 40, height: 100 }
|
|
1103
|
+
|
|
634
1104
|
// Build exclusion rects (avoid legend if it's placed)
|
|
635
|
-
const exclude_rects = []
|
|
1105
|
+
const exclude_rects: Rect[] = []
|
|
636
1106
|
if (legend_element && legend_placement) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1107
|
+
exclude_rects.push({
|
|
1108
|
+
x: legend_placement.x,
|
|
1109
|
+
y: legend_placement.y,
|
|
1110
|
+
width: legend_element.offsetWidth || 120,
|
|
1111
|
+
height: legend_element.offsetHeight || 80,
|
|
1112
|
+
})
|
|
643
1113
|
}
|
|
1114
|
+
|
|
644
1115
|
return compute_element_placement({
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
})
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1116
|
+
plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
|
|
1117
|
+
element_size: colorbar_size,
|
|
1118
|
+
// Colorbar needs slightly more clearance than legend to avoid axis labels
|
|
1119
|
+
axis_clearance: 15,
|
|
1120
|
+
exclude_rects,
|
|
1121
|
+
points: plot_points_for_placement,
|
|
1122
|
+
})
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
// Active legend placement (null if user set explicit position)
|
|
1126
|
+
let active_legend_placement = $derived.by(() => {
|
|
1127
|
+
if (!legend_placement) return null
|
|
1128
|
+
|
|
657
1129
|
// Skip auto-placement if user set explicit position in style
|
|
658
|
-
const legend_style = legend?.style ??
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1130
|
+
const legend_style = legend?.style ?? ``
|
|
1131
|
+
if (
|
|
1132
|
+
/(^|[;{]\s*)(top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(
|
|
1133
|
+
legend_style,
|
|
1134
|
+
)
|
|
1135
|
+
) return null
|
|
1136
|
+
|
|
1137
|
+
return legend_placement
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
// Initialize tweened values for color bar position - create once, update target via effect
|
|
1141
|
+
// untrack() explicitly captures initial tween config (intentional - config set once at mount)
|
|
1142
|
+
const tweened_colorbar_coords = new Tween(
|
|
1143
|
+
{ x: 0, y: 0 },
|
|
1144
|
+
untrack(() => ({ duration: 400, ...(color_bar?.tween ?? {}) })),
|
|
1145
|
+
)
|
|
1146
|
+
// Initialize tweened values for legend position - create once, update target via effect
|
|
1147
|
+
const tweened_legend_coords = new Tween(
|
|
1148
|
+
{ x: 0, y: 0 },
|
|
1149
|
+
untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
// Update placement positions (with animation and stability checks)
|
|
1153
|
+
$effect(() => {
|
|
1154
|
+
if (!width || !height) return
|
|
1155
|
+
|
|
672
1156
|
// Track dimensions for resize detection
|
|
673
|
-
const dims_changed = dim_tracker.has_changed(width, height)
|
|
674
|
-
if (dims_changed)
|
|
675
|
-
|
|
1157
|
+
const dims_changed = dim_tracker.has_changed(width, height)
|
|
1158
|
+
if (dims_changed) dim_tracker.update(width, height)
|
|
1159
|
+
|
|
676
1160
|
// Update colorbar position (stable after initial placement unless responsive)
|
|
677
1161
|
if (color_bar_placement) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1162
|
+
const is_responsive = color_bar?.responsive ?? false
|
|
1163
|
+
const should_update = dims_changed || (!colorbar_hover.is_locked.current &&
|
|
1164
|
+
(is_responsive || !has_initial_colorbar_placement))
|
|
1165
|
+
|
|
1166
|
+
if (should_update) {
|
|
1167
|
+
tweened_colorbar_coords.set(
|
|
1168
|
+
{ x: color_bar_placement.x, y: color_bar_placement.y },
|
|
1169
|
+
has_initial_colorbar_placement ? undefined : { duration: 0 },
|
|
1170
|
+
)
|
|
1171
|
+
if (colorbar_element && !has_initial_colorbar_placement) {
|
|
1172
|
+
has_initial_colorbar_placement = true
|
|
686
1173
|
}
|
|
1174
|
+
}
|
|
687
1175
|
}
|
|
1176
|
+
|
|
688
1177
|
// Update legend position (stable after initial placement unless responsive)
|
|
689
1178
|
if (legend_manual_position && !legend_is_dragging) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1179
|
+
// Immediate update (no animation) for manually dragged positions
|
|
1180
|
+
tweened_legend_coords.set(legend_manual_position, { duration: 0 })
|
|
1181
|
+
} else if (active_legend_placement && !legend_is_dragging) {
|
|
1182
|
+
const is_responsive = legend?.responsive ?? false
|
|
1183
|
+
const should_update = dims_changed || (!legend_hover.is_locked.current &&
|
|
1184
|
+
(is_responsive || !has_initial_legend_placement))
|
|
1185
|
+
|
|
1186
|
+
if (should_update) {
|
|
1187
|
+
tweened_legend_coords.set(
|
|
1188
|
+
{ x: active_legend_placement.x, y: active_legend_placement.y },
|
|
1189
|
+
has_initial_legend_placement ? undefined : { duration: 0 },
|
|
1190
|
+
)
|
|
1191
|
+
if (legend_element) has_initial_legend_placement = true
|
|
1192
|
+
}
|
|
702
1193
|
}
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
// Generate axis ticks - consolidated into single derived for efficiency
|
|
1197
|
+
let axis_ticks = $derived.by(() => {
|
|
1198
|
+
if (!width || !height) return { x: [], x2: [], y: [], y2: [] }
|
|
1199
|
+
|
|
708
1200
|
// X-axis ticks: choose appropriate scale for tick generation
|
|
709
1201
|
// Time scales (format starts with %) use scaleTime for better tick placement
|
|
710
|
-
const x_scale_for_ticks =
|
|
711
|
-
|
|
712
|
-
|
|
1202
|
+
const x_scale_for_ticks = is_time_x
|
|
1203
|
+
? scaleTime().domain([new Date(x_min), new Date(x_max)])
|
|
1204
|
+
: create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1])
|
|
1205
|
+
|
|
1206
|
+
const x2_scale_for_ticks = is_time_x2
|
|
1207
|
+
? scaleTime().domain([new Date(x2_min), new Date(x2_max)])
|
|
1208
|
+
: create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [0, 1])
|
|
1209
|
+
|
|
713
1210
|
return {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1211
|
+
x: generate_ticks(
|
|
1212
|
+
[x_min, x_max],
|
|
1213
|
+
final_x_axis.scale_type ?? `linear`,
|
|
1214
|
+
final_x_axis.ticks,
|
|
1215
|
+
x_scale_for_ticks,
|
|
1216
|
+
{ format: final_x_axis.format },
|
|
1217
|
+
),
|
|
1218
|
+
x2: x2_points.length > 0
|
|
1219
|
+
? generate_ticks(
|
|
1220
|
+
[x2_min, x2_max],
|
|
1221
|
+
final_x2_axis.scale_type ?? `linear`,
|
|
1222
|
+
final_x2_axis.ticks,
|
|
1223
|
+
x2_scale_for_ticks,
|
|
1224
|
+
{ format: final_x2_axis.format },
|
|
1225
|
+
)
|
|
1226
|
+
: [],
|
|
1227
|
+
y: generate_ticks(
|
|
1228
|
+
[y_min, y_max],
|
|
1229
|
+
final_y_axis.scale_type ?? `linear`,
|
|
1230
|
+
final_y_axis.ticks,
|
|
1231
|
+
y_scale_fn,
|
|
1232
|
+
{ default_count: 5 },
|
|
1233
|
+
),
|
|
1234
|
+
y2: y2_points.length > 0
|
|
1235
|
+
? generate_ticks(
|
|
1236
|
+
[y2_min, y2_max],
|
|
1237
|
+
final_y2_axis.scale_type ?? `linear`,
|
|
1238
|
+
final_y2_axis.ticks,
|
|
1239
|
+
y2_scale_fn,
|
|
1240
|
+
{ default_count: 5 },
|
|
1241
|
+
)
|
|
1242
|
+
: [],
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
let x_tick_values = $derived(axis_ticks.x)
|
|
1247
|
+
let x2_tick_values = $derived(axis_ticks.x2)
|
|
1248
|
+
let y_tick_values = $derived(axis_ticks.y)
|
|
1249
|
+
let y2_tick_values = $derived(axis_ticks.y2)
|
|
1250
|
+
|
|
1251
|
+
// Cache measured tick-label widths so expensive text measurement only runs
|
|
1252
|
+
// when tick values/format change, not on every template rerender.
|
|
1253
|
+
let tick_label_widths = $derived({
|
|
1254
|
+
x2_max: measure_max_tick_width(x2_tick_values, final_x2_axis.format ?? ``),
|
|
1255
|
+
y_max: measure_max_tick_width(y_tick_values, final_y_axis.format ?? ``),
|
|
1256
|
+
y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
// Define global handlers reference for adding/removing listeners
|
|
1260
|
+
const on_window_mouse_move = (evt: MouseEvent) => {
|
|
1261
|
+
if (!drag_start_coords || !svg_bounding_box) return // Exit if not dragging or no bounds
|
|
1262
|
+
|
|
728
1263
|
// Calculate mouse position relative to the stored SVG bounding box
|
|
729
|
-
const current_x = evt.clientX - svg_bounding_box.left
|
|
730
|
-
const current_y = evt.clientY - svg_bounding_box.top
|
|
731
|
-
drag_current_coords = { x: current_x, y: current_y }
|
|
1264
|
+
const current_x = evt.clientX - svg_bounding_box.left
|
|
1265
|
+
const current_y = evt.clientY - svg_bounding_box.top
|
|
1266
|
+
drag_current_coords = { x: current_x, y: current_y }
|
|
1267
|
+
|
|
732
1268
|
// Optional: update tooltip only if inside SVG bounds
|
|
733
1269
|
const is_inside_svg = current_x >= 0 &&
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1270
|
+
current_x <= svg_bounding_box.width &&
|
|
1271
|
+
current_y >= 0 &&
|
|
1272
|
+
current_y <= svg_bounding_box.height
|
|
1273
|
+
|
|
737
1274
|
if (is_inside_svg) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const on_window_mouse_up = (_evt) => {
|
|
1275
|
+
// Use the already calculated relative coordinates
|
|
1276
|
+
update_tooltip_point(current_x, current_y)
|
|
1277
|
+
} else tooltip_point = null // Clear tooltip if outside
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const on_window_mouse_up = (_evt: MouseEvent) => {
|
|
745
1281
|
if (drag_start_coords && drag_current_coords) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1282
|
+
// Use current scales to invert screen coords to data coords
|
|
1283
|
+
const start_data_x_val = x_scale_fn.invert(drag_start_coords.x)
|
|
1284
|
+
const end_data_x_val = x_scale_fn.invert(drag_current_coords.x)
|
|
1285
|
+
const start_data_y_val = y_scale_fn.invert(drag_start_coords.y)
|
|
1286
|
+
const end_data_y_val = y_scale_fn.invert(drag_current_coords.y)
|
|
1287
|
+
|
|
1288
|
+
// Ensure range is not zero and order is correct
|
|
1289
|
+
let x1: number, x2: number
|
|
1290
|
+
if (start_data_x_val instanceof Date && end_data_x_val instanceof Date) {
|
|
1291
|
+
x1 = start_data_x_val.getTime()
|
|
1292
|
+
x2 = end_data_x_val.getTime()
|
|
1293
|
+
} else if (
|
|
1294
|
+
typeof start_data_x_val === `number` &&
|
|
1295
|
+
typeof end_data_x_val === `number`
|
|
1296
|
+
) {
|
|
1297
|
+
x1 = start_data_x_val
|
|
1298
|
+
x2 = end_data_x_val
|
|
1299
|
+
} else {
|
|
1300
|
+
console.error(`Mismatched types for x-axis zoom calculation`)
|
|
1301
|
+
// Reset states without zooming if types are wrong
|
|
1302
|
+
drag_start_coords = null
|
|
1303
|
+
drag_current_coords = null
|
|
1304
|
+
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
1305
|
+
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
1306
|
+
return
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const next_x_range: [number, number] = [Math.min(x1, x2), Math.max(x1, x2)]
|
|
1310
|
+
// Y axis is always number
|
|
1311
|
+
const next_y_range: [number, number] = [
|
|
1312
|
+
Math.min(start_data_y_val, end_data_y_val),
|
|
1313
|
+
Math.max(start_data_y_val, end_data_y_val),
|
|
1314
|
+
]
|
|
1315
|
+
|
|
1316
|
+
// Check for minuscule zoom box (e.g. accidental click)
|
|
1317
|
+
const min_zoom_size = 5 // Minimum pixels to trigger zoom
|
|
1318
|
+
const dx = Math.abs(drag_start_coords.x - drag_current_coords.x)
|
|
1319
|
+
const dy = Math.abs(drag_start_coords.y - drag_current_coords.y)
|
|
1320
|
+
|
|
1321
|
+
if (
|
|
1322
|
+
dx > min_zoom_size &&
|
|
1323
|
+
dy > min_zoom_size &&
|
|
1324
|
+
next_x_range[0] !== next_x_range[1] &&
|
|
1325
|
+
next_y_range[0] !== next_y_range[1]
|
|
1326
|
+
) {
|
|
1327
|
+
// Update axis ranges to trigger reactivity (like BarPlot/Histogram do)
|
|
1328
|
+
// Y2 sync is handled by the effect that reacts to y_axis changes
|
|
1329
|
+
x_axis = { ...x_axis, range: next_x_range }
|
|
1330
|
+
y_axis = { ...y_axis, range: next_y_range }
|
|
1331
|
+
|
|
1332
|
+
// X2 axis: invert screen coords using x2 scale
|
|
1333
|
+
if (x2_points.length > 0) {
|
|
1334
|
+
const start_x2_val = x2_scale_fn.invert(drag_start_coords.x)
|
|
1335
|
+
const end_x2_val = x2_scale_fn.invert(drag_current_coords.x)
|
|
1336
|
+
const x2_a = start_x2_val instanceof Date
|
|
1337
|
+
? start_x2_val.getTime()
|
|
1338
|
+
: start_x2_val as number
|
|
1339
|
+
const x2_b = end_x2_val instanceof Date
|
|
1340
|
+
? end_x2_val.getTime()
|
|
1341
|
+
: end_x2_val as number
|
|
1342
|
+
x2_axis = {
|
|
1343
|
+
...x2_axis,
|
|
1344
|
+
range: [Math.min(x2_a, x2_b), Math.max(x2_a, x2_b)],
|
|
1345
|
+
}
|
|
789
1346
|
}
|
|
1347
|
+
}
|
|
790
1348
|
}
|
|
1349
|
+
|
|
791
1350
|
// Reset states and remove listeners
|
|
792
|
-
drag_start_coords = null
|
|
793
|
-
drag_current_coords = null
|
|
794
|
-
svg_bounding_box = null
|
|
795
|
-
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
796
|
-
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
797
|
-
document.body.style.cursor = `default
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const dx = evt.clientX - pan_drag_state.start.x
|
|
804
|
-
const dy = evt.clientY - pan_drag_state.start.y
|
|
1351
|
+
drag_start_coords = null
|
|
1352
|
+
drag_current_coords = null
|
|
1353
|
+
svg_bounding_box = null
|
|
1354
|
+
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
1355
|
+
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
1356
|
+
document.body.style.cursor = `default`
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Pan drag handlers
|
|
1360
|
+
const on_pan_move = (evt: MouseEvent) => {
|
|
1361
|
+
if (!pan_drag_state) return
|
|
1362
|
+
const dx = evt.clientX - pan_drag_state.start.x
|
|
1363
|
+
const dy = evt.clientY - pan_drag_state.start.y
|
|
1364
|
+
|
|
805
1365
|
// Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
|
|
806
1366
|
// Clamp to at least 1 to avoid Infinity deltas when padding equals container size
|
|
807
|
-
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
808
|
-
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
809
|
-
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1367
|
+
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
1368
|
+
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
1369
|
+
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
1370
|
+
|
|
1371
|
+
const x_delta = pixels_to_data_delta(
|
|
1372
|
+
-dx * sensitivity,
|
|
1373
|
+
pan_drag_state.initial_x_range,
|
|
1374
|
+
plot_width,
|
|
1375
|
+
)
|
|
1376
|
+
const x2_delta = pixels_to_data_delta(
|
|
1377
|
+
-dx * sensitivity,
|
|
1378
|
+
pan_drag_state.initial_x2_range,
|
|
1379
|
+
plot_width,
|
|
1380
|
+
)
|
|
1381
|
+
const y_delta = pixels_to_data_delta(
|
|
1382
|
+
dy * sensitivity,
|
|
1383
|
+
pan_drag_state.initial_y_range,
|
|
1384
|
+
plot_height,
|
|
1385
|
+
)
|
|
1386
|
+
const y2_delta = pixels_to_data_delta(
|
|
1387
|
+
dy * sensitivity,
|
|
1388
|
+
pan_drag_state.initial_y2_range,
|
|
1389
|
+
plot_height,
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
zoom_x_range = pan_range(pan_drag_state.initial_x_range, x_delta)
|
|
1393
|
+
zoom_x2_range = pan_range(pan_drag_state.initial_x2_range, x2_delta)
|
|
1394
|
+
zoom_y_range = pan_range(pan_drag_state.initial_y_range, y_delta)
|
|
1395
|
+
zoom_y2_range = get_synced_y2(
|
|
1396
|
+
zoom_y_range,
|
|
1397
|
+
pan_range(pan_drag_state.initial_y2_range, y2_delta),
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const on_pan_end = () => {
|
|
1402
|
+
pan_drag_state = null
|
|
1403
|
+
document.body.style.cursor = ``
|
|
1404
|
+
window.removeEventListener(`mousemove`, on_pan_move)
|
|
1405
|
+
window.removeEventListener(`mouseup`, on_pan_end)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function handle_mouse_down(evt: MouseEvent) {
|
|
1409
|
+
if (!svg_element) return
|
|
1410
|
+
|
|
826
1411
|
// Check if pan is enabled and shift is held for pan mode
|
|
827
|
-
const pan_enabled = pan?.enabled !== false
|
|
1412
|
+
const pan_enabled = pan?.enabled !== false
|
|
828
1413
|
if (pan_enabled && evt.shiftKey) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1414
|
+
evt.preventDefault()
|
|
1415
|
+
pan_drag_state = {
|
|
1416
|
+
start: { x: evt.clientX, y: evt.clientY },
|
|
1417
|
+
initial_x_range: [...zoom_x_range] as [number, number],
|
|
1418
|
+
initial_x2_range: [...zoom_x2_range] as [number, number],
|
|
1419
|
+
initial_y_range: [...zoom_y_range] as [number, number],
|
|
1420
|
+
initial_y2_range: [...zoom_y2_range] as [number, number],
|
|
1421
|
+
}
|
|
1422
|
+
document.body.style.cursor = `grabbing`
|
|
1423
|
+
window.addEventListener(`mousemove`, on_pan_move)
|
|
1424
|
+
window.addEventListener(`mouseup`, on_pan_end)
|
|
1425
|
+
return
|
|
840
1426
|
}
|
|
1427
|
+
|
|
841
1428
|
// Store bounding box first, then calculate coords using it
|
|
842
|
-
svg_bounding_box = svg_element.getBoundingClientRect()
|
|
1429
|
+
svg_bounding_box = svg_element.getBoundingClientRect()
|
|
1430
|
+
|
|
843
1431
|
// Calculate initial coords using the same bounding box that will be used during drag
|
|
844
|
-
const initial_x = evt.clientX - svg_bounding_box.left
|
|
845
|
-
const initial_y = evt.clientY - svg_bounding_box.top
|
|
846
|
-
const coords = { x: initial_x, y: initial_y }
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1432
|
+
const initial_x = evt.clientX - svg_bounding_box.left
|
|
1433
|
+
const initial_y = evt.clientY - svg_bounding_box.top
|
|
1434
|
+
const coords = { x: initial_x, y: initial_y }
|
|
1435
|
+
|
|
1436
|
+
drag_start_coords = coords
|
|
1437
|
+
drag_current_coords = coords
|
|
1438
|
+
|
|
1439
|
+
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
1440
|
+
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
1441
|
+
document.body.style.cursor = `crosshair`
|
|
1442
|
+
evt.preventDefault()
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Wheel handler for pan (requires focus and shift)
|
|
1446
|
+
function handle_wheel(evt: WheelEvent) {
|
|
1447
|
+
const pan_enabled = pan?.enabled !== false
|
|
857
1448
|
// Only capture wheel when focused AND Shift is held
|
|
858
1449
|
// Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
|
|
859
|
-
if (!pan_enabled || !is_focused || !shift_held)
|
|
860
|
-
|
|
861
|
-
evt.preventDefault()
|
|
1450
|
+
if (!pan_enabled || !is_focused || !shift_held) return
|
|
1451
|
+
|
|
1452
|
+
evt.preventDefault()
|
|
1453
|
+
|
|
862
1454
|
// Clamp to at least 1 to avoid Infinity deltas when padding equals container size
|
|
863
|
-
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
864
|
-
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
865
|
-
const sensitivity = pan?.wheel_sensitivity ?? 1
|
|
1455
|
+
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
1456
|
+
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
1457
|
+
const sensitivity = pan?.wheel_sensitivity ?? 1
|
|
1458
|
+
|
|
866
1459
|
// Determine pan direction based on wheel delta
|
|
867
1460
|
// deltaX for horizontal scroll (trackpad), deltaY for vertical
|
|
868
|
-
const x_delta = pixels_to_data_delta(
|
|
869
|
-
|
|
870
|
-
|
|
1461
|
+
const x_delta = pixels_to_data_delta(
|
|
1462
|
+
evt.deltaX * sensitivity,
|
|
1463
|
+
zoom_x_range,
|
|
1464
|
+
plot_width,
|
|
1465
|
+
)
|
|
1466
|
+
const x2_delta = pixels_to_data_delta(
|
|
1467
|
+
evt.deltaX * sensitivity,
|
|
1468
|
+
zoom_x2_range,
|
|
1469
|
+
plot_width,
|
|
1470
|
+
)
|
|
1471
|
+
const y_delta = pixels_to_data_delta(
|
|
1472
|
+
evt.deltaY * sensitivity,
|
|
1473
|
+
zoom_y_range,
|
|
1474
|
+
plot_height,
|
|
1475
|
+
)
|
|
1476
|
+
const y2_delta = pixels_to_data_delta(
|
|
1477
|
+
evt.deltaY * sensitivity,
|
|
1478
|
+
zoom_y2_range,
|
|
1479
|
+
plot_height,
|
|
1480
|
+
)
|
|
1481
|
+
|
|
871
1482
|
if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
else {
|
|
875
|
-
|
|
876
|
-
|
|
1483
|
+
zoom_x_range = pan_range(zoom_x_range, x_delta)
|
|
1484
|
+
zoom_x2_range = pan_range(zoom_x2_range, x2_delta)
|
|
1485
|
+
} else {
|
|
1486
|
+
zoom_y_range = pan_range(zoom_y_range, y_delta)
|
|
1487
|
+
zoom_y2_range = get_synced_y2(zoom_y_range, pan_range(zoom_y2_range, y2_delta))
|
|
877
1488
|
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Touch handlers for pinch-zoom and two-finger pan
|
|
1492
|
+
function handle_touch_start(evt: TouchEvent) {
|
|
1493
|
+
const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
|
|
1494
|
+
if (!touch_enabled || evt.touches.length !== 2) return
|
|
1495
|
+
|
|
1496
|
+
evt.preventDefault()
|
|
1497
|
+
const touches = Array.from(evt.touches)
|
|
886
1498
|
touch_state = {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
evt.
|
|
897
|
-
|
|
898
|
-
|
|
1499
|
+
start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
|
|
1500
|
+
initial_x_range: [...zoom_x_range] as [number, number],
|
|
1501
|
+
initial_x2_range: [...zoom_x2_range] as [number, number],
|
|
1502
|
+
initial_y_range: [...zoom_y_range] as [number, number],
|
|
1503
|
+
initial_y2_range: [...zoom_y2_range] as [number, number],
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function handle_touch_move(evt: TouchEvent) {
|
|
1508
|
+
if (!touch_state || evt.touches.length !== 2) return
|
|
1509
|
+
evt.preventDefault()
|
|
1510
|
+
|
|
1511
|
+
const [t1, t2] = Array.from(evt.touches)
|
|
1512
|
+
const [s1, s2] = touch_state.start_touches
|
|
1513
|
+
|
|
899
1514
|
// Calculate center movement for pan
|
|
900
|
-
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
1515
|
+
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
901
1516
|
const curr_center = {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
905
|
-
const dx = curr_center.x - start_center.x
|
|
906
|
-
const dy = curr_center.y - start_center.y
|
|
1517
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
1518
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
1519
|
+
}
|
|
1520
|
+
const dx = curr_center.x - start_center.x
|
|
1521
|
+
const dy = curr_center.y - start_center.y
|
|
1522
|
+
|
|
907
1523
|
// Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
|
|
908
|
-
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
1524
|
+
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
909
1525
|
// Guard against zero-distance pinch to avoid Infinity scale
|
|
910
|
-
if (start_dist < Number.EPSILON)
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
1526
|
+
if (start_dist < Number.EPSILON) return
|
|
1527
|
+
const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
|
1528
|
+
const scale = curr_dist / start_dist
|
|
1529
|
+
|
|
914
1530
|
// Clamp to at least 1 to avoid Infinity deltas when padding equals container size
|
|
915
|
-
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
916
|
-
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
1531
|
+
const plot_width = Math.max(1, width - pad.l - pad.r)
|
|
1532
|
+
const plot_height = Math.max(1, height - pad.t - pad.b)
|
|
1533
|
+
|
|
917
1534
|
// If scale changed significantly, treat as pinch-zoom
|
|
918
1535
|
// Also guard against scale being too small to avoid division by zero
|
|
919
1536
|
if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1537
|
+
// Pinch zoom centered on gesture center
|
|
1538
|
+
// Divide by scale so spread (scale > 1) = smaller span (zoom in)
|
|
1539
|
+
const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
|
|
1540
|
+
const x2_span = touch_state.initial_x2_range[1] -
|
|
1541
|
+
touch_state.initial_x2_range[0]
|
|
1542
|
+
const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
|
|
1543
|
+
const y2_span = touch_state.initial_y2_range[1] -
|
|
1544
|
+
touch_state.initial_y2_range[0]
|
|
1545
|
+
const x_center =
|
|
1546
|
+
(touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
|
|
1547
|
+
const x2_center =
|
|
1548
|
+
(touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
|
|
1549
|
+
const y_center =
|
|
1550
|
+
(touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
|
|
1551
|
+
const y2_center =
|
|
1552
|
+
(touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
|
|
1553
|
+
|
|
1554
|
+
zoom_x_range = [x_center - x_span / scale / 2, x_center + x_span / scale / 2]
|
|
1555
|
+
zoom_x2_range = [
|
|
1556
|
+
x2_center - x2_span / scale / 2,
|
|
1557
|
+
x2_center + x2_span / scale / 2,
|
|
1558
|
+
]
|
|
1559
|
+
zoom_y_range = [y_center - y_span / scale / 2, y_center + y_span / scale / 2]
|
|
1560
|
+
zoom_y2_range = get_synced_y2(zoom_y_range, [
|
|
1561
|
+
y2_center - y2_span / scale / 2,
|
|
1562
|
+
y2_center + y2_span / scale / 2,
|
|
1563
|
+
])
|
|
1564
|
+
} else {
|
|
1565
|
+
// Pan
|
|
1566
|
+
const x_delta = pixels_to_data_delta(
|
|
1567
|
+
-dx,
|
|
1568
|
+
touch_state.initial_x_range,
|
|
1569
|
+
plot_width,
|
|
1570
|
+
)
|
|
1571
|
+
const x2_delta = pixels_to_data_delta(
|
|
1572
|
+
-dx,
|
|
1573
|
+
touch_state.initial_x2_range,
|
|
1574
|
+
plot_width,
|
|
1575
|
+
)
|
|
1576
|
+
const y_delta = pixels_to_data_delta(
|
|
1577
|
+
dy,
|
|
1578
|
+
touch_state.initial_y_range,
|
|
1579
|
+
plot_height,
|
|
1580
|
+
)
|
|
1581
|
+
const y2_delta = pixels_to_data_delta(
|
|
1582
|
+
dy,
|
|
1583
|
+
touch_state.initial_y2_range,
|
|
1584
|
+
plot_height,
|
|
1585
|
+
)
|
|
1586
|
+
zoom_x_range = pan_range(touch_state.initial_x_range, x_delta)
|
|
1587
|
+
zoom_x2_range = pan_range(touch_state.initial_x2_range, x2_delta)
|
|
1588
|
+
zoom_y_range = pan_range(touch_state.initial_y_range, y_delta)
|
|
1589
|
+
zoom_y2_range = get_synced_y2(
|
|
1590
|
+
zoom_y_range,
|
|
1591
|
+
pan_range(touch_state.initial_y2_range, y2_delta),
|
|
1592
|
+
)
|
|
944
1593
|
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
let
|
|
956
|
-
|
|
957
|
-
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function handle_touch_end() {
|
|
1597
|
+
touch_state = null
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// tooltip logic: find closest point and update tooltip state
|
|
1601
|
+
function update_tooltip_point(x_rel: number, y_rel: number, evt?: MouseEvent) {
|
|
1602
|
+
if (!width || !height) return
|
|
1603
|
+
|
|
1604
|
+
let closest_point: InternalPoint<Metadata> | null = null
|
|
1605
|
+
let closest_series: DataSeries<Metadata> | null = null
|
|
1606
|
+
let min_screen_dist_sq = Infinity
|
|
1607
|
+
const { threshold_px = 20 } = hover_config // Use configured threshold
|
|
1608
|
+
const hover_threshold_px_sq = threshold_px * threshold_px
|
|
1609
|
+
|
|
958
1610
|
// Iterate through points to find the closest one in screen coordinates
|
|
959
1611
|
for (const series_data of filtered_series) {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1612
|
+
if (!series_data?.filtered_data) continue
|
|
1613
|
+
|
|
1614
|
+
const tooltip_use_x2 = series_data.x_axis === `x2`
|
|
1615
|
+
const tooltip_x_scale = tooltip_use_x2 ? x2_scale_fn : x_scale_fn
|
|
1616
|
+
const tooltip_is_time_x = tooltip_use_x2 ? is_time_x2 : is_time_x
|
|
1617
|
+
for (const point of series_data.filtered_data) {
|
|
1618
|
+
// Calculate screen coordinates of the point
|
|
1619
|
+
const point_cx = tooltip_is_time_x
|
|
1620
|
+
? tooltip_x_scale(new Date(point.x))
|
|
1621
|
+
: tooltip_x_scale(point.x)
|
|
1622
|
+
const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(
|
|
1623
|
+
point.y,
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
// Calculate squared screen distance between mouse and point
|
|
1627
|
+
const screen_dx = x_rel - point_cx
|
|
1628
|
+
const screen_dy = y_rel - point_cy
|
|
1629
|
+
const screen_distance_sq = screen_dx * screen_dx + screen_dy * screen_dy
|
|
1630
|
+
|
|
1631
|
+
// Update if this point is closer
|
|
1632
|
+
if (screen_distance_sq < min_screen_dist_sq) {
|
|
1633
|
+
min_screen_dist_sq = screen_distance_sq
|
|
1634
|
+
closest_point = point
|
|
1635
|
+
closest_series = series_data
|
|
978
1636
|
}
|
|
1637
|
+
}
|
|
979
1638
|
}
|
|
1639
|
+
|
|
980
1640
|
// Check if the closest point is within the hover threshold
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1641
|
+
if (
|
|
1642
|
+
closest_point &&
|
|
1643
|
+
closest_series &&
|
|
1644
|
+
min_screen_dist_sq <= hover_threshold_px_sq
|
|
1645
|
+
) {
|
|
1646
|
+
// Construct handler props synchronously to avoid stale derived reads
|
|
1647
|
+
const props = construct_handler_props(closest_point)
|
|
1648
|
+
tooltip_point = closest_point
|
|
1649
|
+
// Construct object matching change signature
|
|
1650
|
+
const { x, y, metadata } = closest_point
|
|
1651
|
+
change({ x, y, metadata, series: closest_series })
|
|
1652
|
+
// Call hover handler with synchronously constructed props
|
|
1653
|
+
if (evt && props) {
|
|
1654
|
+
on_point_hover?.({ ...props, event: evt, point: closest_point })
|
|
1655
|
+
}
|
|
1656
|
+
} else {
|
|
1657
|
+
tooltip_point = null
|
|
1658
|
+
change(null)
|
|
1659
|
+
on_point_hover?.(null)
|
|
999
1660
|
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
charge_strength: 50, // Repulsion strength for markers
|
|
1017
|
-
charge_distance_max: 30, // Limit range of repulsion
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function on_mouse_move(evt: MouseEvent) {
|
|
1664
|
+
hovered = true
|
|
1665
|
+
|
|
1666
|
+
const coords = get_relative_coords(evt)
|
|
1667
|
+
if (!coords) return
|
|
1668
|
+
|
|
1669
|
+
update_tooltip_point(coords.x, coords.y, evt)
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Merge user config with defaults before the effect that uses it
|
|
1673
|
+
let actual_label_config = $derived({
|
|
1674
|
+
sa_iterations: 2000,
|
|
1675
|
+
max_labels: 300,
|
|
1676
|
+
leader_line_threshold: 15,
|
|
1018
1677
|
...label_placement_config,
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
$effect(() => {
|
|
1021
1681
|
if (!width || !height) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1682
|
+
label_positions = {}
|
|
1683
|
+
return
|
|
1024
1684
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1685
|
+
|
|
1686
|
+
label_positions = compute_label_positions(
|
|
1687
|
+
filtered_series,
|
|
1688
|
+
actual_label_config,
|
|
1689
|
+
{ x_scale_fn, y_scale_fn, y2_scale_fn, x_axis: final_x_axis },
|
|
1690
|
+
{ width, height, pad },
|
|
1691
|
+
)
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
// Legend drag handlers
|
|
1695
|
+
function handle_legend_drag_start(event: MouseEvent) {
|
|
1696
|
+
if (!svg_element) return
|
|
1697
|
+
|
|
1698
|
+
legend_is_dragging = true
|
|
1699
|
+
|
|
1032
1700
|
// Get the actual rendered position of the legend element (accounts for transforms)
|
|
1033
|
-
const legend_el = event.currentTarget
|
|
1034
|
-
|
|
1701
|
+
const legend_el = event.currentTarget
|
|
1702
|
+
if (!(legend_el instanceof HTMLElement)) return
|
|
1703
|
+
const legend_rect = legend_el.getBoundingClientRect()
|
|
1704
|
+
|
|
1035
1705
|
// Calculate offset from mouse to legend's actual rendered position relative to SVG
|
|
1036
|
-
const [x, y] = [event.clientX - legend_rect.left, event.clientY - legend_rect.top]
|
|
1037
|
-
legend_drag_offset = { x, y }
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1706
|
+
const [x, y] = [event.clientX - legend_rect.left, event.clientY - legend_rect.top]
|
|
1707
|
+
legend_drag_offset = { x, y }
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function handle_legend_drag(event: MouseEvent) {
|
|
1711
|
+
if (!legend_is_dragging || !svg_element || !legend_element) return
|
|
1712
|
+
|
|
1713
|
+
const svg_rect = svg_element.getBoundingClientRect()
|
|
1714
|
+
|
|
1043
1715
|
// Calculate new position: mouse position relative to SVG, minus the offset within the legend
|
|
1044
|
-
const new_x = event.clientX - svg_rect.left - legend_drag_offset.x
|
|
1045
|
-
const new_y = event.clientY - svg_rect.top - legend_drag_offset.y
|
|
1716
|
+
const new_x = event.clientX - svg_rect.left - legend_drag_offset.x
|
|
1717
|
+
const new_y = event.clientY - svg_rect.top - legend_drag_offset.y
|
|
1718
|
+
|
|
1046
1719
|
// Get actual legend dimensions for accurate bounds checking using the bound element reference
|
|
1047
1720
|
const { width: legend_width, height: legend_height } = legend_element
|
|
1048
|
-
|
|
1721
|
+
.getBoundingClientRect()
|
|
1722
|
+
|
|
1049
1723
|
// Constrain to plot bounds using measured legend size
|
|
1050
|
-
const constrained_x = Math.max(0, Math.min(width - legend_width, new_x))
|
|
1051
|
-
const constrained_y = Math.max(0, Math.min(height - legend_height, new_y))
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1724
|
+
const constrained_x = Math.max(0, Math.min(width - legend_width, new_x))
|
|
1725
|
+
const constrained_y = Math.max(0, Math.min(height - legend_height, new_y))
|
|
1726
|
+
|
|
1727
|
+
legend_manual_position = { x: constrained_x, y: constrained_y }
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function get_screen_coords(point: Point, series?: DataSeries): [number, number] {
|
|
1055
1731
|
// convert data coordinates to potentially non-finite screen coordinates
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1732
|
+
const use_x2 = series?.x_axis === `x2`
|
|
1733
|
+
const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
|
|
1734
|
+
const active_is_time_x = use_x2 ? is_time_x2 : is_time_x
|
|
1735
|
+
const screen_x = active_is_time_x
|
|
1736
|
+
? active_x_scale(new Date(point.x))
|
|
1737
|
+
: active_x_scale(point.x)
|
|
1738
|
+
|
|
1739
|
+
const y_val = point.y
|
|
1060
1740
|
// Determine which y-scale to use based on series y_axis property
|
|
1061
|
-
const use_y2 = series?.y_axis === `y2
|
|
1062
|
-
const y_scale = use_y2 ? y2_scale_fn : y_scale_fn
|
|
1741
|
+
const use_y2 = series?.y_axis === `y2`
|
|
1742
|
+
const y_scale = use_y2 ? y2_scale_fn : y_scale_fn
|
|
1063
1743
|
const y_scale_type = use_y2
|
|
1064
|
-
|
|
1065
|
-
|
|
1744
|
+
? get_scale_type_name(final_y2_axis.scale_type)
|
|
1745
|
+
: get_scale_type_name(final_y_axis.scale_type)
|
|
1066
1746
|
// Only log scale needs domain clamping; linear and arcsinh can handle any value
|
|
1067
|
-
const min_domain_y = y_scale_type === `log` ? y_scale.domain()[0] : -Infinity
|
|
1068
|
-
const safe_y_val = y_scale_type === `log` ? Math.max(y_val, min_domain_y) : y_val
|
|
1069
|
-
const screen_y = y_scale(safe_y_val)
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1747
|
+
const min_domain_y = y_scale_type === `log` ? y_scale.domain()[0] : -Infinity
|
|
1748
|
+
const safe_y_val = y_scale_type === `log` ? Math.max(y_val, min_domain_y) : y_val
|
|
1749
|
+
const screen_y = y_scale(safe_y_val) // This might be non-finite
|
|
1750
|
+
|
|
1751
|
+
return [screen_x, screen_y]
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Helper function to construct ScatterHandlerProps synchronously from InternalPoint
|
|
1755
|
+
function construct_handler_props(
|
|
1756
|
+
point: InternalPoint<Metadata>,
|
|
1757
|
+
): ScatterHandlerProps<Metadata> | null {
|
|
1758
|
+
const hovered_series = series_with_ids[point.series_idx]
|
|
1759
|
+
if (!hovered_series) return null
|
|
1760
|
+
const { x, y, color_value, metadata, series_idx } = point
|
|
1761
|
+
const handler_use_x2 = hovered_series.x_axis === `x2`
|
|
1762
|
+
const handler_x_scale = handler_use_x2 ? x2_scale_fn : x_scale_fn
|
|
1763
|
+
const handler_is_time_x = handler_use_x2 ? is_time_x2 : is_time_x
|
|
1764
|
+
const cx = handler_is_time_x ? handler_x_scale(new Date(x)) : handler_x_scale(x)
|
|
1765
|
+
const cy = (hovered_series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(y)
|
|
1766
|
+
const active_x_config = handler_use_x2 ? final_x2_axis : final_x_axis
|
|
1767
|
+
const active_y_config = hovered_series.y_axis === `y2`
|
|
1768
|
+
? final_y2_axis
|
|
1769
|
+
: final_y_axis
|
|
1082
1770
|
const coords = {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1771
|
+
x,
|
|
1772
|
+
y,
|
|
1773
|
+
cx,
|
|
1774
|
+
cy,
|
|
1775
|
+
x_axis: active_x_config,
|
|
1776
|
+
x2_axis: final_x2_axis,
|
|
1777
|
+
y_axis: active_y_config,
|
|
1778
|
+
y2_axis: final_y2_axis,
|
|
1779
|
+
}
|
|
1091
1780
|
return {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
let
|
|
1117
|
-
|
|
1118
|
-
//
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1781
|
+
...coords,
|
|
1782
|
+
fullscreen,
|
|
1783
|
+
metadata,
|
|
1784
|
+
label: hovered_series.label ?? null,
|
|
1785
|
+
series_idx,
|
|
1786
|
+
x_formatted: format_value(x, active_x_config.format || `.3~s`),
|
|
1787
|
+
y_formatted: format_value(y, active_y_config.format || `.3~s`),
|
|
1788
|
+
color_value: color_value ?? null,
|
|
1789
|
+
colorbar: {
|
|
1790
|
+
value: color_value ?? null,
|
|
1791
|
+
title: color_bar?.title ?? null,
|
|
1792
|
+
scale: color_scale,
|
|
1793
|
+
tick_format: color_bar?.tick_format ?? null,
|
|
1794
|
+
},
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Derive handler props from hovered point for both tooltip and event handlers
|
|
1799
|
+
let handler_props = $derived.by((): ScatterHandlerProps<Metadata> | null => {
|
|
1800
|
+
if (!tooltip_point) return null
|
|
1801
|
+
return construct_handler_props(tooltip_point)
|
|
1802
|
+
})
|
|
1803
|
+
|
|
1804
|
+
let using_controls = $derived(controls.show)
|
|
1805
|
+
let has_multiple_series = $derived(series_with_ids.filter(Boolean).length > 1)
|
|
1806
|
+
|
|
1807
|
+
// Precompute non-click event names from point_events so we don't rebuild
|
|
1808
|
+
// the entries array on every point render.
|
|
1809
|
+
let point_event_names = $derived(
|
|
1810
|
+
point_events
|
|
1811
|
+
? Object.keys(point_events).filter((name) => name !== `onclick`)
|
|
1812
|
+
: [],
|
|
1813
|
+
)
|
|
1814
|
+
|
|
1815
|
+
// Set theme-aware background when entering fullscreen
|
|
1816
|
+
$effect(() => {
|
|
1817
|
+
set_fullscreen_bg(wrapper, fullscreen, `--scatter-fullscreen-bg`)
|
|
1818
|
+
})
|
|
1819
|
+
|
|
1820
|
+
// State accessors for shared axis change handler
|
|
1821
|
+
const axis_state: AxisChangeState<DataSeries<Metadata>> = {
|
|
1822
|
+
get_axis: (axis) => {
|
|
1823
|
+
if (axis === `x`) return x_axis
|
|
1824
|
+
if (axis === `x2`) return x2_axis
|
|
1825
|
+
if (axis === `y`) return y_axis
|
|
1826
|
+
return y2_axis
|
|
1827
|
+
},
|
|
1125
1828
|
set_axis: (axis, config) => {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
else
|
|
1132
|
-
y2_axis = { ...y2_axis, ...config };
|
|
1829
|
+
// Spread into existing state to preserve merged type structure
|
|
1830
|
+
if (axis === `x`) x_axis = { ...x_axis, ...config }
|
|
1831
|
+
else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
|
|
1832
|
+
else if (axis === `y`) y_axis = { ...y_axis, ...config }
|
|
1833
|
+
else y2_axis = { ...y2_axis, ...config }
|
|
1133
1834
|
},
|
|
1134
1835
|
get_series: () => series,
|
|
1135
1836
|
set_series: (new_series) => (series = new_series),
|
|
1136
1837
|
get_loading: () => axis_loading,
|
|
1137
1838
|
set_loading: (axis) => (axis_loading = axis),
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// Create shared handler bound to this component's state
|
|
1842
|
+
// Using $derived so handler updates when callback props change
|
|
1843
|
+
const handle_axis_change = $derived(create_axis_change_handler(
|
|
1844
|
+
axis_state,
|
|
1845
|
+
data_loader,
|
|
1846
|
+
on_axis_change,
|
|
1847
|
+
on_error,
|
|
1848
|
+
))
|
|
1849
|
+
|
|
1850
|
+
let auto_load_attempted = false // prevent infinite retries on failure
|
|
1851
|
+
|
|
1852
|
+
// Auto-load data if series is empty but options exist (runs once)
|
|
1853
|
+
$effect(() => {
|
|
1145
1854
|
if (series.length === 0 && data_loader && !auto_load_attempted) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1855
|
+
// Check x-axis first, then y-axis
|
|
1856
|
+
if (x_axis.options?.length) {
|
|
1857
|
+
auto_load_attempted = true
|
|
1858
|
+
const first_key = x_axis.selected_key ?? x_axis.options[0].key
|
|
1859
|
+
handle_axis_change(`x`, first_key).catch(() => {})
|
|
1860
|
+
} else if (y_axis.options?.length) {
|
|
1861
|
+
auto_load_attempted = true
|
|
1862
|
+
const first_key = y_axis.selected_key ?? y_axis.options[0].key
|
|
1863
|
+
handle_axis_change(`y`, first_key).catch(() => {})
|
|
1864
|
+
}
|
|
1157
1865
|
}
|
|
1158
|
-
})
|
|
1866
|
+
})
|
|
1159
1867
|
</script>
|
|
1160
1868
|
|
|
1161
1869
|
{#snippet fill_regions_layer(fills: typeof computed_fills)}
|
|
@@ -1192,11 +1900,12 @@ $effect(() => {
|
|
|
1192
1900
|
<ReferenceLine
|
|
1193
1901
|
ref_line={line}
|
|
1194
1902
|
line_idx={line.idx}
|
|
1195
|
-
{x_min}
|
|
1196
|
-
{x_max}
|
|
1903
|
+
x_min={line.x_axis === `x2` ? x2_min : x_min}
|
|
1904
|
+
x_max={line.x_axis === `x2` ? x2_max : x_max}
|
|
1197
1905
|
y_min={line.y_axis === `y2` ? y2_min : y_min}
|
|
1198
1906
|
y_max={line.y_axis === `y2` ? y2_max : y_max}
|
|
1199
1907
|
x_scale={x_scale_fn}
|
|
1908
|
+
x2_scale={x2_scale_fn}
|
|
1200
1909
|
y_scale={y_scale_fn}
|
|
1201
1910
|
y2_scale={y2_scale_fn}
|
|
1202
1911
|
{clip_path_id}
|
|
@@ -1247,6 +1956,9 @@ $effect(() => {
|
|
|
1247
1956
|
<svg
|
|
1248
1957
|
bind:this={svg_element}
|
|
1249
1958
|
role="application"
|
|
1959
|
+
aria-label={rest[`aria-label`] ??
|
|
1960
|
+
([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
|
|
1961
|
+
`Scatter plot`)}
|
|
1250
1962
|
tabindex="0"
|
|
1251
1963
|
onfocusin={() => (is_focused = true)}
|
|
1252
1964
|
onfocusout={() => (is_focused = false)}
|
|
@@ -1265,13 +1977,16 @@ $effect(() => {
|
|
|
1265
1977
|
// Reset to current auto ranges (not stale initial_*_range which may have expanded)
|
|
1266
1978
|
// This ensures lazy expansion restarts fresh from current data bounds
|
|
1267
1979
|
initial_x_range = [...auto_x_range] as [number, number]
|
|
1980
|
+
initial_x2_range = [...auto_x2_range] as [number, number]
|
|
1268
1981
|
initial_y_range = [...auto_y_range] as [number, number]
|
|
1269
1982
|
initial_y2_range = [...auto_y2_range] as [number, number]
|
|
1270
1983
|
zoom_x_range = [...auto_x_range] as [number, number]
|
|
1984
|
+
zoom_x2_range = [...auto_x2_range] as [number, number]
|
|
1271
1985
|
zoom_y_range = [...auto_y_range] as [number, number]
|
|
1272
1986
|
zoom_y2_range = get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2)
|
|
1273
1987
|
// Also reset axis props so future data changes recalculate auto ranges
|
|
1274
1988
|
x_axis = { ...x_axis, range: [null, null] }
|
|
1989
|
+
x2_axis = { ...x2_axis, range: [null, null] }
|
|
1275
1990
|
y_axis = { ...y_axis, range: [null, null] }
|
|
1276
1991
|
y2_axis = { ...y2_axis, range: [null, null] }
|
|
1277
1992
|
}}
|
|
@@ -1289,10 +2004,12 @@ $effect(() => {
|
|
|
1289
2004
|
height,
|
|
1290
2005
|
width,
|
|
1291
2006
|
x_scale_fn,
|
|
2007
|
+
x2_scale_fn,
|
|
1292
2008
|
y_scale_fn,
|
|
1293
2009
|
y2_scale_fn,
|
|
1294
2010
|
pad,
|
|
1295
2011
|
x_range: [x_min, x_max],
|
|
2012
|
+
x2_range: [x2_min, x2_max],
|
|
1296
2013
|
y_range: [y_min, y_max],
|
|
1297
2014
|
y2_range: [y2_min, y2_max],
|
|
1298
2015
|
fullscreen,
|
|
@@ -1306,9 +2023,7 @@ $effect(() => {
|
|
|
1306
2023
|
<g class="x-axis">
|
|
1307
2024
|
{#if width > 0 && height > 0}
|
|
1308
2025
|
{#each x_tick_values as tick (tick)}
|
|
1309
|
-
{@const tick_pos_raw =
|
|
1310
|
-
? x_scale_fn(new Date(tick))
|
|
1311
|
-
: x_scale_fn(tick)}
|
|
2026
|
+
{@const tick_pos_raw = is_time_x ? x_scale_fn(new Date(tick)) : x_scale_fn(tick)}
|
|
1312
2027
|
{#if isFinite(tick_pos_raw)}
|
|
1313
2028
|
// Check if tick position is finite
|
|
1314
2029
|
{@const tick_pos = tick_pos_raw}
|
|
@@ -1325,14 +2040,19 @@ $effect(() => {
|
|
|
1325
2040
|
{/if}
|
|
1326
2041
|
<line y1="0" y2={inside ? -5 : 5} stroke="var(--border-color, gray)" />
|
|
1327
2042
|
|
|
1328
|
-
{#if tick >= x_min && tick <= x_max}
|
|
2043
|
+
{#if tick >= Math.min(x_min, x_max) && tick <= Math.max(x_min, x_max)}
|
|
1329
2044
|
{@const base_y = inside ? -8 : 20}
|
|
1330
2045
|
{@const shift = final_x_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
|
|
1331
2046
|
{@const x = shift.x ?? 0}
|
|
1332
2047
|
{@const y = base_y + (shift.y ?? 0)}
|
|
1333
2048
|
{@const custom_label = get_tick_label(tick, final_x_axis.ticks)}
|
|
1334
2049
|
{@const dominant_baseline = inside ? `auto` : `hanging`}
|
|
1335
|
-
<text
|
|
2050
|
+
<text
|
|
2051
|
+
{x}
|
|
2052
|
+
{y}
|
|
2053
|
+
dominant-baseline={dominant_baseline}
|
|
2054
|
+
fill={final_x_axis.color}
|
|
2055
|
+
>
|
|
1336
2056
|
{custom_label ?? format_value(tick, final_x_axis.format ?? ``)}
|
|
1337
2057
|
</text>
|
|
1338
2058
|
{/if}
|
|
@@ -1343,8 +2063,8 @@ $effect(() => {
|
|
|
1343
2063
|
{/if}
|
|
1344
2064
|
|
|
1345
2065
|
<!-- Current frame indicator -->
|
|
1346
|
-
{#if current_x_value
|
|
1347
|
-
{@const current_pos_raw =
|
|
2066
|
+
{#if current_x_value != null}
|
|
2067
|
+
{@const current_pos_raw = is_time_x
|
|
1348
2068
|
? x_scale_fn(new Date(current_x_value))
|
|
1349
2069
|
: x_scale_fn(current_x_value)}
|
|
1350
2070
|
{#if isFinite(current_pos_raw)}
|
|
@@ -1366,28 +2086,18 @@ $effect(() => {
|
|
|
1366
2086
|
{/if}
|
|
1367
2087
|
|
|
1368
2088
|
{#if final_x_axis.label || final_x_axis.options?.length}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
y={height - pad.b - (
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
options={final_x_axis.options}
|
|
1382
|
-
selected_key={final_x_axis.selected_key}
|
|
1383
|
-
loading={axis_loading === `x`}
|
|
1384
|
-
axis_type="x"
|
|
1385
|
-
color={final_x_axis.color}
|
|
1386
|
-
on_select={(key) => handle_axis_change(`x`, key)}
|
|
1387
|
-
class="axis-label x-label"
|
|
1388
|
-
/>
|
|
1389
|
-
</div>
|
|
1390
|
-
</foreignObject>
|
|
2089
|
+
{@const { label_shift, label = ``, options, selected_key, color } = final_x_axis}
|
|
2090
|
+
<AxisLabel
|
|
2091
|
+
x={width / 2 + (label_shift?.x ?? 0)}
|
|
2092
|
+
y={height - pad.b - (label_shift?.y ?? -40)}
|
|
2093
|
+
{label}
|
|
2094
|
+
{options}
|
|
2095
|
+
{selected_key}
|
|
2096
|
+
loading={axis_loading === `x`}
|
|
2097
|
+
axis_type="x"
|
|
2098
|
+
{color}
|
|
2099
|
+
on_select={(key) => handle_axis_change(`x`, key)}
|
|
2100
|
+
/>
|
|
1391
2101
|
{/if}
|
|
1392
2102
|
</g>
|
|
1393
2103
|
|
|
@@ -1415,7 +2125,7 @@ $effect(() => {
|
|
|
1415
2125
|
stroke="var(--border-color, gray)"
|
|
1416
2126
|
/>
|
|
1417
2127
|
|
|
1418
|
-
{#if tick >= y_min && tick <= y_max}
|
|
2128
|
+
{#if tick >= Math.min(y_min, y_max) && tick <= Math.max(y_min, y_max)}
|
|
1419
2129
|
{@const base_x = inside ? 8 : -8}
|
|
1420
2130
|
{@const shift = final_y_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
|
|
1421
2131
|
{@const x = base_x + (shift.x ?? 0)}
|
|
@@ -1436,31 +2146,26 @@ $effect(() => {
|
|
|
1436
2146
|
{/if}
|
|
1437
2147
|
|
|
1438
2148
|
{#if height > 0 && (final_y_axis.label || final_y_axis.options?.length)}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
on_select={(key) => handle_axis_change(`y`, key)}
|
|
1460
|
-
class="axis-label y-label"
|
|
1461
|
-
/>
|
|
1462
|
-
</div>
|
|
1463
|
-
</foreignObject>
|
|
2149
|
+
{@const { label_shift, label = ``, options, selected_key, color, tick } =
|
|
2150
|
+
final_y_axis}
|
|
2151
|
+
{@const y_inside = tick?.label?.inside ?? false}
|
|
2152
|
+
{@const y_label_x = Math.max(
|
|
2153
|
+
12,
|
|
2154
|
+
pad.l - (y_inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
|
|
2155
|
+
) +
|
|
2156
|
+
(label_shift?.x ?? 0)}
|
|
2157
|
+
<AxisLabel
|
|
2158
|
+
x={y_label_x}
|
|
2159
|
+
y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
|
|
2160
|
+
rotate
|
|
2161
|
+
{label}
|
|
2162
|
+
{options}
|
|
2163
|
+
{selected_key}
|
|
2164
|
+
loading={axis_loading === `y`}
|
|
2165
|
+
axis_type="y"
|
|
2166
|
+
{color}
|
|
2167
|
+
on_select={(key) => handle_axis_change(`y`, key)}
|
|
2168
|
+
/>
|
|
1464
2169
|
{/if}
|
|
1465
2170
|
</g>
|
|
1466
2171
|
|
|
@@ -1490,7 +2195,7 @@ $effect(() => {
|
|
|
1490
2195
|
stroke="var(--border-color, gray)"
|
|
1491
2196
|
/>
|
|
1492
2197
|
|
|
1493
|
-
{#if tick >= y2_min && tick <= y2_max}
|
|
2198
|
+
{#if tick >= Math.min(y2_min, y2_max) && tick <= Math.max(y2_min, y2_max)}
|
|
1494
2199
|
{@const base_x = inside ? -8 : 8}
|
|
1495
2200
|
{@const shift = final_y2_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
|
|
1496
2201
|
{@const x = base_x + (shift.x ?? 0)}
|
|
@@ -1511,94 +2216,122 @@ $effect(() => {
|
|
|
1511
2216
|
{/if}
|
|
1512
2217
|
|
|
1513
2218
|
{#if height > 0 && (final_y2_axis.label || final_y2_axis.options?.length)}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
selected_key={final_y2_axis.selected_key}
|
|
1533
|
-
loading={axis_loading === `y2`}
|
|
1534
|
-
axis_type="y2"
|
|
1535
|
-
color={final_y2_axis.color}
|
|
1536
|
-
on_select={(key) => handle_axis_change(`y2`, key)}
|
|
1537
|
-
class="axis-label y2-label"
|
|
1538
|
-
/>
|
|
1539
|
-
</div>
|
|
1540
|
-
</foreignObject>
|
|
2219
|
+
{@const { label_shift, label = ``, options, selected_key, color, tick } =
|
|
2220
|
+
final_y2_axis}
|
|
2221
|
+
{@const inside = tick?.label?.inside ?? false}
|
|
2222
|
+
{@const tick_shift = inside ? 0 : (tick?.label?.shift?.x ?? 0) + 8}
|
|
2223
|
+
{@const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max}
|
|
2224
|
+
<AxisLabel
|
|
2225
|
+
x={width - pad.r + tick_shift + tick_width_contribution +
|
|
2226
|
+
LABEL_GAP_DEFAULT + (label_shift?.x ?? 0)}
|
|
2227
|
+
y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
|
|
2228
|
+
rotate
|
|
2229
|
+
{label}
|
|
2230
|
+
{options}
|
|
2231
|
+
{selected_key}
|
|
2232
|
+
loading={axis_loading === `y2`}
|
|
2233
|
+
axis_type="y2"
|
|
2234
|
+
{color}
|
|
2235
|
+
on_select={(key) => handle_axis_change(`y2`, key)}
|
|
2236
|
+
/>
|
|
1541
2237
|
{/if}
|
|
1542
2238
|
</g>
|
|
1543
2239
|
{/if}
|
|
1544
2240
|
|
|
1545
|
-
<!--
|
|
2241
|
+
<!-- X2-axis (Top) -->
|
|
2242
|
+
{#if x2_points.length > 0}
|
|
2243
|
+
<g class="x2-axis">
|
|
2244
|
+
{#if width > 0 && height > 0}
|
|
2245
|
+
{#each x2_tick_values as tick (tick)}
|
|
2246
|
+
{@const tick_pos_raw = is_time_x2
|
|
2247
|
+
? x2_scale_fn(new Date(tick))
|
|
2248
|
+
: x2_scale_fn(tick)}
|
|
2249
|
+
{#if isFinite(tick_pos_raw)}
|
|
2250
|
+
{@const tick_pos = tick_pos_raw}
|
|
2251
|
+
{#if tick_pos >= pad.l && tick_pos <= width - pad.r}
|
|
2252
|
+
{@const inside = final_x2_axis.tick?.label?.inside ?? false}
|
|
2253
|
+
<g class="tick" transform="translate({tick_pos}, {pad.t})">
|
|
2254
|
+
{#if final_display.x2_grid}
|
|
2255
|
+
<line
|
|
2256
|
+
y1="0"
|
|
2257
|
+
y2={height - pad.b - pad.t}
|
|
2258
|
+
{...DEFAULT_GRID_STYLE}
|
|
2259
|
+
{...(final_x2_axis.grid_style ?? {})}
|
|
2260
|
+
/>
|
|
2261
|
+
{/if}
|
|
2262
|
+
<line
|
|
2263
|
+
y1="0"
|
|
2264
|
+
y2={inside ? 5 : -5}
|
|
2265
|
+
stroke={final_x2_axis.color || `var(--border-color, gray)`}
|
|
2266
|
+
/>
|
|
1546
2267
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2268
|
+
{#if tick >= Math.min(x2_min, x2_max) && tick <= Math.max(x2_min, x2_max)}
|
|
2269
|
+
{@const base_y = inside ? 8 : -20}
|
|
2270
|
+
{@const shift = final_x2_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
|
|
2271
|
+
{@const x = shift.x ?? 0}
|
|
2272
|
+
{@const y = base_y + (shift.y ?? 0)}
|
|
2273
|
+
{@const custom_label = get_tick_label(tick, final_x2_axis.ticks)}
|
|
2274
|
+
{@const dominant_baseline = inside ? `hanging` : `auto`}
|
|
2275
|
+
<text
|
|
2276
|
+
{x}
|
|
2277
|
+
{y}
|
|
2278
|
+
dominant-baseline={dominant_baseline}
|
|
2279
|
+
fill={final_x2_axis.color}
|
|
2280
|
+
>
|
|
2281
|
+
{custom_label ?? format_value(tick, final_x2_axis.format ?? ``)}
|
|
2282
|
+
</text>
|
|
2283
|
+
{/if}
|
|
2284
|
+
</g>
|
|
2285
|
+
{/if}
|
|
2286
|
+
{/if}
|
|
2287
|
+
{/each}
|
|
2288
|
+
{/if}
|
|
1557
2289
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
get_scale_type_name(final_y_axis.scale_type) !== `log` &&
|
|
1575
|
-
y_min <= 0 && y_max >= 0}
|
|
1576
|
-
{@const zero_y_pos = y_scale_fn(0)}
|
|
1577
|
-
{#if isFinite(zero_y_pos)}
|
|
1578
|
-
<line
|
|
1579
|
-
class="zero-line"
|
|
1580
|
-
x1={pad.l}
|
|
1581
|
-
x2={width - pad.r}
|
|
1582
|
-
y1={zero_y_pos}
|
|
1583
|
-
y2={zero_y_pos}
|
|
1584
|
-
/>
|
|
1585
|
-
{/if}
|
|
1586
|
-
{/if}
|
|
1587
|
-
{#if final_display.y_zero_line && y2_points.length > 0 &&
|
|
1588
|
-
get_scale_type_name(final_y2_axis.scale_type) !== `log` && y2_min <= 0 &&
|
|
1589
|
-
y2_max >= 0}
|
|
1590
|
-
{@const zero_y2_pos = y2_scale_fn(0)}
|
|
1591
|
-
{#if isFinite(zero_y2_pos)}
|
|
1592
|
-
<line
|
|
1593
|
-
class="zero-line"
|
|
1594
|
-
x1={pad.l}
|
|
1595
|
-
x2={width - pad.r}
|
|
1596
|
-
y1={zero_y2_pos}
|
|
1597
|
-
y2={zero_y2_pos}
|
|
1598
|
-
/>
|
|
1599
|
-
{/if}
|
|
2290
|
+
{#if final_x2_axis.label || final_x2_axis.options?.length}
|
|
2291
|
+
{@const { label_shift, label = ``, options, selected_key, color } =
|
|
2292
|
+
final_x2_axis}
|
|
2293
|
+
<AxisLabel
|
|
2294
|
+
x={width / 2 + (label_shift?.x ?? 0)}
|
|
2295
|
+
y={Math.max(12, pad.t - (label_shift?.y ?? 40))}
|
|
2296
|
+
{label}
|
|
2297
|
+
{options}
|
|
2298
|
+
{selected_key}
|
|
2299
|
+
loading={axis_loading === `x2`}
|
|
2300
|
+
axis_type="x2"
|
|
2301
|
+
{color}
|
|
2302
|
+
on_select={(key) => handle_axis_change(`x2`, key)}
|
|
2303
|
+
/>
|
|
2304
|
+
{/if}
|
|
2305
|
+
</g>
|
|
1600
2306
|
{/if}
|
|
1601
2307
|
|
|
2308
|
+
<!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
|
|
2309
|
+
|
|
2310
|
+
<ZoomRect start={drag_start_coords} current={drag_current_coords} />
|
|
2311
|
+
|
|
2312
|
+
<ZeroLines
|
|
2313
|
+
display={final_display}
|
|
2314
|
+
{x_scale_fn}
|
|
2315
|
+
{x2_scale_fn}
|
|
2316
|
+
{y_scale_fn}
|
|
2317
|
+
{y2_scale_fn}
|
|
2318
|
+
x_range={zoom_x_range}
|
|
2319
|
+
x2_range={zoom_x2_range}
|
|
2320
|
+
y_range={zoom_y_range}
|
|
2321
|
+
y2_range={zoom_y2_range}
|
|
2322
|
+
x_scale_type={final_x_axis.scale_type}
|
|
2323
|
+
x2_scale_type={final_x2_axis.scale_type}
|
|
2324
|
+
y_scale_type={final_y_axis.scale_type}
|
|
2325
|
+
y2_scale_type={final_y2_axis.scale_type}
|
|
2326
|
+
x_is_time={is_time_x}
|
|
2327
|
+
x2_is_time={is_time_x2}
|
|
2328
|
+
has_x2={x2_points.length > 0}
|
|
2329
|
+
has_y2={y2_points.length > 0}
|
|
2330
|
+
{width}
|
|
2331
|
+
{height}
|
|
2332
|
+
{pad}
|
|
2333
|
+
/>
|
|
2334
|
+
|
|
1602
2335
|
<defs>
|
|
1603
2336
|
<clipPath id={clip_path_id}>
|
|
1604
2337
|
<rect
|
|
@@ -1644,9 +2377,7 @@ $effect(() => {
|
|
|
1644
2377
|
<Line
|
|
1645
2378
|
points={finite_screen_points}
|
|
1646
2379
|
origin={[
|
|
1647
|
-
|
|
1648
|
-
? x_scale_fn(new Date(x_min))
|
|
1649
|
-
: x_scale_fn(x_min),
|
|
2380
|
+
is_time_x ? x_scale_fn(new Date(x_min)) : x_scale_fn(x_min),
|
|
1650
2381
|
series_data.y_axis === `y2` ? y2_scale_fn(y2_min) : y_scale_fn(y_min),
|
|
1651
2382
|
]}
|
|
1652
2383
|
line_color={(tc(`line.color`) ? styles.line?.color : null) ?? color_fallback}
|
|
@@ -1685,9 +2416,7 @@ $effect(() => {
|
|
|
1685
2416
|
...label_style,
|
|
1686
2417
|
offset: {
|
|
1687
2418
|
x: calculated_label_pos.x -
|
|
1688
|
-
(
|
|
1689
|
-
? x_scale_fn(new Date(point.x))
|
|
1690
|
-
: x_scale_fn(point.x)),
|
|
2419
|
+
(is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
|
|
1691
2420
|
y: calculated_label_pos.y - (series_data.y_axis === `y2`
|
|
1692
2421
|
? y2_scale_fn(point.y)
|
|
1693
2422
|
: y_scale_fn(point.y)),
|
|
@@ -1714,6 +2443,7 @@ $effect(() => {
|
|
|
1714
2443
|
tooltip_point?.point_idx === point.point_idx}
|
|
1715
2444
|
is_selected={selected_point?.series_idx === point.series_idx &&
|
|
1716
2445
|
selected_point?.point_idx === point.point_idx}
|
|
2446
|
+
leader_line_threshold={actual_label_config.leader_line_threshold}
|
|
1717
2447
|
style={{
|
|
1718
2448
|
symbol_type: pt?.symbol_type ?? series_default_symbol,
|
|
1719
2449
|
...pt,
|
|
@@ -1744,10 +2474,9 @@ $effect(() => {
|
|
|
1744
2474
|
series_default_color}
|
|
1745
2475
|
{...point_events &&
|
|
1746
2476
|
Object.fromEntries(
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
) => [event_name, (event: Event) => handler({ point, event })]),
|
|
2477
|
+
point_event_names.map((name) => [name, (event: Event) =>
|
|
2478
|
+
point_events?.[name]?.({ point, event })]
|
|
2479
|
+
),
|
|
1751
2480
|
)}
|
|
1752
2481
|
onclick={(event: MouseEvent) => {
|
|
1753
2482
|
// Call user-provided onclick handler first if it exists
|
|
@@ -1830,9 +2559,16 @@ $effect(() => {
|
|
|
1830
2559
|
{#if tooltip}
|
|
1831
2560
|
{@render tooltip(handler_props)}
|
|
1832
2561
|
{:else}
|
|
1833
|
-
{@
|
|
1834
|
-
|
|
1835
|
-
}<br
|
|
2562
|
+
{@const hp = handler_props}
|
|
2563
|
+
{#if has_multiple_series && hp.label}<strong>{hp.label}</strong><br />{/if}
|
|
2564
|
+
{@html sanitize_html(point_label?.text ? `${point_label.text}<br />` : ``)}
|
|
2565
|
+
{@html sanitize_html(hp.x_axis.label || `x`)}: {hp.x_formatted}<br />
|
|
2566
|
+
{@html sanitize_html(hp.y_axis.label || `y`)}: {hp.y_formatted}
|
|
2567
|
+
{#if hp.colorbar?.value != null}
|
|
2568
|
+
<br />{@html sanitize_html(hp.colorbar.title || `Color`)}: {
|
|
2569
|
+
format_value(hp.colorbar.value, hp.colorbar.tick_format || `.3~g`)
|
|
2570
|
+
}
|
|
2571
|
+
{/if}
|
|
1836
2572
|
{/if}
|
|
1837
2573
|
</PlotTooltip>
|
|
1838
2574
|
{/if}
|
|
@@ -1843,21 +2579,24 @@ $effect(() => {
|
|
|
1843
2579
|
toggle_props={{
|
|
1844
2580
|
...controls.toggle_props,
|
|
1845
2581
|
style:
|
|
1846
|
-
`--ctrl-btn-right: var(--fullscreen-btn-offset,
|
|
2582
|
+
`--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
|
|
1847
2583
|
controls.toggle_props?.style ?? ``
|
|
1848
2584
|
}`,
|
|
1849
2585
|
}}
|
|
1850
2586
|
pane_props={controls.pane_props}
|
|
1851
2587
|
bind:x_axis
|
|
2588
|
+
bind:x2_axis
|
|
1852
2589
|
bind:y_axis
|
|
1853
2590
|
bind:y2_axis
|
|
1854
2591
|
bind:display
|
|
1855
2592
|
bind:styles
|
|
1856
2593
|
{auto_x_range}
|
|
2594
|
+
{auto_x2_range}
|
|
1857
2595
|
{auto_y_range}
|
|
1858
2596
|
{auto_y2_range}
|
|
1859
2597
|
bind:selected_series_idx
|
|
1860
2598
|
series={series_with_ids}
|
|
2599
|
+
has_x2_points={x2_points.length > 0}
|
|
1861
2600
|
has_y2_points={y2_points.length > 0}
|
|
1862
2601
|
children={controls_extra}
|
|
1863
2602
|
on_touch={(key) => touched.add(key)}
|
|
@@ -2057,19 +2796,16 @@ $effect(() => {
|
|
|
2057
2796
|
stroke-dasharray: var(--scatter-grid-dash, 4);
|
|
2058
2797
|
stroke-width: var(--scatter-grid-width, 0.4);
|
|
2059
2798
|
}
|
|
2060
|
-
g.x-axis text {
|
|
2799
|
+
g:is(.x-axis, .x2-axis) text {
|
|
2061
2800
|
text-anchor: middle;
|
|
2062
2801
|
dominant-baseline: top;
|
|
2063
2802
|
}
|
|
2064
2803
|
g:is(.y-axis, .y2-axis) text {
|
|
2065
2804
|
dominant-baseline: central;
|
|
2066
2805
|
}
|
|
2067
|
-
g:is(.x-axis, .y-axis, .y2-axis) .tick text {
|
|
2806
|
+
g:is(.x-axis, .x2-axis, .y-axis, .y2-axis) .tick text {
|
|
2068
2807
|
font-size: var(--tick-font-size, 0.8em); /* shrink tick labels */
|
|
2069
2808
|
}
|
|
2070
|
-
foreignobject {
|
|
2071
|
-
overflow: visible;
|
|
2072
|
-
}
|
|
2073
2809
|
.scatter :global(.axis-label) {
|
|
2074
2810
|
text-align: center;
|
|
2075
2811
|
width: 100%;
|
|
@@ -2092,16 +2828,4 @@ $effect(() => {
|
|
|
2092
2828
|
.current-frame-indicator:hover {
|
|
2093
2829
|
opacity: 0.8;
|
|
2094
2830
|
}
|
|
2095
|
-
.zoom-rect {
|
|
2096
|
-
fill: var(--scatter-zoom-rect-fill, rgba(100, 100, 255, 0.2));
|
|
2097
|
-
stroke: var(--scatter-zoom-rect-stroke, rgba(100, 100, 255, 0.8));
|
|
2098
|
-
stroke-width: var(--scatter-zoom-rect-stroke-width, 1);
|
|
2099
|
-
pointer-events: none; /* Prevent rect from interfering with mouse events */
|
|
2100
|
-
}
|
|
2101
|
-
.zero-line {
|
|
2102
|
-
stroke: var(--scatter-zero-line-color, light-dark(black, white));
|
|
2103
|
-
stroke-width: var(--scatter-zero-line-width, 1);
|
|
2104
|
-
stroke-dasharray: none;
|
|
2105
|
-
opacity: var(--scatter-zero-line-opacity, 0.3);
|
|
2106
|
-
}
|
|
2107
2831
|
</style>
|