matterviz 0.3.7 → 0.4.0
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/Icon.svelte +7 -4
- package/dist/MillerIndexInput.svelte +1 -1
- package/dist/api/optimade.js +32 -26
- package/dist/app.css +0 -3
- package/dist/brillouin/BrillouinZone.svelte +8 -3
- package/dist/brillouin/BrillouinZone.svelte.d.ts +2 -1
- package/dist/brillouin/BrillouinZoneScene.svelte +52 -6
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -0
- package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
- package/dist/brillouin/compute.js +10 -14
- package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +12 -15
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +8 -10
- package/dist/chempot-diagram/async-compute.svelte.js +3 -1
- package/dist/chempot-diagram/chempot-worker.js +2 -1
- package/dist/chempot-diagram/compute.d.ts +1 -1
- package/dist/chempot-diagram/compute.js +17 -19
- package/dist/colors/index.js +6 -5
- package/dist/composition/FormulaFilter.svelte +12 -6
- package/dist/composition/PieChart.svelte +6 -5
- package/dist/composition/chem-sys.d.ts +8 -0
- package/dist/composition/chem-sys.js +85 -0
- package/dist/composition/format.js +4 -2
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/composition/parse.js +25 -13
- package/dist/convex-hull/ConvexHull2D.svelte +12 -10
- package/dist/convex-hull/ConvexHull3D.svelte +5 -5
- package/dist/convex-hull/ConvexHull4D.svelte +5 -9
- package/dist/convex-hull/ConvexHullStats.svelte +12 -12
- package/dist/convex-hull/GasPressureControls.svelte +4 -4
- package/dist/convex-hull/TemperatureSlider.svelte +2 -2
- package/dist/convex-hull/demo-temperature.d.ts +1 -1
- package/dist/convex-hull/demo-temperature.js +20 -22
- package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
- package/dist/convex-hull/gas-thermodynamics.js +22 -30
- package/dist/convex-hull/helpers.d.ts +3 -0
- package/dist/convex-hull/helpers.js +17 -9
- package/dist/convex-hull/index.d.ts +1 -1
- package/dist/convex-hull/thermodynamics.js +83 -78
- package/dist/convex-hull/types.d.ts +1 -1
- package/dist/coordination/CoordinationBarPlot.svelte +23 -23
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
- package/dist/element/ElementTile.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSlice.svelte +13 -5
- package/dist/fermi-surface/FermiSurface.svelte +11 -5
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +3 -0
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
- package/dist/fermi-surface/compute.js +59 -59
- package/dist/fermi-surface/export.js +3 -2
- package/dist/fermi-surface/parse.js +7 -4
- package/dist/fermi-surface/types.d.ts +1 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +23 -21
- package/dist/heatmap-matrix/index.js +1 -1
- package/dist/io/decompress.js +4 -2
- package/dist/io/export.d.ts +4 -4
- package/dist/io/export.js +47 -25
- package/dist/io/fetch.js +5 -1
- package/dist/io/file-drop.d.ts +1 -1
- package/dist/io/file-drop.js +35 -36
- package/dist/io/url-drop.js +64 -33
- package/dist/isosurface/parse.js +6 -7
- package/dist/isosurface/slice.js +5 -4
- package/dist/isosurface/types.js +1 -1
- package/dist/keyboard.d.ts +3 -0
- package/dist/keyboard.js +23 -0
- package/dist/labels.d.ts +1 -1
- package/dist/labels.js +8 -7
- package/dist/layout/PropertyFilter.svelte +3 -2
- package/dist/layout/SettingsSection.svelte +1 -1
- package/dist/layout/json-tree/JsonNode.svelte +1 -1
- package/dist/layout/json-tree/JsonTree.svelte +2 -2
- package/dist/layout/json-tree/utils.js +5 -4
- package/dist/marching-cubes.js +8 -13
- package/dist/math.d.ts +5 -1
- package/dist/math.js +24 -9
- package/dist/overlays/DraggablePane.svelte +4 -4
- package/dist/periodic-table/PeriodicTable.svelte +20 -9
- package/dist/periodic-table/PropertySelect.svelte +1 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
- package/dist/phase-diagram/build-diagram.js +2 -2
- package/dist/phase-diagram/parse.js +6 -5
- package/dist/phase-diagram/types.d.ts +1 -1
- package/dist/phase-diagram/utils.d.ts +3 -3
- package/dist/phase-diagram/utils.js +8 -12
- package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +229 -587
- package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +5 -5
- package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
- package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
- package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +6 -6
- package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
- package/dist/plot/bar/data.d.ts +40 -0
- package/dist/plot/bar/data.js +154 -0
- package/dist/plot/bar/geometry.d.ts +39 -0
- package/dist/plot/bar/geometry.js +60 -0
- package/dist/plot/bar/index.d.ts +3 -0
- package/dist/plot/bar/index.js +3 -0
- package/dist/plot/box/BoxPlot.svelte +1462 -0
- package/dist/plot/box/BoxPlot.svelte.d.ts +94 -0
- package/dist/plot/box/BoxPlotControls.svelte +109 -0
- package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
- package/dist/plot/box/Violin.svelte +14 -0
- package/dist/plot/box/Violin.svelte.d.ts +70 -0
- package/dist/plot/box/box-plot.d.ts +55 -0
- package/dist/plot/box/box-plot.js +126 -0
- package/dist/plot/box/index.d.ts +5 -0
- package/dist/plot/box/index.js +5 -0
- package/dist/plot/box/kde.d.ts +16 -0
- package/dist/plot/box/kde.js +160 -0
- package/dist/plot/box/quantile.d.ts +3 -0
- package/dist/plot/box/quantile.js +53 -0
- package/dist/plot/{auto-place.js → core/auto-place.js} +2 -2
- package/dist/plot/core/axis-utils.d.ts +46 -0
- package/dist/plot/core/axis-utils.js +110 -0
- package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
- package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
- package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +36 -33
- package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +2 -2
- package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
- package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
- package/dist/plot/core/components/ControlPane.svelte +46 -0
- package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
- package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
- package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
- package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
- package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
- package/dist/plot/{Line.svelte → core/components/Line.svelte} +30 -13
- package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +7 -5
- package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +3 -2
- package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
- package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
- package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
- package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
- package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
- package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
- package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
- package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
- package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
- package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +4 -4
- package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +2 -2
- package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +7 -7
- package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +2 -2
- package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
- package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
- package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
- package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
- package/dist/plot/core/components/index.d.ts +17 -0
- package/dist/plot/core/components/index.js +17 -0
- package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
- package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +3 -5
- package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
- package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
- package/dist/plot/core/fill-utils.d.ts +33 -0
- package/dist/plot/core/fill-utils.js +388 -0
- package/dist/plot/{hover-lock.svelte.js → core/hover-lock.svelte.js} +5 -6
- package/dist/plot/core/index.d.ts +10 -0
- package/dist/plot/core/index.js +11 -0
- package/dist/plot/core/interactions.d.ts +35 -0
- package/dist/plot/core/interactions.js +195 -0
- package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
- package/dist/plot/{layout.js → core/layout.js} +16 -8
- package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +1 -1
- package/dist/plot/{reference-line.js → core/reference-line.js} +23 -36
- package/dist/plot/{scales.d.ts → core/scales.d.ts} +2 -2
- package/dist/plot/{scales.js → core/scales.js} +84 -85
- package/dist/plot/core/svg.d.ts +2 -0
- package/dist/plot/core/svg.js +41 -0
- package/dist/plot/{types.d.ts → core/types.d.ts} +19 -79
- package/dist/plot/{types.js → core/types.js} +1 -1
- package/dist/plot/{utils → core/utils}/label-placement.d.ts +2 -2
- package/dist/plot/core/utils/series-visibility.d.ts +26 -0
- package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
- package/dist/plot/core/utils.d.ts +11 -0
- package/dist/plot/core/utils.js +27 -0
- package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +154 -294
- package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
- package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
- package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
- package/dist/plot/histogram/index.d.ts +2 -0
- package/dist/plot/histogram/index.js +2 -0
- package/dist/plot/index.d.ts +8 -41
- package/dist/plot/index.js +10 -39
- package/dist/plot/sankey/Sankey.svelte +700 -0
- package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
- package/dist/plot/sankey/SankeyControls.svelte +98 -0
- package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
- package/dist/plot/sankey/index.d.ts +4 -0
- package/dist/plot/sankey/index.js +3 -0
- package/dist/plot/sankey/sankey-types.d.ts +42 -0
- package/dist/plot/sankey/sankey-types.js +4 -0
- package/dist/plot/sankey/sankey.d.ts +52 -0
- package/dist/plot/sankey/sankey.js +187 -0
- package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +61 -59
- package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +4 -4
- package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
- package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
- package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +221 -642
- package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +7 -7
- package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
- package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +1 -1
- package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
- package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
- package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
- package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
- package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +3 -3
- package/dist/plot/scatter/index.d.ts +7 -0
- package/dist/plot/scatter/index.js +5 -0
- package/dist/plot/scatter/scatter-data.d.ts +19 -0
- package/dist/plot/scatter/scatter-data.js +212 -0
- package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +12 -10
- package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +7 -7
- package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +5 -4
- package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +2 -2
- package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +11 -11
- package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +3 -3
- package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +1 -1
- package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +1 -1
- package/dist/plot/scatter-3d/index.d.ts +4 -0
- package/dist/plot/scatter-3d/index.js +4 -0
- package/dist/plot/sunburst/Sunburst.svelte +1045 -0
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +96 -0
- package/dist/plot/sunburst/SunburstControls.svelte +200 -0
- package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
- package/dist/plot/sunburst/index.d.ts +4 -0
- package/dist/plot/sunburst/index.js +4 -0
- package/dist/plot/sunburst/render.d.ts +34 -0
- package/dist/plot/sunburst/render.js +122 -0
- package/dist/plot/sunburst/sunburst.d.ts +62 -0
- package/dist/plot/sunburst/sunburst.js +266 -0
- package/dist/rdf/RdfPlot.svelte +2 -1
- package/dist/rdf/calc-rdf.js +11 -24
- package/dist/sanitize.js +1 -1
- package/dist/settings.d.ts +65 -1
- package/dist/settings.js +262 -0
- package/dist/spectral/Bands.svelte +39 -29
- package/dist/spectral/Bands.svelte.d.ts +3 -4
- package/dist/spectral/BandsAndDos.svelte +1 -1
- package/dist/spectral/BrillouinBandsDos.svelte +39 -27
- package/dist/spectral/Dos.svelte +10 -19
- package/dist/spectral/Dos.svelte.d.ts +2 -2
- package/dist/spectral/helpers.d.ts +3 -1
- package/dist/spectral/helpers.js +95 -29
- package/dist/structure/AtomLegend.svelte +8 -9
- package/dist/structure/CellSelect.svelte +1 -2
- package/dist/structure/Cylinder.svelte +12 -8
- package/dist/structure/Cylinder.svelte.d.ts +4 -1
- package/dist/structure/Structure.svelte +78 -72
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +5 -6
- package/dist/structure/StructureScene.svelte +11 -10
- package/dist/structure/atom-properties.js +6 -6
- package/dist/structure/bond-order-perception.js +1 -1
- package/dist/structure/bonding.d.ts +1 -0
- package/dist/structure/bonding.js +43 -15
- package/dist/structure/export.js +27 -23
- package/dist/structure/index.d.ts +2 -4
- package/dist/structure/index.js +1 -3
- package/dist/structure/label-placement.js +4 -4
- package/dist/structure/measure.d.ts +3 -2
- package/dist/structure/measure.js +6 -5
- package/dist/structure/parse.js +121 -103
- package/dist/structure/pbc.js +4 -0
- package/dist/symmetry/SymmetryStats.svelte +2 -2
- package/dist/symmetry/index.d.ts +1 -1
- package/dist/symmetry/index.js +22 -24
- package/dist/symmetry/spacegroups.d.ts +7 -0
- package/dist/symmetry/spacegroups.js +48 -13
- package/dist/table/HeatmapTable.svelte +63 -11
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/index.d.ts +1 -3
- package/dist/table/index.js +1 -1
- package/dist/theme/index.js +8 -8
- package/dist/tooltip/KCoords.svelte +45 -0
- package/dist/tooltip/KCoords.svelte.d.ts +8 -0
- package/dist/tooltip/index.d.ts +1 -0
- package/dist/tooltip/index.js +1 -0
- package/dist/trajectory/Trajectory.svelte +66 -40
- package/dist/trajectory/Trajectory.svelte.d.ts +2 -1
- package/dist/trajectory/TrajectoryExportPane.svelte +2 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +2 -1
- package/dist/trajectory/format-detect.d.ts +1 -0
- package/dist/trajectory/format-detect.js +25 -11
- package/dist/trajectory/frame-reader.js +17 -50
- package/dist/trajectory/helpers.js +1 -1
- package/dist/trajectory/index.js +1 -1
- package/dist/trajectory/parse/hdf5.js +1 -1
- package/dist/trajectory/parse/index.js +14 -6
- package/dist/trajectory/parse/vasp.js +36 -17
- package/dist/trajectory/parse/xyz.d.ts +24 -0
- package/dist/trajectory/parse/xyz.js +102 -89
- package/dist/trajectory/plotting.d.ts +1 -1
- package/dist/trajectory/plotting.js +15 -15
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +6 -4
- package/dist/xrd/XrdPlot.svelte +2 -1
- package/dist/xrd/calc-xrd.js +15 -12
- package/dist/xrd/parse.js +2 -2
- package/package.json +22 -18
- package/dist/plot/PlotControls.svelte.d.ts +0 -4
- package/dist/plot/axis-utils.d.ts +0 -19
- package/dist/plot/axis-utils.js +0 -78
- package/dist/plot/defaults.d.ts +0 -19
- package/dist/plot/defaults.js +0 -9
- package/dist/plot/fill-utils.d.ts +0 -46
- package/dist/plot/fill-utils.js +0 -322
- package/dist/plot/interactions.d.ts +0 -12
- package/dist/plot/interactions.js +0 -101
- package/dist/plot/svg.d.ts +0 -1
- package/dist/plot/svg.js +0 -11
- package/dist/plot/utils/series-visibility.d.ts +0 -15
- package/dist/plot/utils.d.ts +0 -1
- package/dist/plot/utils.js +0 -14
- /package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +0 -0
- /package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +0 -0
- /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
- /package/dist/plot/{hover-lock.svelte.d.ts → core/hover-lock.svelte.d.ts} +0 -0
- /package/dist/plot/{utils → core/utils}/label-placement.js +0 -0
- /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
<script
|
|
2
|
+
lang="ts"
|
|
3
|
+
generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
|
|
4
|
+
>
|
|
5
|
+
import { format_value } from '../../labels'
|
|
6
|
+
import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
|
|
7
|
+
import type { Vec2 } from '../../math'
|
|
8
|
+
import type {
|
|
9
|
+
BandwidthOption,
|
|
10
|
+
BasePlotProps,
|
|
11
|
+
BoxHandlerProps,
|
|
12
|
+
BoxPlotSeries,
|
|
13
|
+
LegendConfig,
|
|
14
|
+
LegendItem,
|
|
15
|
+
Orientation,
|
|
16
|
+
PanConfig,
|
|
17
|
+
PlotConfig,
|
|
18
|
+
RefLine,
|
|
19
|
+
RefLineEvent,
|
|
20
|
+
ScaleType,
|
|
21
|
+
UserContentProps,
|
|
22
|
+
ViolinKind,
|
|
23
|
+
ViolinSide,
|
|
24
|
+
WhiskerMode,
|
|
25
|
+
} from '..'
|
|
26
|
+
import {
|
|
27
|
+
BoxPlotControls,
|
|
28
|
+
compute_element_placement,
|
|
29
|
+
PlotAxis,
|
|
30
|
+
PlotLegend,
|
|
31
|
+
ReferenceLine,
|
|
32
|
+
} from '..'
|
|
33
|
+
import {
|
|
34
|
+
build_obstacles_norm,
|
|
35
|
+
clip_bar,
|
|
36
|
+
has_explicit_position,
|
|
37
|
+
measured_footprint,
|
|
38
|
+
place_decorations,
|
|
39
|
+
} from '../core/auto-place'
|
|
40
|
+
import { compute_box_stats } from './box-plot'
|
|
41
|
+
import { gaussian_kde, type KdeResult } from './kde'
|
|
42
|
+
import { create_dimension_tracker, create_hover_lock } from '../core/hover-lock.svelte'
|
|
43
|
+
import { create_legend_visibility } from '../core/utils/series-visibility'
|
|
44
|
+
import {
|
|
45
|
+
axis_ranges_equal,
|
|
46
|
+
get_relative_coords,
|
|
47
|
+
MIN_TOUCH_DISTANCE_PIXELS,
|
|
48
|
+
pan_range_by_pixels,
|
|
49
|
+
PINCH_ZOOM_THRESHOLD,
|
|
50
|
+
remove_drag_listeners,
|
|
51
|
+
resolve_axis_ranges,
|
|
52
|
+
sorted_range,
|
|
53
|
+
zoom_range_by_factor,
|
|
54
|
+
} from '../core/interactions'
|
|
55
|
+
import {
|
|
56
|
+
calc_auto_padding,
|
|
57
|
+
filter_padding,
|
|
58
|
+
LABEL_GAP_DEFAULT,
|
|
59
|
+
y2_axis_label_x,
|
|
60
|
+
measure_max_tick_width,
|
|
61
|
+
} from '../core/layout'
|
|
62
|
+
import { LOG_EPS } from '../../math'
|
|
63
|
+
import type { IndexedRefLine } from '../core/reference-line'
|
|
64
|
+
import { group_ref_lines_by_z, index_ref_lines } from '../core/reference-line'
|
|
65
|
+
import {
|
|
66
|
+
create_scale,
|
|
67
|
+
generate_ticks,
|
|
68
|
+
get_nice_data_range,
|
|
69
|
+
get_tick_label,
|
|
70
|
+
} from '../core/scales'
|
|
71
|
+
import type { InitialRanges } from '../core/types'
|
|
72
|
+
import { DEFAULT_SERIES_COLORS } from '../core/types'
|
|
73
|
+
import { unique_id } from '../core/utils'
|
|
74
|
+
import { DEFAULTS } from '../../settings'
|
|
75
|
+
import type { Snippet } from 'svelte'
|
|
76
|
+
import { onDestroy, untrack } from 'svelte'
|
|
77
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
78
|
+
import { Tween, type TweenOptions } from 'svelte/motion'
|
|
79
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
80
|
+
import PlotTooltip from '../core/components/PlotTooltip.svelte'
|
|
81
|
+
import { violin_path } from '../core/svg'
|
|
82
|
+
import ZeroLines from '../core/components/ZeroLines.svelte'
|
|
83
|
+
import ZoomRect from '../core/components/ZoomRect.svelte'
|
|
84
|
+
|
|
85
|
+
// Box style props
|
|
86
|
+
interface BoxStyle {
|
|
87
|
+
color?: string
|
|
88
|
+
opacity?: number
|
|
89
|
+
stroke_width?: number
|
|
90
|
+
stroke_color?: string
|
|
91
|
+
border_radius?: number
|
|
92
|
+
}
|
|
93
|
+
interface WhiskerStyle {
|
|
94
|
+
width?: number
|
|
95
|
+
color?: string
|
|
96
|
+
cap_fraction?: number
|
|
97
|
+
}
|
|
98
|
+
interface BoxLineStyle {
|
|
99
|
+
width?: number
|
|
100
|
+
color?: string
|
|
101
|
+
}
|
|
102
|
+
interface OutlierStyle {
|
|
103
|
+
radius?: number
|
|
104
|
+
opacity?: number
|
|
105
|
+
stroke_width?: number
|
|
106
|
+
}
|
|
107
|
+
interface ViolinStyle {
|
|
108
|
+
opacity?: number
|
|
109
|
+
stroke_width?: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Hover state carries the box payload plus the pixel anchor for the tooltip
|
|
113
|
+
type BoxHover = BoxHandlerProps<Metadata> & { cx: number; cy: number }
|
|
114
|
+
|
|
115
|
+
let {
|
|
116
|
+
series = $bindable([]),
|
|
117
|
+
orientation = $bindable(`vertical`),
|
|
118
|
+
x_axis = $bindable({}),
|
|
119
|
+
x2_axis: x2_axis_prop = $bindable({}),
|
|
120
|
+
y_axis = $bindable({}),
|
|
121
|
+
y2_axis: y2_axis_prop = $bindable({}),
|
|
122
|
+
display = $bindable(DEFAULTS.box.display),
|
|
123
|
+
range_padding = 0.05,
|
|
124
|
+
padding = { t: 20, b: 60, l: 60, r: 20 },
|
|
125
|
+
legend = {},
|
|
126
|
+
show_legend,
|
|
127
|
+
box = {},
|
|
128
|
+
whisker = {},
|
|
129
|
+
median_style = {},
|
|
130
|
+
outlier_style = {},
|
|
131
|
+
whisker_mode = $bindable(DEFAULTS.box.whisker_mode),
|
|
132
|
+
whisker_range = 1.5,
|
|
133
|
+
whisker_percentiles = [5, 95],
|
|
134
|
+
show_outliers = $bindable(DEFAULTS.box.show_outliers),
|
|
135
|
+
show_mean = $bindable(DEFAULTS.box.show_mean),
|
|
136
|
+
show_value_labels = false,
|
|
137
|
+
value_label_stat = `median`,
|
|
138
|
+
value_label_format = `.3~s`,
|
|
139
|
+
kind = $bindable(DEFAULTS.box.kind),
|
|
140
|
+
side = $bindable(DEFAULTS.box.side),
|
|
141
|
+
bandwidth = DEFAULTS.box.bandwidth,
|
|
142
|
+
violin_width = DEFAULTS.box.violin_width,
|
|
143
|
+
violin_style = {},
|
|
144
|
+
kde_points = 100,
|
|
145
|
+
kde_cut = 2,
|
|
146
|
+
kde_max_samples = 5000,
|
|
147
|
+
kde_clip = undefined,
|
|
148
|
+
tooltip,
|
|
149
|
+
user_content,
|
|
150
|
+
hovered = $bindable(false),
|
|
151
|
+
change = () => {},
|
|
152
|
+
on_box_click,
|
|
153
|
+
on_box_hover,
|
|
154
|
+
ref_lines = $bindable([]),
|
|
155
|
+
on_ref_line_click,
|
|
156
|
+
on_ref_line_hover,
|
|
157
|
+
show_controls = $bindable(true),
|
|
158
|
+
controls_open = $bindable(false),
|
|
159
|
+
controls_toggle_props,
|
|
160
|
+
controls_pane_props,
|
|
161
|
+
fullscreen = $bindable(false),
|
|
162
|
+
fullscreen_toggle = true,
|
|
163
|
+
children,
|
|
164
|
+
header_controls,
|
|
165
|
+
controls_extra,
|
|
166
|
+
pan = {},
|
|
167
|
+
...rest
|
|
168
|
+
}: HTMLAttributes<HTMLDivElement> & BasePlotProps & PlotConfig & {
|
|
169
|
+
series?: BoxPlotSeries<Metadata>[]
|
|
170
|
+
orientation?: Orientation
|
|
171
|
+
legend?: LegendConfig | null
|
|
172
|
+
show_legend?: boolean
|
|
173
|
+
box?: BoxStyle
|
|
174
|
+
whisker?: WhiskerStyle
|
|
175
|
+
median_style?: BoxLineStyle
|
|
176
|
+
outlier_style?: OutlierStyle
|
|
177
|
+
whisker_mode?: WhiskerMode
|
|
178
|
+
whisker_range?: number
|
|
179
|
+
whisker_percentiles?: [number, number]
|
|
180
|
+
show_outliers?: boolean
|
|
181
|
+
show_mean?: boolean
|
|
182
|
+
show_value_labels?: boolean
|
|
183
|
+
value_label_stat?: `median` | `mean`
|
|
184
|
+
value_label_format?: string
|
|
185
|
+
kind?: ViolinKind
|
|
186
|
+
side?: ViolinSide
|
|
187
|
+
bandwidth?: BandwidthOption
|
|
188
|
+
violin_width?: number
|
|
189
|
+
violin_style?: ViolinStyle
|
|
190
|
+
kde_points?: number
|
|
191
|
+
kde_cut?: number
|
|
192
|
+
kde_max_samples?: number
|
|
193
|
+
kde_clip?: [number | null, number | null]
|
|
194
|
+
tooltip?: Snippet<[BoxHandlerProps<Metadata>]>
|
|
195
|
+
user_content?: Snippet<[UserContentProps]>
|
|
196
|
+
header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
|
|
197
|
+
controls_extra?: Snippet<[{ orientation: Orientation } & Required<PlotConfig>]>
|
|
198
|
+
change?: (data: BoxHandlerProps<Metadata> | null) => void
|
|
199
|
+
on_box_click?: (
|
|
200
|
+
data: BoxHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
|
|
201
|
+
) => void
|
|
202
|
+
on_box_hover?: (
|
|
203
|
+
data:
|
|
204
|
+
| (BoxHandlerProps<Metadata> & { event: MouseEvent | FocusEvent | KeyboardEvent })
|
|
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
|
+
pan?: PanConfig
|
|
211
|
+
} = $props()
|
|
212
|
+
|
|
213
|
+
let box_state = $derived({ ...DEFAULTS.box.box, ...box })
|
|
214
|
+
let whisker_state = $derived({ ...DEFAULTS.box.whisker, ...whisker })
|
|
215
|
+
let median_state = $derived({ ...DEFAULTS.box.median, ...median_style })
|
|
216
|
+
let outlier_state = $derived({ ...DEFAULTS.box.outlier, ...outlier_style })
|
|
217
|
+
let violin_state = $derived({ ...DEFAULTS.box.violin, ...violin_style })
|
|
218
|
+
|
|
219
|
+
// Merge secondary-axis defaults as deriveds instead of assigning back into the
|
|
220
|
+
// $bindable props (which would push library defaults into the parent's bound state)
|
|
221
|
+
let y2_axis = $derived(
|
|
222
|
+
{
|
|
223
|
+
format: ``,
|
|
224
|
+
scale_type: `linear`,
|
|
225
|
+
ticks: 5,
|
|
226
|
+
range: [null, null],
|
|
227
|
+
...y2_axis_prop,
|
|
228
|
+
} as typeof y2_axis_prop,
|
|
229
|
+
)
|
|
230
|
+
let x2_axis = $derived(
|
|
231
|
+
{
|
|
232
|
+
format: ``,
|
|
233
|
+
scale_type: `linear`,
|
|
234
|
+
ticks: 5,
|
|
235
|
+
range: [null, null],
|
|
236
|
+
...x2_axis_prop,
|
|
237
|
+
} as typeof x2_axis_prop,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
let [width, height] = $state([0, 0])
|
|
241
|
+
let wrapper: HTMLDivElement | undefined = $state()
|
|
242
|
+
let svg_element: SVGElement | null = $state(null)
|
|
243
|
+
const clip_path_id = unique_id(`box-clip`) // stable, collision-resistant (see unique_id)
|
|
244
|
+
|
|
245
|
+
let hovered_ref_line_idx = $state<number | null>(null)
|
|
246
|
+
|
|
247
|
+
let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
|
|
248
|
+
|
|
249
|
+
// === Box stats + slot model ===
|
|
250
|
+
const box_color = (idx: number): string =>
|
|
251
|
+
series[idx]?.color ?? DEFAULT_SERIES_COLORS[idx % DEFAULT_SERIES_COLORS.length]
|
|
252
|
+
|
|
253
|
+
// Which glyph(s) a series draws (per-series kind overrides the component default)
|
|
254
|
+
const effective_kind = (srs: BoxPlotSeries<Metadata>): ViolinKind => srs.kind ?? kind
|
|
255
|
+
const draws_violin = (srs: BoxPlotSeries<Metadata>): boolean => effective_kind(srs) !== `box`
|
|
256
|
+
const draws_box = (srs: BoxPlotSeries<Metadata>): boolean => effective_kind(srs) !== `violin`
|
|
257
|
+
|
|
258
|
+
let box_stats = $derived(
|
|
259
|
+
series.map((srs) =>
|
|
260
|
+
compute_box_stats(srs.y ?? [], {
|
|
261
|
+
whisker_mode: srs.whisker_mode ?? whisker_mode,
|
|
262
|
+
whisker_range: srs.whisker_range ?? whisker_range,
|
|
263
|
+
whisker_percentiles: srs.whisker_percentiles ?? whisker_percentiles,
|
|
264
|
+
collect_outliers: show_outliers && draws_box(srs) && (srs.visible ?? true),
|
|
265
|
+
})
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Slots position boxes/violins along the category axis. Series sharing a `category` occupy
|
|
270
|
+
// one slot (split/grouped violins). Without `category`, each series gets its own slot —
|
|
271
|
+
// byte-identical to the original one-box-per-series behavior. Override tick labels via
|
|
272
|
+
// x_axis.ticks (a Record).
|
|
273
|
+
let use_categories = $derived(series.some((srs) => srs.category != null))
|
|
274
|
+
const slot_key = (srs: BoxPlotSeries<Metadata>, idx: number): string =>
|
|
275
|
+
srs.category ?? `${idx}`
|
|
276
|
+
let slot_list = $derived(
|
|
277
|
+
use_categories
|
|
278
|
+
? [...new Set(series.map(slot_key))]
|
|
279
|
+
: series.map((srs, idx) => srs.label ?? `${idx}`),
|
|
280
|
+
)
|
|
281
|
+
let slot_lookup = $derived(new Map(slot_list.map((slot, idx) => [slot, idx])))
|
|
282
|
+
const slot_of = (idx: number): number =>
|
|
283
|
+
use_categories ? (slot_lookup.get(slot_key(series[idx], idx)) ?? idx) : idx
|
|
284
|
+
let slot_indices = $derived(slot_list.map((_, idx) => idx))
|
|
285
|
+
// A slot's tick label is colored only when a single series occupies it. Precompute
|
|
286
|
+
// slot -> color in one pass so the PlotAxis tick_color callback stays O(1) per tick.
|
|
287
|
+
let slot_colors = $derived.by(() => {
|
|
288
|
+
const by_slot = new SvelteMap<number, number[]>()
|
|
289
|
+
series.forEach((_srs, idx) => {
|
|
290
|
+
const slot = slot_of(idx)
|
|
291
|
+
const idxs = by_slot.get(slot)
|
|
292
|
+
if (idxs) idxs.push(idx)
|
|
293
|
+
else by_slot.set(slot, [idx])
|
|
294
|
+
})
|
|
295
|
+
const colors = new SvelteMap<number, string | undefined>()
|
|
296
|
+
for (const [slot, idxs] of by_slot) {
|
|
297
|
+
colors.set(slot, idxs.length === 1 ? box_color(idxs[0]) : undefined)
|
|
298
|
+
}
|
|
299
|
+
return colors
|
|
300
|
+
})
|
|
301
|
+
let cat_axis = $derived(orientation === `horizontal` ? `y` : `x`)
|
|
302
|
+
|
|
303
|
+
type Box = {
|
|
304
|
+
series: BoxPlotSeries<Metadata>
|
|
305
|
+
idx: number
|
|
306
|
+
slot: number
|
|
307
|
+
stats: (typeof box_stats)[number]
|
|
308
|
+
}
|
|
309
|
+
let visible_boxes = $derived<Box[]>(
|
|
310
|
+
series
|
|
311
|
+
.map((srs, idx) => ({ series: srs, idx, slot: slot_of(idx), stats: box_stats[idx] }))
|
|
312
|
+
.filter((box_item) => box_item.series.visible ?? true),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// KDE per visible violin series, keyed by series index (bandwidth from the full sample)
|
|
316
|
+
let violin_kdes = $derived.by(() => {
|
|
317
|
+
const map = new SvelteMap<number, KdeResult>()
|
|
318
|
+
const [val_axis, val_axis2] = orientation === `vertical`
|
|
319
|
+
? [y_axis, y2_axis]
|
|
320
|
+
: [x_axis, x2_axis]
|
|
321
|
+
for (const box_item of visible_boxes) {
|
|
322
|
+
if (!draws_violin(box_item.series)) continue
|
|
323
|
+
const samples = box_item.series.y ?? []
|
|
324
|
+
let clip = box_item.series.clip ?? kde_clip
|
|
325
|
+
// On a log value axis the KDE grid tail (data_min - cut*bandwidth) is usually <= 0 →
|
|
326
|
+
// NaN pixels + LOG_EPS range pollution. Clamp the grid to the smallest positive sample.
|
|
327
|
+
if ((is_secondary(box_item.series) ? val_axis2 : val_axis).scale_type === `log`) {
|
|
328
|
+
const min_pos = samples.reduce((min, val) => (val > 0 && val < min ? val : min), Infinity)
|
|
329
|
+
// Guard: no positive samples → min_pos is Infinity; leave clip unchanged so the KDE
|
|
330
|
+
// never receives a non-finite lower bound
|
|
331
|
+
if (Number.isFinite(min_pos)) {
|
|
332
|
+
clip = [Math.max(clip?.[0] ?? -Infinity, min_pos), clip?.[1] ?? null]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
map.set(
|
|
336
|
+
box_item.idx,
|
|
337
|
+
gaussian_kde(samples, {
|
|
338
|
+
bandwidth: box_item.series.bandwidth ?? bandwidth,
|
|
339
|
+
n_points: kde_points,
|
|
340
|
+
cut: kde_cut,
|
|
341
|
+
clip,
|
|
342
|
+
max_samples: kde_max_samples,
|
|
343
|
+
}),
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
return map
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// The horizontal category pixel axis is inverted, so flip the half-violin side to keep
|
|
350
|
+
// `positive` meaning "above the center line" (vertical/`both` pass through unchanged)
|
|
351
|
+
const to_screen_side = (eff_side: ViolinSide, vertical: boolean): ViolinSide =>
|
|
352
|
+
vertical || eff_side === `both` ? eff_side : eff_side === `positive` ? `negative` : `positive`
|
|
353
|
+
|
|
354
|
+
// Peak density per violin, computed once on data change (avoids spreading kde.density into
|
|
355
|
+
// Math.max — unsafe for large kde_points — and re-deriving it on every render/hover).
|
|
356
|
+
let violin_max_density = $derived.by(() => {
|
|
357
|
+
const map = new SvelteMap<number, number>()
|
|
358
|
+
for (const [idx, kde] of violin_kdes) {
|
|
359
|
+
let max = 0
|
|
360
|
+
for (const den of kde.density) if (den > max) max = den
|
|
361
|
+
map.set(idx, max)
|
|
362
|
+
}
|
|
363
|
+
return map
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// Which boxes live on the secondary value axis (y2 for vertical, x2 for horizontal)
|
|
367
|
+
const is_secondary = (srs: BoxPlotSeries<Metadata>): boolean =>
|
|
368
|
+
orientation === `vertical` ? srs.y_axis === `y2` : srs.x_axis === `x2`
|
|
369
|
+
let secondary_boxes = $derived(visible_boxes.filter((box_item) => is_secondary(box_item.series)))
|
|
370
|
+
let has_secondary = $derived(secondary_boxes.length > 0)
|
|
371
|
+
|
|
372
|
+
// Collect value-axis points (whiskers, quartiles, outliers, KDE tails) for auto-range
|
|
373
|
+
const value_points = (boxes: Box[]): { x: number; y: number }[] =>
|
|
374
|
+
boxes.flatMap((box_item) => {
|
|
375
|
+
const { whisker_low, whisker_high, q1, q3, median, mean, outliers } = box_item.stats
|
|
376
|
+
const vals = [whisker_low, whisker_high, q1, q3, median]
|
|
377
|
+
// keep the drawn mean line in range even when hidden outliers drag it past the whiskers
|
|
378
|
+
if (show_mean) vals.push(mean)
|
|
379
|
+
// outliers are sorted ascending; auto-range only needs their extremes (avoids
|
|
380
|
+
// spreading a potentially huge array as call args)
|
|
381
|
+
if (show_outliers && outliers.length > 0) {
|
|
382
|
+
vals.push(outliers[0], outliers[outliers.length - 1])
|
|
383
|
+
}
|
|
384
|
+
const kde = violin_kdes.get(box_item.idx)
|
|
385
|
+
if (kde && kde.grid.length > 0) vals.push(kde.grid[0], kde.grid[kde.grid.length - 1])
|
|
386
|
+
return vals.filter(Number.isFinite).map((val) => ({ x: 0, y: val }))
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
let auto_ranges = $derived.by(() => {
|
|
390
|
+
const cat_count = slot_list.length
|
|
391
|
+
const cat_range: Vec2 = cat_count > 0 ? [-0.5, cat_count - 0.5] : [0, 1]
|
|
392
|
+
|
|
393
|
+
const primary_boxes = visible_boxes.filter((box_item) => !is_secondary(box_item.series))
|
|
394
|
+
const calc_value_range = (
|
|
395
|
+
boxes: Box[],
|
|
396
|
+
limit: [number | null, number | null],
|
|
397
|
+
scale_type: ScaleType,
|
|
398
|
+
): Vec2 => {
|
|
399
|
+
const pts = value_points(boxes)
|
|
400
|
+
if (pts.length === 0) return [0, 1]
|
|
401
|
+
return get_nice_data_range(pts, (pt) => pt.y, limit, scale_type, range_padding, false)
|
|
402
|
+
}
|
|
403
|
+
const vertical = orientation === `vertical`
|
|
404
|
+
const value_primary = calc_value_range(
|
|
405
|
+
primary_boxes,
|
|
406
|
+
(vertical ? y_axis.range : x_axis.range) ?? [null, null],
|
|
407
|
+
(vertical ? y_axis.scale_type : x_axis.scale_type) ?? `linear`,
|
|
408
|
+
)
|
|
409
|
+
const value_secondary = calc_value_range(
|
|
410
|
+
secondary_boxes,
|
|
411
|
+
(vertical ? y2_axis.range : x2_axis.range) ?? [null, null],
|
|
412
|
+
(vertical ? y2_axis.scale_type : x2_axis.scale_type) ?? `linear`,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return vertical
|
|
416
|
+
? ({ x: cat_range, x2: [0, 1] as Vec2, y: value_primary, y2: value_secondary })
|
|
417
|
+
: ({ x: value_primary, x2: value_secondary, y: cat_range, y2: [0, 1] as Vec2 })
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
let ranges = $state<{
|
|
421
|
+
initial: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
422
|
+
current: { x: Vec2; x2: Vec2; y: Vec2; y2: Vec2 }
|
|
423
|
+
}>({
|
|
424
|
+
initial: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
425
|
+
current: { x: [0, 1], x2: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
$effect(() => { // sync ranges from axis.range overrides / auto ranges
|
|
429
|
+
// resolve_axis_ranges returns null for transient non-finite bounds (skip: writing
|
|
430
|
+
// NaN breaks scales and, since NaN !== NaN, loops the effect)
|
|
431
|
+
const next = resolve_axis_ranges({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis }, auto_ranges)
|
|
432
|
+
if (!next) return
|
|
433
|
+
// untrack the read of `ranges` so the assignment can't re-trigger this effect
|
|
434
|
+
// (reading + writing the same state otherwise causes effect_update_depth_exceeded).
|
|
435
|
+
const init = untrack(() => ranges.initial)
|
|
436
|
+
if (!axis_ranges_equal(init, next)) {
|
|
437
|
+
ranges = { initial: { ...next }, current: { ...next } }
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const default_padding = { t: 20, b: 60, l: 60, r: 20 }
|
|
442
|
+
let base_pad = $derived(filter_padding(padding, default_padding))
|
|
443
|
+
|
|
444
|
+
$effect(() => { // dynamic padding from tick label widths
|
|
445
|
+
const new_pad = width && height && ticks.y.length > 0
|
|
446
|
+
? calc_auto_padding({
|
|
447
|
+
padding,
|
|
448
|
+
default_padding,
|
|
449
|
+
x2_axis: { ...x2_axis, tick_values: ticks.x2 },
|
|
450
|
+
y_axis: { ...y_axis, tick_values: ticks.y },
|
|
451
|
+
y2_axis: { ...y2_axis, tick_values: ticks.y2 },
|
|
452
|
+
})
|
|
453
|
+
: filter_padding(padding, default_padding)
|
|
454
|
+
if (
|
|
455
|
+
width && height && orientation === `vertical` && has_secondary && ticks.y2.length > 0
|
|
456
|
+
) {
|
|
457
|
+
const inside = y2_axis.tick?.label?.inside ?? false
|
|
458
|
+
const tick_shift = inside ? 0 : (y2_axis.tick?.label?.shift?.x ?? 0) + 8
|
|
459
|
+
const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max
|
|
460
|
+
const label_space = y2_axis.label ? 20 : 0
|
|
461
|
+
new_pad.r = Math.max(new_pad.r, tick_shift + tick_width_contribution + 30 + label_space)
|
|
462
|
+
}
|
|
463
|
+
if (base_pad.t !== new_pad.t || base_pad.b !== new_pad.b ||
|
|
464
|
+
base_pad.l !== new_pad.l || base_pad.r !== new_pad.r) base_pad = new_pad
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
let legend_element = $state<HTMLDivElement | undefined>()
|
|
468
|
+
const legend_footprint = $derived(measured_footprint(legend_element, { width: 120, height: 60 }))
|
|
469
|
+
const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
|
|
470
|
+
|
|
471
|
+
// Obstacle field in normalized [0,1] coords: each box modeled as a whisker-spanning segment
|
|
472
|
+
const obstacles_norm = $derived.by(() => {
|
|
473
|
+
if (!width || !height || visible_boxes.length === 0) return []
|
|
474
|
+
const base_w = width - base_pad.l - base_pad.r
|
|
475
|
+
const base_h = height - base_pad.t - base_pad.b
|
|
476
|
+
if (base_w <= 0 || base_h <= 0) return []
|
|
477
|
+
const vertical = orientation === `vertical`
|
|
478
|
+
const segs: { points: { x: number; y: number }[]; draws_line: boolean }[] = []
|
|
479
|
+
for (const box_item of visible_boxes) {
|
|
480
|
+
const { whisker_low, whisker_high, median } = box_item.stats
|
|
481
|
+
if (!Number.isFinite(median)) continue
|
|
482
|
+
const secondary = is_secondary(box_item.series)
|
|
483
|
+
const cat_rng = vertical ? ranges.current.x : ranges.current.y
|
|
484
|
+
const val_rng = vertical
|
|
485
|
+
? (secondary ? ranges.current.y2 : ranges.current.y)
|
|
486
|
+
: (secondary ? ranges.current.x2 : ranges.current.x)
|
|
487
|
+
const cat_span = cat_rng[1] - cat_rng[0]
|
|
488
|
+
const val_span = val_rng[1] - val_rng[0]
|
|
489
|
+
if (cat_span === 0 || val_span === 0) continue
|
|
490
|
+
const cross = (box_item.slot - cat_rng[0]) / cat_span
|
|
491
|
+
const lo = (whisker_low - val_rng[0]) / val_span
|
|
492
|
+
const hi = (whisker_high - val_rng[0]) / val_span
|
|
493
|
+
const seg = vertical
|
|
494
|
+
? clip_bar(true, cross, 1 - hi, 1 - lo)
|
|
495
|
+
: clip_bar(false, 1 - cross, lo, hi)
|
|
496
|
+
if (seg) segs.push(seg)
|
|
497
|
+
}
|
|
498
|
+
return build_obstacles_norm(segs, base_w, base_h)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
const should_show_legend = $derived(show_legend ?? false)
|
|
502
|
+
const decor = $derived.by(() =>
|
|
503
|
+
place_decorations({
|
|
504
|
+
base_pad,
|
|
505
|
+
width,
|
|
506
|
+
height,
|
|
507
|
+
obstacles_norm,
|
|
508
|
+
legend: legend != null && should_show_legend && legend_element != null &&
|
|
509
|
+
!legend_has_explicit_pos
|
|
510
|
+
? { footprint: legend_footprint, clearance: legend?.axis_clearance }
|
|
511
|
+
: null,
|
|
512
|
+
})
|
|
513
|
+
)
|
|
514
|
+
const pad = $derived(decor.pad)
|
|
515
|
+
const legend_auto_outside = $derived(decor.legend_outside)
|
|
516
|
+
const legend_outside_x = $derived(decor.legend_pos.x)
|
|
517
|
+
const legend_outside_y = $derived(decor.legend_pos.y)
|
|
518
|
+
const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
|
|
519
|
+
const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
|
|
520
|
+
|
|
521
|
+
let scales = $derived({
|
|
522
|
+
x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [pad.l, width - pad.r]),
|
|
523
|
+
x2: create_scale(x2_axis.scale_type ?? `linear`, ranges.current.x2, [pad.l, width - pad.r]),
|
|
524
|
+
y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
|
|
525
|
+
y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Value scale for a box (vertical -> y/y2, horizontal -> x/x2), made log-safe: on a
|
|
529
|
+
// log value axis, stats at values <= 0 (whisker_low is often exactly 0; negative
|
|
530
|
+
// outliers) have no finite pixel. Clamp to LOG_EPS so whiskers/boxes/labels draw
|
|
531
|
+
// toward the plot edge (the clip group crops the overshoot) instead of NaN coords.
|
|
532
|
+
const box_val_scale = (srs: BoxPlotSeries<Metadata>): (val: number) => number => {
|
|
533
|
+
const vertical = orientation === `vertical`
|
|
534
|
+
const secondary = is_secondary(srs)
|
|
535
|
+
const scale = vertical
|
|
536
|
+
? (secondary ? scales.y2 : scales.y)
|
|
537
|
+
: (secondary ? scales.x2 : scales.x)
|
|
538
|
+
const axis = vertical ? (secondary ? y2_axis : y_axis) : (secondary ? x2_axis : x_axis)
|
|
539
|
+
return axis.scale_type === `log` ? (val) => scale(Math.max(val, LOG_EPS)) : scale
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Categorical tick labels (slot index -> category name) unless user provides a label mapping
|
|
543
|
+
let effective_cat_ticks = $derived.by(() => {
|
|
544
|
+
if (slot_list.length === 0) return undefined
|
|
545
|
+
const user_ticks = cat_axis === `x` ? x_axis.ticks : y_axis.ticks
|
|
546
|
+
if (user_ticks != null && typeof user_ticks === `object` && !Array.isArray(user_ticks)) {
|
|
547
|
+
return user_ticks
|
|
548
|
+
}
|
|
549
|
+
return Object.fromEntries(slot_list.map((cat, idx) => [idx, cat]))
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
let ticks = $derived({
|
|
553
|
+
x: width && height
|
|
554
|
+
? (cat_axis === `x` ? slot_indices : generate_ticks(
|
|
555
|
+
ranges.current.x,
|
|
556
|
+
x_axis.scale_type ?? `linear`,
|
|
557
|
+
x_axis.ticks,
|
|
558
|
+
scales.x,
|
|
559
|
+
{ default_count: 8 },
|
|
560
|
+
))
|
|
561
|
+
: [],
|
|
562
|
+
y: width && height
|
|
563
|
+
? (cat_axis === `y` ? slot_indices : generate_ticks(
|
|
564
|
+
ranges.current.y,
|
|
565
|
+
y_axis.scale_type ?? `linear`,
|
|
566
|
+
y_axis.ticks,
|
|
567
|
+
scales.y,
|
|
568
|
+
{ default_count: 6 },
|
|
569
|
+
))
|
|
570
|
+
: [],
|
|
571
|
+
y2: width && height && has_secondary && orientation === `vertical`
|
|
572
|
+
? generate_ticks(ranges.current.y2, y2_axis.scale_type ?? `linear`, y2_axis.ticks, scales.y2, {
|
|
573
|
+
default_count: 6,
|
|
574
|
+
})
|
|
575
|
+
: [],
|
|
576
|
+
x2: width && height && has_secondary && orientation === `horizontal`
|
|
577
|
+
? generate_ticks(ranges.current.x2, x2_axis.scale_type ?? `linear`, x2_axis.ticks, scales.x2, {
|
|
578
|
+
default_count: 8,
|
|
579
|
+
})
|
|
580
|
+
: [],
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
let tick_label_widths = $derived({
|
|
584
|
+
y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
|
|
585
|
+
y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
|
|
586
|
+
x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// === Interaction state (pan / zoom / touch) ===
|
|
590
|
+
let drag_state = $state<{
|
|
591
|
+
start: { x: number; y: number } | null
|
|
592
|
+
current: { x: number; y: number } | null
|
|
593
|
+
bounds: DOMRect | null
|
|
594
|
+
}>({ start: null, current: null, bounds: null })
|
|
595
|
+
let is_focused = $state(false)
|
|
596
|
+
let shift_held = $state(false)
|
|
597
|
+
let pan_drag_state = $state<InitialRanges & { start: { x: number; y: number } } | null>(null)
|
|
598
|
+
let touch_state = $state<InitialRanges & { start_touches: { x: number; y: number }[] } | null>(
|
|
599
|
+
null,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
const on_window_mouse_move = (evt: MouseEvent) => {
|
|
603
|
+
if (!drag_state.start || !drag_state.bounds) return
|
|
604
|
+
drag_state.current = {
|
|
605
|
+
x: evt.clientX - drag_state.bounds.left,
|
|
606
|
+
y: evt.clientY - drag_state.bounds.top,
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const on_window_mouse_up = () => {
|
|
610
|
+
if (drag_state.start && drag_state.current) {
|
|
611
|
+
const x1 = scales.x.invert(drag_state.start.x) as number
|
|
612
|
+
const x2 = scales.x.invert(drag_state.current.x) as number
|
|
613
|
+
const y1 = scales.y.invert(drag_state.start.y)
|
|
614
|
+
const y2 = scales.y.invert(drag_state.current.y)
|
|
615
|
+
const y2_1 = scales.y2.invert(drag_state.start.y)
|
|
616
|
+
const y2_2 = scales.y2.invert(drag_state.current.y)
|
|
617
|
+
const x2_1 = scales.x2.invert(drag_state.start.x) as number
|
|
618
|
+
const x2_2 = scales.x2.invert(drag_state.current.x) as number
|
|
619
|
+
const dx = Math.abs(drag_state.start.x - drag_state.current.x)
|
|
620
|
+
const dy = Math.abs(drag_state.start.y - drag_state.current.y)
|
|
621
|
+
if (dx > 5 && dy > 5 && Number.isFinite(x1) && Number.isFinite(x2)) {
|
|
622
|
+
x_axis = { ...x_axis, range: sorted_range(x1, x2) }
|
|
623
|
+
// the secondary value axis is x2 only in horizontal mode, y2 only in vertical
|
|
624
|
+
// (is_secondary keys off orientation); writing the off-orientation axis would
|
|
625
|
+
// store a phantom range from its [0, 1] sentinel scale into the bound prop
|
|
626
|
+
if (
|
|
627
|
+
has_secondary && orientation === `horizontal` &&
|
|
628
|
+
Number.isFinite(x2_1) && Number.isFinite(x2_2)
|
|
629
|
+
) {
|
|
630
|
+
x2_axis_prop = { ...x2_axis_prop, range: sorted_range(x2_1, x2_2) }
|
|
631
|
+
}
|
|
632
|
+
y_axis = { ...y_axis, range: sorted_range(y1, y2) }
|
|
633
|
+
if (
|
|
634
|
+
has_secondary && orientation === `vertical` &&
|
|
635
|
+
Number.isFinite(y2_1) && Number.isFinite(y2_2)
|
|
636
|
+
) {
|
|
637
|
+
y2_axis_prop = { ...y2_axis_prop, range: sorted_range(y2_1, y2_2) }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
drag_state = { start: null, current: null, bounds: null }
|
|
642
|
+
window.removeEventListener(`mousemove`, on_window_mouse_move)
|
|
643
|
+
window.removeEventListener(`mouseup`, on_window_mouse_up)
|
|
644
|
+
document.body.style.cursor = `default`
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Pan/zoom all four axes from an interaction-start snapshot, each in its own
|
|
648
|
+
// scale's transform space (log axes pan by a constant factor, linear by a shift)
|
|
649
|
+
const pan_all_axes = (init: InitialRanges, dx_px: number, dy_px: number) => {
|
|
650
|
+
ranges.current.x = pan_range_by_pixels(init.initial_x_range, dx_px, chart_width, x_axis.scale_type)
|
|
651
|
+
ranges.current.x2 = pan_range_by_pixels(init.initial_x2_range, dx_px, chart_width, x2_axis.scale_type)
|
|
652
|
+
ranges.current.y = pan_range_by_pixels(init.initial_y_range, dy_px, chart_height, y_axis.scale_type)
|
|
653
|
+
ranges.current.y2 = pan_range_by_pixels(init.initial_y2_range, dy_px, chart_height, y2_axis.scale_type)
|
|
654
|
+
}
|
|
655
|
+
const zoom_all_axes = (init: InitialRanges, factor: number) => {
|
|
656
|
+
ranges.current.x = zoom_range_by_factor(init.initial_x_range, factor, x_axis.scale_type)
|
|
657
|
+
ranges.current.x2 = zoom_range_by_factor(init.initial_x2_range, factor, x2_axis.scale_type)
|
|
658
|
+
ranges.current.y = zoom_range_by_factor(init.initial_y_range, factor, y_axis.scale_type)
|
|
659
|
+
ranges.current.y2 = zoom_range_by_factor(init.initial_y2_range, factor, y2_axis.scale_type)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Pan drag handler (drag direction inverted on x for natural pan feel)
|
|
663
|
+
const on_pan_move = (evt: MouseEvent) => {
|
|
664
|
+
if (!pan_drag_state) return
|
|
665
|
+
const sensitivity = pan?.drag_sensitivity ?? 1
|
|
666
|
+
pan_all_axes(
|
|
667
|
+
pan_drag_state,
|
|
668
|
+
-(evt.clientX - pan_drag_state.start.x) * sensitivity,
|
|
669
|
+
(evt.clientY - pan_drag_state.start.y) * sensitivity,
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
const on_pan_end = () => {
|
|
673
|
+
pan_drag_state = null
|
|
674
|
+
document.body.style.cursor = ``
|
|
675
|
+
window.removeEventListener(`mousemove`, on_pan_move)
|
|
676
|
+
window.removeEventListener(`mouseup`, on_pan_end)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Tear down any window listeners + cursor override if the component unmounts mid-drag
|
|
680
|
+
// (mouseup/panend would otherwise never fire, leaking listeners and a stuck cursor).
|
|
681
|
+
// onDestroy also runs during SSR teardown, where window/document don't exist.
|
|
682
|
+
onDestroy(() => {
|
|
683
|
+
remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end])
|
|
684
|
+
drag_state = { start: null, current: null, bounds: null }
|
|
685
|
+
pan_drag_state = null
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
function handle_mouse_down(evt: MouseEvent) {
|
|
689
|
+
const coords = get_relative_coords(evt)
|
|
690
|
+
if (!coords || !svg_element) return
|
|
691
|
+
const pan_enabled = pan?.enabled !== false
|
|
692
|
+
if (pan_enabled && evt.shiftKey) {
|
|
693
|
+
evt.preventDefault()
|
|
694
|
+
pan_drag_state = {
|
|
695
|
+
start: { x: evt.clientX, y: evt.clientY },
|
|
696
|
+
initial_x_range: [...ranges.current.x] as Vec2,
|
|
697
|
+
initial_x2_range: [...ranges.current.x2] as Vec2,
|
|
698
|
+
initial_y_range: [...ranges.current.y] as Vec2,
|
|
699
|
+
initial_y2_range: [...ranges.current.y2] as Vec2,
|
|
700
|
+
}
|
|
701
|
+
document.body.style.cursor = `grabbing`
|
|
702
|
+
window.addEventListener(`mousemove`, on_pan_move)
|
|
703
|
+
window.addEventListener(`mouseup`, on_pan_end)
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
drag_state = { start: coords, current: coords, bounds: svg_element.getBoundingClientRect() }
|
|
707
|
+
window.addEventListener(`mousemove`, on_window_mouse_move)
|
|
708
|
+
window.addEventListener(`mouseup`, on_window_mouse_up)
|
|
709
|
+
evt.preventDefault()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function handle_wheel(evt: WheelEvent) {
|
|
713
|
+
const pan_enabled = pan?.enabled !== false
|
|
714
|
+
if (!pan_enabled || !is_focused || !shift_held) return
|
|
715
|
+
evt.preventDefault()
|
|
716
|
+
const sensitivity = pan?.wheel_sensitivity ?? 1
|
|
717
|
+
if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
|
|
718
|
+
const dx = evt.deltaX * sensitivity
|
|
719
|
+
ranges.current.x = pan_range_by_pixels(ranges.current.x, dx, chart_width, x_axis.scale_type)
|
|
720
|
+
ranges.current.x2 = pan_range_by_pixels(ranges.current.x2, dx, chart_width, x2_axis.scale_type)
|
|
721
|
+
} else {
|
|
722
|
+
const dy = evt.deltaY * sensitivity
|
|
723
|
+
ranges.current.y = pan_range_by_pixels(ranges.current.y, dy, chart_height, y_axis.scale_type)
|
|
724
|
+
ranges.current.y2 = pan_range_by_pixels(ranges.current.y2, dy, chart_height, y2_axis.scale_type)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function handle_touch_start(evt: TouchEvent) {
|
|
729
|
+
const touch_enabled = pan?.enabled !== false && pan?.touch_enabled !== false
|
|
730
|
+
if (!touch_enabled || evt.touches.length !== 2) return
|
|
731
|
+
evt.preventDefault()
|
|
732
|
+
const touches = Array.from(evt.touches)
|
|
733
|
+
touch_state = {
|
|
734
|
+
start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
|
|
735
|
+
initial_x_range: [...ranges.current.x] as Vec2,
|
|
736
|
+
initial_x2_range: [...ranges.current.x2] as Vec2,
|
|
737
|
+
initial_y_range: [...ranges.current.y] as Vec2,
|
|
738
|
+
initial_y2_range: [...ranges.current.y2] as Vec2,
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function handle_touch_move(evt: TouchEvent) {
|
|
742
|
+
if (!touch_state || evt.touches.length !== 2) return
|
|
743
|
+
evt.preventDefault()
|
|
744
|
+
const [t1, t2] = Array.from(evt.touches)
|
|
745
|
+
const [s1, s2] = touch_state.start_touches
|
|
746
|
+
const start_center = { x: (s1.x + s2.x) / 2, y: (s1.y + s2.y) / 2 }
|
|
747
|
+
const curr_center = { x: (t1.clientX + t2.clientX) / 2, y: (t1.clientY + t2.clientY) / 2 }
|
|
748
|
+
const dx = curr_center.x - start_center.x
|
|
749
|
+
const dy = curr_center.y - start_center.y
|
|
750
|
+
const start_dist = Math.hypot(s2.x - s1.x, s2.y - s1.y)
|
|
751
|
+
// ignore near-coincident touches so curr_dist / start_dist can't blow up the scale
|
|
752
|
+
if (start_dist < MIN_TOUCH_DISTANCE_PIXELS) return
|
|
753
|
+
const curr_dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
|
|
754
|
+
const scale = curr_dist / start_dist
|
|
755
|
+
// Pinch zoom about the view center (spread = zoom in, pinch = zoom out)
|
|
756
|
+
if (Math.abs(scale - 1) > PINCH_ZOOM_THRESHOLD && scale > Number.EPSILON) {
|
|
757
|
+
zoom_all_axes(touch_state, scale)
|
|
758
|
+
} else pan_all_axes(touch_state, -dx, dy)
|
|
759
|
+
}
|
|
760
|
+
const handle_touch_end = () => (touch_state = null)
|
|
761
|
+
|
|
762
|
+
// === Legend ===
|
|
763
|
+
let legend_data = $derived<LegendItem[]>(
|
|
764
|
+
series.map((srs, idx) => ({
|
|
765
|
+
series_idx: idx,
|
|
766
|
+
label: srs.label ?? `Box ${idx + 1}`,
|
|
767
|
+
visible: srs.visible ?? true,
|
|
768
|
+
legend_group: srs.legend_group,
|
|
769
|
+
display_style: { symbol_type: `Square` as const, symbol_color: box_color(idx) },
|
|
770
|
+
})),
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
const legend_vis = create_legend_visibility(() => series, (next) => (series = next))
|
|
774
|
+
|
|
775
|
+
let box_points_for_placement = $derived.by(() => {
|
|
776
|
+
if (!width || !height || visible_boxes.length === 0) return []
|
|
777
|
+
const vertical = orientation === `vertical`
|
|
778
|
+
return visible_boxes
|
|
779
|
+
.map((box_item) => {
|
|
780
|
+
const val_scale = box_val_scale(box_item.series)
|
|
781
|
+
const cat_scale = vertical ? scales.x : scales.y
|
|
782
|
+
const cc = cat_scale(box_item.slot)
|
|
783
|
+
const vc = val_scale(box_item.stats.median)
|
|
784
|
+
return vertical ? { x: cc, y: vc } : { x: vc, y: cc }
|
|
785
|
+
})
|
|
786
|
+
.filter(({ x, y }) => isFinite(x) && isFinite(y))
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
let hovered_legend_series_idx = $state<number | null>(null)
|
|
790
|
+
const legend_hover = create_hover_lock()
|
|
791
|
+
const dim_tracker = create_dimension_tracker()
|
|
792
|
+
let has_initial_legend_placement = $state(false)
|
|
793
|
+
$effect(() => () => legend_hover.cleanup())
|
|
794
|
+
|
|
795
|
+
let legend_placement = $derived.by(() => {
|
|
796
|
+
if (!should_show_legend || !width || !height) return null
|
|
797
|
+
return compute_element_placement({
|
|
798
|
+
plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
|
|
799
|
+
element: legend_element,
|
|
800
|
+
element_size: { width: 120, height: 60 },
|
|
801
|
+
axis_clearance: legend?.axis_clearance,
|
|
802
|
+
exclude_rects: [],
|
|
803
|
+
points: box_points_for_placement,
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
const tweened_legend_coords = new Tween(
|
|
808
|
+
{ x: 0, y: 0 },
|
|
809
|
+
untrack(() => ({ duration: 400, ...legend?.tween })),
|
|
810
|
+
)
|
|
811
|
+
$effect(() => {
|
|
812
|
+
if (!width || !height || !legend_placement) return
|
|
813
|
+
const dims_changed = dim_tracker.has_changed(width, height)
|
|
814
|
+
if (dims_changed) dim_tracker.update(width, height)
|
|
815
|
+
const is_responsive = legend?.responsive ?? false
|
|
816
|
+
const should_update = dims_changed ||
|
|
817
|
+
(!legend_hover.is_locked.current && (is_responsive || !has_initial_legend_placement))
|
|
818
|
+
if (should_update) {
|
|
819
|
+
tweened_legend_coords.set(
|
|
820
|
+
{ x: legend_placement.x, y: legend_placement.y },
|
|
821
|
+
has_initial_legend_placement ? undefined : { duration: 0 },
|
|
822
|
+
)
|
|
823
|
+
if (legend_element) has_initial_legend_placement = true
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
// === Tooltip / hover ===
|
|
828
|
+
let hover_info = $state<BoxHover | null>(null)
|
|
829
|
+
|
|
830
|
+
function get_box_data(box_item: Box, color: string): BoxHover {
|
|
831
|
+
const vertical = orientation === `vertical`
|
|
832
|
+
const val_scale = box_val_scale(box_item.series)
|
|
833
|
+
const cat_scale = vertical ? scales.x : scales.y
|
|
834
|
+
const cc = cat_scale(box_item.slot)
|
|
835
|
+
const v_hi = val_scale(box_item.stats.whisker_high)
|
|
836
|
+
const v_lo = val_scale(box_item.stats.whisker_low)
|
|
837
|
+
const [cx, cy] = vertical ? [cc, Math.min(v_hi, v_lo)] : [Math.max(v_hi, v_lo), cc]
|
|
838
|
+
const active_y_axis = (vertical ? (box_item.series.y_axis ?? `y1`) : `y1`) as `y1` | `y2`
|
|
839
|
+
const active_x_axis = (vertical ? `x1` : (box_item.series.x_axis ?? `x1`)) as `x1` | `x2`
|
|
840
|
+
return {
|
|
841
|
+
x: vertical ? box_item.slot : box_item.stats.median,
|
|
842
|
+
y: vertical ? box_item.stats.median : box_item.slot,
|
|
843
|
+
stats: box_item.stats,
|
|
844
|
+
color,
|
|
845
|
+
label: box_item.series.label ?? null,
|
|
846
|
+
category_label: slot_list[box_item.slot],
|
|
847
|
+
metadata: box_item.series.metadata,
|
|
848
|
+
series_idx: box_item.idx,
|
|
849
|
+
box_idx: box_item.idx,
|
|
850
|
+
active_x_axis,
|
|
851
|
+
active_y_axis,
|
|
852
|
+
x_axis: active_x_axis === `x2` ? x2_axis : x_axis,
|
|
853
|
+
x2_axis,
|
|
854
|
+
y_axis: active_y_axis === `y2` ? y2_axis : y_axis,
|
|
855
|
+
y2_axis,
|
|
856
|
+
cx,
|
|
857
|
+
cy,
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const handle_box_hover = (box_item: Box, color: string) => (event: MouseEvent) => {
|
|
862
|
+
hovered = true
|
|
863
|
+
const data = get_box_data(box_item, color)
|
|
864
|
+
// Anchor the tooltip at the cursor (cx/cy default to the box center) so it follows the
|
|
865
|
+
// mouse — boxes/violins are wide, and a center anchor lands far from the pointer.
|
|
866
|
+
const rect = svg_element?.getBoundingClientRect()
|
|
867
|
+
if (rect) {
|
|
868
|
+
data.cx = event.clientX - rect.left
|
|
869
|
+
data.cy = event.clientY - rect.top
|
|
870
|
+
}
|
|
871
|
+
hover_info = data
|
|
872
|
+
change(hover_info)
|
|
873
|
+
on_box_hover?.({ ...hover_info, event })
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Set theme-aware background when entering fullscreen
|
|
877
|
+
$effect(() => set_fullscreen_bg(wrapper, fullscreen, `--boxplot-fullscreen-bg`))
|
|
878
|
+
|
|
879
|
+
// Value label helper
|
|
880
|
+
const value_label_for = (stats: Box[`stats`]): string =>
|
|
881
|
+
format_value(value_label_stat === `mean` ? stats.mean : stats.median, value_label_format)
|
|
882
|
+
</script>
|
|
883
|
+
|
|
884
|
+
{#snippet seg(
|
|
885
|
+
p1: [number, number],
|
|
886
|
+
p2: [number, number],
|
|
887
|
+
stroke: string,
|
|
888
|
+
sw: number,
|
|
889
|
+
dash?: string,
|
|
890
|
+
)}
|
|
891
|
+
<line
|
|
892
|
+
x1={p1[0]}
|
|
893
|
+
y1={p1[1]}
|
|
894
|
+
x2={p2[0]}
|
|
895
|
+
y2={p2[1]}
|
|
896
|
+
{stroke}
|
|
897
|
+
stroke-width={sw}
|
|
898
|
+
stroke-dasharray={dash}
|
|
899
|
+
/>
|
|
900
|
+
{/snippet}
|
|
901
|
+
|
|
902
|
+
{#snippet ref_lines_layer(lines: IndexedRefLine[])}
|
|
903
|
+
{#each lines as line (line.id ?? line.idx)}
|
|
904
|
+
<ReferenceLine
|
|
905
|
+
ref_line={line}
|
|
906
|
+
line_idx={line.idx}
|
|
907
|
+
x_min={line.x_axis === `x2` ? ranges.current.x2[0] : ranges.current.x[0]}
|
|
908
|
+
x_max={line.x_axis === `x2` ? ranges.current.x2[1] : ranges.current.x[1]}
|
|
909
|
+
y_min={line.y_axis === `y2` ? ranges.current.y2[0] : ranges.current.y[0]}
|
|
910
|
+
y_max={line.y_axis === `y2` ? ranges.current.y2[1] : ranges.current.y[1]}
|
|
911
|
+
x_scale={scales.x}
|
|
912
|
+
x2_scale={scales.x2}
|
|
913
|
+
y_scale={scales.y}
|
|
914
|
+
y2_scale={scales.y2}
|
|
915
|
+
{clip_path_id}
|
|
916
|
+
hovered_line_idx={hovered_ref_line_idx}
|
|
917
|
+
on_click={(event: RefLineEvent) => {
|
|
918
|
+
line.on_click?.(event)
|
|
919
|
+
on_ref_line_click?.(event)
|
|
920
|
+
}}
|
|
921
|
+
on_hover={(event: RefLineEvent | null) => {
|
|
922
|
+
hovered_ref_line_idx = event?.line_idx ?? null
|
|
923
|
+
line.on_hover?.(event)
|
|
924
|
+
on_ref_line_hover?.(event)
|
|
925
|
+
}}
|
|
926
|
+
/>
|
|
927
|
+
{/each}
|
|
928
|
+
{/snippet}
|
|
929
|
+
|
|
930
|
+
<svelte:window
|
|
931
|
+
onkeydown={(evt) => {
|
|
932
|
+
if (evt.key === `Escape` && fullscreen) {
|
|
933
|
+
evt.preventDefault()
|
|
934
|
+
fullscreen = false
|
|
935
|
+
}
|
|
936
|
+
if (evt.key === `Shift`) shift_held = true
|
|
937
|
+
}}
|
|
938
|
+
onkeyup={(evt) => {
|
|
939
|
+
if (evt.key === `Shift`) shift_held = false
|
|
940
|
+
}}
|
|
941
|
+
/>
|
|
942
|
+
|
|
943
|
+
<div
|
|
944
|
+
bind:this={wrapper}
|
|
945
|
+
bind:clientWidth={width}
|
|
946
|
+
bind:clientHeight={height}
|
|
947
|
+
{...rest}
|
|
948
|
+
class="box-plot {rest.class ?? ``}"
|
|
949
|
+
class:fullscreen
|
|
950
|
+
>
|
|
951
|
+
{#if width && height}
|
|
952
|
+
<div class="header-controls">
|
|
953
|
+
{@render header_controls?.({ height, width, fullscreen })}
|
|
954
|
+
{#if fullscreen_toggle}
|
|
955
|
+
<FullscreenToggle bind:fullscreen />
|
|
956
|
+
{/if}
|
|
957
|
+
</div>
|
|
958
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
959
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
960
|
+
<svg
|
|
961
|
+
bind:this={svg_element}
|
|
962
|
+
role="application"
|
|
963
|
+
aria-label={rest[`aria-label`] ??
|
|
964
|
+
([x_axis.label, y_axis.label].filter(Boolean).join(` vs `) || `Box plot`)}
|
|
965
|
+
tabindex="0"
|
|
966
|
+
onfocusin={() => (is_focused = true)}
|
|
967
|
+
onfocusout={() => (is_focused = false)}
|
|
968
|
+
onmousedown={handle_mouse_down}
|
|
969
|
+
ondblclick={() => {
|
|
970
|
+
ranges.current.x = [...ranges.initial.x] as Vec2
|
|
971
|
+
ranges.current.x2 = [...ranges.initial.x2] as Vec2
|
|
972
|
+
ranges.current.y = [...ranges.initial.y] as Vec2
|
|
973
|
+
ranges.current.y2 = [...ranges.initial.y2] as Vec2
|
|
974
|
+
x_axis = { ...x_axis, range: [null, null] }
|
|
975
|
+
x2_axis_prop = { ...x2_axis_prop, range: [null, null] }
|
|
976
|
+
y_axis = { ...y_axis, range: [null, null] }
|
|
977
|
+
y2_axis_prop = { ...y2_axis_prop, range: [null, null] }
|
|
978
|
+
}}
|
|
979
|
+
onmouseleave={() => {
|
|
980
|
+
hovered = false
|
|
981
|
+
hover_info = null
|
|
982
|
+
change(null)
|
|
983
|
+
on_box_hover?.(null)
|
|
984
|
+
}}
|
|
985
|
+
onwheel={handle_wheel}
|
|
986
|
+
ontouchstart={handle_touch_start}
|
|
987
|
+
ontouchmove={handle_touch_move}
|
|
988
|
+
ontouchend={handle_touch_end}
|
|
989
|
+
ontouchcancel={handle_touch_end}
|
|
990
|
+
style:cursor={pan_drag_state
|
|
991
|
+
? `grabbing`
|
|
992
|
+
: shift_held && pan?.enabled !== false
|
|
993
|
+
? `grab`
|
|
994
|
+
: `crosshair`}
|
|
995
|
+
>
|
|
996
|
+
<ZoomRect start={drag_state.start} current={drag_state.current} />
|
|
997
|
+
|
|
998
|
+
{@render user_content?.({
|
|
999
|
+
height,
|
|
1000
|
+
width,
|
|
1001
|
+
x_scale_fn: scales.x,
|
|
1002
|
+
x2_scale_fn: scales.x2,
|
|
1003
|
+
y_scale_fn: scales.y,
|
|
1004
|
+
y2_scale_fn: scales.y2,
|
|
1005
|
+
pad,
|
|
1006
|
+
x_range: ranges.current.x,
|
|
1007
|
+
x2_range: ranges.current.x2,
|
|
1008
|
+
y_range: ranges.current.y,
|
|
1009
|
+
y2_range: ranges.current.y2,
|
|
1010
|
+
fullscreen,
|
|
1011
|
+
})}
|
|
1012
|
+
|
|
1013
|
+
{@render ref_lines_layer(ref_lines_by_z.below_grid)}
|
|
1014
|
+
|
|
1015
|
+
<PlotAxis
|
|
1016
|
+
side="x"
|
|
1017
|
+
ticks={ticks.x as number[]}
|
|
1018
|
+
place={scales.x}
|
|
1019
|
+
axis={x_axis}
|
|
1020
|
+
domain={ranges.current.x as Vec2}
|
|
1021
|
+
{pad}
|
|
1022
|
+
{width}
|
|
1023
|
+
{height}
|
|
1024
|
+
show_grid={display.x_grid}
|
|
1025
|
+
tick_label={(tick) =>
|
|
1026
|
+
get_tick_label(tick, cat_axis === `x` ? effective_cat_ticks : x_axis.ticks)}
|
|
1027
|
+
tick_color={cat_axis === `x` ? (tick) => slot_colors.get(tick) : undefined}
|
|
1028
|
+
label_x={pad.l + chart_width / 2 + (x_axis.label_shift?.x ?? 0)}
|
|
1029
|
+
label_y={height - pad.b / 3 + (x_axis.label_shift?.y ?? 0)}
|
|
1030
|
+
/>
|
|
1031
|
+
|
|
1032
|
+
{#if has_secondary && orientation === `horizontal`}
|
|
1033
|
+
<PlotAxis
|
|
1034
|
+
side="x2"
|
|
1035
|
+
ticks={ticks.x2 as number[]}
|
|
1036
|
+
place={scales.x2}
|
|
1037
|
+
axis={x2_axis}
|
|
1038
|
+
domain={ranges.current.x2 as Vec2}
|
|
1039
|
+
{pad}
|
|
1040
|
+
{width}
|
|
1041
|
+
{height}
|
|
1042
|
+
show_grid={display.x2_grid}
|
|
1043
|
+
tick_label={(tick) => get_tick_label(tick, x2_axis.ticks)}
|
|
1044
|
+
label_x={pad.l + chart_width / 2 + (x2_axis.label_shift?.x ?? 0)}
|
|
1045
|
+
label_y={Math.max(12, pad.t - (x2_axis.label_shift?.y ?? 40))}
|
|
1046
|
+
/>
|
|
1047
|
+
{/if}
|
|
1048
|
+
|
|
1049
|
+
<PlotAxis
|
|
1050
|
+
side="y"
|
|
1051
|
+
ticks={ticks.y as number[]}
|
|
1052
|
+
place={scales.y}
|
|
1053
|
+
axis={y_axis}
|
|
1054
|
+
domain={ranges.current.y as Vec2}
|
|
1055
|
+
{pad}
|
|
1056
|
+
{width}
|
|
1057
|
+
{height}
|
|
1058
|
+
show_grid={display.y_grid}
|
|
1059
|
+
tick_label={(tick) =>
|
|
1060
|
+
get_tick_label(tick, cat_axis === `y` ? effective_cat_ticks : y_axis.ticks)}
|
|
1061
|
+
tick_color={cat_axis === `y` ? (tick) => slot_colors.get(tick) : undefined}
|
|
1062
|
+
label_x={Math.max(
|
|
1063
|
+
12,
|
|
1064
|
+
pad.l - (y_axis.tick?.label?.inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
|
|
1065
|
+
) + (y_axis.label_shift?.x ?? 0)}
|
|
1066
|
+
label_y={pad.t + chart_height / 2 + (y_axis.label_shift?.y ?? 0)}
|
|
1067
|
+
/>
|
|
1068
|
+
|
|
1069
|
+
{#if has_secondary && orientation === `vertical`}
|
|
1070
|
+
<PlotAxis
|
|
1071
|
+
side="y2"
|
|
1072
|
+
ticks={ticks.y2 as number[]}
|
|
1073
|
+
place={scales.y2}
|
|
1074
|
+
axis={y2_axis}
|
|
1075
|
+
domain={ranges.current.y2 as Vec2}
|
|
1076
|
+
{pad}
|
|
1077
|
+
{width}
|
|
1078
|
+
{height}
|
|
1079
|
+
show_grid={display.y2_grid}
|
|
1080
|
+
tick_label={(tick) => get_tick_label(tick, y2_axis.ticks)}
|
|
1081
|
+
label_x={y2_axis_label_x(y2_axis, width, pad.r, tick_label_widths.y2_max)}
|
|
1082
|
+
label_y={pad.t + chart_height / 2 + (y2_axis.label_shift?.y ?? 0)}
|
|
1083
|
+
/>
|
|
1084
|
+
{/if}
|
|
1085
|
+
|
|
1086
|
+
<defs>
|
|
1087
|
+
<clipPath id={clip_path_id}>
|
|
1088
|
+
<rect x={pad.l} y={pad.t} width={chart_width} height={chart_height} />
|
|
1089
|
+
</clipPath>
|
|
1090
|
+
</defs>
|
|
1091
|
+
|
|
1092
|
+
<!-- Chart content is clipped in two groups so reference lines can interleave
|
|
1093
|
+
at their z positions while staying outside the chart clip: each line still
|
|
1094
|
+
self-clips to the plot area inside ReferenceLine, only its annotation text
|
|
1095
|
+
is allowed to overflow the plot edges. -->
|
|
1096
|
+
<g clip-path="url(#{clip_path_id})">
|
|
1097
|
+
<ZeroLines
|
|
1098
|
+
{display}
|
|
1099
|
+
x_scale_fn={scales.x}
|
|
1100
|
+
x2_scale_fn={scales.x2}
|
|
1101
|
+
y_scale_fn={scales.y}
|
|
1102
|
+
y2_scale_fn={scales.y2}
|
|
1103
|
+
x_range={ranges.current.x}
|
|
1104
|
+
x2_range={ranges.current.x2}
|
|
1105
|
+
y_range={ranges.current.y}
|
|
1106
|
+
y2_range={ranges.current.y2}
|
|
1107
|
+
x_scale_type={x_axis.scale_type}
|
|
1108
|
+
x2_scale_type={x2_axis.scale_type}
|
|
1109
|
+
y_scale_type={y_axis.scale_type}
|
|
1110
|
+
y2_scale_type={y2_axis.scale_type}
|
|
1111
|
+
has_x2={has_secondary && orientation === `horizontal`}
|
|
1112
|
+
has_y2={has_secondary && orientation === `vertical`}
|
|
1113
|
+
{width}
|
|
1114
|
+
{height}
|
|
1115
|
+
{pad}
|
|
1116
|
+
/>
|
|
1117
|
+
</g>
|
|
1118
|
+
|
|
1119
|
+
{@render ref_lines_layer(ref_lines_by_z.below_lines)}
|
|
1120
|
+
|
|
1121
|
+
<!-- Boxes -->
|
|
1122
|
+
<g clip-path="url(#{clip_path_id})">
|
|
1123
|
+
{#each visible_boxes as box_item (box_item.series.id ?? box_item.idx)}
|
|
1124
|
+
{@const stats = box_item.stats}
|
|
1125
|
+
{#if Number.isFinite(stats.median)}
|
|
1126
|
+
{@const vertical = orientation === `vertical`}
|
|
1127
|
+
{@const cat_scale = vertical ? scales.x : scales.y}
|
|
1128
|
+
{@const val_scale = box_val_scale(box_item.series)}
|
|
1129
|
+
{@const color = box_color(box_item.idx)}
|
|
1130
|
+
{@const draw_box = draws_box(box_item.series)}
|
|
1131
|
+
{@const kde = violin_kdes.get(box_item.idx)}
|
|
1132
|
+
{@const eff_side = box_item.series.side ?? side}
|
|
1133
|
+
{@const bw = box_item.series.box_width ??
|
|
1134
|
+
(kde ? DEFAULTS.box.violin_box_width : DEFAULTS.box.box_width)}
|
|
1135
|
+
{@const c_lo = cat_scale(box_item.slot - bw / 2)}
|
|
1136
|
+
{@const c_hi = cat_scale(box_item.slot + bw / 2)}
|
|
1137
|
+
{@const c_center = cat_scale(box_item.slot)}
|
|
1138
|
+
{@const cap = Math.abs(c_hi - c_lo) * (whisker_state.cap_fraction ?? 0.5) / 2}
|
|
1139
|
+
{@const cap_lo = c_center - cap}
|
|
1140
|
+
{@const cap_hi = c_center + cap}
|
|
1141
|
+
{@const v_q1 = val_scale(stats.q1)}
|
|
1142
|
+
{@const v_q3 = val_scale(stats.q3)}
|
|
1143
|
+
{@const v_med = val_scale(stats.median)}
|
|
1144
|
+
{@const v_wl = val_scale(stats.whisker_low)}
|
|
1145
|
+
{@const v_wh = val_scale(stats.whisker_high)}
|
|
1146
|
+
{@const v_mean = val_scale(stats.mean)}
|
|
1147
|
+
{@const pt = (cross: number, val: number): [number, number] =>
|
|
1148
|
+
vertical ? [cross, val] : [val, cross]}
|
|
1149
|
+
{@const [q1x, q1y] = pt(c_lo, v_q1)}
|
|
1150
|
+
{@const [q3x, q3y] = pt(c_hi, v_q3)}
|
|
1151
|
+
{@const [wlx, wly] = pt(c_lo, v_wl)}
|
|
1152
|
+
{@const [whx, why] = pt(c_hi, v_wh)}
|
|
1153
|
+
{@const box_x = Math.min(q1x, q3x)}
|
|
1154
|
+
{@const box_y = Math.min(q1y, q3y)}
|
|
1155
|
+
{@const box_w = Math.abs(q3x - q1x)}
|
|
1156
|
+
{@const box_h = Math.abs(q3y - q1y)}
|
|
1157
|
+
{@const hit_x = Math.min(wlx, whx)}
|
|
1158
|
+
{@const hit_y = Math.min(wly, why)}
|
|
1159
|
+
{@const hit_w = Math.abs(whx - wlx)}
|
|
1160
|
+
{@const hit_h = Math.abs(why - wly)}
|
|
1161
|
+
{@const [label_x, label_y] = vertical
|
|
1162
|
+
? [c_center, Math.min(v_wh, v_wl) - 6]
|
|
1163
|
+
: [Math.max(v_wh, v_wl) + 6, c_center]}
|
|
1164
|
+
{@const violin_half = Math.abs(
|
|
1165
|
+
cat_scale(box_item.slot + (box_item.series.violin_width ?? violin_width) / 2) -
|
|
1166
|
+
c_center,
|
|
1167
|
+
)}
|
|
1168
|
+
{@const max_density = kde ? (violin_max_density.get(box_item.idx) ?? 0) : 0}
|
|
1169
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
|
1170
|
+
<g
|
|
1171
|
+
class="box-series"
|
|
1172
|
+
data-box-idx={box_item.idx}
|
|
1173
|
+
role="button"
|
|
1174
|
+
tabindex="0"
|
|
1175
|
+
aria-label={`box ${box_item.idx + 1}: ${box_item.series.label ?? ``}`}
|
|
1176
|
+
style:cursor={on_box_click ? `pointer` : undefined}
|
|
1177
|
+
opacity={hovered_legend_series_idx !== null &&
|
|
1178
|
+
hovered_legend_series_idx !== box_item.idx
|
|
1179
|
+
? 0.25
|
|
1180
|
+
: 1}
|
|
1181
|
+
onmousemove={handle_box_hover(box_item, color)}
|
|
1182
|
+
onmouseleave={() => {
|
|
1183
|
+
hover_info = null
|
|
1184
|
+
change(null)
|
|
1185
|
+
on_box_hover?.(null)
|
|
1186
|
+
}}
|
|
1187
|
+
onclick={(evt) => on_box_click?.({ ...get_box_data(box_item, color), event: evt })}
|
|
1188
|
+
onkeydown={(evt) => {
|
|
1189
|
+
if (evt.key === `Enter` || evt.key === ` `) {
|
|
1190
|
+
evt.preventDefault()
|
|
1191
|
+
on_box_click?.({ ...get_box_data(box_item, color), event: evt })
|
|
1192
|
+
}
|
|
1193
|
+
}}
|
|
1194
|
+
>
|
|
1195
|
+
<!-- violin (KDE density) -->
|
|
1196
|
+
{#if kde && max_density > 0}
|
|
1197
|
+
{@const grid_px = kde.grid.map((g_val) => val_scale(g_val))}
|
|
1198
|
+
{@const offsets = kde.density.map((den) => (den / max_density) * violin_half)}
|
|
1199
|
+
{@const screen_side = to_screen_side(eff_side, vertical)}
|
|
1200
|
+
<path
|
|
1201
|
+
class="violin-area"
|
|
1202
|
+
d={violin_path(grid_px, offsets, c_center, screen_side, pt)}
|
|
1203
|
+
fill={color}
|
|
1204
|
+
fill-opacity={violin_state.opacity}
|
|
1205
|
+
stroke={color}
|
|
1206
|
+
stroke-width={violin_state.stroke_width}
|
|
1207
|
+
/>
|
|
1208
|
+
{/if}
|
|
1209
|
+
{#if draw_box}
|
|
1210
|
+
{@const wc = whisker_state.color}
|
|
1211
|
+
{@const ww = whisker_state.width}
|
|
1212
|
+
<!-- whiskers + caps -->
|
|
1213
|
+
{@render seg(pt(c_center, v_q1), pt(c_center, v_wl), wc, ww)}
|
|
1214
|
+
{@render seg(pt(c_center, v_q3), pt(c_center, v_wh), wc, ww)}
|
|
1215
|
+
{#if cap > 0}
|
|
1216
|
+
{@render seg(pt(cap_lo, v_wl), pt(cap_hi, v_wl), wc, ww)}
|
|
1217
|
+
{@render seg(pt(cap_lo, v_wh), pt(cap_hi, v_wh), wc, ww)}
|
|
1218
|
+
{/if}
|
|
1219
|
+
<!-- IQR box -->
|
|
1220
|
+
<rect
|
|
1221
|
+
class="iqr-box"
|
|
1222
|
+
x={box_x}
|
|
1223
|
+
y={box_y}
|
|
1224
|
+
width={Math.max(1, box_w)}
|
|
1225
|
+
height={Math.max(1, box_h)}
|
|
1226
|
+
rx={box_state.border_radius}
|
|
1227
|
+
ry={box_state.border_radius}
|
|
1228
|
+
fill={color}
|
|
1229
|
+
fill-opacity={box_state.opacity}
|
|
1230
|
+
stroke={box_state.stroke_color}
|
|
1231
|
+
stroke-width={box_state.stroke_width}
|
|
1232
|
+
/>
|
|
1233
|
+
<!-- median (solid) and mean (dashed) -->
|
|
1234
|
+
{@render seg(pt(c_lo, v_med), pt(c_hi, v_med), median_state.color, median_state.width)}
|
|
1235
|
+
{#if show_mean}
|
|
1236
|
+
{@render seg(
|
|
1237
|
+
pt(c_lo, v_mean),
|
|
1238
|
+
pt(c_hi, v_mean),
|
|
1239
|
+
median_state.color,
|
|
1240
|
+
median_state.width,
|
|
1241
|
+
`3 2`,
|
|
1242
|
+
)}
|
|
1243
|
+
{/if}
|
|
1244
|
+
<!-- outliers -->
|
|
1245
|
+
{#if show_outliers}
|
|
1246
|
+
{#each stats.outliers as outlier, out_idx (out_idx)}
|
|
1247
|
+
{@const [ox, oy] = pt(c_center, val_scale(outlier))}
|
|
1248
|
+
<circle
|
|
1249
|
+
cx={ox}
|
|
1250
|
+
cy={oy}
|
|
1251
|
+
r={outlier_state.radius}
|
|
1252
|
+
fill={color}
|
|
1253
|
+
fill-opacity={outlier_state.opacity}
|
|
1254
|
+
stroke={box_state.stroke_color}
|
|
1255
|
+
stroke-width={outlier_state.stroke_width}
|
|
1256
|
+
/>
|
|
1257
|
+
{/each}
|
|
1258
|
+
{/if}
|
|
1259
|
+
{/if}
|
|
1260
|
+
<!-- value label -->
|
|
1261
|
+
{#if show_value_labels}
|
|
1262
|
+
<text
|
|
1263
|
+
x={label_x}
|
|
1264
|
+
y={label_y}
|
|
1265
|
+
text-anchor={vertical ? `middle` : `start`}
|
|
1266
|
+
dominant-baseline={vertical ? `auto` : `central`}
|
|
1267
|
+
class="value-label"
|
|
1268
|
+
fill={color}
|
|
1269
|
+
>
|
|
1270
|
+
{value_label_for(stats)}
|
|
1271
|
+
</text>
|
|
1272
|
+
{/if}
|
|
1273
|
+
<!-- transparent backing so the box/whisker region is hoverable (the violin
|
|
1274
|
+
path is a painted child and bubbles to the group's pointer handlers too) -->
|
|
1275
|
+
<rect
|
|
1276
|
+
class="hover-target"
|
|
1277
|
+
x={hit_x}
|
|
1278
|
+
y={hit_y}
|
|
1279
|
+
width={Math.max(1, hit_w)}
|
|
1280
|
+
height={Math.max(1, hit_h)}
|
|
1281
|
+
fill="transparent"
|
|
1282
|
+
/>
|
|
1283
|
+
</g>
|
|
1284
|
+
{/if}
|
|
1285
|
+
{/each}
|
|
1286
|
+
</g>
|
|
1287
|
+
|
|
1288
|
+
{@render ref_lines_layer(ref_lines_by_z.below_points)}
|
|
1289
|
+
{@render ref_lines_layer(ref_lines_by_z.above_all)}
|
|
1290
|
+
</svg>
|
|
1291
|
+
|
|
1292
|
+
{#if legend && should_show_legend}
|
|
1293
|
+
{@const legend_left = legend_auto_outside
|
|
1294
|
+
? legend_outside_x
|
|
1295
|
+
: legend_placement
|
|
1296
|
+
? tweened_legend_coords.current.x
|
|
1297
|
+
: pad.l + 10}
|
|
1298
|
+
{@const legend_top = legend_auto_outside
|
|
1299
|
+
? legend_outside_y
|
|
1300
|
+
: legend_placement
|
|
1301
|
+
? tweened_legend_coords.current.y
|
|
1302
|
+
: pad.t + 10}
|
|
1303
|
+
<PlotLegend
|
|
1304
|
+
bind:root_element={legend_element}
|
|
1305
|
+
{...legend}
|
|
1306
|
+
series_data={legend_data}
|
|
1307
|
+
on_toggle={legend?.on_toggle ?? legend_vis.on_toggle}
|
|
1308
|
+
on_group_toggle={legend?.on_group_toggle ?? legend_vis.on_group_toggle}
|
|
1309
|
+
on_double_click={legend?.on_double_click ?? legend_vis.on_double_click}
|
|
1310
|
+
on_hover_change={legend_hover.set_locked}
|
|
1311
|
+
on_item_hover={(item) =>
|
|
1312
|
+
(hovered_legend_series_idx = item != null && item.series_idx >= 0
|
|
1313
|
+
? item.series_idx
|
|
1314
|
+
: null)}
|
|
1315
|
+
active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
|
|
1316
|
+
style={`position: absolute; left: ${legend_left}px; top: ${legend_top}px; pointer-events: auto; ${
|
|
1317
|
+
legend?.style || ``
|
|
1318
|
+
}`}
|
|
1319
|
+
/>
|
|
1320
|
+
{/if}
|
|
1321
|
+
|
|
1322
|
+
{#if hover_info && hovered}
|
|
1323
|
+
<PlotTooltip
|
|
1324
|
+
x={hover_info.cx}
|
|
1325
|
+
y={hover_info.cy}
|
|
1326
|
+
offset={{ x: 10, y: 5 }}
|
|
1327
|
+
constrain_to={{ width, height }}
|
|
1328
|
+
fallback_size={{ width: 140, height: 50 }}
|
|
1329
|
+
bg_color={hover_info.color}
|
|
1330
|
+
>
|
|
1331
|
+
{#if tooltip}
|
|
1332
|
+
{@render tooltip({ ...hover_info, fullscreen })}
|
|
1333
|
+
{:else}
|
|
1334
|
+
{@const fmt = (orientation === `vertical` ? y_axis.format : x_axis.format) || `.3~s`}
|
|
1335
|
+
{@const stat = hover_info.stats}
|
|
1336
|
+
{@const rows = [
|
|
1337
|
+
[`max`, stat.whisker_high],
|
|
1338
|
+
[`q3`, stat.q3],
|
|
1339
|
+
[`median`, stat.median],
|
|
1340
|
+
[`q1`, stat.q1],
|
|
1341
|
+
[`min`, stat.whisker_low],
|
|
1342
|
+
...(show_mean ? [[`mean`, stat.mean] as const] : []),
|
|
1343
|
+
] as const}
|
|
1344
|
+
{#if hover_info.category_label}
|
|
1345
|
+
<div><strong>{hover_info.category_label}</strong></div>
|
|
1346
|
+
{/if}
|
|
1347
|
+
{#each rows as [label, value] (label)}
|
|
1348
|
+
<div>{label}: {format_value(value, fmt)}</div>
|
|
1349
|
+
{/each}
|
|
1350
|
+
{#if show_outliers && stat.outliers.length > 0}
|
|
1351
|
+
<div>outliers: {stat.outliers.length}</div>
|
|
1352
|
+
{/if}
|
|
1353
|
+
{/if}
|
|
1354
|
+
</PlotTooltip>
|
|
1355
|
+
{/if}
|
|
1356
|
+
|
|
1357
|
+
{#if show_controls}
|
|
1358
|
+
<BoxPlotControls
|
|
1359
|
+
toggle_props={{
|
|
1360
|
+
...controls_toggle_props,
|
|
1361
|
+
style: `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); ${
|
|
1362
|
+
controls_toggle_props?.style ?? ``
|
|
1363
|
+
}`,
|
|
1364
|
+
}}
|
|
1365
|
+
pane_props={controls_pane_props}
|
|
1366
|
+
bind:show_controls
|
|
1367
|
+
bind:controls_open
|
|
1368
|
+
bind:orientation
|
|
1369
|
+
bind:whisker_mode
|
|
1370
|
+
bind:show_outliers
|
|
1371
|
+
bind:show_mean
|
|
1372
|
+
bind:kind
|
|
1373
|
+
bind:side
|
|
1374
|
+
bind:x_axis
|
|
1375
|
+
bind:x2_axis={x2_axis_prop}
|
|
1376
|
+
bind:y_axis
|
|
1377
|
+
bind:y2_axis={y2_axis_prop}
|
|
1378
|
+
bind:display
|
|
1379
|
+
auto_x_range={auto_ranges.x as Vec2}
|
|
1380
|
+
auto_x2_range={auto_ranges.x2 as Vec2}
|
|
1381
|
+
auto_y_range={auto_ranges.y as Vec2}
|
|
1382
|
+
auto_y2_range={auto_ranges.y2 as Vec2}
|
|
1383
|
+
has_x2_points={has_secondary && orientation === `horizontal`}
|
|
1384
|
+
has_y2_points={has_secondary && orientation === `vertical`}
|
|
1385
|
+
children={controls_extra}
|
|
1386
|
+
/>
|
|
1387
|
+
{/if}
|
|
1388
|
+
{/if}
|
|
1389
|
+
|
|
1390
|
+
{@render children?.({ height, width, fullscreen })}
|
|
1391
|
+
</div>
|
|
1392
|
+
|
|
1393
|
+
<style>
|
|
1394
|
+
.box-plot {
|
|
1395
|
+
position: relative;
|
|
1396
|
+
width: 100%;
|
|
1397
|
+
height: var(--boxplot-height, auto);
|
|
1398
|
+
min-height: var(--boxplot-min-height, 300px);
|
|
1399
|
+
container-type: size;
|
|
1400
|
+
z-index: var(--boxplot-z-index, auto);
|
|
1401
|
+
border-radius: var(--boxplot-border-radius, var(--border-radius, 3pt));
|
|
1402
|
+
flex: var(--boxplot-flex, 1);
|
|
1403
|
+
display: var(--boxplot-display, flex);
|
|
1404
|
+
flex-direction: column;
|
|
1405
|
+
background: var(--boxplot-bg, var(--plot-bg));
|
|
1406
|
+
}
|
|
1407
|
+
.box-plot.fullscreen {
|
|
1408
|
+
position: fixed;
|
|
1409
|
+
top: 0;
|
|
1410
|
+
left: 0;
|
|
1411
|
+
width: 100vw !important;
|
|
1412
|
+
height: 100vh !important;
|
|
1413
|
+
z-index: var(--boxplot-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
|
|
1414
|
+
margin: 0;
|
|
1415
|
+
border-radius: 0;
|
|
1416
|
+
background: var(--boxplot-fullscreen-bg, var(--boxplot-bg, var(--plot-bg)));
|
|
1417
|
+
max-height: none !important;
|
|
1418
|
+
overflow: hidden;
|
|
1419
|
+
/* border-top (not padding-top): bind:clientHeight includes padding but excludes
|
|
1420
|
+
borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
|
|
1421
|
+
border-top: var(--plot-fullscreen-padding-top, 2em) solid
|
|
1422
|
+
var(--boxplot-fullscreen-bg, var(--boxplot-bg, var(--plot-bg, transparent)));
|
|
1423
|
+
box-sizing: border-box;
|
|
1424
|
+
}
|
|
1425
|
+
.header-controls {
|
|
1426
|
+
position: absolute;
|
|
1427
|
+
top: var(--ctrl-btn-top, 5pt);
|
|
1428
|
+
right: var(--fullscreen-btn-right, 4px);
|
|
1429
|
+
z-index: var(--fullscreen-btn-z-index, 10);
|
|
1430
|
+
display: flex;
|
|
1431
|
+
align-items: center;
|
|
1432
|
+
gap: 8px;
|
|
1433
|
+
}
|
|
1434
|
+
.header-controls :global(.fullscreen-toggle) {
|
|
1435
|
+
position: static;
|
|
1436
|
+
opacity: 1;
|
|
1437
|
+
}
|
|
1438
|
+
.box-plot :global(.pane-toggle),
|
|
1439
|
+
.box-plot .header-controls {
|
|
1440
|
+
opacity: 0;
|
|
1441
|
+
transition: opacity 0.2s, background-color 0.2s;
|
|
1442
|
+
}
|
|
1443
|
+
.box-plot:hover :global(.pane-toggle),
|
|
1444
|
+
.box-plot:hover .header-controls,
|
|
1445
|
+
.box-plot :global(.pane-toggle:focus-visible),
|
|
1446
|
+
.box-plot :global(.pane-toggle[aria-expanded='true']),
|
|
1447
|
+
.box-plot .header-controls:focus-within {
|
|
1448
|
+
opacity: 1;
|
|
1449
|
+
}
|
|
1450
|
+
svg {
|
|
1451
|
+
width: var(--boxplot-svg-width, 100%);
|
|
1452
|
+
height: var(--boxplot-svg-height, 100%);
|
|
1453
|
+
flex: var(--boxplot-svg-flex, 1);
|
|
1454
|
+
overflow: var(--boxplot-svg-overflow, visible);
|
|
1455
|
+
fill: var(--text-color);
|
|
1456
|
+
font-weight: var(--scatter-font-weight);
|
|
1457
|
+
font-size: var(--scatter-font-size);
|
|
1458
|
+
}
|
|
1459
|
+
.value-label {
|
|
1460
|
+
font-size: 11px;
|
|
1461
|
+
}
|
|
1462
|
+
</style>
|