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
package/dist/plot/BarPlot.svelte
CHANGED
|
@@ -1,35 +1,227 @@
|
|
|
1
1
|
<script
|
|
2
2
|
lang="ts"
|
|
3
3
|
generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
|
|
4
|
-
>
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
4
|
+
>
|
|
5
|
+
import type { D3ColorSchemeName, D3InterpolateName } from '../colors'
|
|
6
|
+
import { format_value } from '../labels'
|
|
7
|
+
import { sanitize_html } from '../sanitize'
|
|
8
|
+
import { FullscreenToggle, set_fullscreen_bg } from '../layout'
|
|
9
|
+
import type {
|
|
10
|
+
AxisLoadError,
|
|
11
|
+
BarHandlerProps,
|
|
12
|
+
BarMode,
|
|
13
|
+
BarSeries,
|
|
14
|
+
BarStyle,
|
|
15
|
+
BasePlotProps,
|
|
16
|
+
DataLoaderFn,
|
|
17
|
+
InitialRanges,
|
|
18
|
+
InternalPoint,
|
|
19
|
+
LegendConfig,
|
|
20
|
+
LegendItem,
|
|
21
|
+
LineStyle,
|
|
22
|
+
Orientation,
|
|
23
|
+
PanConfig,
|
|
24
|
+
PlotConfig,
|
|
25
|
+
RefLine,
|
|
26
|
+
RefLineEvent,
|
|
27
|
+
ScaleType,
|
|
28
|
+
TweenedOptions,
|
|
29
|
+
UserContentProps,
|
|
30
|
+
XyObj,
|
|
31
|
+
} from './'
|
|
32
|
+
import {
|
|
33
|
+
AxisLabel,
|
|
34
|
+
BarPlotControls,
|
|
35
|
+
compute_element_placement,
|
|
36
|
+
PlotLegend,
|
|
37
|
+
ReferenceLine,
|
|
38
|
+
ScatterPoint,
|
|
39
|
+
} from './'
|
|
40
|
+
import type { AxisChangeState } from './axis-utils'
|
|
41
|
+
import { create_axis_change_handler } from './axis-utils'
|
|
42
|
+
import { process_prop } from './data-transform'
|
|
43
|
+
import {
|
|
44
|
+
create_dimension_tracker,
|
|
45
|
+
create_hover_lock,
|
|
46
|
+
} from './hover-lock.svelte'
|
|
47
|
+
import {
|
|
48
|
+
get_relative_coords,
|
|
49
|
+
pan_range,
|
|
50
|
+
PINCH_ZOOM_THRESHOLD,
|
|
51
|
+
pixels_to_data_delta,
|
|
52
|
+
} from './interactions'
|
|
53
|
+
import type { IndexedRefLine } from './reference-line'
|
|
54
|
+
import { group_ref_lines_by_z, index_ref_lines } from './reference-line'
|
|
55
|
+
import {
|
|
56
|
+
create_color_scale,
|
|
57
|
+
create_scale,
|
|
58
|
+
create_size_scale,
|
|
59
|
+
generate_ticks,
|
|
60
|
+
get_nice_data_range,
|
|
61
|
+
get_tick_label,
|
|
62
|
+
} from './scales'
|
|
63
|
+
import {
|
|
64
|
+
DEFAULT_GRID_STYLE,
|
|
65
|
+
DEFAULT_MARKERS,
|
|
66
|
+
get_scale_type_name,
|
|
67
|
+
} from './types'
|
|
68
|
+
import { DEFAULTS } from '../settings'
|
|
69
|
+
import { extent } from 'd3-array'
|
|
70
|
+
import type { Snippet } from 'svelte'
|
|
71
|
+
import { untrack } from 'svelte'
|
|
72
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
73
|
+
import { Tween } from 'svelte/motion'
|
|
74
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
75
|
+
import type { Vec2 } from '../math'
|
|
76
|
+
import {
|
|
77
|
+
calc_auto_padding,
|
|
78
|
+
constrain_tooltip_position,
|
|
79
|
+
filter_padding,
|
|
80
|
+
LABEL_GAP_DEFAULT,
|
|
81
|
+
measure_max_tick_width,
|
|
82
|
+
} from './layout'
|
|
83
|
+
import PlotTooltip from './PlotTooltip.svelte'
|
|
84
|
+
import { bar_path } from './svg'
|
|
85
|
+
import ZeroLines from './ZeroLines.svelte'
|
|
86
|
+
import ZoomRect from './ZoomRect.svelte'
|
|
87
|
+
|
|
88
|
+
// Handler props for line marker events (extends BarHandlerProps with point-specific data)
|
|
89
|
+
interface LineMarkerHandlerProps extends BarHandlerProps<Metadata> {
|
|
90
|
+
point: InternalPoint<Metadata>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extended point type with computed screen coordinates (used internally for rendering)
|
|
94
|
+
type LineSeriesPoint = InternalPoint<Metadata> & {
|
|
95
|
+
x: number // Screen x coordinate
|
|
96
|
+
y: number // Screen y coordinate
|
|
97
|
+
data_x: number // Original data x value
|
|
98
|
+
data_y: number // Original data y value
|
|
99
|
+
idx: number // Index in series
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let {
|
|
103
|
+
series = $bindable([]),
|
|
104
|
+
orientation = $bindable(`vertical`),
|
|
105
|
+
mode = $bindable(`overlay`),
|
|
106
|
+
x_axis = $bindable({}),
|
|
107
|
+
x2_axis = $bindable({}),
|
|
108
|
+
y_axis = $bindable({}),
|
|
109
|
+
y2_axis = $bindable({}),
|
|
110
|
+
display = $bindable(DEFAULTS.bar.display),
|
|
111
|
+
x_range = [null, null],
|
|
112
|
+
x2_range = [null, null],
|
|
113
|
+
y_range = [null, null],
|
|
114
|
+
y2_range = [null, null],
|
|
115
|
+
range_padding = 0.05,
|
|
116
|
+
padding = { t: 20, b: 60, l: 60, r: 20 },
|
|
117
|
+
legend = {},
|
|
118
|
+
show_legend,
|
|
119
|
+
bar = {},
|
|
120
|
+
line = {},
|
|
121
|
+
tooltip,
|
|
122
|
+
user_content,
|
|
123
|
+
hovered = $bindable(false),
|
|
124
|
+
change = () => {},
|
|
125
|
+
on_bar_click,
|
|
126
|
+
on_bar_hover,
|
|
127
|
+
// Line marker props (matching ScatterPlot)
|
|
128
|
+
color_scale = {
|
|
129
|
+
type: `linear`,
|
|
130
|
+
scheme: `interpolateViridis`,
|
|
131
|
+
value_range: undefined,
|
|
132
|
+
},
|
|
133
|
+
size_scale = { type: `linear`, radius_range: [2, 10], value_range: undefined },
|
|
134
|
+
point_tween,
|
|
135
|
+
on_point_click,
|
|
136
|
+
on_point_hover,
|
|
137
|
+
ref_lines = $bindable([]),
|
|
138
|
+
on_ref_line_click,
|
|
139
|
+
on_ref_line_hover,
|
|
140
|
+
show_controls = $bindable(true),
|
|
141
|
+
controls_open = $bindable(false),
|
|
142
|
+
controls_toggle_props,
|
|
143
|
+
controls_pane_props,
|
|
144
|
+
fullscreen = $bindable(false),
|
|
145
|
+
fullscreen_toggle = true,
|
|
146
|
+
children,
|
|
147
|
+
header_controls,
|
|
148
|
+
controls_extra,
|
|
149
|
+
data_loader,
|
|
150
|
+
on_axis_change,
|
|
151
|
+
on_error,
|
|
152
|
+
pan = {},
|
|
153
|
+
...rest
|
|
154
|
+
}: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
|
|
155
|
+
series?: BarSeries<Metadata>[]
|
|
156
|
+
// Component-specific props
|
|
157
|
+
orientation?: Orientation
|
|
158
|
+
mode?: BarMode
|
|
159
|
+
legend?: LegendConfig | null
|
|
160
|
+
show_legend?: boolean
|
|
161
|
+
bar?: BarStyle
|
|
162
|
+
line?: LineStyle
|
|
163
|
+
tooltip?: Snippet<[BarHandlerProps<Metadata>]>
|
|
164
|
+
user_content?: Snippet<[UserContentProps]>
|
|
165
|
+
header_controls?: Snippet<
|
|
166
|
+
[{ height: number; width: number; fullscreen: boolean }]
|
|
167
|
+
>
|
|
168
|
+
controls_extra?: Snippet<
|
|
169
|
+
[{ orientation: Orientation; mode: BarMode } & Required<PlotConfig>]
|
|
170
|
+
>
|
|
171
|
+
change?: (data: BarHandlerProps<Metadata> | null) => void
|
|
172
|
+
on_bar_click?: (
|
|
173
|
+
data: BarHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
|
|
174
|
+
) => void
|
|
175
|
+
on_bar_hover?: (
|
|
176
|
+
data:
|
|
177
|
+
| (BarHandlerProps<Metadata> & {
|
|
178
|
+
event: MouseEvent | FocusEvent | KeyboardEvent
|
|
179
|
+
})
|
|
180
|
+
| null,
|
|
181
|
+
) => void
|
|
182
|
+
// Line marker props (matching ScatterPlot)
|
|
183
|
+
// Note: For line series with markers, BOTH on_bar_* AND on_point_* events fire.
|
|
184
|
+
// Use on_point_* for marker-specific data (includes `point` with InternalPoint details)
|
|
185
|
+
// or on_bar_* for backward compatibility with bar-style event handling.
|
|
186
|
+
color_scale?: {
|
|
187
|
+
type?: ScaleType
|
|
188
|
+
scheme?: D3ColorSchemeName | D3InterpolateName
|
|
189
|
+
value_range?: [number, number]
|
|
190
|
+
} | D3InterpolateName
|
|
191
|
+
size_scale?: {
|
|
192
|
+
type?: ScaleType
|
|
193
|
+
radius_range?: [number, number]
|
|
194
|
+
value_range?: [number, number]
|
|
195
|
+
}
|
|
196
|
+
point_tween?: TweenedOptions<XyObj>
|
|
197
|
+
on_point_click?: (
|
|
198
|
+
data: LineMarkerHandlerProps & { event: MouseEvent | KeyboardEvent },
|
|
199
|
+
) => void
|
|
200
|
+
on_point_hover?: (
|
|
201
|
+
data:
|
|
202
|
+
| (LineMarkerHandlerProps & {
|
|
203
|
+
event: MouseEvent | FocusEvent | KeyboardEvent
|
|
204
|
+
})
|
|
205
|
+
| null,
|
|
206
|
+
) => void
|
|
207
|
+
ref_lines?: RefLine[]
|
|
208
|
+
on_ref_line_click?: (event: RefLineEvent) => void
|
|
209
|
+
on_ref_line_hover?: (event: RefLineEvent | null) => void
|
|
210
|
+
// Interactive axis props
|
|
211
|
+
data_loader?: DataLoaderFn<Metadata, BarSeries<Metadata>>
|
|
212
|
+
on_axis_change?: (
|
|
213
|
+
axis: `x` | `x2` | `y` | `y2`,
|
|
214
|
+
key: string,
|
|
215
|
+
new_series: BarSeries<Metadata>[],
|
|
216
|
+
) => void
|
|
217
|
+
on_error?: (error: AxisLoadError) => void
|
|
218
|
+
pan?: PanConfig
|
|
219
|
+
} = $props()
|
|
220
|
+
|
|
221
|
+
// Initialize bar, line, y2_axis with defaults - using $derived for reactivity
|
|
222
|
+
let bar_state = $derived({ ...DEFAULTS.bar.bar, ...bar })
|
|
223
|
+
let line_state = $derived({ ...DEFAULTS.bar.line, ...line })
|
|
224
|
+
y2_axis = {
|
|
33
225
|
format: ``,
|
|
34
226
|
scale_type: `linear`,
|
|
35
227
|
ticks: 5,
|
|
@@ -37,636 +229,1082 @@ y2_axis = {
|
|
|
37
229
|
tick: { label: { shift: { x: 0, y: 0 } } }, // base offset handled in rendering
|
|
38
230
|
range: [null, null],
|
|
39
231
|
...y2_axis,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let
|
|
232
|
+
}
|
|
233
|
+
x2_axis = {
|
|
234
|
+
format: ``,
|
|
235
|
+
scale_type: `linear`,
|
|
236
|
+
ticks: 5,
|
|
237
|
+
label_shift: { x: 0, y: 40 },
|
|
238
|
+
tick: { label: { shift: { x: 0, y: 0 } } },
|
|
239
|
+
range: [null, null],
|
|
240
|
+
...x2_axis,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let [width, height] = $state([0, 0])
|
|
244
|
+
let wrapper: HTMLDivElement | undefined = $state()
|
|
245
|
+
let svg_element: SVGElement | null = $state(null)
|
|
246
|
+
let clip_path_id = `chart-clip-${crypto?.randomUUID?.()}`
|
|
247
|
+
|
|
248
|
+
// Reference line hover state
|
|
249
|
+
let hovered_ref_line_idx = $state<number | null>(null)
|
|
250
|
+
|
|
251
|
+
// Interactive axis loading state
|
|
252
|
+
let axis_loading = $state<`x` | `x2` | `y` | `y2` | null>(null)
|
|
253
|
+
|
|
254
|
+
// Compute ref_lines with index and group by z-index (using shared utilities)
|
|
255
|
+
let indexed_ref_lines = $derived(index_ref_lines(ref_lines))
|
|
256
|
+
let ref_lines_by_z = $derived(group_ref_lines_by_z(indexed_ref_lines))
|
|
257
|
+
|
|
258
|
+
// === Categorical Normalization ===
|
|
259
|
+
// Internal type with guaranteed numeric x (for downstream scale/rendering code)
|
|
260
|
+
type NumericBarSeries = Omit<BarSeries<Metadata>, `x`> & { x: readonly number[] }
|
|
261
|
+
|
|
262
|
+
let is_categorical = $derived(
|
|
263
|
+
series.some((srs) => srs.x.some((val) => typeof val === `string`)),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
let category_list = $derived.by(() => {
|
|
267
|
+
if (!is_categorical) return [] as string[]
|
|
268
|
+
if (x_axis.categories?.length) return [...x_axis.categories]
|
|
269
|
+
return [...new Set(series.flatMap((srs) => srs.x.map(String)))]
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
let category_indices = $derived(
|
|
273
|
+
category_list.length ? category_list.map((_, idx) => idx) : null,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
let internal_series = $derived.by<NumericBarSeries[]>(() => {
|
|
277
|
+
// safe: when !category_indices, all x values are numeric (is_categorical is false)
|
|
278
|
+
if (!category_indices) return series as unknown as NumericBarSeries[]
|
|
279
|
+
return series.map((srs) => {
|
|
280
|
+
const orig_map = new Map(srs.x.map((val, idx) => [String(val), idx]))
|
|
281
|
+
if (orig_map.size < srs.x.length) {
|
|
282
|
+
console.warn(
|
|
283
|
+
`BarPlot: series "${
|
|
284
|
+
srs.label ?? `?`
|
|
285
|
+
}" has duplicate x values — last occurrence wins`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
// Resolve original index for each category (undefined if series lacks it)
|
|
289
|
+
const orig_indices = category_list.map((cat) => orig_map.get(cat))
|
|
290
|
+
const remap = <T>(arr: readonly T[] | null | undefined, fallback: T): T[] =>
|
|
291
|
+
orig_indices.map((oi) => oi != null ? (arr?.[oi] ?? fallback) : fallback)
|
|
292
|
+
const bw_arr = Array.isArray(srs.bar_width) ? srs.bar_width : null
|
|
293
|
+
const meta_arr = Array.isArray(srs.metadata) ? srs.metadata : null
|
|
294
|
+
return {
|
|
295
|
+
...srs,
|
|
296
|
+
x: category_indices,
|
|
297
|
+
y: remap(srs.y, srs.render_mode === `line` ? NaN : 0),
|
|
298
|
+
labels: remap(srs.labels, null),
|
|
299
|
+
metadata: orig_indices.map((oi) =>
|
|
300
|
+
oi != null ? (meta_arr ? meta_arr[oi] : srs.metadata) : undefined
|
|
301
|
+
) as Metadata[],
|
|
302
|
+
...(bw_arr ? { bar_width: remap(bw_arr, 0.5) } : {}),
|
|
303
|
+
...(srs.color_values ? { color_values: remap(srs.color_values, null) } : {}),
|
|
304
|
+
...(srs.size_values ? { size_values: remap(srs.size_values, null) } : {}),
|
|
305
|
+
} as NumericBarSeries
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Compute auto ranges from visible series
|
|
310
|
+
let visible_series = $derived(
|
|
311
|
+
internal_series.filter((srs) => srs?.visible ?? true),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// Separate series by y-axis
|
|
315
|
+
let y1_series = $derived(
|
|
316
|
+
visible_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`),
|
|
317
|
+
)
|
|
318
|
+
let y2_series = $derived(
|
|
319
|
+
visible_series.filter((srs) => srs.y_axis === `y2`),
|
|
320
|
+
)
|
|
321
|
+
let x2_series = $derived(
|
|
322
|
+
visible_series.filter((srs) => srs.x_axis === `x2`),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
let auto_ranges = $derived.by(() => {
|
|
58
326
|
// Calculate separate ranges for y1 and y2 axes
|
|
59
|
-
const calc_y_range = (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
327
|
+
const calc_y_range = (
|
|
328
|
+
series_list: typeof visible_series,
|
|
329
|
+
y_limit: typeof y_range,
|
|
330
|
+
scale_type: ScaleType,
|
|
331
|
+
) => {
|
|
332
|
+
let points = series_list.flatMap((srs) =>
|
|
333
|
+
srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// In stacked mode, calculate stacked totals for accurate range (only for bars on the same axis)
|
|
337
|
+
if (mode === `stacked`) {
|
|
338
|
+
const stacked_totals = new SvelteMap<number, { pos: number; neg: number }>()
|
|
339
|
+
|
|
340
|
+
// Only include visible bar series (not lines) in stacking
|
|
341
|
+
series_list
|
|
342
|
+
.filter((srs) => srs.render_mode !== `line`)
|
|
343
|
+
.forEach((srs) =>
|
|
344
|
+
srs.x.forEach((x_val, idx) => {
|
|
345
|
+
const y_val = srs.y[idx] ?? 0
|
|
346
|
+
const totals = stacked_totals.get(x_val) ?? { pos: 0, neg: 0 }
|
|
347
|
+
if (y_val >= 0) totals.pos += y_val
|
|
348
|
+
else totals.neg += y_val
|
|
349
|
+
stacked_totals.set(x_val, totals)
|
|
350
|
+
})
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Replace points with stacked totals + line series (which don't stack)
|
|
354
|
+
points = [
|
|
355
|
+
...Array.from(stacked_totals).flatMap(([x_val, { pos, neg }]) => [
|
|
356
|
+
...(pos > 0 ? [{ x: x_val, y: pos }] : []),
|
|
357
|
+
...(neg < 0 ? [{ x: x_val, y: neg }] : []),
|
|
358
|
+
]),
|
|
359
|
+
...series_list
|
|
360
|
+
.filter((srs) => srs.render_mode === `line`)
|
|
361
|
+
.flatMap((srs) =>
|
|
362
|
+
srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))
|
|
363
|
+
),
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!points.length) return [0, 1]
|
|
368
|
+
|
|
369
|
+
let y_range = get_nice_data_range(
|
|
370
|
+
points,
|
|
371
|
+
(pt) => pt.y,
|
|
372
|
+
y_limit,
|
|
373
|
+
scale_type,
|
|
374
|
+
range_padding,
|
|
375
|
+
false,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// For bar plots, ensure the value axis starts at 0 unless there are negative values
|
|
379
|
+
// Only apply zero-clamping for linear and arcsinh scales (not log)
|
|
380
|
+
const type_name = get_scale_type_name(scale_type)
|
|
381
|
+
if (type_name === `linear` || type_name === `arcsinh`) {
|
|
382
|
+
const has_negative = points.some((pt) => pt.y < 0)
|
|
383
|
+
const has_positive = points.some((pt) => pt.y > 0)
|
|
384
|
+
|
|
385
|
+
// Only adjust if no explicit y_range is set
|
|
386
|
+
if (y_limit?.[0] == null && y_limit?.[1] == null) {
|
|
387
|
+
if (has_positive && !has_negative) y_range = [0, y_range[1]]
|
|
388
|
+
else if (has_negative && !has_positive) y_range = [y_range[0], 0]
|
|
103
389
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return y_range
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Get x values split by axis for range calculation
|
|
396
|
+
// For categorical data, use fixed range centered on integer indices
|
|
397
|
+
let x_auto_range: number[]
|
|
398
|
+
if (category_list.length) {
|
|
399
|
+
x_auto_range = [-0.5, category_list.length - 0.5]
|
|
400
|
+
} else {
|
|
401
|
+
const x1_x_points = visible_series
|
|
402
|
+
.filter((srs) => (srs.x_axis ?? `x1`) === `x1`)
|
|
403
|
+
.flatMap((srs) => srs.x.map((x_val) => ({ x: x_val, y: 0 })))
|
|
404
|
+
x_auto_range = x1_x_points.length
|
|
405
|
+
? get_nice_data_range(
|
|
406
|
+
x1_x_points,
|
|
407
|
+
(pt) => pt.x,
|
|
408
|
+
x_range,
|
|
409
|
+
x_axis.scale_type ?? `linear`,
|
|
410
|
+
range_padding,
|
|
411
|
+
x_axis.format?.startsWith(`%`) || false,
|
|
412
|
+
)
|
|
413
|
+
: [0, 1]
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const x2_x_points = x2_series.flatMap((srs) =>
|
|
417
|
+
srs.x.map((x_val) => ({ x: x_val, y: 0 }))
|
|
418
|
+
)
|
|
419
|
+
const x2_scale_type = x2_axis.scale_type ?? `linear`
|
|
420
|
+
const x2_auto_range = x2_x_points.length
|
|
421
|
+
? get_nice_data_range(
|
|
422
|
+
x2_x_points,
|
|
423
|
+
(pt) => pt.x,
|
|
424
|
+
x2_range,
|
|
425
|
+
x2_scale_type,
|
|
426
|
+
range_padding,
|
|
427
|
+
x2_axis.format?.startsWith(`%`) || false,
|
|
428
|
+
)
|
|
429
|
+
: [0, 1]
|
|
430
|
+
|
|
431
|
+
const y1_range = calc_y_range(y1_series, y_range, y_axis.scale_type ?? `linear`)
|
|
432
|
+
const y2_auto_range = calc_y_range(
|
|
433
|
+
y2_series,
|
|
434
|
+
y2_range,
|
|
435
|
+
y2_axis.scale_type ?? `linear`,
|
|
436
|
+
)
|
|
437
|
+
|
|
114
438
|
// Map data ranges to axis ranges depending on orientation
|
|
115
439
|
return orientation === `horizontal`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
440
|
+
? ({ x: y1_range, x2: x2_auto_range, y: x_auto_range, y2: y2_auto_range })
|
|
441
|
+
: ({ x: x_auto_range, x2: x2_auto_range, y: y1_range, y2: y2_auto_range })
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// Initialize and current ranges
|
|
445
|
+
let ranges = $state<{
|
|
446
|
+
initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
447
|
+
current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
448
|
+
}>({
|
|
449
|
+
initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
450
|
+
current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
$effect(() => { // handle x_axis.range / x2_axis.range / y_axis.range / y2_axis.range changes
|
|
125
454
|
const new_x = [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
]
|
|
455
|
+
x_axis.range?.[0] ?? auto_ranges.x[0],
|
|
456
|
+
x_axis.range?.[1] ?? auto_ranges.x[1],
|
|
457
|
+
] as Vec2
|
|
458
|
+
const new_x2 = [
|
|
459
|
+
x2_axis.range?.[0] ?? auto_ranges.x2[0],
|
|
460
|
+
x2_axis.range?.[1] ?? auto_ranges.x2[1],
|
|
461
|
+
] as Vec2
|
|
129
462
|
const new_y = [
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
]
|
|
463
|
+
y_axis.range?.[0] ?? auto_ranges.y[0],
|
|
464
|
+
y_axis.range?.[1] ?? auto_ranges.y[1],
|
|
465
|
+
] as Vec2
|
|
133
466
|
const new_y2 = [
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
]
|
|
467
|
+
y2_axis.range?.[0] ?? auto_ranges.y2[0],
|
|
468
|
+
y2_axis.range?.[1] ?? auto_ranges.y2[1],
|
|
469
|
+
] as Vec2
|
|
137
470
|
// Only update if the initial (data-driven) ranges changed, not when user pans
|
|
138
471
|
// Comparing against initial preserves user's pan/zoom state
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
472
|
+
if (
|
|
473
|
+
ranges.initial.x[0] !== new_x[0] ||
|
|
474
|
+
ranges.initial.x[1] !== new_x[1] ||
|
|
475
|
+
ranges.initial.x2[0] !== new_x2[0] ||
|
|
476
|
+
ranges.initial.x2[1] !== new_x2[1] ||
|
|
477
|
+
ranges.initial.y[0] !== new_y[0] ||
|
|
478
|
+
ranges.initial.y[1] !== new_y[1] ||
|
|
479
|
+
ranges.initial.y2[0] !== new_y2[0] ||
|
|
480
|
+
ranges.initial.y2[1] !== new_y2[1]
|
|
481
|
+
) {
|
|
482
|
+
ranges = {
|
|
483
|
+
initial: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
|
|
484
|
+
current: { x: new_x, x2: new_x2, y: new_y, y2: new_y2 },
|
|
485
|
+
}
|
|
149
486
|
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// Layout: dynamic padding based on tick label widths
|
|
490
|
+
const default_padding = { t: 20, b: 60, l: 60, r: 20 }
|
|
491
|
+
let pad = $derived(filter_padding(padding, default_padding))
|
|
492
|
+
|
|
493
|
+
// Update padding when format or ticks change
|
|
494
|
+
$effect(() => {
|
|
156
495
|
const new_pad = width && height && ticks.y.length
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
496
|
+
? calc_auto_padding({
|
|
497
|
+
padding,
|
|
498
|
+
default_padding,
|
|
499
|
+
x2_axis: { ...x2_axis, tick_values: ticks.x2 },
|
|
500
|
+
y_axis: { ...y_axis, tick_values: ticks.y },
|
|
501
|
+
y2_axis: { ...y2_axis, tick_values: ticks.y2 },
|
|
502
|
+
})
|
|
503
|
+
: filter_padding(padding, default_padding)
|
|
164
504
|
// Expand right padding if y2 ticks are shown (only for vertical orientation)
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
505
|
+
if (
|
|
506
|
+
width && height && y2_series.length && ticks.y2.length &&
|
|
507
|
+
orientation === `vertical`
|
|
508
|
+
) {
|
|
509
|
+
// Need space for: tick shift + tick width + gap (30px) + label space (20px if present)
|
|
510
|
+
// When ticks are inside, they don't contribute to padding
|
|
511
|
+
const inside = y2_axis.tick?.label?.inside ?? false
|
|
512
|
+
const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
|
|
513
|
+
const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
|
|
514
|
+
const label_space = y2_axis.label ? 20 : 0
|
|
515
|
+
new_pad.r = Math.max(
|
|
516
|
+
new_pad.r,
|
|
517
|
+
tick_shift + tick_width_contribution + 30 + label_space,
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
// Expand top padding if x2 ticks are shown (only for vertical orientation)
|
|
521
|
+
if (
|
|
522
|
+
width && height && x2_series.length && ticks.x2.length &&
|
|
523
|
+
orientation === `vertical`
|
|
524
|
+
) {
|
|
525
|
+
const inside = x2_axis.tick?.label?.inside ?? false
|
|
526
|
+
const tick_shift = inside ? 0 : Math.abs(x2_axis.tick?.label?.shift?.y ?? 0) + 5
|
|
527
|
+
const tick_height = inside ? 0 : 16
|
|
528
|
+
const label_space = x2_axis.label ? 20 : 0
|
|
529
|
+
new_pad.t = Math.max(new_pad.t, tick_shift + tick_height + 30 + label_space)
|
|
175
530
|
}
|
|
531
|
+
|
|
176
532
|
// Only update if padding actually changed (prevents infinite loop)
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
533
|
+
if (
|
|
534
|
+
pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
|
|
535
|
+
pad.r !== new_pad.r
|
|
536
|
+
) pad = new_pad
|
|
537
|
+
})
|
|
538
|
+
const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
|
|
539
|
+
const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
|
|
540
|
+
|
|
541
|
+
// Scales
|
|
542
|
+
let scales = $derived({
|
|
185
543
|
x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [
|
|
186
|
-
|
|
187
|
-
|
|
544
|
+
pad.l,
|
|
545
|
+
width - pad.r,
|
|
546
|
+
]),
|
|
547
|
+
x2: create_scale(x2_axis.scale_type ?? `linear`, ranges.current.x2, [
|
|
548
|
+
pad.l,
|
|
549
|
+
width - pad.r,
|
|
188
550
|
]),
|
|
189
551
|
y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [
|
|
190
|
-
|
|
191
|
-
|
|
552
|
+
height - pad.b,
|
|
553
|
+
pad.t,
|
|
192
554
|
]),
|
|
193
555
|
y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [
|
|
194
|
-
|
|
195
|
-
|
|
556
|
+
height - pad.b,
|
|
557
|
+
pad.t,
|
|
196
558
|
]),
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
let
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// Compute plot center for point tweening origin
|
|
562
|
+
let plot_center_x = $derived(pad.l + (width - pad.r - pad.l) / 2)
|
|
563
|
+
let plot_center_y = $derived(pad.t + (height - pad.b - pad.t) / 2)
|
|
564
|
+
|
|
565
|
+
// Compute color values from line series for color scaling (filter to numbers only)
|
|
566
|
+
let all_color_values = $derived(
|
|
567
|
+
visible_series
|
|
568
|
+
.filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
|
|
569
|
+
.flatMap((srs: BarSeries<Metadata>) =>
|
|
570
|
+
(srs.color_values ?? []).filter(
|
|
571
|
+
(val): val is number => typeof val === `number`,
|
|
572
|
+
)
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
// Create auto color range (safely handle empty arrays or undefined extent results)
|
|
577
|
+
let auto_color_range: [number, number] = $derived.by(() => {
|
|
578
|
+
if (all_color_values.length === 0) return [0, 1]
|
|
579
|
+
const [min_val, max_val] = extent(all_color_values)
|
|
580
|
+
return [min_val ?? 0, max_val ?? 1]
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// All size values from line series (for size scale, filter to numbers only)
|
|
584
|
+
let all_size_values = $derived(
|
|
585
|
+
visible_series
|
|
586
|
+
.filter((srs: BarSeries<Metadata>) => srs.render_mode === `line`)
|
|
587
|
+
.flatMap((srs: BarSeries<Metadata>) =>
|
|
588
|
+
[...(srs.size_values ?? [])].filter(
|
|
589
|
+
(val): val is number => typeof val === `number`,
|
|
590
|
+
)
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
// Color scale function (using shared utility)
|
|
595
|
+
let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range))
|
|
596
|
+
|
|
597
|
+
// Size scale function (using shared utility)
|
|
598
|
+
let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
|
|
599
|
+
|
|
600
|
+
// Auto-generate tick labels for categorical data (unless user provides explicit ticks)
|
|
601
|
+
// In vertical mode categories are on x-axis; in horizontal mode on y-axis
|
|
602
|
+
let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
|
|
603
|
+
let effective_cat_ticks = $derived.by(() => {
|
|
604
|
+
if (!category_list.length) return undefined
|
|
605
|
+
// Only respect user ticks when they're a Record (custom label mapping),
|
|
606
|
+
// not a number (tick count) or array (tick positions)
|
|
607
|
+
const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
|
|
608
|
+
if (
|
|
609
|
+
user_ticks != null && typeof user_ticks === `object` &&
|
|
610
|
+
!Array.isArray(user_ticks)
|
|
611
|
+
) return user_ticks
|
|
612
|
+
return Object.fromEntries(
|
|
613
|
+
category_list.map((cat, idx) => [idx, cat]),
|
|
614
|
+
) as Record<number, string>
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
// Ticks
|
|
618
|
+
let ticks = $derived({
|
|
222
619
|
x: width && height
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
620
|
+
? (category_indices && cat_axis === `x` ? category_indices : generate_ticks(
|
|
621
|
+
ranges.current.x,
|
|
622
|
+
x_axis.scale_type ?? `linear`,
|
|
623
|
+
x_axis.ticks,
|
|
624
|
+
scales.x,
|
|
625
|
+
{ default_count: 8 },
|
|
626
|
+
))
|
|
627
|
+
: [],
|
|
227
628
|
y: width && height
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
629
|
+
? (category_indices && cat_axis === `y` ? category_indices : generate_ticks(
|
|
630
|
+
ranges.current.y,
|
|
631
|
+
y_axis.scale_type ?? `linear`,
|
|
632
|
+
y_axis.ticks,
|
|
633
|
+
scales.y,
|
|
634
|
+
{ default_count: 6 },
|
|
635
|
+
))
|
|
636
|
+
: [],
|
|
232
637
|
y2: width && height && y2_series.length > 0 && orientation === `vertical`
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
638
|
+
? generate_ticks(
|
|
639
|
+
ranges.current.y2,
|
|
640
|
+
y2_axis.scale_type ?? `linear`,
|
|
641
|
+
y2_axis.ticks,
|
|
642
|
+
scales.y2,
|
|
643
|
+
{
|
|
644
|
+
default_count: 6,
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
: [],
|
|
648
|
+
x2: width && height && x2_series.length > 0 && orientation === `vertical`
|
|
649
|
+
? generate_ticks(
|
|
650
|
+
ranges.current.x2,
|
|
651
|
+
x2_axis.scale_type ?? `linear`,
|
|
652
|
+
x2_axis.ticks,
|
|
653
|
+
scales.x2,
|
|
654
|
+
{
|
|
655
|
+
default_count: 8,
|
|
656
|
+
},
|
|
657
|
+
)
|
|
658
|
+
: [],
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
// Cache measured tick-label widths so expensive canvas text measurement
|
|
662
|
+
// only runs when ticks/format change, not on every template rerender.
|
|
663
|
+
let tick_label_widths = $derived({
|
|
664
|
+
y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
|
|
665
|
+
y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
|
|
666
|
+
x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
// Zoom drag state
|
|
670
|
+
let drag_state = $state<{
|
|
671
|
+
start: { x: number; y: number } | null
|
|
672
|
+
current: { x: number; y: number } | null
|
|
673
|
+
bounds: DOMRect | null
|
|
674
|
+
}>({ start: null, current: null, bounds: null })
|
|
675
|
+
|
|
676
|
+
// Pan state
|
|
677
|
+
let is_focused = $state(false)
|
|
678
|
+
let shift_held = $state(false)
|
|
679
|
+
let pan_drag_state = $state<
|
|
680
|
+
InitialRanges & { start: { x: number; y: number } } | null
|
|
681
|
+
>(null)
|
|
682
|
+
let touch_state = $state<
|
|
683
|
+
InitialRanges & { start_touches: { x: number; y: number }[] } | null
|
|
684
|
+
>(null)
|
|
685
|
+
const on_window_mouse_move = (evt: MouseEvent) => {
|
|
686
|
+
if (!drag_state.start || !drag_state.bounds) return
|
|
248
687
|
drag_state.current = {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const on_window_mouse_up = () => {
|
|
688
|
+
x: evt.clientX - drag_state.bounds.left,
|
|
689
|
+
y: evt.clientY - drag_state.bounds.top,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const on_window_mouse_up = () => {
|
|
254
693
|
if (drag_state.start && drag_state.current) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
694
|
+
const x1_raw = scales.x.invert(drag_state.start.x) as number | Date
|
|
695
|
+
const x2_raw = scales.x.invert(drag_state.current.x) as number | Date
|
|
696
|
+
const y1 = scales.y.invert(drag_state.start.y)
|
|
697
|
+
const y2 = scales.y.invert(drag_state.current.y)
|
|
698
|
+
const y2_1 = scales.y2.invert(drag_state.start.y)
|
|
699
|
+
const y2_2 = scales.y2.invert(drag_state.current.y)
|
|
700
|
+
const x2a_1_raw = scales.x2.invert(drag_state.start.x) as number | Date
|
|
701
|
+
const x2a_2_raw = scales.x2.invert(drag_state.current.x) as number | Date
|
|
702
|
+
const dx = Math.abs(drag_state.start.x - drag_state.current.x)
|
|
703
|
+
const dy = Math.abs(drag_state.start.y - drag_state.current.y)
|
|
704
|
+
|
|
705
|
+
let xr1: number, xr2: number
|
|
706
|
+
if (x1_raw instanceof Date && x2_raw instanceof Date) {
|
|
707
|
+
;[xr1, xr2] = [x1_raw.getTime(), x2_raw.getTime()]
|
|
708
|
+
} else if (typeof x1_raw === `number` && typeof x2_raw === `number`) {
|
|
709
|
+
;[xr1, xr2] = [x1_raw, x2_raw]
|
|
710
|
+
} else [xr1, xr2] = [NaN, NaN] // bail: mixed types
|
|
711
|
+
|
|
712
|
+
let x2r1: number, x2r2: number
|
|
713
|
+
if (x2a_1_raw instanceof Date && x2a_2_raw instanceof Date) {
|
|
714
|
+
;[x2r1, x2r2] = [x2a_1_raw.getTime(), x2a_2_raw.getTime()]
|
|
715
|
+
} else if (typeof x2a_1_raw === `number` && typeof x2a_2_raw === `number`) {
|
|
716
|
+
;[x2r1, x2r2] = [x2a_1_raw, x2a_2_raw]
|
|
717
|
+
} else [x2r1, x2r2] = [NaN, NaN]
|
|
718
|
+
|
|
719
|
+
if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
|
|
720
|
+
// Update axis ranges to trigger reactivity and prevent effect from overriding
|
|
721
|
+
x_axis = { ...x_axis, range: [Math.min(xr1, xr2), Math.max(xr1, xr2)] }
|
|
722
|
+
if (x2_series.length > 0 && Number.isFinite(x2r1) && Number.isFinite(x2r2)) {
|
|
723
|
+
x2_axis = {
|
|
724
|
+
...x2_axis,
|
|
725
|
+
range: [Math.min(x2r1, x2r2), Math.max(x2r1, x2r2)],
|
|
726
|
+
}
|
|
279
727
|
}
|
|
728
|
+
y_axis = { ...y_axis, range: [Math.min(y1, y2), Math.max(y1, y2)] }
|
|
729
|
+
y2_axis = { ...y2_axis, range: [Math.min(y2_1, y2_2), Math.max(y2_1, y2_2)] }
|
|
730
|
+
}
|
|
280
731
|
}
|
|
281
|
-
drag_state = { start: null, current: null, bounds: null }
|
|
282
|
-
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
283
|
-
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
284
|
-
document.body.style.cursor = `default
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const dx = evt.clientX - pan_drag_state.start.x
|
|
291
|
-
const dy = evt.clientY - pan_drag_state.start.y
|
|
732
|
+
drag_state = { start: null, current: null, bounds: null }
|
|
733
|
+
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
734
|
+
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
735
|
+
document.body.style.cursor = `default`
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Pan drag handlers
|
|
739
|
+
const on_pan_move = (evt: MouseEvent) => {
|
|
740
|
+
if (!pan_drag_state) return
|
|
741
|
+
const dx = evt.clientX - pan_drag_state.start.x
|
|
742
|
+
const dy = evt.clientY - pan_drag_state.start.y
|
|
743
|
+
|
|
292
744
|
// Convert pixel delta to data delta (note: drag direction is inverted for natural pan feel)
|
|
293
|
-
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
745
|
+
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
746
|
+
|
|
747
|
+
const x_delta = pixels_to_data_delta(
|
|
748
|
+
-dx * sensitivity,
|
|
749
|
+
pan_drag_state.initial_x_range,
|
|
750
|
+
chart_width,
|
|
751
|
+
)
|
|
752
|
+
const x2_delta = pixels_to_data_delta(
|
|
753
|
+
-dx * sensitivity,
|
|
754
|
+
pan_drag_state.initial_x2_range,
|
|
755
|
+
chart_width,
|
|
756
|
+
)
|
|
757
|
+
const y_delta = pixels_to_data_delta(
|
|
758
|
+
dy * sensitivity,
|
|
759
|
+
pan_drag_state.initial_y_range,
|
|
760
|
+
chart_height,
|
|
761
|
+
)
|
|
762
|
+
const y2_delta = pixels_to_data_delta(
|
|
763
|
+
dy * sensitivity,
|
|
764
|
+
pan_drag_state.initial_y2_range,
|
|
765
|
+
chart_height,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
ranges.current.x = pan_range(pan_drag_state.initial_x_range, x_delta)
|
|
769
|
+
ranges.current.x2 = pan_range(pan_drag_state.initial_x2_range, x2_delta)
|
|
770
|
+
ranges.current.y = pan_range(pan_drag_state.initial_y_range, y_delta)
|
|
771
|
+
ranges.current.y2 = pan_range(pan_drag_state.initial_y2_range, y2_delta)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const on_pan_end = () => {
|
|
775
|
+
pan_drag_state = null
|
|
776
|
+
document.body.style.cursor = ``
|
|
777
|
+
window.removeEventListener(`mousemove`, on_pan_move)
|
|
778
|
+
window.removeEventListener(`mouseup`, on_pan_end)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function handle_mouse_down(evt: MouseEvent) {
|
|
782
|
+
const coords = get_relative_coords(evt)
|
|
783
|
+
if (!coords || !svg_element) return
|
|
784
|
+
|
|
311
785
|
// Check if pan is enabled and shift is held for pan mode
|
|
312
|
-
const pan_enabled = pan?.enabled !== false
|
|
786
|
+
const pan_enabled = pan?.enabled !== false
|
|
313
787
|
if (pan_enabled && evt.shiftKey) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
788
|
+
evt.preventDefault()
|
|
789
|
+
pan_drag_state = {
|
|
790
|
+
start: { x: evt.clientX, y: evt.clientY },
|
|
791
|
+
initial_x_range: [...ranges.current.x] as [number, number],
|
|
792
|
+
initial_x2_range: [...ranges.current.x2] as [number, number],
|
|
793
|
+
initial_y_range: [...ranges.current.y] as [number, number],
|
|
794
|
+
initial_y2_range: [...ranges.current.y2] as [number, number],
|
|
795
|
+
}
|
|
796
|
+
document.body.style.cursor = `grabbing`
|
|
797
|
+
window.addEventListener(`mousemove`, on_pan_move)
|
|
798
|
+
window.addEventListener(`mouseup`, on_pan_end)
|
|
799
|
+
return
|
|
325
800
|
}
|
|
801
|
+
|
|
326
802
|
drag_state = {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
332
|
-
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
333
|
-
evt.preventDefault()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
803
|
+
start: coords,
|
|
804
|
+
current: coords,
|
|
805
|
+
bounds: svg_element.getBoundingClientRect(),
|
|
806
|
+
}
|
|
807
|
+
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
808
|
+
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
809
|
+
evt.preventDefault()
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Wheel handler for pan (requires focus and shift)
|
|
813
|
+
function handle_wheel(evt: WheelEvent) {
|
|
814
|
+
const pan_enabled = pan?.enabled !== false
|
|
338
815
|
// Only capture wheel when focused AND Shift is held
|
|
339
816
|
// Use shift_held state (tracked via keydown/keyup) for compatibility with synthetic events
|
|
340
|
-
if (!pan_enabled || !is_focused || !shift_held)
|
|
341
|
-
|
|
342
|
-
evt.preventDefault()
|
|
343
|
-
|
|
817
|
+
if (!pan_enabled || !is_focused || !shift_held) return
|
|
818
|
+
|
|
819
|
+
evt.preventDefault()
|
|
820
|
+
|
|
821
|
+
const sensitivity = pan?.wheel_sensitivity ?? 1
|
|
822
|
+
|
|
344
823
|
// Determine pan direction based on wheel delta
|
|
345
|
-
const x_delta = pixels_to_data_delta(
|
|
346
|
-
|
|
347
|
-
|
|
824
|
+
const x_delta = pixels_to_data_delta(
|
|
825
|
+
evt.deltaX * sensitivity,
|
|
826
|
+
ranges.current.x,
|
|
827
|
+
chart_width,
|
|
828
|
+
)
|
|
829
|
+
const x2_delta = pixels_to_data_delta(
|
|
830
|
+
evt.deltaX * sensitivity,
|
|
831
|
+
ranges.current.x2,
|
|
832
|
+
chart_width,
|
|
833
|
+
)
|
|
834
|
+
const y_delta = pixels_to_data_delta(
|
|
835
|
+
evt.deltaY * sensitivity,
|
|
836
|
+
ranges.current.y,
|
|
837
|
+
chart_height,
|
|
838
|
+
)
|
|
839
|
+
const y2_delta = pixels_to_data_delta(
|
|
840
|
+
evt.deltaY * sensitivity,
|
|
841
|
+
ranges.current.y2,
|
|
842
|
+
chart_height,
|
|
843
|
+
)
|
|
844
|
+
|
|
348
845
|
if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
|
|
349
|
-
|
|
846
|
+
ranges.current.x = pan_range(ranges.current.x, x_delta)
|
|
847
|
+
ranges.current.x2 = pan_range(ranges.current.x2, x2_delta)
|
|
848
|
+
} else {
|
|
849
|
+
ranges.current.y = pan_range(ranges.current.y, y_delta)
|
|
850
|
+
ranges.current.y2 = pan_range(ranges.current.y2, y2_delta)
|
|
350
851
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return;
|
|
361
|
-
evt.preventDefault();
|
|
362
|
-
const touches = Array.from(evt.touches);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Touch handlers for pinch-zoom and two-finger pan
|
|
855
|
+
function handle_touch_start(evt: TouchEvent) {
|
|
856
|
+
const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
|
|
857
|
+
if (!touch_enabled || evt.touches.length !== 2) return
|
|
858
|
+
|
|
859
|
+
evt.preventDefault()
|
|
860
|
+
const touches = Array.from(evt.touches)
|
|
363
861
|
touch_state = {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
evt.
|
|
374
|
-
|
|
375
|
-
|
|
862
|
+
start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
|
|
863
|
+
initial_x_range: [...ranges.current.x] as [number, number],
|
|
864
|
+
initial_x2_range: [...ranges.current.x2] as [number, number],
|
|
865
|
+
initial_y_range: [...ranges.current.y] as [number, number],
|
|
866
|
+
initial_y2_range: [...ranges.current.y2] as [number, number],
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function handle_touch_move(evt: TouchEvent) {
|
|
871
|
+
if (!touch_state || evt.touches.length !== 2) return
|
|
872
|
+
evt.preventDefault()
|
|
873
|
+
|
|
874
|
+
const [t1, t2] = Array.from(evt.touches)
|
|
875
|
+
const [s1, s2] = touch_state.start_touches
|
|
876
|
+
|
|
376
877
|
// Calculate center movement for pan
|
|
377
|
-
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
878
|
+
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
378
879
|
const curr_center = {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
const dx = curr_center.x - start_center.x
|
|
383
|
-
const dy = curr_center.y - start_center.y
|
|
880
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
881
|
+
y: (t1.clientY + t2.clientY) / 2,
|
|
882
|
+
}
|
|
883
|
+
const dx = curr_center.x - start_center.x
|
|
884
|
+
const dy = curr_center.y - start_center.y
|
|
885
|
+
|
|
384
886
|
// Calculate pinch scale (curr/start so spread = zoom out, pinch = zoom in)
|
|
385
|
-
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
887
|
+
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
386
888
|
// Guard against zero-distance pinch to avoid Infinity scale
|
|
387
|
-
if (start_dist < Number.EPSILON)
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
889
|
+
if (start_dist < Number.EPSILON) return
|
|
890
|
+
const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
|
891
|
+
const scale = curr_dist / start_dist
|
|
892
|
+
|
|
391
893
|
// If scale changed significantly, treat as pinch-zoom
|
|
392
894
|
// Also guard against scale being too small to avoid division by zero
|
|
393
895
|
if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
896
|
+
// Pinch zoom centered on gesture center
|
|
897
|
+
// Divide by scale so spread (scale > 1) = smaller span (zoom in)
|
|
898
|
+
const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0]
|
|
899
|
+
const x2_span = touch_state.initial_x2_range[1] -
|
|
900
|
+
touch_state.initial_x2_range[0]
|
|
901
|
+
const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0]
|
|
902
|
+
const y2_span = touch_state.initial_y2_range[1] -
|
|
903
|
+
touch_state.initial_y2_range[0]
|
|
904
|
+
const x_center =
|
|
905
|
+
(touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2
|
|
906
|
+
const x2_center =
|
|
907
|
+
(touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2
|
|
908
|
+
const y_center =
|
|
909
|
+
(touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2
|
|
910
|
+
const y2_center =
|
|
911
|
+
(touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2
|
|
912
|
+
|
|
913
|
+
ranges.current.x = [
|
|
914
|
+
x_center - x_span / scale / 2,
|
|
915
|
+
x_center + x_span / scale / 2,
|
|
916
|
+
]
|
|
917
|
+
ranges.current.x2 = [
|
|
918
|
+
x2_center - x2_span / scale / 2,
|
|
919
|
+
x2_center + x2_span / scale / 2,
|
|
920
|
+
]
|
|
921
|
+
ranges.current.y = [
|
|
922
|
+
y_center - y_span / scale / 2,
|
|
923
|
+
y_center + y_span / scale / 2,
|
|
924
|
+
]
|
|
925
|
+
ranges.current.y2 = [
|
|
926
|
+
y2_center - y2_span / scale / 2,
|
|
927
|
+
y2_center + y2_span / scale / 2,
|
|
928
|
+
]
|
|
929
|
+
} else {
|
|
930
|
+
// Pan
|
|
931
|
+
const x_delta = pixels_to_data_delta(
|
|
932
|
+
-dx,
|
|
933
|
+
touch_state.initial_x_range,
|
|
934
|
+
chart_width,
|
|
935
|
+
)
|
|
936
|
+
const x2_delta = pixels_to_data_delta(
|
|
937
|
+
-dx,
|
|
938
|
+
touch_state.initial_x2_range,
|
|
939
|
+
chart_width,
|
|
940
|
+
)
|
|
941
|
+
const y_delta = pixels_to_data_delta(
|
|
942
|
+
dy,
|
|
943
|
+
touch_state.initial_y_range,
|
|
944
|
+
chart_height,
|
|
945
|
+
)
|
|
946
|
+
const y2_delta = pixels_to_data_delta(
|
|
947
|
+
dy,
|
|
948
|
+
touch_state.initial_y2_range,
|
|
949
|
+
chart_height,
|
|
950
|
+
)
|
|
951
|
+
ranges.current.x = pan_range(touch_state.initial_x_range, x_delta)
|
|
952
|
+
ranges.current.x2 = pan_range(touch_state.initial_x2_range, x2_delta)
|
|
953
|
+
ranges.current.y = pan_range(touch_state.initial_y_range, y_delta)
|
|
954
|
+
ranges.current.y2 = pan_range(touch_state.initial_y2_range, y2_delta)
|
|
424
955
|
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function handle_touch_end() {
|
|
959
|
+
touch_state = null
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Legend data and handlers
|
|
963
|
+
let legend_data = $derived.by<LegendItem[]>(() =>
|
|
964
|
+
series.map((srs: BarSeries<Metadata>, idx: number) => {
|
|
965
|
+
const is_line = srs.render_mode === `line`
|
|
966
|
+
const series_markers = srs.markers ?? DEFAULT_MARKERS
|
|
967
|
+
const has_line = series_markers === `line` || series_markers === `line+points`
|
|
968
|
+
const has_points = series_markers === `points` ||
|
|
969
|
+
series_markers === `line+points`
|
|
970
|
+
const series_color = srs.color ?? (is_line ? line_state.color : bar_state.color)
|
|
971
|
+
|
|
972
|
+
// Get point style for symbol color (handle array or single object)
|
|
973
|
+
const first_point_style = Array.isArray(srs.point_style)
|
|
439
974
|
? srs.point_style[0]
|
|
440
|
-
: srs.point_style
|
|
441
|
-
|
|
442
|
-
|
|
975
|
+
: srs.point_style
|
|
976
|
+
const first_color_value = srs.color_values?.[0]
|
|
977
|
+
const point_color = first_color_value != null
|
|
443
978
|
? color_scale_fn(first_color_value)
|
|
444
|
-
: first_point_style?.fill ?? series_color
|
|
445
|
-
|
|
979
|
+
: first_point_style?.fill ?? series_color
|
|
980
|
+
|
|
981
|
+
if (is_line) {
|
|
446
982
|
// Line series: show line and/or symbol based on markers
|
|
447
983
|
return {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
984
|
+
series_idx: idx,
|
|
985
|
+
label: srs.label ?? `Series ${idx + 1}`,
|
|
986
|
+
visible: srs.visible ?? true,
|
|
987
|
+
legend_group: srs.legend_group,
|
|
988
|
+
display_style: {
|
|
989
|
+
...(has_line
|
|
990
|
+
? {
|
|
991
|
+
line_color: series_color,
|
|
992
|
+
line_dash: srs.line_style?.line_dash,
|
|
993
|
+
}
|
|
994
|
+
: {}),
|
|
995
|
+
...(has_points
|
|
996
|
+
? {
|
|
997
|
+
symbol_type: first_point_style?.symbol_type ??
|
|
998
|
+
DEFAULTS.scatter.symbol_type,
|
|
999
|
+
symbol_color: point_color,
|
|
1000
|
+
}
|
|
1001
|
+
: {}),
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Bar series: show square symbol
|
|
1006
|
+
return {
|
|
471
1007
|
series_idx: idx,
|
|
472
1008
|
label: srs.label ?? `Series ${idx + 1}`,
|
|
473
1009
|
visible: srs.visible ?? true,
|
|
474
1010
|
legend_group: srs.legend_group,
|
|
475
1011
|
display_style: {
|
|
476
|
-
|
|
477
|
-
|
|
1012
|
+
symbol_type: `Square` as const,
|
|
1013
|
+
symbol_color: series_color,
|
|
478
1014
|
},
|
|
479
|
-
|
|
480
|
-
})
|
|
481
|
-
|
|
1015
|
+
}
|
|
1016
|
+
})
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
function toggle_series_visibility(series_idx: number) {
|
|
482
1020
|
if (series_idx >= 0 && series_idx < series.length) {
|
|
483
|
-
|
|
1021
|
+
series = series.map((srs, idx) =>
|
|
1022
|
+
idx === series_idx ? { ...srs, visible: !(srs.visible ?? true) } : srs
|
|
1023
|
+
)
|
|
484
1024
|
}
|
|
485
|
-
}
|
|
486
|
-
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function toggle_group_visibility(_group_name: string, series_indices: number[]) {
|
|
487
1028
|
// Filter to valid indices upfront (consistent with shared toggle_group_visibility)
|
|
488
|
-
const valid_indices = series_indices.filter((idx) =>
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1029
|
+
const valid_indices = series_indices.filter((idx) =>
|
|
1030
|
+
idx >= 0 && idx < series.length
|
|
1031
|
+
)
|
|
1032
|
+
if (valid_indices.length === 0) return
|
|
1033
|
+
|
|
1034
|
+
const idx_set = new Set(valid_indices)
|
|
492
1035
|
// Check if all series in the group are currently visible
|
|
493
|
-
const all_visible = valid_indices.every((idx) => series[idx].visible ?? true)
|
|
1036
|
+
const all_visible = valid_indices.every((idx) => series[idx].visible ?? true)
|
|
494
1037
|
// Toggle: if all visible, hide all; otherwise show all
|
|
495
|
-
const new_visibility = !all_visible
|
|
496
|
-
series = series.map((srs, idx) =>
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1038
|
+
const new_visibility = !all_visible
|
|
1039
|
+
series = series.map((srs, idx) =>
|
|
1040
|
+
idx_set.has(idx) ? { ...srs, visible: new_visibility } : srs
|
|
1041
|
+
)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Collect bar and line positions for legend placement
|
|
1045
|
+
let bar_points_for_placement = $derived.by(() => {
|
|
1046
|
+
if (!width || !height || !visible_series.length) return []
|
|
1047
|
+
|
|
1048
|
+
return internal_series.flatMap((srs, series_idx) => {
|
|
1049
|
+
if (!(srs?.visible ?? true)) return []
|
|
1050
|
+
const is_line = srs.render_mode === `line`
|
|
1051
|
+
const series_offsets = stacked_offsets[series_idx] ?? []
|
|
1052
|
+
const use_y2 = srs.y_axis === `y2`
|
|
1053
|
+
const y_scale = use_y2 ? scales.y2 : scales.y
|
|
1054
|
+
const use_x2_pl = srs.x_axis === `x2`
|
|
1055
|
+
const x_scale_pl = use_x2_pl ? scales.x2 : scales.x
|
|
1056
|
+
return srs.x
|
|
1057
|
+
.map((x_val, bar_idx) => {
|
|
1058
|
+
const y_val = srs.y[bar_idx]
|
|
1059
|
+
const base = !is_line && mode === `stacked`
|
|
1060
|
+
? (series_offsets[bar_idx] ?? 0)
|
|
1061
|
+
: 0
|
|
1062
|
+
const [bar_x, bar_y] = orientation === `vertical`
|
|
1063
|
+
? [x_scale_pl(x_val), y_scale(base + y_val)]
|
|
1064
|
+
: [x_scale_pl(base + y_val), scales.y(x_val)]
|
|
1065
|
+
return { x: bar_x, y: bar_y }
|
|
519
1066
|
})
|
|
520
|
-
|
|
521
|
-
})
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1067
|
+
.filter(({ x, y }) => isFinite(x) && isFinite(y))
|
|
1068
|
+
})
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
// Legend placement stability state
|
|
1072
|
+
let legend_element = $state<HTMLDivElement | undefined>()
|
|
1073
|
+
const legend_hover = create_hover_lock()
|
|
1074
|
+
const dim_tracker = create_dimension_tracker()
|
|
1075
|
+
let has_initial_legend_placement = $state(false)
|
|
1076
|
+
|
|
1077
|
+
// Clear pending hover lock timeout on unmount
|
|
1078
|
+
$effect(() => () => legend_hover.cleanup())
|
|
1079
|
+
|
|
1080
|
+
// Calculate best legend placement using continuous grid sampling
|
|
1081
|
+
let legend_placement = $derived.by(() => {
|
|
1082
|
+
const should_show = show_legend !== undefined ? show_legend : series.length > 1
|
|
1083
|
+
if (!should_show || !width || !height) return null
|
|
1084
|
+
|
|
535
1085
|
// Use measured size if available, otherwise estimate
|
|
536
1086
|
const legend_size = legend_element
|
|
537
|
-
|
|
538
|
-
|
|
1087
|
+
? { width: legend_element.offsetWidth, height: legend_element.offsetHeight }
|
|
1088
|
+
: { width: 120, height: 60 }
|
|
1089
|
+
|
|
539
1090
|
const result = compute_element_placement({
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1091
|
+
plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
|
|
1092
|
+
element_size: legend_size,
|
|
1093
|
+
axis_clearance: legend?.axis_clearance,
|
|
1094
|
+
exclude_rects: [],
|
|
1095
|
+
points: bar_points_for_placement,
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
return result
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
// Tweened legend coordinates for smooth animation - create once, update target via effect
|
|
1102
|
+
// untrack() explicitly captures initial tween config (intentional - config set once at mount)
|
|
1103
|
+
const tweened_legend_coords = new Tween(
|
|
1104
|
+
{ x: 0, y: 0 },
|
|
1105
|
+
untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
// Update legend position with stability checks
|
|
1109
|
+
$effect(() => {
|
|
1110
|
+
if (!width || !height || !legend_placement) return
|
|
1111
|
+
|
|
555
1112
|
// Track dimensions for resize detection
|
|
556
|
-
const dims_changed = dim_tracker.has_changed(width, height)
|
|
557
|
-
if (dims_changed)
|
|
558
|
-
|
|
559
|
-
const is_responsive = legend?.responsive ?? false
|
|
1113
|
+
const dims_changed = dim_tracker.has_changed(width, height)
|
|
1114
|
+
if (dims_changed) dim_tracker.update(width, height)
|
|
1115
|
+
|
|
1116
|
+
const is_responsive = legend?.responsive ?? false
|
|
560
1117
|
// Only update if: resize occurred, OR (not hover-locked AND (responsive OR not yet initially placed))
|
|
561
1118
|
const should_update = dims_changed || (!legend_hover.is_locked.current &&
|
|
562
|
-
|
|
1119
|
+
(is_responsive || !has_initial_legend_placement))
|
|
1120
|
+
|
|
563
1121
|
if (should_update) {
|
|
564
|
-
|
|
1122
|
+
tweened_legend_coords.set(
|
|
1123
|
+
{ x: legend_placement.x, y: legend_placement.y },
|
|
565
1124
|
// Skip animation on initial placement to avoid jump from (0, 0)
|
|
566
|
-
has_initial_legend_placement ? undefined : { duration: 0 }
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1125
|
+
has_initial_legend_placement ? undefined : { duration: 0 },
|
|
1126
|
+
)
|
|
1127
|
+
// Only lock position after we have actual measured size
|
|
1128
|
+
if (legend_element) {
|
|
1129
|
+
has_initial_legend_placement = true
|
|
1130
|
+
}
|
|
571
1131
|
}
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
let
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// Tooltip state
|
|
1135
|
+
let hover_info = $state<BarHandlerProps<Metadata> | null>(null)
|
|
1136
|
+
let tooltip_el = $state<HTMLDivElement | undefined>()
|
|
1137
|
+
|
|
1138
|
+
function get_bar_data(
|
|
1139
|
+
series_idx: number,
|
|
1140
|
+
bar_idx: number,
|
|
1141
|
+
color: string,
|
|
1142
|
+
): BarHandlerProps<Metadata> {
|
|
1143
|
+
const srs = internal_series[series_idx]
|
|
1144
|
+
const [x, y] = [srs.x[bar_idx], srs.y[bar_idx]]
|
|
1145
|
+
const [orient_x, orient_y] = orientation === `horizontal` ? [y, x] : [x, y]
|
|
580
1146
|
const metadata = Array.isArray(srs.metadata)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const label = srs.labels?.[bar_idx] ?? null
|
|
584
|
-
const active_y_axis = srs.y_axis ?? `y1
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1147
|
+
? srs.metadata[bar_idx]
|
|
1148
|
+
: srs.metadata
|
|
1149
|
+
const label = srs.labels?.[bar_idx] ?? null
|
|
1150
|
+
const active_y_axis = srs.y_axis ?? `y1`
|
|
1151
|
+
const active_x_axis = srs.x_axis ?? `x1`
|
|
1152
|
+
const category_label = category_list[x]
|
|
1153
|
+
const coords = {
|
|
1154
|
+
x,
|
|
1155
|
+
y,
|
|
1156
|
+
orient_x,
|
|
1157
|
+
orient_y,
|
|
1158
|
+
x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
|
|
1159
|
+
x2_axis,
|
|
1160
|
+
y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
|
|
1161
|
+
y2_axis,
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
...coords,
|
|
1165
|
+
metadata,
|
|
1166
|
+
color,
|
|
1167
|
+
label,
|
|
1168
|
+
series_idx,
|
|
1169
|
+
bar_idx,
|
|
1170
|
+
active_y_axis,
|
|
1171
|
+
active_x_axis,
|
|
1172
|
+
category_label,
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Find the point closest to the cursor on a polyline overlay (O(n) scan).
|
|
1177
|
+
function find_closest_point(
|
|
1178
|
+
evt: MouseEvent,
|
|
1179
|
+
points: LineSeriesPoint[],
|
|
1180
|
+
): LineSeriesPoint | null {
|
|
1181
|
+
const svg_el = (evt.target as Element).closest(`svg`)
|
|
1182
|
+
if (!svg_el) return null
|
|
1183
|
+
const rect = svg_el.getBoundingClientRect()
|
|
1184
|
+
const mx = evt.clientX - rect.left
|
|
1185
|
+
const my = evt.clientY - rect.top
|
|
1186
|
+
let best: LineSeriesPoint | null = null
|
|
1187
|
+
let best_dist = Infinity
|
|
1188
|
+
for (const pt of points) {
|
|
1189
|
+
const dist = (pt.x - mx) ** 2 + (pt.y - my) ** 2
|
|
1190
|
+
if (dist < best_dist) {
|
|
1191
|
+
best_dist = dist
|
|
1192
|
+
best = pt
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return best
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const line_point_fill = (pt: LineSeriesPoint, series_color: string): string =>
|
|
1199
|
+
pt.color_value != null
|
|
1200
|
+
? color_scale_fn(pt.color_value)
|
|
1201
|
+
: pt.point_style?.fill ?? series_color
|
|
1202
|
+
|
|
1203
|
+
const handle_bar_hover =
|
|
1204
|
+
(series_idx: number, bar_idx: number, color: string) => (event: MouseEvent) => {
|
|
1205
|
+
hovered = true
|
|
1206
|
+
hover_info = get_bar_data(series_idx, bar_idx, color)
|
|
1207
|
+
change(hover_info)
|
|
1208
|
+
on_bar_hover?.({ ...hover_info, event })
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Stack offsets (only for bar series in stacked mode, grouped by y-axis)
|
|
1212
|
+
let stacked_offsets = $derived.by(() => {
|
|
1213
|
+
if (mode !== `stacked`) return [] as number[][]
|
|
1214
|
+
const max_len = Math.max(
|
|
1215
|
+
0,
|
|
1216
|
+
...internal_series.map((srs) => srs.y.length),
|
|
1217
|
+
)
|
|
1218
|
+
const offsets = internal_series.map(() =>
|
|
1219
|
+
Array.from({ length: max_len }, () => 0)
|
|
1220
|
+
)
|
|
1221
|
+
|
|
600
1222
|
// Separate accumulators for y1 and y2 axes
|
|
601
|
-
const y1_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
602
|
-
const y1_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
603
|
-
const y2_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
604
|
-
const y2_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
})
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1223
|
+
const y1_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
1224
|
+
const y1_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
1225
|
+
const y2_pos_acc = Array.from({ length: max_len }, () => 0)
|
|
1226
|
+
const y2_neg_acc = Array.from({ length: max_len }, () => 0)
|
|
1227
|
+
|
|
1228
|
+
internal_series.forEach((srs, series_idx) => {
|
|
1229
|
+
if (!(srs?.visible ?? true) || srs.render_mode === `line`) return
|
|
1230
|
+
|
|
1231
|
+
const use_y2 = srs.y_axis === `y2`
|
|
1232
|
+
const pos_acc = use_y2 ? y2_pos_acc : y1_pos_acc
|
|
1233
|
+
const neg_acc = use_y2 ? y2_neg_acc : y1_neg_acc
|
|
1234
|
+
|
|
1235
|
+
for (let bar_idx = 0; bar_idx < max_len; bar_idx++) {
|
|
1236
|
+
const y_val = srs.y[bar_idx] ?? 0
|
|
1237
|
+
const acc = y_val >= 0 ? pos_acc : neg_acc
|
|
1238
|
+
offsets[series_idx][bar_idx] = acc[bar_idx]
|
|
1239
|
+
acc[bar_idx] += y_val
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
return offsets
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
// Calculate group positions for grouped mode (side-by-side bars)
|
|
1246
|
+
let group_info = $derived.by(() => {
|
|
1247
|
+
if (mode !== `grouped`) return { bar_series_count: 0, bar_series_indices: [] }
|
|
1248
|
+
const bar_series_indices = internal_series
|
|
1249
|
+
.map((srs, idx) =>
|
|
1250
|
+
(srs?.visible ?? true) && srs.render_mode !== `line` ? idx : -1
|
|
1251
|
+
)
|
|
1252
|
+
.filter((idx) => idx >= 0)
|
|
1253
|
+
return { bar_series_count: bar_series_indices.length, bar_series_indices }
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
// Set theme-aware background when entering fullscreen
|
|
1257
|
+
$effect(() => {
|
|
1258
|
+
set_fullscreen_bg(wrapper, fullscreen, `--barplot-fullscreen-bg`)
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
// State accessors for shared axis change handler
|
|
1262
|
+
const axis_state: AxisChangeState<BarSeries<Metadata>> = {
|
|
1263
|
+
get_axis: (axis) => {
|
|
1264
|
+
if (axis === `x`) return x_axis
|
|
1265
|
+
if (axis === `x2`) return x2_axis
|
|
1266
|
+
if (axis === `y`) return y_axis
|
|
1267
|
+
return y2_axis
|
|
1268
|
+
},
|
|
636
1269
|
set_axis: (axis, config) => {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
else
|
|
643
|
-
y2_axis = { ...y2_axis, ...config };
|
|
1270
|
+
// Spread into existing state to preserve merged type structure
|
|
1271
|
+
if (axis === `x`) x_axis = { ...x_axis, ...config }
|
|
1272
|
+
else if (axis === `x2`) x2_axis = { ...x2_axis, ...config }
|
|
1273
|
+
else if (axis === `y`) y_axis = { ...y_axis, ...config }
|
|
1274
|
+
else y2_axis = { ...y2_axis, ...config }
|
|
644
1275
|
},
|
|
645
1276
|
get_series: () => series,
|
|
646
1277
|
set_series: (new_series) => (series = new_series),
|
|
647
1278
|
get_loading: () => axis_loading,
|
|
648
1279
|
set_loading: (axis) => (axis_loading = axis),
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Create shared handler bound to this component's state
|
|
1283
|
+
// Using $derived so handler updates when callback props change
|
|
1284
|
+
const handle_axis_change = $derived(create_axis_change_handler(
|
|
1285
|
+
axis_state,
|
|
1286
|
+
data_loader,
|
|
1287
|
+
on_axis_change,
|
|
1288
|
+
on_error,
|
|
1289
|
+
))
|
|
1290
|
+
|
|
1291
|
+
let auto_load_attempted = false // prevent infinite retries on failure
|
|
1292
|
+
|
|
1293
|
+
// Auto-load data if series is empty but options exist (runs once)
|
|
1294
|
+
$effect(() => {
|
|
656
1295
|
if (series.length === 0 && data_loader && !auto_load_attempted) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
1296
|
+
// Check x-axis first, then y-axis
|
|
1297
|
+
if (x_axis.options?.length) {
|
|
1298
|
+
auto_load_attempted = true
|
|
1299
|
+
const first_key = x_axis.selected_key ?? x_axis.options[0].key
|
|
1300
|
+
handle_axis_change(`x`, first_key).catch(() => {})
|
|
1301
|
+
} else if (y_axis.options?.length) {
|
|
1302
|
+
auto_load_attempted = true
|
|
1303
|
+
const first_key = y_axis.selected_key ?? y_axis.options[0].key
|
|
1304
|
+
handle_axis_change(`y`, first_key).catch(() => {})
|
|
1305
|
+
}
|
|
668
1306
|
}
|
|
669
|
-
})
|
|
1307
|
+
})
|
|
670
1308
|
</script>
|
|
671
1309
|
|
|
672
1310
|
{#snippet ref_lines_layer(lines: IndexedRefLine[])}
|
|
@@ -674,11 +1312,12 @@ $effect(() => {
|
|
|
674
1312
|
<ReferenceLine
|
|
675
1313
|
ref_line={line}
|
|
676
1314
|
line_idx={line.idx}
|
|
677
|
-
x_min={ranges.current.x[0]}
|
|
678
|
-
x_max={ranges.current.x[1]}
|
|
1315
|
+
x_min={line.x_axis === `x2` ? ranges.current.x2[0] : ranges.current.x[0]}
|
|
1316
|
+
x_max={line.x_axis === `x2` ? ranges.current.x2[1] : ranges.current.x[1]}
|
|
679
1317
|
y_min={line.y_axis === `y2` ? ranges.current.y2[0] : ranges.current.y[0]}
|
|
680
1318
|
y_max={line.y_axis === `y2` ? ranges.current.y2[1] : ranges.current.y[1]}
|
|
681
1319
|
x_scale={scales.x}
|
|
1320
|
+
x2_scale={scales.x2}
|
|
682
1321
|
y_scale={scales.y}
|
|
683
1322
|
y2_scale={scales.y2}
|
|
684
1323
|
{clip_path_id}
|
|
@@ -729,6 +1368,8 @@ $effect(() => {
|
|
|
729
1368
|
<svg
|
|
730
1369
|
bind:this={svg_element}
|
|
731
1370
|
role="application"
|
|
1371
|
+
aria-label={rest[`aria-label`] ??
|
|
1372
|
+
([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Bar chart`)}
|
|
732
1373
|
tabindex="0"
|
|
733
1374
|
onfocusin={() => (is_focused = true)}
|
|
734
1375
|
onfocusout={() => (is_focused = false)}
|
|
@@ -736,10 +1377,12 @@ $effect(() => {
|
|
|
736
1377
|
ondblclick={() => {
|
|
737
1378
|
// Reset zoom to initial ranges (undo any pan/zoom)
|
|
738
1379
|
ranges.current.x = [...ranges.initial.x] as [number, number]
|
|
1380
|
+
ranges.current.x2 = [...ranges.initial.x2] as [number, number]
|
|
739
1381
|
ranges.current.y = [...ranges.initial.y] as [number, number]
|
|
740
1382
|
ranges.current.y2 = [...ranges.initial.y2] as [number, number]
|
|
741
1383
|
// Also reset axis props so future data changes recalculate auto ranges
|
|
742
1384
|
x_axis = { ...x_axis, range: [null, null] }
|
|
1385
|
+
x2_axis = { ...x2_axis, range: [null, null] }
|
|
743
1386
|
y_axis = { ...y_axis, range: [null, null] }
|
|
744
1387
|
y2_axis = { ...y2_axis, range: [null, null] }
|
|
745
1388
|
}}
|
|
@@ -759,26 +1402,19 @@ $effect(() => {
|
|
|
759
1402
|
? `grab`
|
|
760
1403
|
: `crosshair`}
|
|
761
1404
|
>
|
|
762
|
-
|
|
763
|
-
{#if drag_state.start && drag_state.current && isFinite(drag_state.start.x) &&
|
|
764
|
-
isFinite(drag_state.start.y) && isFinite(drag_state.current.x) &&
|
|
765
|
-
isFinite(drag_state.current.y)}
|
|
766
|
-
{@const x = Math.min(drag_state.start.x, drag_state.current.x)}
|
|
767
|
-
{@const y = Math.min(drag_state.start.y, drag_state.current.y)}
|
|
768
|
-
{@const rect_w = Math.abs(drag_state.start.x - drag_state.current.x)}
|
|
769
|
-
{@const rect_h = Math.abs(drag_state.start.y - drag_state.current.y)}
|
|
770
|
-
<rect class="zoom-rect" {x} {y} width={rect_w} height={rect_h} />
|
|
771
|
-
{/if}
|
|
1405
|
+
<ZoomRect start={drag_state.start} current={drag_state.current} />
|
|
772
1406
|
|
|
773
1407
|
<!-- User content (custom overlays, reference lines, etc.) -->
|
|
774
1408
|
{@render user_content?.({
|
|
775
1409
|
height,
|
|
776
1410
|
width,
|
|
777
1411
|
x_scale_fn: scales.x,
|
|
1412
|
+
x2_scale_fn: scales.x2,
|
|
778
1413
|
y_scale_fn: scales.y,
|
|
779
1414
|
y2_scale_fn: scales.y2,
|
|
780
1415
|
pad,
|
|
781
1416
|
x_range: ranges.current.x,
|
|
1417
|
+
x2_range: ranges.current.x2,
|
|
782
1418
|
y_range: ranges.current.y,
|
|
783
1419
|
y2_range: ranges.current.y2,
|
|
784
1420
|
fullscreen,
|
|
@@ -833,37 +1469,106 @@ $effect(() => {
|
|
|
833
1469
|
? `rotate(${rotation}, ${shift_x}, ${text_y})`
|
|
834
1470
|
: undefined}
|
|
835
1471
|
>
|
|
836
|
-
{
|
|
1472
|
+
{
|
|
1473
|
+
get_tick_label(
|
|
1474
|
+
tick as number,
|
|
1475
|
+
cat_axis === `x` ? effective_cat_ticks : x_axis.ticks,
|
|
1476
|
+
) ??
|
|
1477
|
+
format_value(tick, x_axis.format)
|
|
1478
|
+
}
|
|
837
1479
|
</text>
|
|
838
1480
|
</g>
|
|
839
1481
|
{/if}
|
|
840
1482
|
{/each}
|
|
841
1483
|
{#if x_axis.label || x_axis.options?.length}
|
|
842
|
-
{@const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
options={x_axis.options}
|
|
855
|
-
selected_key={x_axis.selected_key}
|
|
856
|
-
loading={axis_loading === `x`}
|
|
857
|
-
axis_type="x"
|
|
858
|
-
color={x_axis.color}
|
|
859
|
-
on_select={(key) => handle_axis_change(`x`, key)}
|
|
860
|
-
class="axis-label x-label"
|
|
861
|
-
/>
|
|
862
|
-
</div>
|
|
863
|
-
</foreignObject>
|
|
1484
|
+
{@const { label_shift, label = ``, options, selected_key, color } = x_axis}
|
|
1485
|
+
<AxisLabel
|
|
1486
|
+
x={pad.l + chart_width / 2 + (label_shift?.x ?? 0)}
|
|
1487
|
+
y={height - (pad.b / 3) + (label_shift?.y ?? 0)}
|
|
1488
|
+
{label}
|
|
1489
|
+
{options}
|
|
1490
|
+
{selected_key}
|
|
1491
|
+
loading={axis_loading === `x`}
|
|
1492
|
+
axis_type="x"
|
|
1493
|
+
{color}
|
|
1494
|
+
on_select={(key) => handle_axis_change(`x`, key)}
|
|
1495
|
+
/>
|
|
864
1496
|
{/if}
|
|
865
1497
|
</g>
|
|
866
1498
|
|
|
1499
|
+
<!-- X2-axis (Top) -->
|
|
1500
|
+
<!-- Note: x2 axis is only supported for vertical orientation -->
|
|
1501
|
+
{#if x2_series.length > 0 && orientation === `vertical`}
|
|
1502
|
+
<g class="x2-axis">
|
|
1503
|
+
<line
|
|
1504
|
+
x1={pad.l}
|
|
1505
|
+
x2={width - pad.r}
|
|
1506
|
+
y1={pad.t}
|
|
1507
|
+
y2={pad.t}
|
|
1508
|
+
stroke={x2_axis.color || `var(--border-color, gray)`}
|
|
1509
|
+
stroke-width="1"
|
|
1510
|
+
/>
|
|
1511
|
+
{#each ticks.x2 as tick (tick)}
|
|
1512
|
+
{@const tick_x = scales.x2(tick as number)}
|
|
1513
|
+
{#if isFinite(tick_x)}
|
|
1514
|
+
{@const rotation = x2_axis.tick?.label?.rotation ?? 0}
|
|
1515
|
+
{@const shift_x = x2_axis.tick?.label?.shift?.x ?? 0}
|
|
1516
|
+
{@const shift_y = x2_axis.tick?.label?.shift?.y ?? 0}
|
|
1517
|
+
{@const inside = x2_axis.tick?.label?.inside ?? false}
|
|
1518
|
+
{@const base_y = inside ? 8 : (rotation !== 0 ? -8 : -18)}
|
|
1519
|
+
{@const text_y = base_y + shift_y}
|
|
1520
|
+
{@const text_anchor = rotation !== 0 ? (inside ? `start` : `end`) : `middle`}
|
|
1521
|
+
{@const dominant_baseline = inside ? `hanging` : `auto`}
|
|
1522
|
+
<g class="tick" transform="translate({tick_x}, {pad.t})">
|
|
1523
|
+
{#if display.x2_grid}
|
|
1524
|
+
<line
|
|
1525
|
+
y1="0"
|
|
1526
|
+
y2={height - pad.b - pad.t}
|
|
1527
|
+
{...DEFAULT_GRID_STYLE}
|
|
1528
|
+
{...(x2_axis.grid_style ?? {})}
|
|
1529
|
+
/>
|
|
1530
|
+
{/if}
|
|
1531
|
+
<line
|
|
1532
|
+
y1={inside ? 5 : 0}
|
|
1533
|
+
y2={inside ? 0 : -5}
|
|
1534
|
+
stroke={x2_axis.color || `var(--border-color, gray)`}
|
|
1535
|
+
stroke-width="1"
|
|
1536
|
+
/>
|
|
1537
|
+
<text
|
|
1538
|
+
x={shift_x}
|
|
1539
|
+
y={text_y}
|
|
1540
|
+
text-anchor={text_anchor}
|
|
1541
|
+
dominant-baseline={dominant_baseline}
|
|
1542
|
+
fill={x2_axis.color || `var(--text-color)`}
|
|
1543
|
+
transform={rotation !== 0
|
|
1544
|
+
? `rotate(${rotation}, ${shift_x}, ${text_y})`
|
|
1545
|
+
: undefined}
|
|
1546
|
+
>
|
|
1547
|
+
{
|
|
1548
|
+
get_tick_label(tick as number, x2_axis.ticks) ??
|
|
1549
|
+
format_value(tick, x2_axis.format)
|
|
1550
|
+
}
|
|
1551
|
+
</text>
|
|
1552
|
+
</g>
|
|
1553
|
+
{/if}
|
|
1554
|
+
{/each}
|
|
1555
|
+
{#if x2_axis.label || x2_axis.options?.length}
|
|
1556
|
+
{@const { label_shift, label = ``, options, selected_key, color } = x2_axis}
|
|
1557
|
+
<AxisLabel
|
|
1558
|
+
x={pad.l + chart_width / 2 + (label_shift?.x ?? 0)}
|
|
1559
|
+
y={Math.max(12, pad.t - (label_shift?.y ?? 40))}
|
|
1560
|
+
{label}
|
|
1561
|
+
{options}
|
|
1562
|
+
{selected_key}
|
|
1563
|
+
loading={axis_loading === `x2`}
|
|
1564
|
+
axis_type="x2"
|
|
1565
|
+
{color}
|
|
1566
|
+
on_select={(key) => handle_axis_change(`x2`, key)}
|
|
1567
|
+
/>
|
|
1568
|
+
{/if}
|
|
1569
|
+
</g>
|
|
1570
|
+
{/if}
|
|
1571
|
+
|
|
867
1572
|
<!-- Y-axis -->
|
|
868
1573
|
<g class="y-axis">
|
|
869
1574
|
<line
|
|
@@ -909,47 +1614,36 @@ $effect(() => {
|
|
|
909
1614
|
? `rotate(${rotation}, ${text_x}, ${shift_y})`
|
|
910
1615
|
: undefined}
|
|
911
1616
|
>
|
|
912
|
-
{
|
|
1617
|
+
{
|
|
1618
|
+
get_tick_label(
|
|
1619
|
+
tick as number,
|
|
1620
|
+
cat_axis === `y` ? effective_cat_ticks : y_axis.ticks,
|
|
1621
|
+
) ??
|
|
1622
|
+
format_value(tick, y_axis.format)
|
|
1623
|
+
}
|
|
913
1624
|
</text>
|
|
914
1625
|
</g>
|
|
915
1626
|
{/if}
|
|
916
1627
|
{/each}
|
|
917
1628
|
{#if y_axis.label || y_axis.options?.length}
|
|
918
|
-
{@const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
height={AXIS_LABEL_CONTAINER.height}
|
|
937
|
-
style="overflow: visible; pointer-events: none"
|
|
938
|
-
transform="rotate(-90, {y_label_x}, {y_label_y})"
|
|
939
|
-
>
|
|
940
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
|
|
941
|
-
<InteractiveAxisLabel
|
|
942
|
-
label={y_axis.label ?? ``}
|
|
943
|
-
options={y_axis.options}
|
|
944
|
-
selected_key={y_axis.selected_key}
|
|
945
|
-
loading={axis_loading === `y`}
|
|
946
|
-
axis_type="y"
|
|
947
|
-
color={y_axis.color}
|
|
948
|
-
on_select={(key) => handle_axis_change(`y`, key)}
|
|
949
|
-
class="axis-label y-label"
|
|
950
|
-
/>
|
|
951
|
-
</div>
|
|
952
|
-
</foreignObject>
|
|
1629
|
+
{@const { label_shift, label = ``, options, selected_key, color, tick } = y_axis}
|
|
1630
|
+
{@const y_inside = tick?.label?.inside ?? false}
|
|
1631
|
+
<AxisLabel
|
|
1632
|
+
x={Math.max(
|
|
1633
|
+
12,
|
|
1634
|
+
pad.l - (y_inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
|
|
1635
|
+
) +
|
|
1636
|
+
(label_shift?.x ?? 0)}
|
|
1637
|
+
y={pad.t + chart_height / 2 + (label_shift?.y ?? 0)}
|
|
1638
|
+
rotate
|
|
1639
|
+
{label}
|
|
1640
|
+
{options}
|
|
1641
|
+
{selected_key}
|
|
1642
|
+
loading={axis_loading === `y`}
|
|
1643
|
+
axis_type="y"
|
|
1644
|
+
{color}
|
|
1645
|
+
on_select={(key) => handle_axis_change(`y`, key)}
|
|
1646
|
+
/>
|
|
953
1647
|
{/if}
|
|
954
1648
|
</g>
|
|
955
1649
|
|
|
@@ -999,51 +1693,33 @@ $effect(() => {
|
|
|
999
1693
|
? `rotate(${rotation}, ${shift_x}, ${shift_y})`
|
|
1000
1694
|
: undefined}
|
|
1001
1695
|
>
|
|
1002
|
-
{
|
|
1696
|
+
{
|
|
1697
|
+
get_tick_label(tick as number, y2_axis.ticks) ??
|
|
1698
|
+
format_value(tick, y2_axis.format)
|
|
1699
|
+
}
|
|
1003
1700
|
</text>
|
|
1004
1701
|
</g>
|
|
1005
1702
|
{/if}
|
|
1006
1703
|
{/each}
|
|
1007
1704
|
{#if y2_axis.label || y2_axis.options?.length}
|
|
1008
|
-
{@const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
<foreignObject
|
|
1027
|
-
x={y2_label_x - AXIS_LABEL_CONTAINER.x_offset}
|
|
1028
|
-
y={y2_label_y - AXIS_LABEL_CONTAINER.y_offset}
|
|
1029
|
-
width={AXIS_LABEL_CONTAINER.width}
|
|
1030
|
-
height={AXIS_LABEL_CONTAINER.height}
|
|
1031
|
-
style="overflow: visible; pointer-events: none"
|
|
1032
|
-
transform="rotate(-90, {y2_label_x}, {y2_label_y})"
|
|
1033
|
-
>
|
|
1034
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
|
|
1035
|
-
<InteractiveAxisLabel
|
|
1036
|
-
label={y2_axis.label ?? ``}
|
|
1037
|
-
options={y2_axis.options}
|
|
1038
|
-
selected_key={y2_axis.selected_key}
|
|
1039
|
-
loading={axis_loading === `y2`}
|
|
1040
|
-
axis_type="y2"
|
|
1041
|
-
color={y2_axis.color}
|
|
1042
|
-
on_select={(key) => handle_axis_change(`y2`, key)}
|
|
1043
|
-
class="axis-label y2-label"
|
|
1044
|
-
/>
|
|
1045
|
-
</div>
|
|
1046
|
-
</foreignObject>
|
|
1705
|
+
{@const { label_shift, label = ``, options, selected_key, color, tick } =
|
|
1706
|
+
y2_axis}
|
|
1707
|
+
{@const inside = tick?.label?.inside ?? false}
|
|
1708
|
+
{@const tick_shift = inside ? 0 : (tick?.label?.shift?.x ?? 0) + 8}
|
|
1709
|
+
{@const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max}
|
|
1710
|
+
<AxisLabel
|
|
1711
|
+
x={width - pad.r + tick_shift + tick_width_contribution +
|
|
1712
|
+
LABEL_GAP_DEFAULT + (label_shift?.x ?? 0)}
|
|
1713
|
+
y={pad.t + chart_height / 2 + (label_shift?.y ?? 0)}
|
|
1714
|
+
rotate
|
|
1715
|
+
{label}
|
|
1716
|
+
{options}
|
|
1717
|
+
{selected_key}
|
|
1718
|
+
loading={axis_loading === `y2`}
|
|
1719
|
+
axis_type="y2"
|
|
1720
|
+
{color}
|
|
1721
|
+
on_select={(key) => handle_axis_change(`y2`, key)}
|
|
1722
|
+
/>
|
|
1047
1723
|
{/if}
|
|
1048
1724
|
</g>
|
|
1049
1725
|
{/if}
|
|
@@ -1057,43 +1733,34 @@ $effect(() => {
|
|
|
1057
1733
|
|
|
1058
1734
|
<!-- Clipped content: zero lines, bars, and lines -->
|
|
1059
1735
|
<g clip-path="url(#{clip_path_id})">
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
{
|
|
1065
|
-
{
|
|
1066
|
-
|
|
1067
|
-
{
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
{
|
|
1073
|
-
{
|
|
1074
|
-
|
|
1075
|
-
{
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
{
|
|
1081
|
-
|
|
1082
|
-
<line
|
|
1083
|
-
class="zero-line"
|
|
1084
|
-
x1={pad.l}
|
|
1085
|
-
x2={width - pad.r}
|
|
1086
|
-
y1={zero_y2}
|
|
1087
|
-
y2={zero_y2}
|
|
1088
|
-
/>
|
|
1089
|
-
{/if}
|
|
1090
|
-
{/if}
|
|
1736
|
+
<ZeroLines
|
|
1737
|
+
{display}
|
|
1738
|
+
x_scale_fn={scales.x}
|
|
1739
|
+
x2_scale_fn={scales.x2}
|
|
1740
|
+
y_scale_fn={scales.y}
|
|
1741
|
+
y2_scale_fn={scales.y2}
|
|
1742
|
+
x_range={ranges.current.x}
|
|
1743
|
+
x2_range={ranges.current.x2}
|
|
1744
|
+
y_range={ranges.current.y}
|
|
1745
|
+
y2_range={ranges.current.y2}
|
|
1746
|
+
x_scale_type={x_axis.scale_type}
|
|
1747
|
+
x2_scale_type={x2_axis.scale_type}
|
|
1748
|
+
y_scale_type={y_axis.scale_type}
|
|
1749
|
+
y2_scale_type={y2_axis.scale_type}
|
|
1750
|
+
x_is_time={x_axis.format?.startsWith(`%`) ?? false}
|
|
1751
|
+
x2_is_time={x2_axis.format?.startsWith(`%`) ?? false}
|
|
1752
|
+
has_x2={x2_series.length > 0}
|
|
1753
|
+
has_y2={y2_series.length > 0}
|
|
1754
|
+
{width}
|
|
1755
|
+
{height}
|
|
1756
|
+
{pad}
|
|
1757
|
+
/>
|
|
1091
1758
|
|
|
1092
1759
|
<!-- Reference lines: below lines -->
|
|
1093
1760
|
{@render ref_lines_layer(ref_lines_by_z.below_lines)}
|
|
1094
1761
|
|
|
1095
1762
|
<!-- Bars and Lines -->
|
|
1096
|
-
{#each
|
|
1763
|
+
{#each internal_series as srs, series_idx (srs?.id ?? series_idx)}
|
|
1097
1764
|
{#if srs?.visible ?? true}
|
|
1098
1765
|
{@const is_line = srs.render_mode === `line`}
|
|
1099
1766
|
<g
|
|
@@ -1107,6 +1774,8 @@ $effect(() => {
|
|
|
1107
1774
|
{@const line_dash = srs.line_style?.line_dash ?? `none`}
|
|
1108
1775
|
{@const use_y2 = srs.y_axis === `y2`}
|
|
1109
1776
|
{@const y_scale = use_y2 ? scales.y2 : scales.y}
|
|
1777
|
+
{@const use_x2 = srs.x_axis === `x2`}
|
|
1778
|
+
{@const x_scale = use_x2 ? scales.x2 : scales.x}
|
|
1110
1779
|
{@const series_markers = srs.markers ?? DEFAULT_MARKERS}
|
|
1111
1780
|
{@const show_line = series_markers === `line` ||
|
|
1112
1781
|
series_markers === `line+points`}
|
|
@@ -1116,8 +1785,8 @@ $effect(() => {
|
|
|
1116
1785
|
const y_val = srs.y[idx]
|
|
1117
1786
|
// Lines don't stack - they show absolute values (useful for totals/trends)
|
|
1118
1787
|
const plot_x = orientation === `vertical`
|
|
1119
|
-
?
|
|
1120
|
-
:
|
|
1788
|
+
? x_scale(x_val)
|
|
1789
|
+
: x_scale(y_val)
|
|
1121
1790
|
const plot_y = orientation === `vertical`
|
|
1122
1791
|
? y_scale(y_val)
|
|
1123
1792
|
: scales.y(x_val)
|
|
@@ -1148,9 +1817,12 @@ $effect(() => {
|
|
|
1148
1817
|
point_idx: idx,
|
|
1149
1818
|
} as LineSeriesPoint
|
|
1150
1819
|
}).filter((pt) => isFinite(pt.x) && isFinite(pt.y))}
|
|
1151
|
-
{
|
|
1820
|
+
{@const polyline_str = show_line && points.length > 1
|
|
1821
|
+
? points.map((pt) => `${pt.x},${pt.y}`).join(` `)
|
|
1822
|
+
: ``}
|
|
1823
|
+
{#if polyline_str}
|
|
1152
1824
|
<polyline
|
|
1153
|
-
points={
|
|
1825
|
+
points={polyline_str}
|
|
1154
1826
|
fill="none"
|
|
1155
1827
|
stroke={color}
|
|
1156
1828
|
stroke-width={stroke_width}
|
|
@@ -1159,41 +1831,58 @@ $effect(() => {
|
|
|
1159
1831
|
stroke-linecap="round"
|
|
1160
1832
|
/>
|
|
1161
1833
|
{/if}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1834
|
+
{#if polyline_str && !show_points && (on_bar_hover || on_bar_click)}
|
|
1835
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1836
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1165
1837
|
<polyline
|
|
1166
|
-
points={
|
|
1838
|
+
points={polyline_str}
|
|
1167
1839
|
fill="none"
|
|
1168
1840
|
stroke="transparent"
|
|
1169
1841
|
stroke-width={Math.max(10, stroke_width * 3)}
|
|
1170
1842
|
stroke-linejoin="round"
|
|
1171
1843
|
stroke-linecap="round"
|
|
1172
1844
|
style:cursor={on_bar_click ? `pointer` : undefined}
|
|
1845
|
+
onmousemove={(evt) => {
|
|
1846
|
+
const pt = find_closest_point(evt, points)
|
|
1847
|
+
if (!pt) return
|
|
1848
|
+
hovered = true
|
|
1849
|
+
const fill = line_point_fill(pt, color)
|
|
1850
|
+
hover_info = get_bar_data(series_idx, pt.idx, fill)
|
|
1851
|
+
change(hover_info)
|
|
1852
|
+
on_bar_hover?.({ ...hover_info!, event: evt })
|
|
1853
|
+
}}
|
|
1854
|
+
onmouseleave={() => {
|
|
1855
|
+
change(null)
|
|
1856
|
+
hover_info = null
|
|
1857
|
+
on_bar_hover?.(null)
|
|
1858
|
+
}}
|
|
1859
|
+
onclick={(evt) => {
|
|
1860
|
+
const pt = find_closest_point(evt, points)
|
|
1861
|
+
if (!pt) return
|
|
1862
|
+
const fill = line_point_fill(pt, color)
|
|
1863
|
+
const bar_data = get_bar_data(series_idx, pt.idx, fill)
|
|
1864
|
+
on_bar_click?.({ ...bar_data, event: evt })
|
|
1865
|
+
}}
|
|
1173
1866
|
/>
|
|
1174
1867
|
{/if}
|
|
1175
1868
|
{#if show_points}
|
|
1176
1869
|
{@const clickable = on_bar_click || on_point_click}
|
|
1177
|
-
{@const get_pt = (evt: Event) =>
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
)}
|
|
1186
|
-
{@const fill = (pt: LineSeriesPoint) =>
|
|
1187
|
-
pt.color_value != null
|
|
1188
|
-
? color_scale_fn(pt.color_value)
|
|
1189
|
-
: pt.point_style?.fill ?? color}
|
|
1870
|
+
{@const get_pt = (evt: Event) => {
|
|
1871
|
+
const attr = evt.target instanceof Element
|
|
1872
|
+
? evt.target.closest(`[data-bar-idx]`)?.getAttribute(
|
|
1873
|
+
`data-bar-idx`,
|
|
1874
|
+
)
|
|
1875
|
+
: null
|
|
1876
|
+
return points.find((pt) => pt.idx === parseInt(attr ?? ``, 10))
|
|
1877
|
+
}}
|
|
1190
1878
|
{@const set_hover = (
|
|
1191
1879
|
pt: LineSeriesPoint | null,
|
|
1192
1880
|
evt: MouseEvent | FocusEvent,
|
|
1193
1881
|
) => {
|
|
1194
1882
|
if (pt) {
|
|
1195
1883
|
hovered = true
|
|
1196
|
-
|
|
1884
|
+
const fill = line_point_fill(pt, color)
|
|
1885
|
+
hover_info = get_bar_data(series_idx, pt.idx, fill)
|
|
1197
1886
|
change(hover_info)
|
|
1198
1887
|
} else {
|
|
1199
1888
|
change(null)
|
|
@@ -1208,13 +1897,15 @@ $effect(() => {
|
|
|
1208
1897
|
pt: LineSeriesPoint,
|
|
1209
1898
|
evt: MouseEvent | KeyboardEvent,
|
|
1210
1899
|
) => {
|
|
1211
|
-
const
|
|
1900
|
+
const fill = line_point_fill(pt, color)
|
|
1901
|
+
const bar_data = get_bar_data(series_idx, pt.idx, fill)
|
|
1212
1902
|
on_bar_click?.({ ...bar_data, event: evt })
|
|
1213
1903
|
on_point_click?.({ ...bar_data, event: evt, point: pt })
|
|
1214
1904
|
}}
|
|
1215
1905
|
{@const leaving = (evt: MouseEvent | FocusEvent) =>
|
|
1216
|
-
(evt.relatedTarget
|
|
1217
|
-
evt.
|
|
1906
|
+
(evt.relatedTarget instanceof Element
|
|
1907
|
+
? evt.relatedTarget.closest(`.line-points`)
|
|
1908
|
+
: null) !== evt.currentTarget}
|
|
1218
1909
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_mouse_events_have_key_events -->
|
|
1219
1910
|
<g
|
|
1220
1911
|
class="line-points"
|
|
@@ -1247,7 +1938,7 @@ $effect(() => {
|
|
|
1247
1938
|
>
|
|
1248
1939
|
{#each points as pt (pt.idx)}
|
|
1249
1940
|
{@const sty = pt.point_style}
|
|
1250
|
-
{@const fl =
|
|
1941
|
+
{@const fl = line_point_fill(pt, color)}
|
|
1251
1942
|
{@const rad = pt.size_value != null
|
|
1252
1943
|
? size_scale_fn(pt.size_value)
|
|
1253
1944
|
: sty?.radius ?? 4}
|
|
@@ -1306,9 +1997,11 @@ $effect(() => {
|
|
|
1306
1997
|
{@const val = y_val}
|
|
1307
1998
|
{@const use_y2 = srs.y_axis === `y2`}
|
|
1308
1999
|
{@const y_scale = use_y2 ? scales.y2 : scales.y}
|
|
2000
|
+
{@const use_x2_bar = srs.x_axis === `x2`}
|
|
2001
|
+
{@const x_scale_bar = use_x2_bar ? scales.x2 : scales.x}
|
|
1309
2002
|
{@const [cat_scale, val_scale] = is_vertical
|
|
1310
|
-
? [
|
|
1311
|
-
: [scales.y,
|
|
2003
|
+
? [x_scale_bar, y_scale]
|
|
2004
|
+
: [scales.y, x_scale_bar]}
|
|
1312
2005
|
{@const c0 = cat_scale(cat_val + group_offset - half)}
|
|
1313
2006
|
{@const c1 = cat_scale(cat_val + group_offset + half)}
|
|
1314
2007
|
{@const v0 = val_scale(base)}
|
|
@@ -1405,7 +2098,9 @@ $effect(() => {
|
|
|
1405
2098
|
{/if}
|
|
1406
2099
|
|
|
1407
2100
|
{#if hover_info && hovered}
|
|
1408
|
-
{@const cx = scales.x(
|
|
2101
|
+
{@const cx = (hover_info.active_x_axis === `x2` ? scales.x2 : scales.x)(
|
|
2102
|
+
hover_info.orient_x,
|
|
2103
|
+
)}
|
|
1409
2104
|
{@const cy = (hover_info.active_y_axis === `y2` ? scales.y2 : scales.y)(
|
|
1410
2105
|
hover_info.orient_y,
|
|
1411
2106
|
)}
|
|
@@ -1418,7 +2113,6 @@ $effect(() => {
|
|
|
1418
2113
|
height,
|
|
1419
2114
|
{ offset_x: 10, offset_y: 5 },
|
|
1420
2115
|
)}
|
|
1421
|
-
{@const active_y_config = hover_info.active_y_axis === `y2` ? y2_axis : y_axis}
|
|
1422
2116
|
<PlotTooltip
|
|
1423
2117
|
x={tooltip_pos.x}
|
|
1424
2118
|
y={tooltip_pos.y}
|
|
@@ -1429,14 +2123,20 @@ $effect(() => {
|
|
|
1429
2123
|
{#if tooltip}
|
|
1430
2124
|
{@render tooltip({ ...hover_info, fullscreen })}
|
|
1431
2125
|
{:else}
|
|
2126
|
+
{@const series_label = series[hover_info.series_idx]?.label}
|
|
2127
|
+
{#if series.length > 1 && series_label}
|
|
2128
|
+
<div><strong>{series_label}</strong></div>
|
|
2129
|
+
{/if}
|
|
1432
2130
|
<div>
|
|
1433
|
-
{@html x_axis.label || `x`}: {
|
|
1434
|
-
|
|
2131
|
+
{@html sanitize_html(hover_info.x_axis.label || `x`)}: {
|
|
2132
|
+
(cat_axis === `x` ? hover_info.category_label : undefined) ??
|
|
2133
|
+
format_value(hover_info.orient_x, hover_info.x_axis.format || `.3~s`)
|
|
1435
2134
|
}
|
|
1436
2135
|
</div>
|
|
1437
2136
|
<div>
|
|
1438
|
-
{@html
|
|
1439
|
-
|
|
2137
|
+
{@html sanitize_html(hover_info.y_axis.label || `y`)}: {
|
|
2138
|
+
(cat_axis === `y` ? hover_info.category_label : undefined) ??
|
|
2139
|
+
format_value(hover_info.orient_y, hover_info.y_axis.format || `.3~s`)
|
|
1440
2140
|
}
|
|
1441
2141
|
</div>
|
|
1442
2142
|
{/if}
|
|
@@ -1447,7 +2147,7 @@ $effect(() => {
|
|
|
1447
2147
|
<BarPlotControls
|
|
1448
2148
|
toggle_props={{
|
|
1449
2149
|
...controls_toggle_props,
|
|
1450
|
-
style: `--ctrl-btn-right: var(--fullscreen-btn-offset,
|
|
2150
|
+
style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); ${
|
|
1451
2151
|
controls_toggle_props?.style ?? ``
|
|
1452
2152
|
}`,
|
|
1453
2153
|
}}
|
|
@@ -1457,12 +2157,16 @@ $effect(() => {
|
|
|
1457
2157
|
bind:orientation
|
|
1458
2158
|
bind:mode
|
|
1459
2159
|
bind:x_axis
|
|
2160
|
+
bind:x2_axis
|
|
1460
2161
|
bind:y_axis
|
|
1461
2162
|
bind:y2_axis
|
|
1462
2163
|
bind:display
|
|
1463
2164
|
auto_x_range={auto_ranges.x as Vec2}
|
|
2165
|
+
auto_x2_range={auto_ranges.x2 as Vec2}
|
|
1464
2166
|
auto_y_range={auto_ranges.y as Vec2}
|
|
1465
2167
|
auto_y2_range={auto_ranges.y2 as Vec2}
|
|
2168
|
+
has_x2_points={x2_series.length > 0}
|
|
2169
|
+
has_y2_points={y2_series.length > 0}
|
|
1466
2170
|
children={controls_extra}
|
|
1467
2171
|
/>
|
|
1468
2172
|
{/if}
|
|
@@ -1542,22 +2246,11 @@ $effect(() => {
|
|
|
1542
2246
|
border: var(--barplot-dragover-border, var(--dragover-border));
|
|
1543
2247
|
background-color: var(--barplot-dragover-bg, var(--dragover-bg));
|
|
1544
2248
|
}
|
|
1545
|
-
g:is(.x-axis, .y-axis, .y2-axis) .tick text {
|
|
2249
|
+
g:is(.x-axis, .x2-axis, .y-axis, .y2-axis) .tick text {
|
|
1546
2250
|
font-size: var(--tick-font-size, 0.8em);
|
|
1547
2251
|
}
|
|
1548
|
-
.zoom-rect {
|
|
1549
|
-
fill: var(--barplot-zoom-rect-fill, rgba(100, 100, 255, 0.2));
|
|
1550
|
-
stroke: var(--barplot-zoom-rect-stroke, rgba(100, 100, 255, 0.8));
|
|
1551
|
-
stroke-width: var(--barplot-zoom-rect-stroke-width, 1);
|
|
1552
|
-
pointer-events: none;
|
|
1553
|
-
}
|
|
1554
2252
|
.bar-label {
|
|
1555
2253
|
fill: var(--text-color);
|
|
1556
2254
|
font-size: 11px;
|
|
1557
2255
|
}
|
|
1558
|
-
.zero-line {
|
|
1559
|
-
stroke: var(--barplot-zero-line-color, light-dark(black, white));
|
|
1560
|
-
stroke-width: var(--barplot-zero-line-width, 1);
|
|
1561
|
-
opacity: var(--barplot-zero-line-opacity, 0.3);
|
|
1562
|
-
}
|
|
1563
2256
|
</style>
|