matterviz 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,851 +1,1105 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { D3InterpolateName } from '../colors'
|
|
3
|
+
import { is_color, pick_contrast_color } from '../colors'
|
|
4
|
+
import { format_num } from '../labels'
|
|
5
|
+
import type { AxisConfig } from '../plot'
|
|
6
|
+
import ColorBar from '../plot/ColorBar.svelte'
|
|
7
|
+
import * as d3_sc from 'd3-scale-chromatic'
|
|
8
|
+
import { type ComponentProps, onDestroy, onMount, type Snippet } from 'svelte'
|
|
9
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
10
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
11
|
+
import HeatmapMatrixControls from './HeatmapMatrixControls.svelte'
|
|
12
|
+
import type {
|
|
13
|
+
AxisItem,
|
|
14
|
+
CellContext,
|
|
15
|
+
DomainMode,
|
|
16
|
+
HeatmapExportFormat,
|
|
17
|
+
HeatmapTooltipProp,
|
|
18
|
+
LegendPosition,
|
|
19
|
+
NormalizeMode,
|
|
20
|
+
SymmetricMode,
|
|
21
|
+
} from './index'
|
|
22
|
+
import { matrix_to_rows, rows_to_csv } from './index'
|
|
23
|
+
import { make_color_override_key } from './shared'
|
|
24
|
+
|
|
25
|
+
type CellValue = number | string | null
|
|
26
|
+
type ColorBarOrientation = `vertical` | `horizontal`
|
|
27
|
+
type SelectionMode = `single` | `multi` | `range`
|
|
28
|
+
type AxisOrderKey = `label` | `key` | `sort_value`
|
|
29
|
+
type AxisOrder = AxisOrderKey | ((a: AxisItem, b: AxisItem) => number)
|
|
30
|
+
type CellPos = { x_idx: number; y_idx: number }
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
// Data props
|
|
34
|
+
x_items,
|
|
35
|
+
y_items,
|
|
36
|
+
values = [],
|
|
37
|
+
color_scale = $bindable(`interpolateViridis`),
|
|
38
|
+
color_scale_range = [null, null],
|
|
39
|
+
color_overrides = {},
|
|
40
|
+
missing_color = `transparent`,
|
|
41
|
+
log = false,
|
|
42
|
+
value_transform,
|
|
43
|
+
normalize = `linear`,
|
|
44
|
+
domain_mode = `auto`,
|
|
45
|
+
quantile_clip = [0.02, 0.98],
|
|
46
|
+
show_legend = false,
|
|
47
|
+
legend_position = `bottom`,
|
|
48
|
+
legend_label = `Value`,
|
|
49
|
+
legend_ticks = 5,
|
|
50
|
+
legend_format = `.3~f`,
|
|
51
|
+
// Interaction props
|
|
52
|
+
active_cell = $bindable(null),
|
|
53
|
+
selected_cells = $bindable([]),
|
|
54
|
+
selection_mode = `single`,
|
|
55
|
+
pinned_cell = $bindable(null),
|
|
56
|
+
tooltip_mode = `hover`,
|
|
57
|
+
disabled = false,
|
|
58
|
+
onclick,
|
|
59
|
+
ondblclick,
|
|
60
|
+
onselect,
|
|
61
|
+
onpin,
|
|
62
|
+
oncontextmenu,
|
|
63
|
+
enable_brush = false,
|
|
64
|
+
onbrush,
|
|
65
|
+
// Display props
|
|
66
|
+
tile_size = `6px`,
|
|
67
|
+
gap = `0px`,
|
|
68
|
+
hide_empty = false,
|
|
69
|
+
show_x_labels = true,
|
|
70
|
+
show_y_labels = true,
|
|
71
|
+
stagger_axis_labels = `auto`,
|
|
72
|
+
symmetric: symmetric_prop = false,
|
|
73
|
+
symmetric_label_position = `diagonal`,
|
|
74
|
+
label_style = ``,
|
|
75
|
+
x_order,
|
|
76
|
+
y_order,
|
|
77
|
+
highlight_x_keys = [],
|
|
78
|
+
highlight_y_keys = [],
|
|
79
|
+
search_query = ``,
|
|
80
|
+
sticky_x_labels = false,
|
|
81
|
+
sticky_y_labels = false,
|
|
82
|
+
virtualize = false,
|
|
83
|
+
overscan = 3,
|
|
84
|
+
export_formats = [`csv`, `json`],
|
|
85
|
+
onexport,
|
|
86
|
+
show_gridlines = false,
|
|
87
|
+
gridline_color = `color-mix(in srgb, currentColor 18%, transparent)`,
|
|
88
|
+
gridline_width = `1px`,
|
|
89
|
+
animate_updates = false,
|
|
90
|
+
animation_duration = `120ms`,
|
|
91
|
+
show_row_summaries = false,
|
|
92
|
+
show_col_summaries = false,
|
|
93
|
+
summary_fn,
|
|
94
|
+
theme = `default`,
|
|
95
|
+
// Controls pane
|
|
96
|
+
show_controls = false,
|
|
97
|
+
controls_open = $bindable(false),
|
|
98
|
+
controls_props = {},
|
|
99
|
+
controls_children,
|
|
100
|
+
// Cell value display
|
|
101
|
+
show_values = false,
|
|
102
|
+
// Axis config (label used as axis title)
|
|
103
|
+
x_axis = {},
|
|
104
|
+
y_axis = {},
|
|
105
|
+
// Snippet props
|
|
106
|
+
tooltip = false,
|
|
107
|
+
cell,
|
|
108
|
+
x_label_cell,
|
|
109
|
+
y_label_cell,
|
|
110
|
+
children,
|
|
111
|
+
...rest
|
|
112
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, `onclick` | `ondblclick`> & {
|
|
113
|
+
x_items: AxisItem[]
|
|
114
|
+
y_items: AxisItem[]
|
|
115
|
+
values?:
|
|
116
|
+
| CellValue[][]
|
|
117
|
+
| Record<string, Record<string, CellValue>>
|
|
118
|
+
color_scale?: D3InterpolateName | ((val: number) => string)
|
|
119
|
+
color_scale_range?: [number | null, number | null]
|
|
120
|
+
color_overrides?: Record<string, string>
|
|
121
|
+
missing_color?: string
|
|
122
|
+
log?: boolean
|
|
123
|
+
value_transform?: (
|
|
124
|
+
value: number,
|
|
125
|
+
ctx: { x_item: AxisItem; y_item: AxisItem; x_idx: number; y_idx: number },
|
|
126
|
+
) => number | null
|
|
127
|
+
normalize?: NormalizeMode
|
|
128
|
+
domain_mode?: DomainMode
|
|
129
|
+
quantile_clip?: [number, number]
|
|
130
|
+
show_legend?: boolean
|
|
131
|
+
legend_position?: LegendPosition
|
|
132
|
+
legend_label?: string
|
|
133
|
+
legend_ticks?: number
|
|
134
|
+
legend_format?: string
|
|
135
|
+
active_cell?: { x_idx: number; y_idx: number } | null
|
|
136
|
+
selected_cells?: CellPos[]
|
|
137
|
+
selection_mode?: SelectionMode
|
|
138
|
+
pinned_cell?: CellPos | null
|
|
139
|
+
tooltip_mode?: `hover` | `pinned` | `both`
|
|
140
|
+
disabled?: boolean
|
|
141
|
+
onclick?: (cell: CellContext) => void
|
|
142
|
+
ondblclick?: (cell: CellContext) => void
|
|
143
|
+
onselect?: (cells: CellPos[]) => void
|
|
144
|
+
onpin?: (cell: CellPos | null) => void
|
|
145
|
+
oncontextmenu?: (cell: CellContext, event: MouseEvent) => void
|
|
146
|
+
enable_brush?: boolean
|
|
147
|
+
onbrush?: (payload: {
|
|
148
|
+
x_range: [number, number]
|
|
149
|
+
y_range: [number, number]
|
|
150
|
+
cells: CellContext[]
|
|
151
|
+
}) => void
|
|
152
|
+
tile_size?: string
|
|
153
|
+
gap?: string
|
|
154
|
+
// false: show all rows/cols. 'compact': remove all-null rows/cols.
|
|
155
|
+
// 'gaps': keep grid positions but hide all-null rows/cols (preserves alignment).
|
|
156
|
+
hide_empty?: false | `compact` | `gaps`
|
|
157
|
+
show_x_labels?: boolean
|
|
158
|
+
show_y_labels?: boolean
|
|
159
|
+
stagger_axis_labels?: boolean | `auto`
|
|
160
|
+
symmetric?: SymmetricMode
|
|
161
|
+
symmetric_label_position?: `diagonal` | `edge`
|
|
162
|
+
label_style?: string
|
|
163
|
+
x_order?: AxisOrder
|
|
164
|
+
y_order?: AxisOrder
|
|
165
|
+
highlight_x_keys?: string[]
|
|
166
|
+
highlight_y_keys?: string[]
|
|
167
|
+
search_query?: string
|
|
168
|
+
sticky_x_labels?: boolean
|
|
169
|
+
sticky_y_labels?: boolean
|
|
170
|
+
virtualize?: boolean
|
|
171
|
+
overscan?: number
|
|
172
|
+
export_formats?: HeatmapExportFormat[]
|
|
173
|
+
onexport?: (format: HeatmapExportFormat, payload: unknown) => void
|
|
174
|
+
show_gridlines?: boolean
|
|
175
|
+
gridline_color?: string
|
|
176
|
+
gridline_width?: string
|
|
177
|
+
animate_updates?: boolean
|
|
178
|
+
animation_duration?: string
|
|
179
|
+
show_row_summaries?: boolean
|
|
180
|
+
show_col_summaries?: boolean
|
|
181
|
+
summary_fn?: (values: number[]) => number | null
|
|
182
|
+
theme?: `default` | `light` | `dark` | `publication`
|
|
183
|
+
// Controls pane (opt-in, renders HeatmapMatrixControls inside the shell)
|
|
184
|
+
show_controls?: boolean
|
|
185
|
+
controls_open?: boolean
|
|
186
|
+
controls_props?: Partial<ComponentProps<typeof HeatmapMatrixControls>>
|
|
187
|
+
controls_children?: Snippet<[{ controls_open: boolean }]>
|
|
188
|
+
// Cell value display (true uses '.3~g', string is a format_num spec; ignored when cell snippet is set)
|
|
189
|
+
show_values?: boolean | string
|
|
190
|
+
// Axis config (label used as axis title)
|
|
191
|
+
x_axis?: AxisConfig
|
|
192
|
+
y_axis?: AxisConfig
|
|
193
|
+
tooltip?: HeatmapTooltipProp
|
|
194
|
+
cell?: Snippet<[CellContext]>
|
|
195
|
+
x_label_cell?: Snippet<[{ item: AxisItem; idx: number }]>
|
|
196
|
+
y_label_cell?: Snippet<[{ item: AxisItem; idx: number }]>
|
|
197
|
+
children?: Snippet
|
|
198
|
+
} = $props()
|
|
199
|
+
|
|
200
|
+
// Normalize symmetric prop: true→'lower', otherwise pass through
|
|
201
|
+
const symmetric = $derived(
|
|
202
|
+
symmetric_prop === true ? `lower` : symmetric_prop,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Check if a cell should be skipped in symmetric mode
|
|
206
|
+
function is_hidden_cell(x_idx: number, y_idx: number): boolean {
|
|
207
|
+
if (symmetric === `lower`) return x_idx > y_idx
|
|
208
|
+
if (symmetric === `upper`) return x_idx < y_idx
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// === Value resolution ===
|
|
213
|
+
let x_keys = $derived(x_items.map((item) => item.key ?? item.label))
|
|
214
|
+
let y_keys = $derived(y_items.map((item) => item.key ?? item.label))
|
|
215
|
+
let highlight_x_key_set = $derived(new SvelteSet(highlight_x_keys))
|
|
216
|
+
let highlight_y_key_set = $derived(new SvelteSet(highlight_y_keys))
|
|
217
|
+
let search_query_norm = $derived(search_query.trim().toLowerCase())
|
|
218
|
+
|
|
219
|
+
let get_value = $derived.by(() => {
|
|
42
220
|
if (Array.isArray(values)) {
|
|
43
|
-
|
|
44
|
-
|
|
221
|
+
const matrix_values = values as CellValue[][]
|
|
222
|
+
return (x_idx: number, y_idx: number): CellValue =>
|
|
223
|
+
matrix_values[y_idx]?.[x_idx] ?? null
|
|
45
224
|
}
|
|
46
225
|
// Record<y_key, Record<x_key, value>>
|
|
47
|
-
const record = values
|
|
48
|
-
return (x_idx, y_idx) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
226
|
+
const record = values as Record<string, Record<string, CellValue>>
|
|
227
|
+
return (x_idx: number, y_idx: number): CellValue => {
|
|
228
|
+
const y_key = y_keys[y_idx]
|
|
229
|
+
const x_key = x_keys[x_idx]
|
|
230
|
+
return record[y_key]?.[x_key] ?? null
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// === Visibility filtering ===
|
|
235
|
+
// Single pass to find which columns and rows have at least one non-null value
|
|
236
|
+
function sort_indices(
|
|
237
|
+
indices: number[],
|
|
238
|
+
items: AxisItem[],
|
|
239
|
+
axis_order: AxisOrder | undefined,
|
|
240
|
+
): number[] {
|
|
241
|
+
if (!axis_order) return indices
|
|
242
|
+
const sorted = [...indices]
|
|
60
243
|
if (typeof axis_order === `function`) {
|
|
61
|
-
|
|
62
|
-
|
|
244
|
+
sorted.sort((idx_a, idx_b) => axis_order(items[idx_a], items[idx_b]))
|
|
245
|
+
return sorted
|
|
63
246
|
}
|
|
64
247
|
sorted.sort((idx_a, idx_b) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
77
|
-
return sorted
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
248
|
+
const item_a = items[idx_a]
|
|
249
|
+
const item_b = items[idx_b]
|
|
250
|
+
if (axis_order === `sort_value`) {
|
|
251
|
+
const a_val = item_a.sort_value ?? Number.POSITIVE_INFINITY
|
|
252
|
+
const b_val = item_b.sort_value ?? Number.POSITIVE_INFINITY
|
|
253
|
+
return a_val - b_val
|
|
254
|
+
}
|
|
255
|
+
if (axis_order === `key`) {
|
|
256
|
+
return (item_a.key ?? item_a.label).localeCompare(item_b.key ?? item_b.label)
|
|
257
|
+
}
|
|
258
|
+
return item_a.label.localeCompare(item_b.label)
|
|
259
|
+
})
|
|
260
|
+
return sorted
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let { vis_x, vis_y } = $derived.by(() => {
|
|
264
|
+
const all_x = Array.from({ length: x_items.length }, (_, idx) => idx)
|
|
265
|
+
const all_y = Array.from({ length: y_items.length }, (_, idx) => idx)
|
|
82
266
|
const filtered_x = search_query_norm
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
267
|
+
? all_x.filter((idx) => {
|
|
268
|
+
const item = x_items[idx]
|
|
269
|
+
const key = item.key ?? item.label
|
|
270
|
+
return key.toLowerCase().includes(search_query_norm) ||
|
|
271
|
+
item.label.toLowerCase().includes(search_query_norm)
|
|
272
|
+
})
|
|
273
|
+
: all_x
|
|
90
274
|
const filtered_y = search_query_norm
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
275
|
+
? all_y.filter((idx) => {
|
|
276
|
+
const item = y_items[idx]
|
|
277
|
+
const key = item.key ?? item.label
|
|
278
|
+
return key.toLowerCase().includes(search_query_norm) ||
|
|
279
|
+
item.label.toLowerCase().includes(search_query_norm)
|
|
280
|
+
})
|
|
281
|
+
: all_y
|
|
98
282
|
if (!hide_empty) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
283
|
+
return {
|
|
284
|
+
vis_x: sort_indices(filtered_x, x_items, x_order),
|
|
285
|
+
vis_y: sort_indices(filtered_y, y_items, y_order),
|
|
286
|
+
}
|
|
103
287
|
}
|
|
104
|
-
|
|
105
|
-
const
|
|
288
|
+
|
|
289
|
+
const col_has_data = new Array(x_items.length).fill(false)
|
|
290
|
+
const row_has_data = new Array(y_items.length).fill(false)
|
|
106
291
|
for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
292
|
+
for (let x_idx = 0; x_idx < x_items.length; x_idx++) {
|
|
293
|
+
if (get_value(x_idx, y_idx) !== null) {
|
|
294
|
+
col_has_data[x_idx] = true
|
|
295
|
+
row_has_data[y_idx] = true
|
|
112
296
|
}
|
|
297
|
+
}
|
|
113
298
|
}
|
|
114
299
|
return {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
300
|
+
vis_x: sort_indices(
|
|
301
|
+
filtered_x.filter((idx) => col_has_data[idx]),
|
|
302
|
+
x_items,
|
|
303
|
+
x_order,
|
|
304
|
+
),
|
|
305
|
+
vis_y: sort_indices(
|
|
306
|
+
filtered_y.filter((idx) => row_has_data[idx]),
|
|
307
|
+
y_items,
|
|
308
|
+
y_order,
|
|
309
|
+
),
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// === Color computation ===
|
|
314
|
+
let color_scale_fn = $derived.by(() => {
|
|
315
|
+
if (typeof color_scale === `function`) return color_scale
|
|
316
|
+
const named_scale = d3_sc[color_scale]
|
|
317
|
+
return typeof named_scale === `function` ? named_scale : d3_sc.interpolateViridis
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
function get_transformed_value(x_idx: number, y_idx: number): number | null {
|
|
321
|
+
const raw_value = get_value(x_idx, y_idx)
|
|
322
|
+
if (typeof raw_value !== `number` || !Number.isFinite(raw_value)) return null
|
|
323
|
+
if (!value_transform) return raw_value
|
|
132
324
|
const transformed_value = value_transform(raw_value, {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
})
|
|
138
|
-
if (transformed_value === null || !Number.isFinite(transformed_value))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
function get_quantile(sorted_values, quantile) {
|
|
143
|
-
if (!sorted_values.length)
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const numeric_values = [];
|
|
325
|
+
x_item: x_items[x_idx],
|
|
326
|
+
y_item: y_items[y_idx],
|
|
327
|
+
x_idx,
|
|
328
|
+
y_idx,
|
|
329
|
+
})
|
|
330
|
+
if (transformed_value === null || !Number.isFinite(transformed_value)) return null
|
|
331
|
+
return transformed_value
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function get_quantile(sorted_values: number[], quantile: number): number {
|
|
335
|
+
if (!sorted_values.length) return 0
|
|
336
|
+
const clipped_quantile = Math.max(0, Math.min(1, quantile))
|
|
337
|
+
const float_idx = (sorted_values.length - 1) * clipped_quantile
|
|
338
|
+
const low_idx = Math.floor(float_idx)
|
|
339
|
+
const high_idx = Math.ceil(float_idx)
|
|
340
|
+
if (low_idx === high_idx) return sorted_values[low_idx]
|
|
341
|
+
const low_weight = high_idx - float_idx
|
|
342
|
+
const high_weight = float_idx - low_idx
|
|
343
|
+
return sorted_values[low_idx] * low_weight + sorted_values[high_idx] * high_weight
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let valid_numeric_values = $derived.by(() => {
|
|
347
|
+
const numeric_values: number[] = []
|
|
157
348
|
for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
numeric_values.push(value);
|
|
165
|
-
}
|
|
349
|
+
for (let x_idx = 0; x_idx < x_items.length; x_idx++) {
|
|
350
|
+
if (is_hidden_cell(x_idx, y_idx)) continue
|
|
351
|
+
const value = get_transformed_value(x_idx, y_idx)
|
|
352
|
+
if (value === null) continue
|
|
353
|
+
numeric_values.push(value)
|
|
354
|
+
}
|
|
166
355
|
}
|
|
167
|
-
return numeric_values
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let max = -Infinity
|
|
356
|
+
return numeric_values
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Single-pass min/max to avoid spreading large arrays into Math.min/max
|
|
360
|
+
let [auto_min, auto_max] = $derived.by(() => {
|
|
361
|
+
let [min, max] = [Infinity, -Infinity]
|
|
173
362
|
for (const value of valid_numeric_values) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (value > max)
|
|
177
|
-
max = value;
|
|
363
|
+
if (value < min) min = value
|
|
364
|
+
if (value > max) max = value
|
|
178
365
|
}
|
|
179
|
-
return min <= max ? [min, max] : [0, 1]
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const sorted_values =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
366
|
+
return min <= max ? [min, max] as const : [0, 1] as const
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
let [robust_min, robust_max] = $derived.by(() => {
|
|
370
|
+
if (!valid_numeric_values.length) return [0, 1] as const
|
|
371
|
+
const sorted_values = valid_numeric_values.toSorted((value_a, value_b) =>
|
|
372
|
+
value_a - value_b
|
|
373
|
+
)
|
|
374
|
+
const [q_low, q_high] = quantile_clip
|
|
375
|
+
const clipped_min = get_quantile(sorted_values, q_low)
|
|
376
|
+
const clipped_max = get_quantile(sorted_values, q_high)
|
|
188
377
|
return clipped_min <= clipped_max
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
378
|
+
? [clipped_min, clipped_max] as const
|
|
379
|
+
: [clipped_max, clipped_min] as const
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
let [domain_min, domain_max] = $derived.by(() => {
|
|
383
|
+
if (
|
|
384
|
+
domain_mode === `fixed` &&
|
|
385
|
+
color_scale_range[0] !== null &&
|
|
386
|
+
color_scale_range[1] !== null
|
|
387
|
+
) {
|
|
388
|
+
return [color_scale_range[0], color_scale_range[1]] as const
|
|
197
389
|
}
|
|
198
|
-
if (domain_mode === `robust`)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let cs_min = $derived(color_scale_range[0] ?? domain_min)
|
|
203
|
-
let cs_max = $derived(color_scale_range[1] ?? domain_max)
|
|
204
|
-
let use_log_norm = $derived(normalize === `log` || log)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
390
|
+
if (domain_mode === `robust`) return [robust_min, robust_max] as const
|
|
391
|
+
return [auto_min, auto_max] as const
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
let cs_min = $derived(color_scale_range[0] ?? domain_min)
|
|
395
|
+
let cs_max = $derived(color_scale_range[1] ?? domain_max)
|
|
396
|
+
let use_log_norm = $derived(normalize === `log` || log)
|
|
397
|
+
|
|
398
|
+
// Map a single value to a background color
|
|
399
|
+
function value_to_color(val: CellValue): string | null {
|
|
400
|
+
if (val === null) return missing_color || null
|
|
209
401
|
if (typeof val === `string`) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return missing_color || null;
|
|
402
|
+
if (is_color(val)) return val
|
|
403
|
+
return missing_color || null
|
|
213
404
|
}
|
|
214
|
-
if (!Number.isFinite(val) || !color_scale_fn)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return color_scale_fn(0.5);
|
|
405
|
+
if (!Number.isFinite(val) || !color_scale_fn) return missing_color || null
|
|
406
|
+
if (use_log_norm && val <= 0) return missing_color || null
|
|
407
|
+
|
|
408
|
+
const span = cs_max - cs_min
|
|
409
|
+
if (!Number.isFinite(span) || span === 0) return color_scale_fn(0.5)
|
|
410
|
+
|
|
221
411
|
let normalized = typeof normalize === `function`
|
|
222
|
-
|
|
223
|
-
|
|
412
|
+
? normalize(val, cs_min, cs_max)
|
|
413
|
+
: (val - cs_min) / span
|
|
224
414
|
if (use_log_norm) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
415
|
+
const is_descending_range = cs_min > cs_max
|
|
416
|
+
const lower_bound = Math.min(cs_min, cs_max)
|
|
417
|
+
const upper_bound = Math.max(cs_min, cs_max)
|
|
418
|
+
if (upper_bound <= 0) return missing_color || null
|
|
419
|
+
const safe_lower_bound = Math.max(lower_bound, Number.MIN_VALUE)
|
|
420
|
+
const safe_value = Math.max(val, safe_lower_bound)
|
|
421
|
+
const log_min = Math.log(safe_lower_bound)
|
|
422
|
+
const log_max = Math.log(upper_bound)
|
|
423
|
+
if (
|
|
424
|
+
!Number.isFinite(log_min) || !Number.isFinite(log_max) || log_max === log_min
|
|
425
|
+
) {
|
|
426
|
+
return color_scale_fn(0.5)
|
|
427
|
+
}
|
|
428
|
+
const log_normalized = (Math.log(safe_value) - log_min) / (log_max - log_min)
|
|
429
|
+
normalized = is_descending_range ? 1 - log_normalized : log_normalized
|
|
239
430
|
}
|
|
240
|
-
if (!Number.isFinite(normalized))
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Batch compute background colors as a flat array indexed by y_idx * n_x + x_idx.
|
|
245
|
-
// Text colors are only computed when a cell snippet is provided (otherwise cells have no text).
|
|
246
|
-
let n_x = $derived(x_items.length)
|
|
247
|
-
let bg_flat = $derived.by(() => {
|
|
248
|
-
const n_y = y_items.length
|
|
249
|
-
const colors = new Array(n_x * n_y)
|
|
431
|
+
if (!Number.isFinite(normalized)) return missing_color || null
|
|
432
|
+
return color_scale_fn(Math.max(0, Math.min(1, normalized)))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Batch compute background colors as a flat array indexed by y_idx * n_x + x_idx.
|
|
436
|
+
// Text colors are only computed when a cell snippet is provided (otherwise cells have no text).
|
|
437
|
+
let n_x = $derived(x_items.length)
|
|
438
|
+
let bg_flat = $derived.by(() => {
|
|
439
|
+
const n_y = y_items.length
|
|
440
|
+
const colors = new Array<string | null>(n_x * n_y)
|
|
250
441
|
for (let y_idx = 0; y_idx < n_y; y_idx++) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
const override_key = make_color_override_key(x_keys[x_idx], y_keys[y_idx]);
|
|
258
|
-
const raw_value = get_value(x_idx, y_idx);
|
|
259
|
-
const transformed_value = typeof raw_value === `number`
|
|
260
|
-
? get_transformed_value(x_idx, y_idx)
|
|
261
|
-
: raw_value;
|
|
262
|
-
colors[row_offset + x_idx] = override_key in color_overrides
|
|
263
|
-
? color_overrides[override_key]
|
|
264
|
-
: value_to_color(transformed_value);
|
|
442
|
+
const row_offset = y_idx * n_x
|
|
443
|
+
for (let x_idx = 0; x_idx < n_x; x_idx++) {
|
|
444
|
+
if (is_hidden_cell(x_idx, y_idx)) {
|
|
445
|
+
colors[row_offset + x_idx] = null
|
|
446
|
+
continue
|
|
265
447
|
}
|
|
448
|
+
const override_key = make_color_override_key(x_keys[x_idx], y_keys[y_idx])
|
|
449
|
+
const raw_value = get_value(x_idx, y_idx)
|
|
450
|
+
const transformed_value = typeof raw_value === `number`
|
|
451
|
+
? get_transformed_value(x_idx, y_idx)
|
|
452
|
+
: raw_value
|
|
453
|
+
colors[row_offset + x_idx] = override_key in color_overrides
|
|
454
|
+
? color_overrides[override_key]
|
|
455
|
+
: value_to_color(transformed_value)
|
|
456
|
+
}
|
|
266
457
|
}
|
|
267
|
-
return colors
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
458
|
+
return colors
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
const to_contrast_colors = (bg_values: Array<string | null>): Array<string | null> =>
|
|
462
|
+
bg_values.map((bg_color) =>
|
|
463
|
+
bg_color ? pick_contrast_color({ bg_color }) : null
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
// Compute text colors when cells render content that needs contrast (cell snippet or show_values)
|
|
467
|
+
let text_flat = $derived.by(() => {
|
|
468
|
+
if (!cell && !show_values) return null
|
|
469
|
+
return to_contrast_colors(bg_flat)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Keep selected outlines visible against each cell's background.
|
|
473
|
+
let selected_outline_flat = $derived.by(() => to_contrast_colors(bg_flat))
|
|
474
|
+
|
|
475
|
+
const get_flat_idx = (x_idx: number, y_idx: number): number => y_idx * n_x + x_idx
|
|
476
|
+
|
|
477
|
+
// Look up bg color by indices
|
|
478
|
+
const get_bg = (x_idx: number, y_idx: number): string | null =>
|
|
479
|
+
bg_flat[get_flat_idx(x_idx, y_idx)]
|
|
480
|
+
|
|
481
|
+
// === Cell context builder (only called for clicks, not per-hover) ===
|
|
482
|
+
function build_cell_context(x_idx: number, y_idx: number): CellContext {
|
|
289
483
|
return {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
let
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let
|
|
308
|
-
let
|
|
309
|
-
let
|
|
310
|
-
let
|
|
311
|
-
let
|
|
312
|
-
let
|
|
313
|
-
let
|
|
314
|
-
let
|
|
315
|
-
let
|
|
316
|
-
let
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
let
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let
|
|
345
|
-
let
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
484
|
+
x_item: x_items[x_idx],
|
|
485
|
+
y_item: y_items[y_idx],
|
|
486
|
+
x_idx,
|
|
487
|
+
y_idx,
|
|
488
|
+
value: get_value(x_idx, y_idx),
|
|
489
|
+
bg_color: get_bg(x_idx, y_idx),
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// === Fully imperative hover management ===
|
|
494
|
+
// ZERO $state writes during mouseover — all DOM updates are direct.
|
|
495
|
+
// This avoids Svelte's reactive flush which would re-evaluate effects.
|
|
496
|
+
const is_browser = typeof window !== `undefined`
|
|
497
|
+
let tooltip_div: HTMLDivElement | undefined = $state()
|
|
498
|
+
let active_cell_raf = 0 // rAF handle for deferred active_cell update
|
|
499
|
+
let click_timeout_id: ReturnType<typeof setTimeout> | null = null
|
|
500
|
+
const dblclick_delay_ms = 250
|
|
501
|
+
let last_hover_x = -1
|
|
502
|
+
let last_hover_y = -1
|
|
503
|
+
let matrix_el: HTMLDivElement | undefined = $state()
|
|
504
|
+
let scroll_left = $state(0)
|
|
505
|
+
let scroll_top = $state(0)
|
|
506
|
+
let viewport_width = $state(0)
|
|
507
|
+
let viewport_height = $state(0)
|
|
508
|
+
let grid_offset_left = $state(0)
|
|
509
|
+
let grid_offset_top = $state(0)
|
|
510
|
+
let brush_start: CellPos | null = $state(null)
|
|
511
|
+
let brush_end: CellPos | null = $state(null)
|
|
512
|
+
let last_selected_cell: CellPos | null = $state(null)
|
|
513
|
+
|
|
514
|
+
// In symmetric mode, labels can either stay on outer edges ('edge')
|
|
515
|
+
// or move toward the missing triangle and hug the diagonal ('diagonal').
|
|
516
|
+
let use_diagonal_symmetric_labels = $derived(
|
|
517
|
+
symmetric && symmetric_label_position === `diagonal`,
|
|
518
|
+
)
|
|
519
|
+
let use_staggered_x_labels = $derived(
|
|
520
|
+
stagger_axis_labels === true ||
|
|
521
|
+
(stagger_axis_labels === `auto` && vis_x.length >= 24),
|
|
522
|
+
)
|
|
523
|
+
let use_staggered_y_labels = $derived(
|
|
524
|
+
stagger_axis_labels === true ||
|
|
525
|
+
(stagger_axis_labels === `auto` && vis_y.length >= 24),
|
|
526
|
+
)
|
|
527
|
+
let use_side_split_x_labels = $derived(
|
|
528
|
+
use_staggered_x_labels && !use_diagonal_symmetric_labels,
|
|
529
|
+
)
|
|
530
|
+
// Don't split y-labels to both sides when symmetric -- one side has no cells
|
|
531
|
+
let use_side_split_y_labels = $derived(use_staggered_y_labels && !symmetric)
|
|
532
|
+
// For 'gaps' mode: explicit grid placement to preserve positional alignment
|
|
533
|
+
let gaps_mode = $derived(hide_empty === `gaps`)
|
|
534
|
+
let visible_col_count = $derived(gaps_mode ? x_items.length : vis_x.length)
|
|
535
|
+
let visible_row_count = $derived(gaps_mode ? y_items.length : vis_y.length)
|
|
536
|
+
let show_bottom_summary_row = $derived(show_col_summaries)
|
|
537
|
+
let show_right_summary_col = $derived(show_row_summaries)
|
|
538
|
+
let grid_col_count = $derived(visible_col_count + (show_right_summary_col ? 1 : 0))
|
|
539
|
+
let grid_row_count = $derived(visible_row_count + (show_bottom_summary_row ? 1 : 0))
|
|
540
|
+
|
|
541
|
+
const cell_pos_key = (x_idx: number, y_idx: number): string => `${x_idx}:${y_idx}`
|
|
542
|
+
|
|
543
|
+
let selected_cell_key_set = $derived(
|
|
544
|
+
new SvelteSet(
|
|
545
|
+
selected_cells.map((cell_pos) => cell_pos_key(cell_pos.x_idx, cell_pos.y_idx)),
|
|
546
|
+
),
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
function parse_px_size(size: string): number {
|
|
550
|
+
const parsed = Number.parseFloat(size)
|
|
551
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 12
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let tile_size_px = $derived(parse_px_size(tile_size))
|
|
555
|
+
let gap_px = $derived(parse_px_size(gap))
|
|
556
|
+
let tile_stride_px = $derived(tile_size_px + gap_px)
|
|
557
|
+
let render_vis_x = $derived.by(() => {
|
|
558
|
+
if (!virtualize) return vis_x
|
|
559
|
+
const raw_start_pos =
|
|
560
|
+
Math.floor((scroll_left - grid_offset_left) / tile_stride_px) - overscan
|
|
561
|
+
const start_pos = Math.max(0, raw_start_pos)
|
|
562
|
+
const raw_end_pos =
|
|
563
|
+
Math.ceil((scroll_left - grid_offset_left + viewport_width) / tile_stride_px) +
|
|
564
|
+
overscan
|
|
565
|
+
const end_pos = Math.min(vis_x.length, raw_end_pos)
|
|
566
|
+
return vis_x.slice(start_pos, end_pos)
|
|
567
|
+
})
|
|
568
|
+
let render_vis_y = $derived.by(() => {
|
|
569
|
+
if (!virtualize) return vis_y
|
|
570
|
+
const raw_start_pos =
|
|
571
|
+
Math.floor((scroll_top - grid_offset_top) / tile_stride_px) - overscan
|
|
572
|
+
const start_pos = Math.max(0, raw_start_pos)
|
|
573
|
+
const raw_end_pos =
|
|
574
|
+
Math.ceil((scroll_top - grid_offset_top + viewport_height) / tile_stride_px) +
|
|
575
|
+
overscan
|
|
576
|
+
const end_pos = Math.min(vis_y.length, raw_end_pos)
|
|
577
|
+
return vis_y.slice(start_pos, end_pos)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
function is_selected_cell(x_idx: number, y_idx: number): boolean {
|
|
581
|
+
return selected_cell_key_set.has(cell_pos_key(x_idx, y_idx))
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let vis_x_pos_map = $derived.by(() => {
|
|
585
|
+
const position_map = new SvelteMap<number, number>()
|
|
372
586
|
for (const [vis_pos, item_idx] of vis_x.entries()) {
|
|
373
|
-
|
|
587
|
+
position_map.set(item_idx, vis_pos)
|
|
374
588
|
}
|
|
375
|
-
return position_map
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
|
|
589
|
+
return position_map
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
let vis_y_pos_map = $derived.by(() => {
|
|
593
|
+
const position_map = new SvelteMap<number, number>()
|
|
379
594
|
for (const [vis_pos, item_idx] of vis_y.entries()) {
|
|
380
|
-
|
|
595
|
+
position_map.set(item_idx, vis_pos)
|
|
381
596
|
}
|
|
382
|
-
return position_map
|
|
383
|
-
})
|
|
384
|
-
let highlight_x_by_idx = $derived(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
597
|
+
return position_map
|
|
598
|
+
})
|
|
599
|
+
let highlight_x_by_idx = $derived(
|
|
600
|
+
new SvelteSet(
|
|
601
|
+
vis_x.filter((idx) =>
|
|
602
|
+
highlight_x_key_set.has(x_items[idx].key ?? x_items[idx].label)
|
|
603
|
+
),
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
let highlight_y_by_idx = $derived(
|
|
607
|
+
new SvelteSet(
|
|
608
|
+
vis_y.filter((idx) =>
|
|
609
|
+
highlight_y_key_set.has(y_items[idx].key ?? y_items[idx].label)
|
|
610
|
+
),
|
|
611
|
+
),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
function get_vis_col(item_idx: number): number | null {
|
|
615
|
+
if (gaps_mode) return item_idx
|
|
616
|
+
return vis_x_pos_map.get(item_idx) ?? null
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function get_vis_row(item_idx: number): number | null {
|
|
620
|
+
if (gaps_mode) return item_idx
|
|
621
|
+
return vis_y_pos_map.get(item_idx) ?? null
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function x_label_diag_grid_row(x_idx: number): number | undefined {
|
|
625
|
+
const vis_row = get_vis_row(x_idx)
|
|
626
|
+
if (vis_row === null) return undefined
|
|
400
627
|
if (symmetric === `upper`) {
|
|
401
|
-
|
|
402
|
-
|
|
628
|
+
// Upper triangle: place x label below diagonal (in empty lower-left area)
|
|
629
|
+
return Math.min(visible_row_count + 1, vis_row + 3)
|
|
403
630
|
}
|
|
404
631
|
// Lower/default: place x label above diagonal (in empty upper-right area)
|
|
405
|
-
return Math.max(1, vis_row + 1)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return vis_col + 2
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return vis_row + 2
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return cell_grid_col(x_idx)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
632
|
+
return Math.max(1, vis_row + 1)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function x_label_diag_grid_col(x_idx: number): number | undefined {
|
|
636
|
+
const vis_col = get_vis_col(x_idx)
|
|
637
|
+
if (vis_col === null) return undefined
|
|
638
|
+
return vis_col + 2
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function y_label_edge_grid_row(y_idx: number): number | undefined {
|
|
642
|
+
const vis_row = get_vis_row(y_idx)
|
|
643
|
+
if (vis_row === null) return undefined
|
|
644
|
+
return vis_row + 2
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function x_label_grid_col(x_idx: number): number | undefined {
|
|
648
|
+
if (use_diagonal_symmetric_labels) return x_label_diag_grid_col(x_idx)
|
|
649
|
+
return cell_grid_col(x_idx)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function x_label_grid_row(x_idx: number): number | undefined {
|
|
653
|
+
if (use_diagonal_symmetric_labels) return x_label_diag_grid_row(x_idx)
|
|
427
654
|
if (use_side_split_x_labels && x_idx % 2 !== 0) {
|
|
428
|
-
|
|
655
|
+
return visible_row_count + 2 + (show_bottom_summary_row ? 1 : 0)
|
|
429
656
|
}
|
|
430
|
-
return 1
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
657
|
+
return 1
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Upper symmetric or staggered odd labels: place on right side
|
|
661
|
+
function y_label_grid_col(y_idx: number): number {
|
|
434
662
|
if (symmetric === `upper` || (use_side_split_y_labels && y_idx % 2 !== 0)) {
|
|
435
|
-
|
|
663
|
+
return visible_col_count + 2 + (show_right_summary_col ? 1 : 0)
|
|
436
664
|
}
|
|
437
|
-
return 1
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return vis_col + 2
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return vis_row + 2
|
|
450
|
-
}
|
|
451
|
-
|
|
665
|
+
return 1
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function cell_grid_col(x_idx: number): number | undefined {
|
|
669
|
+
const vis_col = get_vis_col(x_idx)
|
|
670
|
+
if (vis_col === null) return undefined
|
|
671
|
+
return vis_col + 2
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function cell_grid_row(y_idx: number): number | undefined {
|
|
675
|
+
const vis_row = get_vis_row(y_idx)
|
|
676
|
+
if (vis_row === null) return undefined
|
|
677
|
+
return vis_row + 2
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function schedule_raf(callback: () => void): number {
|
|
452
681
|
if (!is_browser) {
|
|
453
|
-
|
|
454
|
-
|
|
682
|
+
callback()
|
|
683
|
+
return 0
|
|
455
684
|
}
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
clearTimeout(click_timeout_id)
|
|
467
|
-
click_timeout_id = null
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
685
|
+
return globalThis.requestAnimationFrame(callback)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function cancel_raf(raf_handle: number): void {
|
|
689
|
+
if (!is_browser || raf_handle === 0) return
|
|
690
|
+
globalThis.cancelAnimationFrame(raf_handle)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function clear_pending_click(): void {
|
|
694
|
+
if (click_timeout_id === null) return
|
|
695
|
+
clearTimeout(click_timeout_id)
|
|
696
|
+
click_timeout_id = null
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function parse_cell_indices(
|
|
700
|
+
cell_el: HTMLElement,
|
|
701
|
+
): { x_idx: number; y_idx: number } | null {
|
|
702
|
+
const x_value = Number(cell_el.dataset.x)
|
|
703
|
+
const y_value = Number(cell_el.dataset.y)
|
|
704
|
+
if (!Number.isInteger(x_value) || !Number.isInteger(y_value)) return null
|
|
705
|
+
return { x_idx: x_value, y_idx: y_value }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function get_cell_context_from_target(
|
|
709
|
+
event_target: EventTarget | null,
|
|
710
|
+
): CellContext | null {
|
|
711
|
+
const cell_el = get_cell_el_from_target(event_target)
|
|
712
|
+
if (!cell_el) return null
|
|
713
|
+
const indices = parse_cell_indices(cell_el)
|
|
714
|
+
if (!indices) return null
|
|
715
|
+
return build_cell_context(indices.x_idx, indices.y_idx)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function trigger_click(cell_context: CellContext): void {
|
|
719
|
+
if (!onclick) return
|
|
488
720
|
if (!ondblclick) {
|
|
489
|
-
|
|
490
|
-
|
|
721
|
+
onclick(cell_context)
|
|
722
|
+
return
|
|
491
723
|
}
|
|
492
|
-
clear_pending_click()
|
|
724
|
+
clear_pending_click()
|
|
493
725
|
click_timeout_id = setTimeout(() => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}, dblclick_delay_ms)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
726
|
+
onclick(cell_context)
|
|
727
|
+
click_timeout_id = null
|
|
728
|
+
}, dblclick_delay_ms)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function get_cell_el_from_target(
|
|
732
|
+
event_target: EventTarget | null,
|
|
733
|
+
): HTMLElement | null {
|
|
734
|
+
const target_node = event_target
|
|
735
|
+
if (!(target_node instanceof Element)) return null
|
|
502
736
|
if (target_node instanceof HTMLElement && target_node.dataset.x !== undefined) {
|
|
503
|
-
|
|
737
|
+
return target_node
|
|
504
738
|
}
|
|
505
|
-
const closest_cell = target_node.closest(`[data-x][data-y]`)
|
|
506
|
-
return closest_cell instanceof HTMLElement ? closest_cell : null
|
|
507
|
-
}
|
|
508
|
-
|
|
739
|
+
const closest_cell = target_node.closest(`[data-x][data-y]`)
|
|
740
|
+
return closest_cell instanceof HTMLElement ? closest_cell : null
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function update_selected_cells(
|
|
744
|
+
event: MouseEvent,
|
|
745
|
+
clicked_cell: CellPos,
|
|
746
|
+
): void {
|
|
509
747
|
if (selection_mode === `single`) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
748
|
+
selected_cells = [clicked_cell]
|
|
749
|
+
last_selected_cell = clicked_cell
|
|
750
|
+
onselect?.(selected_cells)
|
|
751
|
+
return
|
|
514
752
|
}
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
753
|
+
if (
|
|
754
|
+
selection_mode === `range` &&
|
|
755
|
+
event.shiftKey &&
|
|
756
|
+
last_selected_cell
|
|
757
|
+
) {
|
|
758
|
+
const x_min = Math.min(last_selected_cell.x_idx, clicked_cell.x_idx)
|
|
759
|
+
const x_max = Math.max(last_selected_cell.x_idx, clicked_cell.x_idx)
|
|
760
|
+
const y_min = Math.min(last_selected_cell.y_idx, clicked_cell.y_idx)
|
|
761
|
+
const y_max = Math.max(last_selected_cell.y_idx, clicked_cell.y_idx)
|
|
762
|
+
const next_cells: CellPos[] = []
|
|
763
|
+
for (let y_idx = y_min; y_idx <= y_max; y_idx++) {
|
|
764
|
+
for (let x_idx = x_min; x_idx <= x_max; x_idx++) {
|
|
765
|
+
if (is_hidden_cell(x_idx, y_idx)) continue
|
|
766
|
+
next_cells.push({ x_idx, y_idx })
|
|
529
767
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const clicked_key = cell_pos_key(clicked_cell.x_idx, clicked_cell.y_idx);
|
|
535
|
-
const next_cells = [...selected_cells];
|
|
536
|
-
const existing_idx = next_cells.findIndex((pos) => cell_pos_key(pos.x_idx, pos.y_idx) === clicked_key);
|
|
537
|
-
const toggle_mode = selection_mode === `multi` && (event.metaKey || event.ctrlKey);
|
|
538
|
-
if (existing_idx >= 0 && toggle_mode) {
|
|
539
|
-
next_cells.splice(existing_idx, 1);
|
|
540
|
-
}
|
|
541
|
-
else if (selection_mode === `multi` && toggle_mode) {
|
|
542
|
-
next_cells.push(clicked_cell);
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
next_cells.splice(0, next_cells.length, clicked_cell);
|
|
768
|
+
}
|
|
769
|
+
selected_cells = next_cells
|
|
770
|
+
onselect?.(selected_cells)
|
|
771
|
+
return
|
|
546
772
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
773
|
+
const clicked_key = cell_pos_key(clicked_cell.x_idx, clicked_cell.y_idx)
|
|
774
|
+
const next_cells = [...selected_cells]
|
|
775
|
+
const existing_idx = next_cells.findIndex((pos) =>
|
|
776
|
+
cell_pos_key(pos.x_idx, pos.y_idx) === clicked_key
|
|
777
|
+
)
|
|
778
|
+
const toggle_mode = selection_mode === `multi` && (event.metaKey || event.ctrlKey)
|
|
779
|
+
if (existing_idx >= 0 && toggle_mode) next_cells.splice(existing_idx, 1)
|
|
780
|
+
else if (selection_mode === `multi` && toggle_mode) next_cells.push(clicked_cell)
|
|
781
|
+
else next_cells.splice(0, next_cells.length, clicked_cell)
|
|
782
|
+
selected_cells = next_cells
|
|
783
|
+
last_selected_cell = clicked_cell
|
|
784
|
+
onselect?.(selected_cells)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function update_tooltip_position(client_x: number, client_y: number): void {
|
|
788
|
+
if (!tooltip_div) return
|
|
789
|
+
const tw = tooltip_div.offsetWidth
|
|
790
|
+
const th = tooltip_div.offsetHeight
|
|
791
|
+
// Flip to opposite side of cursor when near viewport edges
|
|
792
|
+
const left = client_x + 10 + tw > globalThis.innerWidth ? client_x - 10 - tw : client_x + 10
|
|
793
|
+
const top = client_y + 12 + th > globalThis.innerHeight ? client_y - 12 - th : client_y + 12
|
|
794
|
+
tooltip_div.style.left = `${Math.max(0, left)}px`
|
|
795
|
+
tooltip_div.style.top = `${Math.max(0, top)}px`
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function set_pinned_cell(next_cell: CellPos | null): void {
|
|
799
|
+
pinned_cell = next_cell
|
|
800
|
+
onpin?.(next_cell)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Write default tooltip content imperatively (no reactive state)
|
|
804
|
+
function update_tooltip_content(
|
|
805
|
+
td: HTMLElement,
|
|
806
|
+
x_idx: number,
|
|
807
|
+
y_idx: number,
|
|
808
|
+
): void {
|
|
809
|
+
const x_label = x_items[x_idx]?.label ?? ``
|
|
810
|
+
const y_label = y_items[y_idx]?.label ?? ``
|
|
811
|
+
const val = get_value(x_idx, y_idx)
|
|
812
|
+
const value_str = val == null
|
|
813
|
+
? ``
|
|
814
|
+
: typeof val === `number`
|
|
815
|
+
? format_num(val)
|
|
816
|
+
: String(val)
|
|
571
817
|
td.textContent = value_str
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const cell_el = get_cell_el_from_target(event.target)
|
|
579
|
-
if (!cell_el)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const { x_idx, y_idx } = indices;
|
|
818
|
+
? `${x_label} - ${y_label}: ${value_str}`
|
|
819
|
+
: `${x_label} - ${y_label}`
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function handle_mouseover(event: MouseEvent) {
|
|
823
|
+
if (disabled) return
|
|
824
|
+
const cell_el = get_cell_el_from_target(event.target)
|
|
825
|
+
if (!cell_el) return
|
|
826
|
+
const indices = parse_cell_indices(cell_el)
|
|
827
|
+
if (!indices) return
|
|
828
|
+
const { x_idx, y_idx } = indices
|
|
829
|
+
|
|
585
830
|
// Ignore redundant enters on the same cell (can happen with nested children)
|
|
586
831
|
if (last_hover_x === x_idx && last_hover_y === y_idx) {
|
|
587
|
-
|
|
832
|
+
return
|
|
588
833
|
}
|
|
589
|
-
last_hover_x = x_idx
|
|
590
|
-
last_hover_y = y_idx
|
|
834
|
+
last_hover_x = x_idx
|
|
835
|
+
last_hover_y = y_idx
|
|
836
|
+
|
|
591
837
|
// Defer bindable writes out of the hot mouseover path
|
|
592
|
-
cancel_raf(active_cell_raf)
|
|
838
|
+
cancel_raf(active_cell_raf)
|
|
593
839
|
active_cell_raf = schedule_raf(() => {
|
|
594
|
-
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (tooltip === false || !tooltip_div || tooltip_mode === `pinned`)
|
|
599
|
-
|
|
840
|
+
active_cell = { x_idx, y_idx }
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
if (enable_brush && brush_start) brush_end = { x_idx, y_idx }
|
|
844
|
+
if (tooltip === false || !tooltip_div || tooltip_mode === `pinned`) return
|
|
845
|
+
|
|
600
846
|
// Use viewport coordinates to avoid forced layout reads on large grids
|
|
601
|
-
update_tooltip_position(event.clientX, event.clientY)
|
|
602
|
-
tooltip_div.classList.add(`visible`)
|
|
847
|
+
update_tooltip_position(event.clientX, event.clientY)
|
|
848
|
+
tooltip_div.classList.add(`visible`)
|
|
849
|
+
|
|
603
850
|
if (typeof tooltip === `function`) {
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
update_tooltip_content(tooltip_div, x_idx, y_idx);
|
|
851
|
+
tooltip_cell = build_cell_context(x_idx, y_idx)
|
|
852
|
+
} else {
|
|
853
|
+
update_tooltip_content(tooltip_div, x_idx, y_idx)
|
|
608
854
|
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const related = event.relatedTarget
|
|
614
|
-
if (related?.closest?.(`[data-x][data-y]`))
|
|
615
|
-
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function handle_mouseout(event: MouseEvent) {
|
|
858
|
+
if (disabled) return
|
|
859
|
+
const related = event.relatedTarget as HTMLElement | null
|
|
860
|
+
if (related?.closest?.(`[data-x][data-y]`)) return
|
|
616
861
|
// Clear active state imperatively
|
|
617
|
-
last_hover_x = -1
|
|
618
|
-
last_hover_y = -1
|
|
862
|
+
last_hover_x = -1
|
|
863
|
+
last_hover_y = -1
|
|
619
864
|
const keep_tooltip_visible = tooltip_mode === `pinned` ||
|
|
620
|
-
|
|
865
|
+
(tooltip_mode === `both` && pinned_cell !== null)
|
|
621
866
|
if (!keep_tooltip_visible) {
|
|
622
|
-
|
|
867
|
+
tooltip_div?.classList.remove(`visible`)
|
|
623
868
|
}
|
|
624
869
|
// Defer reactive cleanup to rAF
|
|
625
|
-
cancel_raf(active_cell_raf)
|
|
870
|
+
cancel_raf(active_cell_raf)
|
|
626
871
|
active_cell_raf = schedule_raf(() => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
function handle_click(event) {
|
|
633
|
-
if (disabled)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
update_selected_cells(event, {
|
|
639
|
-
x_idx: cell_context.x_idx,
|
|
640
|
-
y_idx: cell_context.y_idx,
|
|
641
|
-
});
|
|
872
|
+
active_cell = null
|
|
873
|
+
if (!keep_tooltip_visible) tooltip_cell = null
|
|
874
|
+
})
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function handle_click(event: MouseEvent) {
|
|
878
|
+
if (disabled) return
|
|
879
|
+
const cell_context = get_cell_context_from_target(event.target)
|
|
880
|
+
if (!cell_context) return
|
|
881
|
+
const { x_idx, y_idx } = cell_context
|
|
882
|
+
update_selected_cells(event, { x_idx, y_idx })
|
|
642
883
|
if (tooltip_mode === `both` || tooltip_mode === `pinned`) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
else {
|
|
651
|
-
update_tooltip_content(tooltip_div, cell_context.x_idx, cell_context.y_idx);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
884
|
+
set_pinned_cell({ x_idx, y_idx })
|
|
885
|
+
if (tooltip !== false && tooltip_div) {
|
|
886
|
+
update_tooltip_position(event.clientX, event.clientY)
|
|
887
|
+
tooltip_div.classList.add(`visible`)
|
|
888
|
+
if (typeof tooltip === `function`) tooltip_cell = cell_context
|
|
889
|
+
else update_tooltip_content(tooltip_div, x_idx, y_idx)
|
|
890
|
+
}
|
|
654
891
|
}
|
|
655
|
-
if (!onclick)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
function handle_dblclick(event) {
|
|
660
|
-
if (disabled || !ondblclick)
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
brush_end = { x_idx: cell_context.x_idx, y_idx: cell_context.y_idx };
|
|
685
|
-
}
|
|
686
|
-
function handle_mouseup() {
|
|
892
|
+
if (!onclick) return
|
|
893
|
+
trigger_click(cell_context)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function handle_dblclick(event: MouseEvent) {
|
|
897
|
+
if (disabled || !ondblclick) return
|
|
898
|
+
const cell_context = get_cell_context_from_target(event.target)
|
|
899
|
+
if (!cell_context) return
|
|
900
|
+
clear_pending_click()
|
|
901
|
+
ondblclick(cell_context)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function handle_contextmenu(event: MouseEvent): void {
|
|
905
|
+
if (disabled || !oncontextmenu) return
|
|
906
|
+
const cell_context = get_cell_context_from_target(event.target)
|
|
907
|
+
if (!cell_context) return
|
|
908
|
+
event.preventDefault()
|
|
909
|
+
oncontextmenu(cell_context, event)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function handle_mousedown(event: MouseEvent): void {
|
|
913
|
+
if (disabled || !enable_brush) return
|
|
914
|
+
const cell_context = get_cell_context_from_target(event.target)
|
|
915
|
+
if (!cell_context) return
|
|
916
|
+
brush_start = { x_idx: cell_context.x_idx, y_idx: cell_context.y_idx }
|
|
917
|
+
brush_end = { x_idx: cell_context.x_idx, y_idx: cell_context.y_idx }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function handle_mouseup(): void {
|
|
687
921
|
if (!enable_brush || !brush_start || !brush_end || !onbrush) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
922
|
+
brush_start = null
|
|
923
|
+
brush_end = null
|
|
924
|
+
return
|
|
691
925
|
}
|
|
692
|
-
const x_min = Math.min(brush_start.x_idx, brush_end.x_idx)
|
|
693
|
-
const x_max = Math.max(brush_start.x_idx, brush_end.x_idx)
|
|
694
|
-
const y_min = Math.min(brush_start.y_idx, brush_end.y_idx)
|
|
695
|
-
const y_max = Math.max(brush_start.y_idx, brush_end.y_idx)
|
|
696
|
-
const cells = []
|
|
926
|
+
const x_min = Math.min(brush_start.x_idx, brush_end.x_idx)
|
|
927
|
+
const x_max = Math.max(brush_start.x_idx, brush_end.x_idx)
|
|
928
|
+
const y_min = Math.min(brush_start.y_idx, brush_end.y_idx)
|
|
929
|
+
const y_max = Math.max(brush_start.y_idx, brush_end.y_idx)
|
|
930
|
+
const cells: CellContext[] = []
|
|
697
931
|
for (let y_idx = y_min; y_idx <= y_max; y_idx++) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
932
|
+
for (let x_idx = x_min; x_idx <= x_max; x_idx++) {
|
|
933
|
+
if (is_hidden_cell(x_idx, y_idx)) continue
|
|
934
|
+
cells.push(build_cell_context(x_idx, y_idx))
|
|
935
|
+
}
|
|
703
936
|
}
|
|
704
|
-
onbrush({ x_range: [x_min, x_max], y_range: [y_min, y_max], cells })
|
|
705
|
-
brush_start = null
|
|
706
|
-
brush_end = null
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
target.focus()
|
|
713
|
-
active_cell = { x_idx, y_idx }
|
|
714
|
-
return true
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (!(active_el.dataset.x && active_el.dataset.y))
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (event.key === `
|
|
729
|
-
x_step = 1;
|
|
730
|
-
else if (event.key === `ArrowLeft`)
|
|
731
|
-
x_step = -1;
|
|
732
|
-
else if (event.key === `ArrowDown`)
|
|
733
|
-
y_step = 1;
|
|
734
|
-
else if (event.key === `ArrowUp`)
|
|
735
|
-
y_step = -1;
|
|
937
|
+
onbrush({ x_range: [x_min, x_max], y_range: [y_min, y_max], cells })
|
|
938
|
+
brush_start = null
|
|
939
|
+
brush_end = null
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function focus_cell(x_idx: number, y_idx: number): boolean {
|
|
943
|
+
const target = matrix_el?.querySelector(`[data-x="${x_idx}"][data-y="${y_idx}"]`)
|
|
944
|
+
if (!(target instanceof HTMLElement)) return false
|
|
945
|
+
target.focus()
|
|
946
|
+
active_cell = { x_idx, y_idx }
|
|
947
|
+
return true
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function handle_keydown(event: KeyboardEvent): void {
|
|
951
|
+
const active_el = document.activeElement
|
|
952
|
+
if (!(active_el instanceof HTMLElement)) return
|
|
953
|
+
if (!(active_el.dataset.x && active_el.dataset.y)) return
|
|
954
|
+
const x_idx = Number(active_el.dataset.x)
|
|
955
|
+
const y_idx = Number(active_el.dataset.y)
|
|
956
|
+
if (!Number.isInteger(x_idx) || !Number.isInteger(y_idx)) return
|
|
957
|
+
let [x_step, y_step] = [0, 0]
|
|
958
|
+
if (event.key === `ArrowRight`) x_step = 1
|
|
959
|
+
else if (event.key === `ArrowLeft`) x_step = -1
|
|
960
|
+
else if (event.key === `ArrowDown`) y_step = 1
|
|
961
|
+
else if (event.key === `ArrowUp`) y_step = -1
|
|
736
962
|
else if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === `e`) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
event.preventDefault();
|
|
745
|
-
let next_x = x_idx;
|
|
746
|
-
let next_y = y_idx;
|
|
747
|
-
const max_steps = Math.max(x_items.length, y_items.length) + 1;
|
|
963
|
+
const format = export_formats[0]
|
|
964
|
+
if (format && onexport) onexport(format, build_export_payload(format))
|
|
965
|
+
return
|
|
966
|
+
} else return
|
|
967
|
+
event.preventDefault()
|
|
968
|
+
let [next_x, next_y] = [x_idx, y_idx]
|
|
969
|
+
const max_steps = Math.max(x_items.length, y_items.length) + 1
|
|
748
970
|
for (let step_idx = 0; step_idx < max_steps; step_idx++) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
971
|
+
next_x += x_step
|
|
972
|
+
next_y += y_step
|
|
973
|
+
if (
|
|
974
|
+
next_x < 0 || next_y < 0 || next_x >= x_items.length ||
|
|
975
|
+
next_y >= y_items.length
|
|
976
|
+
) {
|
|
977
|
+
return
|
|
978
|
+
}
|
|
979
|
+
if (is_hidden_cell(next_x, next_y)) continue
|
|
980
|
+
if (focus_cell(next_x, next_y)) return
|
|
759
981
|
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function build_export_payload(format: HeatmapExportFormat): unknown {
|
|
985
|
+
const rows = matrix_to_rows(
|
|
986
|
+
vis_x.map((x_idx) => x_items[x_idx]),
|
|
987
|
+
vis_y.map((y_idx) => y_items[y_idx]),
|
|
988
|
+
vis_y.map((y_idx) => vis_x.map((x_idx) => get_value(x_idx, y_idx))),
|
|
989
|
+
)
|
|
990
|
+
if (format === `json`) return rows
|
|
991
|
+
return rows_to_csv(rows)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function update_viewport_state(): void {
|
|
995
|
+
if (!matrix_el) return
|
|
996
|
+
scroll_left = matrix_el.scrollLeft
|
|
997
|
+
scroll_top = matrix_el.scrollTop
|
|
998
|
+
viewport_width = matrix_el.clientWidth
|
|
999
|
+
viewport_height = matrix_el.clientHeight
|
|
1000
|
+
const first_rendered_cell = matrix_el.querySelector(
|
|
1001
|
+
`.cell[data-x][data-y]`,
|
|
1002
|
+
) as HTMLElement | null
|
|
1003
|
+
if (!first_rendered_cell) return
|
|
1004
|
+
const x_idx = Number(first_rendered_cell.dataset.x)
|
|
1005
|
+
const y_idx = Number(first_rendered_cell.dataset.y)
|
|
1006
|
+
if (!Number.isInteger(x_idx) || !Number.isInteger(y_idx)) return
|
|
1007
|
+
const vis_col = get_vis_col(x_idx) ?? 0
|
|
1008
|
+
const vis_row = get_vis_row(y_idx) ?? 0
|
|
1009
|
+
grid_offset_left = first_rendered_cell.offsetLeft - vis_col * tile_stride_px
|
|
1010
|
+
grid_offset_top = first_rendered_cell.offsetTop - vis_row * tile_stride_px
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function compute_summary(values: number[]): number | null {
|
|
1014
|
+
if (!values.length) return null
|
|
1015
|
+
if (summary_fn) return summary_fn(values)
|
|
1016
|
+
const total = values.reduce((sum, value) => sum + value, 0)
|
|
1017
|
+
return total / values.length
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function summarize_axis_values(
|
|
1021
|
+
primary_indices: number[],
|
|
1022
|
+
secondary_indices: number[],
|
|
1023
|
+
get_x_idx: (primary_idx: number, secondary_idx: number) => number,
|
|
1024
|
+
get_y_idx: (primary_idx: number, secondary_idx: number) => number,
|
|
1025
|
+
): SvelteMap<number, number | null> {
|
|
1026
|
+
const summary_map = new SvelteMap<number, number | null>()
|
|
796
1027
|
for (const primary_idx of primary_indices) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
values_for_summary.push(value);
|
|
806
|
-
}
|
|
1028
|
+
const values_for_summary: number[] = []
|
|
1029
|
+
for (const secondary_idx of secondary_indices) {
|
|
1030
|
+
const x_idx = get_x_idx(primary_idx, secondary_idx)
|
|
1031
|
+
const y_idx = get_y_idx(primary_idx, secondary_idx)
|
|
1032
|
+
if (is_hidden_cell(x_idx, y_idx)) continue
|
|
1033
|
+
const value = get_value(x_idx, y_idx)
|
|
1034
|
+
if (typeof value === `number` && Number.isFinite(value)) {
|
|
1035
|
+
values_for_summary.push(value)
|
|
807
1036
|
}
|
|
808
|
-
|
|
1037
|
+
}
|
|
1038
|
+
summary_map.set(primary_idx, compute_summary(values_for_summary))
|
|
809
1039
|
}
|
|
810
|
-
return summary_map
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return summarize_axis_values(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
})
|
|
822
|
-
|
|
823
|
-
let
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1040
|
+
return summary_map
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
let row_summaries = $derived.by(() => {
|
|
1044
|
+
if (!show_row_summaries) return new SvelteMap<number, number | null>()
|
|
1045
|
+
return summarize_axis_values(
|
|
1046
|
+
vis_y,
|
|
1047
|
+
vis_x,
|
|
1048
|
+
(_y_idx, x_idx) => x_idx,
|
|
1049
|
+
(y_idx) => y_idx,
|
|
1050
|
+
)
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
let col_summaries = $derived.by(() => {
|
|
1054
|
+
if (!show_col_summaries) return new SvelteMap<number, number | null>()
|
|
1055
|
+
return summarize_axis_values(
|
|
1056
|
+
vis_x,
|
|
1057
|
+
vis_y,
|
|
1058
|
+
(x_idx) => x_idx,
|
|
1059
|
+
(_x_idx, y_idx) => y_idx,
|
|
1060
|
+
)
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
let legend_orientation = $derived<ColorBarOrientation>(
|
|
1064
|
+
legend_position === `right` ? `vertical` : `horizontal`,
|
|
1065
|
+
)
|
|
1066
|
+
let legend_wrapper_style = $derived.by(() =>
|
|
1067
|
+
legend_position === `right`
|
|
1068
|
+
? `--cbar-height: 120px; --cbar-min-height: 120px; --cbar-max-height: 120px;`
|
|
1069
|
+
: `--cbar-width: 180px;`
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
let has_interaction_handlers = $derived(
|
|
1073
|
+
!disabled &&
|
|
1074
|
+
(
|
|
1075
|
+
Boolean(onclick) ||
|
|
828
1076
|
Boolean(ondblclick) ||
|
|
829
1077
|
Boolean(oncontextmenu) ||
|
|
830
1078
|
selection_mode !== `single` ||
|
|
831
|
-
tooltip_mode !== `hover`
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
let
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1079
|
+
tooltip_mode !== `hover`
|
|
1080
|
+
),
|
|
1081
|
+
)
|
|
1082
|
+
let cell_tag_name = $derived(has_interaction_handlers ? `button` : `div`)
|
|
1083
|
+
let cell_class_name = $derived(
|
|
1084
|
+
has_interaction_handlers ? `cell interactive` : `cell`,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
// Tooltip state: only used for custom tooltip snippets (function tooltips)
|
|
1088
|
+
let tooltip_cell: CellContext | null = $state(null)
|
|
1089
|
+
|
|
1090
|
+
onMount(() => {
|
|
1091
|
+
update_viewport_state()
|
|
1092
|
+
if (!is_browser) return
|
|
1093
|
+
globalThis.addEventListener(`mouseup`, handle_mouseup)
|
|
841
1094
|
return () => {
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1095
|
+
globalThis.removeEventListener(`mouseup`, handle_mouseup)
|
|
1096
|
+
}
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
onDestroy(() => {
|
|
1100
|
+
cancel_raf(active_cell_raf)
|
|
1101
|
+
clear_pending_click()
|
|
1102
|
+
})
|
|
849
1103
|
</script>
|
|
850
1104
|
|
|
851
1105
|
<div
|