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,1045 @@
|
|
|
1
|
+
<script
|
|
2
|
+
lang="ts"
|
|
3
|
+
generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
|
|
4
|
+
>
|
|
5
|
+
import type { D3InterpolateName } from '../../colors'
|
|
6
|
+
import { pick_contrast_color } from '../../colors'
|
|
7
|
+
import { export_svg_as_png, export_svg_as_svg } from '../../io/export'
|
|
8
|
+
import { format_value } from '../../labels'
|
|
9
|
+
import { FullscreenToggle, set_fullscreen_bg } from '../../layout'
|
|
10
|
+
import { DEG_TO_RAD } from '../../math'
|
|
11
|
+
import type {
|
|
12
|
+
BasePlotProps,
|
|
13
|
+
LegendConfig,
|
|
14
|
+
LegendItem,
|
|
15
|
+
SunburstLabelRotation,
|
|
16
|
+
SunburstLabelText,
|
|
17
|
+
SunburstNode,
|
|
18
|
+
SunburstNodeHandlerProps,
|
|
19
|
+
SunburstShape,
|
|
20
|
+
SunburstSort,
|
|
21
|
+
SunburstValueMode,
|
|
22
|
+
} from '..'
|
|
23
|
+
import { ColorBar, PlotLegend, PlotTooltip, SunburstControls } from '..'
|
|
24
|
+
import { closest_data_idx, get_relative_coords } from '../core/interactions'
|
|
25
|
+
import {
|
|
26
|
+
compute_element_placement,
|
|
27
|
+
filter_padding,
|
|
28
|
+
measure_text_width,
|
|
29
|
+
} from '../core/layout'
|
|
30
|
+
import type { Sides } from '../core/layout'
|
|
31
|
+
import { create_color_scale } from '../core/scales'
|
|
32
|
+
import {
|
|
33
|
+
arc_label_transform,
|
|
34
|
+
arrow_nav_target,
|
|
35
|
+
project_arcs,
|
|
36
|
+
type ScreenArc as ScreenArcOf,
|
|
37
|
+
} from './render'
|
|
38
|
+
import { compute_sunburst_layout, type PositionedArc } from './sunburst'
|
|
39
|
+
import { DEFAULTS } from '../../settings'
|
|
40
|
+
import { arc as d3_arc } from 'd3-shape'
|
|
41
|
+
import { type ComponentProps, type Snippet, tick, untrack } from 'svelte'
|
|
42
|
+
import { cubicInOut } from 'svelte/easing'
|
|
43
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
44
|
+
import { Tween, type TweenOptions } from 'svelte/motion'
|
|
45
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
46
|
+
|
|
47
|
+
const DEFAULT_PADDING: Required<Sides> = { t: 10, b: 10, l: 10, r: 10 }
|
|
48
|
+
|
|
49
|
+
// An arc with its current screen-space geometry (angles in radians, radii in px)
|
|
50
|
+
type ScreenArc = ScreenArcOf<Metadata>
|
|
51
|
+
|
|
52
|
+
let {
|
|
53
|
+
data = $bindable([]),
|
|
54
|
+
shape = $bindable(DEFAULTS.sunburst.shape),
|
|
55
|
+
value_mode = $bindable(DEFAULTS.sunburst.value_mode),
|
|
56
|
+
sort = `none`,
|
|
57
|
+
level_lighten = 0,
|
|
58
|
+
min_fraction = $bindable(DEFAULTS.sunburst.min_fraction),
|
|
59
|
+
other_label = `Other`,
|
|
60
|
+
max_depth = $bindable(DEFAULTS.sunburst.max_depth),
|
|
61
|
+
inner_radius = $bindable(DEFAULTS.sunburst.inner_radius),
|
|
62
|
+
pad_angle = $bindable(DEFAULTS.sunburst.pad_angle),
|
|
63
|
+
show_labels = $bindable(DEFAULTS.sunburst.show_labels),
|
|
64
|
+
label_rotation = $bindable(DEFAULTS.sunburst.label_rotation),
|
|
65
|
+
label_text = $bindable(DEFAULTS.sunburst.label_text),
|
|
66
|
+
zoom_on_click = $bindable(DEFAULTS.sunburst.zoom_on_click),
|
|
67
|
+
zoom_root_id = $bindable(null),
|
|
68
|
+
show_breadcrumbs = $bindable(DEFAULTS.sunburst.show_breadcrumbs),
|
|
69
|
+
color_values,
|
|
70
|
+
color_scale = `interpolateViridis`,
|
|
71
|
+
color_range,
|
|
72
|
+
colorbar = {},
|
|
73
|
+
export_buttons = true,
|
|
74
|
+
export_filename = `sunburst`,
|
|
75
|
+
tween,
|
|
76
|
+
value_format = `,`,
|
|
77
|
+
padding = DEFAULT_PADDING,
|
|
78
|
+
legend = {},
|
|
79
|
+
show_legend = false,
|
|
80
|
+
tooltip,
|
|
81
|
+
arc_content,
|
|
82
|
+
center_content,
|
|
83
|
+
hovered = $bindable(false),
|
|
84
|
+
change = () => {},
|
|
85
|
+
on_node_click,
|
|
86
|
+
on_node_hover,
|
|
87
|
+
on_zoom,
|
|
88
|
+
show_controls = $bindable(true),
|
|
89
|
+
controls_open = $bindable(false),
|
|
90
|
+
controls_toggle_props,
|
|
91
|
+
controls_pane_props,
|
|
92
|
+
fullscreen = $bindable(false),
|
|
93
|
+
fullscreen_toggle = true,
|
|
94
|
+
children,
|
|
95
|
+
header_controls,
|
|
96
|
+
controls_extra,
|
|
97
|
+
...rest
|
|
98
|
+
}: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, `change`> & {
|
|
99
|
+
data?: SunburstNode<Metadata> | SunburstNode<Metadata>[]
|
|
100
|
+
shape?: SunburstShape // polar rings (sunburst) or stacked rows (icicle)
|
|
101
|
+
value_mode?: SunburstValueMode
|
|
102
|
+
sort?: SunburstSort
|
|
103
|
+
level_lighten?: number
|
|
104
|
+
// Aggregate sibling arcs below this fraction of the total into one 'Other' leaf
|
|
105
|
+
// per parent (only when >= 2 qualify); 0 disables
|
|
106
|
+
min_fraction?: number
|
|
107
|
+
other_label?: string
|
|
108
|
+
max_depth?: number // rings shown below the current zoom root (0 = all)
|
|
109
|
+
inner_radius?: number // center hole as fraction of outer radius
|
|
110
|
+
pad_angle?: number // degrees between sibling arcs
|
|
111
|
+
show_labels?: boolean
|
|
112
|
+
label_rotation?: SunburstLabelRotation
|
|
113
|
+
label_text?: SunburstLabelText // what labels display (plotly textinfo equivalent)
|
|
114
|
+
zoom_on_click?: boolean
|
|
115
|
+
zoom_root_id?: string | number | null // id of the arc the view is rooted on
|
|
116
|
+
show_breadcrumbs?: boolean // clickable ancestor trail when zoomed
|
|
117
|
+
// Color arcs by a numeric metric (continuous colormap) instead of categorical
|
|
118
|
+
// inheritance; return null to keep an arc's categorical color
|
|
119
|
+
color_values?: (arc: PositionedArc<Metadata>) => number | null
|
|
120
|
+
color_scale?: D3InterpolateName
|
|
121
|
+
color_range?: [number, number] // defaults to the metric's [min, max]
|
|
122
|
+
colorbar?: ComponentProps<typeof ColorBar> | null // null hides it
|
|
123
|
+
export_buttons?: boolean // SVG/PNG download buttons in the controls pane
|
|
124
|
+
export_filename?: string
|
|
125
|
+
tween?: TweenOptions<{ x0: number; x1: number; y0: number; n_rings: number }>
|
|
126
|
+
value_format?: string
|
|
127
|
+
padding?: Sides
|
|
128
|
+
legend?: LegendConfig | null
|
|
129
|
+
show_legend?: boolean
|
|
130
|
+
tooltip?: Snippet<[SunburstNodeHandlerProps<Metadata>]>
|
|
131
|
+
// Fully replace the default arc path. NOTE: this also replaces the built-in
|
|
132
|
+
// hover/focus/click + tooltip wiring, so re-implement any interactivity you
|
|
133
|
+
// need inside the snippet.
|
|
134
|
+
arc_content?: Snippet<
|
|
135
|
+
[{ arc: PositionedArc<Metadata>; a0: number; a1: number; r0: number; r1: number }]
|
|
136
|
+
>
|
|
137
|
+
center_content?: Snippet<
|
|
138
|
+
[{ root: PositionedArc<Metadata> | null; radius: number; zoomed: boolean }]
|
|
139
|
+
>
|
|
140
|
+
change?: (data: SunburstNodeHandlerProps<Metadata> | null) => void
|
|
141
|
+
on_node_click?: (
|
|
142
|
+
data: SunburstNodeHandlerProps<Metadata> & { event: MouseEvent | KeyboardEvent },
|
|
143
|
+
) => void
|
|
144
|
+
on_node_hover?: (
|
|
145
|
+
data:
|
|
146
|
+
| (SunburstNodeHandlerProps<Metadata> & { event: MouseEvent | FocusEvent })
|
|
147
|
+
| null,
|
|
148
|
+
) => void
|
|
149
|
+
on_zoom?: (data: { root: SunburstNodeHandlerProps<Metadata> | null }) => void
|
|
150
|
+
header_controls?: Snippet<[{ height: number; width: number; fullscreen: boolean }]>
|
|
151
|
+
controls_extra?: Snippet<[{ zoom_root_id: string | number | null }]>
|
|
152
|
+
} = $props()
|
|
153
|
+
|
|
154
|
+
let [width, height] = $state([0, 0])
|
|
155
|
+
let wrapper: HTMLDivElement | undefined = $state()
|
|
156
|
+
let svg_element: SVGSVGElement | null = $state(null)
|
|
157
|
+
let center_el: SVGCircleElement | null = $state(null)
|
|
158
|
+
|
|
159
|
+
let hovered_idx = $state<number | null>(null)
|
|
160
|
+
let hover_info = $state<SunburstNodeHandlerProps<Metadata> | null>(null)
|
|
161
|
+
let hover_pos = $state<{ x: number; y: number }>({ x: 0, y: 0 })
|
|
162
|
+
// Depth-1 category ids muted via legend toggle (dimmed, not removed - keeps layout stable)
|
|
163
|
+
let muted_ids = new SvelteSet<string | number>()
|
|
164
|
+
|
|
165
|
+
let pad = $derived(filter_padding(padding, DEFAULT_PADDING))
|
|
166
|
+
let inner_width = $derived(Math.max(0, width - pad.l - pad.r))
|
|
167
|
+
let avail_height = $derived(Math.max(0, height - pad.t - pad.b))
|
|
168
|
+
// measured height of the bottom colorbar, reserved from the chart so it never overlaps
|
|
169
|
+
// the arcs (16px covers its bottom offset + a small gap). reset to 0 when the colorbar
|
|
170
|
+
// is hidden (effect below) since bind:clientHeight doesn't clear on unmount; capped at
|
|
171
|
+
// half the area so a bad measurement can't collapse the chart
|
|
172
|
+
let colorbar_height = $state(0)
|
|
173
|
+
let colorbar_reserve = $derived(
|
|
174
|
+
colorbar_height > 0 ? Math.min(colorbar_height + 16, avail_height / 2) : 0,
|
|
175
|
+
)
|
|
176
|
+
let inner_height = $derived(avail_height - colorbar_reserve)
|
|
177
|
+
|
|
178
|
+
// Degrade to an empty layout (instead of crashing the host page) on invalid data.
|
|
179
|
+
// Layout depends only on data/value semantics - not on size or zoom.
|
|
180
|
+
let layout = $derived.by(() => {
|
|
181
|
+
try {
|
|
182
|
+
return compute_sunburst_layout(data, {
|
|
183
|
+
value_mode,
|
|
184
|
+
sort,
|
|
185
|
+
level_lighten,
|
|
186
|
+
min_fraction,
|
|
187
|
+
other_label,
|
|
188
|
+
})
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error(err)
|
|
191
|
+
return { arcs: [], root: null, max_depth: 0 }
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
let arc_by_id = $derived(new Map(layout.arcs.map((arc) => [arc.id, arc])))
|
|
195
|
+
|
|
196
|
+
// Resolve the zoom root; stale ids (e.g. after a data swap) fall back to the root
|
|
197
|
+
let zoom_root = $derived(
|
|
198
|
+
(zoom_root_id != null ? arc_by_id.get(zoom_root_id) : null) ?? layout.root,
|
|
199
|
+
)
|
|
200
|
+
let zoomed = $derived((zoom_root?.depth ?? 0) > 0)
|
|
201
|
+
|
|
202
|
+
// Drop muted ids that no longer exist when data changes (untrack avoids a
|
|
203
|
+
// self-trigger loop from reading/writing muted_ids in the same effect).
|
|
204
|
+
// Hover/focus state is index-based, so a layout swap would otherwise leave a stale
|
|
205
|
+
// tooltip and highlight whatever unrelated node now occupies the old index.
|
|
206
|
+
$effect(() => {
|
|
207
|
+
const valid = new Set(
|
|
208
|
+
layout.arcs.filter((arc) => arc.depth === 1).map((arc) => arc.id),
|
|
209
|
+
)
|
|
210
|
+
untrack(() => {
|
|
211
|
+
for (const id of muted_ids) if (!valid.has(id)) muted_ids.delete(id)
|
|
212
|
+
set_arc_hover(null)
|
|
213
|
+
focused_idx = null
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// The view window in normalized partition coordinates: the zoom root's angular
|
|
218
|
+
// span + how many rings to show below it
|
|
219
|
+
let view_target = $derived.by(() => {
|
|
220
|
+
const below = layout.root ? layout.max_depth - (zoom_root?.depth ?? 0) : 1
|
|
221
|
+
return {
|
|
222
|
+
x0: zoom_root?.x0 ?? 0,
|
|
223
|
+
x1: zoom_root?.x1 ?? 1,
|
|
224
|
+
y0: zoom_root?.y0 ?? 0,
|
|
225
|
+
n_rings: Math.max(1, max_depth > 0 ? Math.min(max_depth, below) : below),
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Zooming tweens this single object; all arc geometry re-derives from view.current
|
|
230
|
+
// each frame via clamping scales (the classic zoomable-sunburst trick - no per-arc
|
|
231
|
+
// tweens, no re-layout). Tween.of seeds it at view_target (charts load fully drawn)
|
|
232
|
+
// then re-targets on change via a render-effect that reads only view_target, never
|
|
233
|
+
// view.current - so the tween can't feed back into its own target. untrack reads the
|
|
234
|
+
// tween options once at init (they're not meant to update reactively).
|
|
235
|
+
const view = Tween.of(
|
|
236
|
+
() => view_target,
|
|
237
|
+
untrack(() => ({ duration: 400, easing: cubicInOut, ...tween })),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Pixel geometry
|
|
241
|
+
let radius = $derived(Math.max(0, Math.min(inner_width, inner_height) / 2))
|
|
242
|
+
let cx = $derived(pad.l + inner_width / 2)
|
|
243
|
+
let cy = $derived(pad.t + inner_height / 2)
|
|
244
|
+
// Min 14px center hole when zoomed so there's always a zoom-out click target
|
|
245
|
+
let hole_r = $derived(Math.max(inner_radius * radius, zoomed ? 14 : 0))
|
|
246
|
+
|
|
247
|
+
let screen_geom = $derived({ shape, inner_width, inner_height, radius, hole_r })
|
|
248
|
+
|
|
249
|
+
// Projected with view.current once per animation frame; project_arcs is also called
|
|
250
|
+
// with view.target where settled geometry suffices (e.g. legend placement, which
|
|
251
|
+
// shouldn't rerun per frame)
|
|
252
|
+
let projection = $derived(project_arcs(layout.arcs, view.current, screen_geom))
|
|
253
|
+
let screen_arcs = $derived(projection.all)
|
|
254
|
+
// Rendering iterates only non-collapsed arcs - when zoomed into a small subtree of
|
|
255
|
+
// a large hierarchy this keeps per-frame template work proportional to what's on screen
|
|
256
|
+
let visible_arcs = $derived(projection.visible)
|
|
257
|
+
|
|
258
|
+
// Roving tabindex: exactly one arc is in the tab order (the last-focused one, else
|
|
259
|
+
// the first visible clickable arc); arrow keys move focus between arcs. Without
|
|
260
|
+
// this, tabbing through a large chart would visit every single arc.
|
|
261
|
+
let focused_idx = $state<number | null>(null)
|
|
262
|
+
let roving_idx = $derived.by(() => {
|
|
263
|
+
if (focused_idx != null && screen_arcs[focused_idx]?.visible) return focused_idx
|
|
264
|
+
return visible_arcs.find((screen) => arc_clickable(screen.arc))?.arc.node_idx ?? null
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
let arc_gen = $derived(
|
|
268
|
+
d3_arc<ScreenArc>()
|
|
269
|
+
.startAngle((d) => d.a0)
|
|
270
|
+
.endAngle((d) => d.a1)
|
|
271
|
+
.innerRadius((d) => d.r0)
|
|
272
|
+
.outerRadius((d) => d.r1)
|
|
273
|
+
.padAngle(pad_angle * DEG_TO_RAD)
|
|
274
|
+
.padRadius(radius || 1),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Path data for one arc/rect in the current shape
|
|
278
|
+
const screen_path = (screen: ScreenArc): string =>
|
|
279
|
+
shape === `icicle`
|
|
280
|
+
? `M${screen.a0},${screen.r0}H${screen.a1}V${screen.r1}H${screen.a0}Z`
|
|
281
|
+
: arc_gen(screen) ?? ``
|
|
282
|
+
|
|
283
|
+
// The chart group's transform: sunburst draws around the center, icicle from the
|
|
284
|
+
// top-left of the padded plot area
|
|
285
|
+
let chart_transform = $derived(
|
|
286
|
+
shape === `icicle` ? `translate(${pad.l}, ${pad.t})` : `translate(${cx}, ${cy})`,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
// Arc centroid in container (pad-offset) pixel space, for tooltip + legend placement
|
|
290
|
+
const arc_center = (d: ScreenArc): { x: number; y: number } => {
|
|
291
|
+
if (shape === `icicle`) {
|
|
292
|
+
return { x: pad.l + (d.a0 + d.a1) / 2, y: pad.t + (d.r0 + d.r1) / 2 }
|
|
293
|
+
}
|
|
294
|
+
const mid_a = (d.a0 + d.a1) / 2
|
|
295
|
+
const mid_r = (d.r0 + d.r1) / 2
|
|
296
|
+
return { x: cx + Math.sin(mid_a) * mid_r, y: cy - Math.cos(mid_a) * mid_r }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Continuous metric coloring: when color_values is given, arcs are colored by their
|
|
300
|
+
// metric on a d3 colormap (arcs returning null keep their categorical color).
|
|
301
|
+
// The user accessor runs exactly once per arc.
|
|
302
|
+
let metric = $derived.by<{ range: [number, number]; colors: string[] } | null>(() => {
|
|
303
|
+
if (!color_values) return null
|
|
304
|
+
const vals = layout.arcs.map((arc) => {
|
|
305
|
+
const val = arc.depth === 0 ? null : color_values(arc)
|
|
306
|
+
return val != null && Number.isFinite(val) ? val : null
|
|
307
|
+
})
|
|
308
|
+
const finite = vals.filter((val) => val != null)
|
|
309
|
+
if (finite.length === 0) return null
|
|
310
|
+
const range = color_range ?? [Math.min(...finite), Math.max(...finite)]
|
|
311
|
+
const scale = create_color_scale({ scheme: color_scale, value_range: range }, range)
|
|
312
|
+
return {
|
|
313
|
+
range,
|
|
314
|
+
colors: vals.map((val, idx) =>
|
|
315
|
+
val == null ? layout.arcs[idx].color : `${scale(val)}`
|
|
316
|
+
),
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
const arc_color = (arc: PositionedArc<Metadata>): string =>
|
|
320
|
+
metric?.colors[arc.node_idx] ?? arc.color
|
|
321
|
+
// release the colorbar's reserved chart space when it's not rendered
|
|
322
|
+
$effect(() => {
|
|
323
|
+
if (!metric || colorbar == null) colorbar_height = 0
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Predicate keeping the hovered arc + its ancestors/descendants fully opaque.
|
|
327
|
+
// Pre-order indexing makes both tests O(1): a subtree is the contiguous index
|
|
328
|
+
// range [node_idx, subtree_end].
|
|
329
|
+
let active = $derived.by(() => {
|
|
330
|
+
if (hovered_idx == null) return null
|
|
331
|
+
const hov = layout.arcs[hovered_idx]
|
|
332
|
+
if (!hov) return null
|
|
333
|
+
return (arc: PositionedArc<Metadata>): boolean =>
|
|
334
|
+
(arc.node_idx >= hov.node_idx && arc.node_idx <= hov.subtree_end) ||
|
|
335
|
+
(hov.node_idx >= arc.node_idx && hov.node_idx <= arc.subtree_end)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const is_muted = (arc: PositionedArc<Metadata>): boolean =>
|
|
339
|
+
arc.path.length > 0 && muted_ids.has(arc.path[0])
|
|
340
|
+
|
|
341
|
+
const MUTED_OPACITY = 0.12
|
|
342
|
+
const arc_opacity = (arc: PositionedArc<Metadata>): number => {
|
|
343
|
+
if (is_muted(arc)) return MUTED_OPACITY
|
|
344
|
+
if (active && !active(arc)) return 0.3
|
|
345
|
+
return 1
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Black/white label text, whichever contrasts with the arc's fill (light arcs from
|
|
349
|
+
// explicit colors or level_lighten would hide white labels, esp. when highlighted).
|
|
350
|
+
// Memoized per color string - parsing/luminance would otherwise run per label per
|
|
351
|
+
// animation frame, and distinct arc colors are few.
|
|
352
|
+
const contrast_cache = new Map<string, string>()
|
|
353
|
+
const label_color = (arc: PositionedArc<Metadata>): string => {
|
|
354
|
+
const fill = arc_color(arc)
|
|
355
|
+
let contrast = contrast_cache.get(fill)
|
|
356
|
+
if (contrast === undefined) {
|
|
357
|
+
contrast = pick_contrast_color({ bg_color: fill })
|
|
358
|
+
contrast_cache.set(fill, contrast)
|
|
359
|
+
}
|
|
360
|
+
return contrast
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Parent arc of an arc (null for the root) and its display name
|
|
364
|
+
const parent_of = (arc: PositionedArc<Metadata>): PositionedArc<Metadata> | null =>
|
|
365
|
+
arc.parent_idx != null ? layout.arcs[arc.parent_idx] : null
|
|
366
|
+
const arc_name = (arc: PositionedArc<Metadata>): string => arc.label ?? `${arc.id}`
|
|
367
|
+
|
|
368
|
+
function make_node_props(
|
|
369
|
+
arc: PositionedArc<Metadata>,
|
|
370
|
+
): SunburstNodeHandlerProps<Metadata> {
|
|
371
|
+
// Handler props are the arc minus its screen geometry, plus the parent id
|
|
372
|
+
const { x0, x1, y0, y1, subtree_end, parent_idx, ...node } = arc
|
|
373
|
+
return {
|
|
374
|
+
...node,
|
|
375
|
+
type: `node`,
|
|
376
|
+
color: arc_color(arc),
|
|
377
|
+
parent_id: parent_of(arc)?.id ?? null,
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Anchor the tooltip at the cursor (mouse hover) so it follows the pointer across
|
|
382
|
+
// wide arcs; fall back to the arc centroid on keyboard focus (no cursor).
|
|
383
|
+
const event_pos = (event?: MouseEvent | FocusEvent): { x: number; y: number } | null =>
|
|
384
|
+
event instanceof MouseEvent ? get_relative_coords(event, svg_element) : null
|
|
385
|
+
|
|
386
|
+
function set_arc_hover(
|
|
387
|
+
screen: ScreenArc | null,
|
|
388
|
+
event?: MouseEvent | FocusEvent,
|
|
389
|
+
) {
|
|
390
|
+
// Same arc as before: only the cursor anchor moves - skip rebuilding the handler
|
|
391
|
+
// payload and re-firing change/on_node_hover on every mousemove within an arc.
|
|
392
|
+
// Requires hover_info: legend item hover sets hovered_idx alone (for dimming), and
|
|
393
|
+
// skipping then would leave the arc's own tooltip permanently suppressed.
|
|
394
|
+
if (screen && screen.arc.node_idx === hovered_idx && hover_info) {
|
|
395
|
+
hover_pos = event_pos(event) ?? hover_pos
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
if (screen) {
|
|
399
|
+
hovered = true
|
|
400
|
+
hovered_idx = screen.arc.node_idx
|
|
401
|
+
hover_info = make_node_props(screen.arc)
|
|
402
|
+
hover_pos = event_pos(event) ?? arc_center(screen)
|
|
403
|
+
change(hover_info)
|
|
404
|
+
if (event) on_node_hover?.({ ...hover_info, event })
|
|
405
|
+
} else {
|
|
406
|
+
// Already clear: don't re-fire change(null)/on_node_hover(null) - both the svg
|
|
407
|
+
// and chart group have mouseleave handlers, and zoom_to clears unconditionally
|
|
408
|
+
if (hovered_idx == null && hover_info == null) return
|
|
409
|
+
hovered = false
|
|
410
|
+
hovered_idx = null
|
|
411
|
+
hover_info = null
|
|
412
|
+
change(null)
|
|
413
|
+
on_node_hover?.(null)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const screen_arc_from_event = (event: Event): ScreenArc | null => {
|
|
418
|
+
const idx = closest_data_idx(event, `data-sunburst-node-idx`, svg_element)
|
|
419
|
+
return idx == null ? null : screen_arcs[idx] ?? null
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function handle_arc_hover_event(event: MouseEvent | FocusEvent) {
|
|
423
|
+
const screen = screen_arc_from_event(event)
|
|
424
|
+
// roving tabindex follows keyboard focus
|
|
425
|
+
if (event.type === `focusin` && screen) focused_idx = screen.arc.node_idx
|
|
426
|
+
set_arc_hover(screen, event)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Re-root the view on the given arc (or the data root when null) and notify
|
|
430
|
+
function zoom_to(arc: PositionedArc<Metadata> | null) {
|
|
431
|
+
zoom_root_id = arc && arc.depth > 0 ? arc.id : null
|
|
432
|
+
// The clicked arc collapses into the hole - drop the now-stale hover/tooltip
|
|
433
|
+
set_arc_hover(null)
|
|
434
|
+
on_zoom?.({ root: arc && arc.depth > 0 ? make_node_props(arc) : null })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// True while the user has an uncollapsed text selection inside this chart. Labels
|
|
438
|
+
// are selectable text, and the mouseup that ends a selection drag also fires a
|
|
439
|
+
// click - selecting a label must not zoom or fire on_node_click.
|
|
440
|
+
function selection_in_chart(): boolean {
|
|
441
|
+
const selection = globalThis.getSelection?.()
|
|
442
|
+
return Boolean(
|
|
443
|
+
selection && !selection.isCollapsed && selection.anchorNode &&
|
|
444
|
+
wrapper?.contains(selection.anchorNode),
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function handle_arc_click(event: MouseEvent | KeyboardEvent) {
|
|
449
|
+
if (event instanceof MouseEvent && selection_in_chart()) return
|
|
450
|
+
const screen = screen_arc_from_event(event)
|
|
451
|
+
if (!screen) return
|
|
452
|
+
const { arc } = screen
|
|
453
|
+
on_node_click?.({ ...make_node_props(arc), event })
|
|
454
|
+
if (zoom_on_click && !arc.is_leaf && arc.id !== zoom_root?.id) zoom_to(arc)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function zoom_out(event?: Event) {
|
|
458
|
+
if (event instanceof MouseEvent && selection_in_chart()) return
|
|
459
|
+
if (!zoomed) return
|
|
460
|
+
zoom_to(breadcrumb_arcs.at(-2) ?? null)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Double-clicking empty chart background resets the zoom to the root (double-
|
|
464
|
+
// clicking an arc or label is click-to-zoom/text-selection territory, not a reset;
|
|
465
|
+
// the center zoom-out button already fired its own click action twice - compounding
|
|
466
|
+
// a third full reset would teleport step-by-step zoom-outs straight to the root)
|
|
467
|
+
function handle_dblclick(event: MouseEvent) {
|
|
468
|
+
if (screen_arc_from_event(event) || selection_in_chart()) return
|
|
469
|
+
const target = event.target as Element | null
|
|
470
|
+
if (target?.closest?.(`.center-circle, .center-label`)) return
|
|
471
|
+
if (zoomed) zoom_to(null)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const focus_arc = (idx: number | null) => {
|
|
475
|
+
if (idx == null) return
|
|
476
|
+
svg_element
|
|
477
|
+
?.querySelector<SVGPathElement>(`.arcs [data-sunburst-node-idx="${idx}"]`)
|
|
478
|
+
?.focus()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Arrow-key navigation: left/right cycle through visible siblings (wrapping),
|
|
482
|
+
// down enters the first child, up returns to the parent. The pre-order walk
|
|
483
|
+
// lives in render.ts (arrow_nav_target); this wrapper supplies the event's arc
|
|
484
|
+
// and the current screen-space visibility.
|
|
485
|
+
const nav_target_from_event = (event: KeyboardEvent): number | null => {
|
|
486
|
+
const cur = screen_arc_from_event(event)?.arc
|
|
487
|
+
if (!cur) return null
|
|
488
|
+
return arrow_nav_target(
|
|
489
|
+
layout.arcs,
|
|
490
|
+
(idx) => screen_arcs[idx]?.visible ?? false,
|
|
491
|
+
cur.node_idx,
|
|
492
|
+
event.key,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const is_activation_key = (evt: KeyboardEvent) => [`Enter`, ` `].includes(evt.key)
|
|
497
|
+
|
|
498
|
+
function handle_arc_keydown(event: KeyboardEvent) {
|
|
499
|
+
const nav_target = nav_target_from_event(event)
|
|
500
|
+
if (nav_target != null) {
|
|
501
|
+
event.preventDefault()
|
|
502
|
+
focus_arc(nav_target)
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
if (!is_activation_key(event)) return
|
|
506
|
+
event.preventDefault()
|
|
507
|
+
const prev_root = zoom_root_id
|
|
508
|
+
handle_arc_click(event)
|
|
509
|
+
// Zooming via keyboard unmounts the focused arc - move focus to the center circle
|
|
510
|
+
// (the zoom-out button) so keyboard users stay inside the chart. In icicle mode
|
|
511
|
+
// focus the new root's first child (pre-order: node_idx + 1): the clicked arc
|
|
512
|
+
// itself collapses to zero height once the zoom tween settles, so focusing it
|
|
513
|
+
// (the roving index) would drop focus to <body> mid-animation.
|
|
514
|
+
if (zoom_root_id !== prev_root) {
|
|
515
|
+
tick().then(() => {
|
|
516
|
+
if (shape === `sunburst`) center_el?.focus()
|
|
517
|
+
else focus_arc(zoom_root ? zoom_root.node_idx + 1 : roving_idx)
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function handle_center_keydown(event: KeyboardEvent) {
|
|
523
|
+
if (!is_activation_key(event)) return
|
|
524
|
+
event.preventDefault()
|
|
525
|
+
zoom_out()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const arc_clickable = (arc: PositionedArc<Metadata>): boolean =>
|
|
529
|
+
Boolean(on_node_click) || (zoom_on_click && !arc.is_leaf)
|
|
530
|
+
|
|
531
|
+
// Measure label fit in the font labels actually render in (respects the
|
|
532
|
+
// --sunburst-font-size CSS var instead of assuming 11px). Memoized because canvas
|
|
533
|
+
// measureText is far too slow to run for every visible arc on every tween frame.
|
|
534
|
+
let label_font = $derived.by(() => {
|
|
535
|
+
if (!svg_element) return `11px sans-serif`
|
|
536
|
+
const { fontSize, fontFamily } = getComputedStyle(svg_element)
|
|
537
|
+
return `${fontSize} ${fontFamily}`
|
|
538
|
+
})
|
|
539
|
+
const text_width_cache = new Map<string, number>()
|
|
540
|
+
function cached_text_width(text: string, font: string): number {
|
|
541
|
+
const key = `${font}|${text}`
|
|
542
|
+
let text_width = text_width_cache.get(key)
|
|
543
|
+
if (text_width === undefined) {
|
|
544
|
+
if (text_width_cache.size > 10_000) text_width_cache.clear() // growth guard
|
|
545
|
+
text_width = measure_text_width(text, font)
|
|
546
|
+
text_width_cache.set(key, text_width)
|
|
547
|
+
}
|
|
548
|
+
return text_width
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// What an arc's label displays, per the label_text mode (plotly textinfo equivalent)
|
|
552
|
+
const arc_label_str = (arc: PositionedArc<Metadata>): string => {
|
|
553
|
+
const name = arc_name(arc)
|
|
554
|
+
if (label_text === `label`) return name
|
|
555
|
+
const val = format_value(arc.value, value_format)
|
|
556
|
+
if (label_text === `value`) return val
|
|
557
|
+
const pct = format_value(arc.fraction, `.1%`)
|
|
558
|
+
if (label_text === `percent`) return pct
|
|
559
|
+
return label_text === `label+value` ? `${name} ${val}` : `${name} ${pct}`
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Per-arc label text, measured width and aria string - all view-independent, so
|
|
563
|
+
// computed once per layout/label-option change instead of per animation frame
|
|
564
|
+
// (format_value + canvas measureText would otherwise run per visible arc per frame)
|
|
565
|
+
let arc_info = $derived(
|
|
566
|
+
layout.arcs.map((arc) => {
|
|
567
|
+
const text = arc_label_str(arc)
|
|
568
|
+
return {
|
|
569
|
+
text,
|
|
570
|
+
width: cached_text_width(text, label_font),
|
|
571
|
+
aria: `${arc_name(arc)}: ${arc.value}`,
|
|
572
|
+
}
|
|
573
|
+
}),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
// Label text + placement transform for an arc; null = doesn't fit, hide the label
|
|
577
|
+
function label_attrs(d: ScreenArc): { transform: string; text: string } | null {
|
|
578
|
+
const { text, width: text_w } = arc_info[d.arc.node_idx]
|
|
579
|
+
if (!text) return null
|
|
580
|
+
const transform = arc_label_transform(d, text_w, shape, label_rotation)
|
|
581
|
+
return transform ? { transform, text } : null
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Legend: one item per depth-1 category, toggling mutes (dims) rather than removes.
|
|
585
|
+
let depth1_arcs = $derived(layout.arcs.filter((arc) => arc.depth === 1))
|
|
586
|
+
let legend_visible = $derived(show_legend && legend != null && depth1_arcs.length > 1)
|
|
587
|
+
let legend_element = $state<HTMLDivElement | undefined>()
|
|
588
|
+
let legend_placement = $derived.by(() => {
|
|
589
|
+
if (!legend_visible || !width || !height) return null
|
|
590
|
+
// Place against the settled (target) geometry, not the animated view - placement
|
|
591
|
+
// is stable during zoom tweens and compute_element_placement runs once per zoom
|
|
592
|
+
// instead of once per frame
|
|
593
|
+
const settled = project_arcs(layout.arcs, view.target, screen_geom).visible
|
|
594
|
+
return compute_element_placement({
|
|
595
|
+
plot_bounds: { x: pad.l, y: pad.t, width: inner_width, height: inner_height },
|
|
596
|
+
element: legend_element,
|
|
597
|
+
element_size: { width: 120, height: 60 },
|
|
598
|
+
axis_clearance: legend?.axis_clearance,
|
|
599
|
+
exclude_rects: [],
|
|
600
|
+
points: settled.map(arc_center),
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
let legend_data = $derived.by<LegendItem[]>(() =>
|
|
604
|
+
depth1_arcs.map((arc, idx) => ({
|
|
605
|
+
series_idx: idx,
|
|
606
|
+
label: arc_name(arc),
|
|
607
|
+
visible: !muted_ids.has(arc.id),
|
|
608
|
+
display_style: { symbol_type: `Square` as const, symbol_color: arc_color(arc) },
|
|
609
|
+
}))
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
function toggle_category(series_idx: number) {
|
|
613
|
+
const id = depth1_arcs[series_idx]?.id
|
|
614
|
+
if (id === undefined) return
|
|
615
|
+
if (muted_ids.has(id)) muted_ids.delete(id)
|
|
616
|
+
else muted_ids.add(id)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
$effect(() => set_fullscreen_bg(wrapper, fullscreen, `--sunburst-fullscreen-bg`))
|
|
620
|
+
|
|
621
|
+
let center_label = $derived(zoom_root?.label ?? (zoomed ? `${zoom_root?.id}` : ``))
|
|
622
|
+
// Where the center circle takes you on click (parent of the current zoom root)
|
|
623
|
+
let zoom_out_label = $derived.by(() => {
|
|
624
|
+
const parent = breadcrumb_arcs.at(-2)
|
|
625
|
+
if (!parent) return ``
|
|
626
|
+
return parent.depth === 0 ? `full chart` : arc_name(parent)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
// Ancestor chain from the root to the current zoom root (clickable breadcrumb trail)
|
|
630
|
+
let breadcrumb_arcs = $derived.by(() => {
|
|
631
|
+
if (!zoom_root || zoom_root.depth === 0) return []
|
|
632
|
+
const chain: PositionedArc<Metadata>[] = []
|
|
633
|
+
for (
|
|
634
|
+
let cur: PositionedArc<Metadata> | null = zoom_root;
|
|
635
|
+
cur;
|
|
636
|
+
cur = parent_of(cur)
|
|
637
|
+
) chain.unshift(cur)
|
|
638
|
+
return chain
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// Styles the component applies via CSS that exported standalone SVGs must carry
|
|
642
|
+
// as presentation attributes (inlined onto a clone by the io/export helpers)
|
|
643
|
+
const export_inline_styles = [
|
|
644
|
+
`fill`,
|
|
645
|
+
`stroke`,
|
|
646
|
+
`stroke-width`,
|
|
647
|
+
`text-anchor`,
|
|
648
|
+
`dominant-baseline`,
|
|
649
|
+
`font-size`,
|
|
650
|
+
`font-family`,
|
|
651
|
+
`font-weight`,
|
|
652
|
+
`opacity`,
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
function export_chart(format: `svg` | `png`) {
|
|
656
|
+
if (!svg_element) return
|
|
657
|
+
if (format === `svg`) {
|
|
658
|
+
export_svg_as_svg(svg_element, `${export_filename}.svg`, export_inline_styles)
|
|
659
|
+
} else {
|
|
660
|
+
export_svg_as_png(svg_element, `${export_filename}.png`, 150, export_inline_styles)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
</script>
|
|
664
|
+
|
|
665
|
+
<svelte:window
|
|
666
|
+
onkeydown={(evt) => {
|
|
667
|
+
if (evt.key !== `Escape`) return
|
|
668
|
+
// only react when the user is interacting with this chart (pointer over it,
|
|
669
|
+
// focus inside it, or fullscreen) - Escape zooms out one level, then exits
|
|
670
|
+
// fullscreen once at the root
|
|
671
|
+
const within = fullscreen || Boolean(wrapper?.matches(`:hover`)) ||
|
|
672
|
+
Boolean(wrapper && document.activeElement && wrapper.contains(document.activeElement))
|
|
673
|
+
if (!within) return
|
|
674
|
+
if (zoomed) {
|
|
675
|
+
evt.preventDefault()
|
|
676
|
+
zoom_out()
|
|
677
|
+
} else if (fullscreen) {
|
|
678
|
+
evt.preventDefault()
|
|
679
|
+
fullscreen = false
|
|
680
|
+
}
|
|
681
|
+
}}
|
|
682
|
+
/>
|
|
683
|
+
|
|
684
|
+
<div
|
|
685
|
+
bind:this={wrapper}
|
|
686
|
+
bind:clientWidth={width}
|
|
687
|
+
bind:clientHeight={height}
|
|
688
|
+
{...rest}
|
|
689
|
+
class="sunburst {rest.class ?? ``}"
|
|
690
|
+
class:fullscreen
|
|
691
|
+
class:icicle={shape === `icicle`}
|
|
692
|
+
>
|
|
693
|
+
{#if width && height}
|
|
694
|
+
<div class="header-controls">
|
|
695
|
+
{@render header_controls?.({ height, width, fullscreen })}
|
|
696
|
+
{#if show_controls}
|
|
697
|
+
<SunburstControls
|
|
698
|
+
toggle_props={{
|
|
699
|
+
...controls_toggle_props,
|
|
700
|
+
// join the header flex row instead of absolute positioning (overrides
|
|
701
|
+
// ControlPane's default; flex layout can't overlap with the other buttons)
|
|
702
|
+
style: `position: static; ${controls_toggle_props?.style ?? ``}`,
|
|
703
|
+
}}
|
|
704
|
+
pane_props={controls_pane_props}
|
|
705
|
+
bind:show_controls
|
|
706
|
+
bind:controls_open
|
|
707
|
+
bind:shape
|
|
708
|
+
bind:value_mode
|
|
709
|
+
bind:max_depth
|
|
710
|
+
bind:inner_radius
|
|
711
|
+
bind:pad_angle
|
|
712
|
+
bind:min_fraction
|
|
713
|
+
bind:show_labels
|
|
714
|
+
bind:label_rotation
|
|
715
|
+
bind:label_text
|
|
716
|
+
bind:zoom_on_click
|
|
717
|
+
bind:show_breadcrumbs
|
|
718
|
+
{export_buttons}
|
|
719
|
+
on_export={export_chart}
|
|
720
|
+
>
|
|
721
|
+
{@render controls_extra?.({ zoom_root_id })}
|
|
722
|
+
</SunburstControls>
|
|
723
|
+
{/if}
|
|
724
|
+
{#if fullscreen_toggle}
|
|
725
|
+
<FullscreenToggle bind:fullscreen />
|
|
726
|
+
{/if}
|
|
727
|
+
</div>
|
|
728
|
+
{#if show_breadcrumbs && breadcrumb_arcs.length > 0}
|
|
729
|
+
<nav class="breadcrumbs" aria-label="zoom path">
|
|
730
|
+
{#each breadcrumb_arcs as crumb, crumb_idx (crumb.node_idx)}
|
|
731
|
+
{#if crumb_idx > 0}<span class="breadcrumb-sep" aria-hidden="true">›</span>{/if}
|
|
732
|
+
<button
|
|
733
|
+
type="button"
|
|
734
|
+
class="breadcrumb"
|
|
735
|
+
disabled={crumb_idx === breadcrumb_arcs.length - 1}
|
|
736
|
+
onclick={() => zoom_to(crumb)}
|
|
737
|
+
>
|
|
738
|
+
{crumb.depth === 0 ? `all` : crumb.label ?? crumb.id}
|
|
739
|
+
</button>
|
|
740
|
+
{/each}
|
|
741
|
+
</nav>
|
|
742
|
+
{/if}
|
|
743
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
744
|
+
<svg
|
|
745
|
+
bind:this={svg_element}
|
|
746
|
+
viewBox="0 0 {width} {height}"
|
|
747
|
+
role="application"
|
|
748
|
+
aria-label={rest[`aria-label`] ?? `${shape === `icicle` ? `Icicle` : `Sunburst`} chart`}
|
|
749
|
+
onmouseleave={() => set_arc_hover(null)}
|
|
750
|
+
>
|
|
751
|
+
<!-- Hover/click delegation sits on the chart group (not the arcs group) so
|
|
752
|
+
labels - which carry the same data-sunburst-node-idx and are selectable text -
|
|
753
|
+
forward interactions to their arc instead of swallowing them -->
|
|
754
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
755
|
+
<g
|
|
756
|
+
transform={chart_transform}
|
|
757
|
+
onmousemove={handle_arc_hover_event}
|
|
758
|
+
onmouseleave={() => set_arc_hover(null)}
|
|
759
|
+
onfocusin={handle_arc_hover_event}
|
|
760
|
+
onfocusout={() => set_arc_hover(null)}
|
|
761
|
+
onclick={handle_arc_click}
|
|
762
|
+
ondblclick={handle_dblclick}
|
|
763
|
+
onkeydown={handle_arc_keydown}
|
|
764
|
+
>
|
|
765
|
+
<!-- Arcs -->
|
|
766
|
+
<g class="arcs">
|
|
767
|
+
{#each visible_arcs as screen (screen.arc.node_idx)}
|
|
768
|
+
{#if arc_content}
|
|
769
|
+
{@render arc_content(screen)}
|
|
770
|
+
{:else}
|
|
771
|
+
{@const clickable = arc_clickable(screen.arc)}
|
|
772
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
773
|
+
<path
|
|
774
|
+
d={screen_path(screen)}
|
|
775
|
+
data-sunburst-node-idx={screen.arc.node_idx}
|
|
776
|
+
fill={arc_color(screen.arc)}
|
|
777
|
+
fill-opacity={arc_opacity(screen.arc)}
|
|
778
|
+
role={clickable ? `button` : undefined}
|
|
779
|
+
tabindex={clickable
|
|
780
|
+
? (screen.arc.node_idx === roving_idx ? 0 : -1)
|
|
781
|
+
: undefined}
|
|
782
|
+
aria-label={clickable ? arc_info[screen.arc.node_idx].aria : undefined}
|
|
783
|
+
style:cursor={clickable ? `pointer` : `default`}
|
|
784
|
+
/>
|
|
785
|
+
{/if}
|
|
786
|
+
{/each}
|
|
787
|
+
</g>
|
|
788
|
+
|
|
789
|
+
<!-- Arc labels: selectable text; data-sunburst-node-idx forwards hover/click
|
|
790
|
+
to the underlying arc via the chart-group delegation above -->
|
|
791
|
+
{#if show_labels}
|
|
792
|
+
<g class="arc-labels">
|
|
793
|
+
{#each visible_arcs as screen (screen.arc.node_idx)}
|
|
794
|
+
{@const lbl = label_attrs(screen)}
|
|
795
|
+
{#if lbl}
|
|
796
|
+
<text
|
|
797
|
+
class="arc-label"
|
|
798
|
+
data-sunburst-node-idx={screen.arc.node_idx}
|
|
799
|
+
transform={lbl.transform}
|
|
800
|
+
fill={label_color(screen.arc)}
|
|
801
|
+
fill-opacity={is_muted(screen.arc) ? MUTED_OPACITY : undefined}
|
|
802
|
+
style:cursor={arc_clickable(screen.arc) ? `pointer` : `text`}
|
|
803
|
+
>
|
|
804
|
+
{lbl.text}
|
|
805
|
+
</text>
|
|
806
|
+
{/if}
|
|
807
|
+
{/each}
|
|
808
|
+
</g>
|
|
809
|
+
{/if}
|
|
810
|
+
|
|
811
|
+
{#if shape === `sunburst`}
|
|
812
|
+
<!-- Center: zoom-out button + current-root summary -->
|
|
813
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
814
|
+
<circle
|
|
815
|
+
bind:this={center_el}
|
|
816
|
+
class="center-circle"
|
|
817
|
+
r={hole_r}
|
|
818
|
+
role={zoomed ? `button` : undefined}
|
|
819
|
+
tabindex={zoomed ? 0 : undefined}
|
|
820
|
+
aria-label={zoomed ? `zoom out to ${zoom_out_label}` : undefined}
|
|
821
|
+
style:cursor={zoomed ? `pointer` : `default`}
|
|
822
|
+
style:pointer-events={zoomed ? `auto` : `none`}
|
|
823
|
+
onclick={zoom_out}
|
|
824
|
+
onkeydown={handle_center_keydown}
|
|
825
|
+
/>
|
|
826
|
+
{#if center_content}
|
|
827
|
+
{@render center_content({ root: zoom_root, radius: hole_r, zoomed })}
|
|
828
|
+
{:else if hole_r >= 18 && zoom_root}
|
|
829
|
+
<!-- Selectable text overlaying the center circle; clicks forward to the
|
|
830
|
+
same zoom-out action as the circle (which also handles keyboard) -->
|
|
831
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
832
|
+
<text
|
|
833
|
+
class="center-label"
|
|
834
|
+
style:cursor={zoomed ? `pointer` : `text`}
|
|
835
|
+
onclick={zoom_out}
|
|
836
|
+
>
|
|
837
|
+
{#if center_label}
|
|
838
|
+
<tspan x="0" dy={zoom_root.value ? `-0.3em` : `0.35em`}>
|
|
839
|
+
{center_label}
|
|
840
|
+
</tspan>
|
|
841
|
+
{/if}
|
|
842
|
+
<tspan x="0" dy={center_label ? `1.2em` : `0.35em`} class="center-value">
|
|
843
|
+
{format_value(zoom_root.value, value_format)}
|
|
844
|
+
</tspan>
|
|
845
|
+
</text>
|
|
846
|
+
{/if}
|
|
847
|
+
{/if}
|
|
848
|
+
</g>
|
|
849
|
+
</svg>
|
|
850
|
+
{/if}
|
|
851
|
+
|
|
852
|
+
{#if hover_info}
|
|
853
|
+
<PlotTooltip
|
|
854
|
+
x={hover_pos.x}
|
|
855
|
+
y={hover_pos.y}
|
|
856
|
+
offset={{ x: 10, y: 5 }}
|
|
857
|
+
constrain_to={{ width, height }}
|
|
858
|
+
fallback_size={{ width: 140, height: 44 }}
|
|
859
|
+
bg_color={hover_info.color}
|
|
860
|
+
>
|
|
861
|
+
{#if tooltip}
|
|
862
|
+
{@render tooltip(hover_info)}
|
|
863
|
+
{:else}
|
|
864
|
+
<strong>{hover_info.label_path.join(` › `)}</strong>: {
|
|
865
|
+
format_value(hover_info.value, value_format)
|
|
866
|
+
}
|
|
867
|
+
({format_value(hover_info.fraction, `.1%`)} of total{
|
|
868
|
+
hover_info.depth > 1
|
|
869
|
+
? `, ${format_value(hover_info.parent_fraction, `.1%`)} of parent`
|
|
870
|
+
: ``
|
|
871
|
+
})
|
|
872
|
+
{/if}
|
|
873
|
+
</PlotTooltip>
|
|
874
|
+
{/if}
|
|
875
|
+
|
|
876
|
+
{#if legend_visible}
|
|
877
|
+
{@const legend_left = legend_placement?.x ?? pad.l + 10}
|
|
878
|
+
{@const legend_top = legend_placement?.y ?? pad.t + 10}
|
|
879
|
+
<PlotLegend
|
|
880
|
+
bind:root_element={legend_element}
|
|
881
|
+
{...legend}
|
|
882
|
+
series_data={legend_data}
|
|
883
|
+
on_toggle={legend?.on_toggle ?? toggle_category}
|
|
884
|
+
on_item_hover={(item) =>
|
|
885
|
+
(hovered_idx = item != null && item.series_idx >= 0
|
|
886
|
+
? depth1_arcs[item.series_idx]?.node_idx ?? null
|
|
887
|
+
: null)}
|
|
888
|
+
style={`position: absolute; left: ${legend_left}px; top: ${legend_top}px; pointer-events: auto; ${
|
|
889
|
+
legend?.style ?? ``
|
|
890
|
+
}`}
|
|
891
|
+
/>
|
|
892
|
+
{/if}
|
|
893
|
+
|
|
894
|
+
{#if metric && colorbar != null}
|
|
895
|
+
<div
|
|
896
|
+
bind:clientHeight={colorbar_height}
|
|
897
|
+
style="position: absolute; bottom: var(--sunburst-colorbar-bottom, 8px); left: 50%; transform: translateX(-50%); width: var(--sunburst-colorbar-width, 40%); min-width: 120px; pointer-events: auto;"
|
|
898
|
+
>
|
|
899
|
+
<ColorBar
|
|
900
|
+
color_scale={color_scale}
|
|
901
|
+
range={metric.range}
|
|
902
|
+
{...colorbar}
|
|
903
|
+
wrapper_style={`width: 100%; ${colorbar?.wrapper_style ?? ``}`}
|
|
904
|
+
/>
|
|
905
|
+
</div>
|
|
906
|
+
{/if}
|
|
907
|
+
|
|
908
|
+
{@render children?.({ height, width, fullscreen })}
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<style>
|
|
912
|
+
.sunburst {
|
|
913
|
+
position: relative;
|
|
914
|
+
width: var(--sunburst-width, 100%);
|
|
915
|
+
height: var(--sunburst-height, auto);
|
|
916
|
+
min-height: var(--sunburst-min-height, 300px);
|
|
917
|
+
container-type: size;
|
|
918
|
+
z-index: var(--sunburst-z-index, auto);
|
|
919
|
+
/* flex-basis auto (not 1 = 0%) so an authored height wins over flex sizing in
|
|
920
|
+
column-flex parents while the chart still grows/shrinks to fill fixed layouts */
|
|
921
|
+
flex: var(--sunburst-flex, 1 1 auto);
|
|
922
|
+
display: var(--sunburst-display, flex);
|
|
923
|
+
flex-direction: column;
|
|
924
|
+
background: var(--sunburst-bg, var(--plot-bg));
|
|
925
|
+
border-radius: var(--sunburst-border-radius, var(--border-radius, 3pt));
|
|
926
|
+
}
|
|
927
|
+
.sunburst.fullscreen {
|
|
928
|
+
position: fixed;
|
|
929
|
+
top: 0;
|
|
930
|
+
left: 0;
|
|
931
|
+
width: 100vw !important;
|
|
932
|
+
height: 100vh !important;
|
|
933
|
+
z-index: var(--sunburst-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
|
|
934
|
+
margin: 0;
|
|
935
|
+
border-radius: 0;
|
|
936
|
+
background: var(--sunburst-fullscreen-bg, var(--sunburst-bg, var(--plot-bg)));
|
|
937
|
+
max-height: none !important;
|
|
938
|
+
overflow: hidden;
|
|
939
|
+
/* border-top (not padding-top): bind:clientHeight includes padding but excludes
|
|
940
|
+
borders - padding made the chart overflow + clip its bottom 2em (x-axis title) */
|
|
941
|
+
border-top: var(--plot-fullscreen-padding-top, 2em) solid
|
|
942
|
+
var(--sunburst-fullscreen-bg, var(--sunburst-bg, var(--plot-bg, transparent)));
|
|
943
|
+
box-sizing: border-box;
|
|
944
|
+
}
|
|
945
|
+
.header-controls {
|
|
946
|
+
position: absolute;
|
|
947
|
+
top: var(--ctrl-btn-top, 5pt);
|
|
948
|
+
right: var(--fullscreen-btn-right, 4px);
|
|
949
|
+
z-index: var(--fullscreen-btn-z-index, 10);
|
|
950
|
+
display: flex;
|
|
951
|
+
align-items: center;
|
|
952
|
+
gap: 8px;
|
|
953
|
+
}
|
|
954
|
+
.header-controls :global(.fullscreen-toggle) {
|
|
955
|
+
position: static;
|
|
956
|
+
opacity: 1;
|
|
957
|
+
}
|
|
958
|
+
.breadcrumb {
|
|
959
|
+
background: var(--sunburst-btn-bg, rgba(128, 128, 128, 0.15));
|
|
960
|
+
color: inherit;
|
|
961
|
+
border: none;
|
|
962
|
+
border-radius: 3pt;
|
|
963
|
+
padding: 1px 6px;
|
|
964
|
+
cursor: pointer;
|
|
965
|
+
font: inherit;
|
|
966
|
+
}
|
|
967
|
+
.breadcrumb:hover:not(:disabled) {
|
|
968
|
+
background: var(--sunburst-btn-hover-bg, rgba(128, 128, 128, 0.35));
|
|
969
|
+
}
|
|
970
|
+
.breadcrumbs {
|
|
971
|
+
position: absolute;
|
|
972
|
+
top: var(--sunburst-breadcrumbs-top, 5pt);
|
|
973
|
+
left: var(--sunburst-breadcrumbs-left, 8px);
|
|
974
|
+
z-index: 9;
|
|
975
|
+
display: flex;
|
|
976
|
+
align-items: center;
|
|
977
|
+
gap: 2px;
|
|
978
|
+
flex-wrap: wrap;
|
|
979
|
+
max-width: 75%;
|
|
980
|
+
font-size: var(--sunburst-breadcrumbs-font-size, 0.85em);
|
|
981
|
+
}
|
|
982
|
+
.breadcrumb:disabled {
|
|
983
|
+
cursor: default;
|
|
984
|
+
font-weight: bold;
|
|
985
|
+
background: transparent;
|
|
986
|
+
}
|
|
987
|
+
.breadcrumb-sep {
|
|
988
|
+
opacity: 0.6;
|
|
989
|
+
}
|
|
990
|
+
.sunburst :global(.pane-toggle),
|
|
991
|
+
.sunburst .header-controls {
|
|
992
|
+
opacity: 0;
|
|
993
|
+
transition: opacity 0.2s, background-color 0.2s;
|
|
994
|
+
}
|
|
995
|
+
.sunburst:hover :global(.pane-toggle),
|
|
996
|
+
.sunburst:hover .header-controls,
|
|
997
|
+
.sunburst :global(.pane-toggle:focus-visible),
|
|
998
|
+
.sunburst :global(.pane-toggle[aria-expanded='true']),
|
|
999
|
+
.sunburst .header-controls:has(:global([aria-expanded='true'])),
|
|
1000
|
+
.sunburst .header-controls:focus-within {
|
|
1001
|
+
opacity: 1;
|
|
1002
|
+
}
|
|
1003
|
+
svg {
|
|
1004
|
+
width: var(--sunburst-svg-width, 100%);
|
|
1005
|
+
height: var(--sunburst-svg-height, 100%);
|
|
1006
|
+
flex: var(--sunburst-svg-flex, 1);
|
|
1007
|
+
overflow: var(--sunburst-svg-overflow, visible);
|
|
1008
|
+
fill: var(--text-color);
|
|
1009
|
+
font-size: var(--sunburst-font-size, 11px);
|
|
1010
|
+
}
|
|
1011
|
+
.arcs path {
|
|
1012
|
+
/* stroke via CSS (not presentation attributes): var() substitution in SVG
|
|
1013
|
+
presentation attributes is not reliably supported across browsers */
|
|
1014
|
+
stroke: var(--sunburst-arc-stroke, var(--plot-bg, white));
|
|
1015
|
+
stroke-width: var(--sunburst-arc-stroke-width, 0.25);
|
|
1016
|
+
transition: fill-opacity 0.15s ease, transform 0.15s ease;
|
|
1017
|
+
/* hover 'pull': scaling about the chart center offsets the arc radially */
|
|
1018
|
+
transform-origin: 0 0;
|
|
1019
|
+
}
|
|
1020
|
+
.sunburst:not(.icicle) .arcs path:hover {
|
|
1021
|
+
transform: scale(var(--sunburst-hover-scale, 1.02));
|
|
1022
|
+
}
|
|
1023
|
+
.center-circle {
|
|
1024
|
+
fill: var(--sunburst-center-bg, transparent);
|
|
1025
|
+
}
|
|
1026
|
+
.arc-label {
|
|
1027
|
+
text-anchor: middle;
|
|
1028
|
+
dominant-baseline: central;
|
|
1029
|
+
/* selectable so labels can be copied; clicks/hover still reach the underlying
|
|
1030
|
+
arc via data-sunburst-node-idx + delegation on the chart group */
|
|
1031
|
+
-webkit-user-select: text;
|
|
1032
|
+
user-select: text;
|
|
1033
|
+
}
|
|
1034
|
+
.center-label {
|
|
1035
|
+
fill: var(--text-color);
|
|
1036
|
+
text-anchor: middle;
|
|
1037
|
+
font-weight: bold;
|
|
1038
|
+
-webkit-user-select: text;
|
|
1039
|
+
user-select: text;
|
|
1040
|
+
}
|
|
1041
|
+
.center-label .center-value {
|
|
1042
|
+
font-weight: normal;
|
|
1043
|
+
opacity: 0.8;
|
|
1044
|
+
}
|
|
1045
|
+
</style>
|