matterviz 0.3.0 → 0.3.2
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/FilePicker.svelte +37 -20
- package/dist/Icon.svelte +2 -2
- package/dist/MillerIndexInput.svelte +60 -0
- package/dist/MillerIndexInput.svelte.d.ts +7 -0
- package/dist/app.css +38 -2
- package/dist/brillouin/BrillouinZone.svelte +20 -62
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
- package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- package/dist/chempot-diagram/color.d.ts +10 -0
- package/dist/chempot-diagram/color.js +33 -0
- package/dist/chempot-diagram/compute.d.ts +38 -0
- package/dist/chempot-diagram/compute.js +650 -0
- package/dist/chempot-diagram/index.d.ts +5 -0
- package/dist/chempot-diagram/index.js +5 -0
- package/dist/chempot-diagram/pointer.d.ts +16 -0
- package/dist/chempot-diagram/pointer.js +40 -0
- package/dist/chempot-diagram/temperature.d.ts +15 -0
- package/dist/chempot-diagram/temperature.js +37 -0
- package/dist/chempot-diagram/types.d.ts +83 -0
- package/dist/chempot-diagram/types.js +27 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +4 -0
- package/dist/composition/BarChart.svelte +13 -22
- package/dist/composition/BubbleChart.svelte +5 -3
- package/dist/composition/FormulaFilter.svelte +770 -90
- package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
- package/dist/composition/PieChart.svelte +43 -18
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/convex-hull/ConvexHull.svelte +14 -1
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +14 -45
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +396 -134
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +93 -42
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +94 -31
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
- package/dist/convex-hull/ConvexHullStats.svelte +697 -128
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
- package/dist/convex-hull/GasPressureControls.svelte +72 -38
- package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
- package/dist/convex-hull/TemperatureSlider.svelte +46 -19
- package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
- package/dist/convex-hull/demo-temperature.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +36 -0
- package/dist/convex-hull/gas-thermodynamics.js +16 -5
- package/dist/convex-hull/helpers.d.ts +7 -1
- package/dist/convex-hull/helpers.js +45 -15
- package/dist/convex-hull/index.d.ts +15 -1
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +106 -17
- package/dist/convex-hull/types.d.ts +7 -0
- package/dist/convex-hull/types.js +11 -0
- package/dist/coordination/CoordinationBarPlot.svelte +29 -46
- package/dist/element/BohrAtom.svelte +1 -1
- package/dist/element/data.js +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -0
- package/dist/element/types.d.ts +1 -0
- package/dist/fermi-surface/FermiSurface.svelte +21 -65
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/compute.js +1 -21
- package/dist/fermi-surface/marching-cubes.d.ts +2 -13
- package/dist/fermi-surface/marching-cubes.js +2 -519
- package/dist/fermi-surface/parse.js +17 -23
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
- package/dist/heatmap-matrix/index.d.ts +53 -0
- package/dist/heatmap-matrix/index.js +100 -0
- package/dist/heatmap-matrix/shared.d.ts +2 -0
- package/dist/heatmap-matrix/shared.js +4 -0
- package/dist/icons.d.ts +119 -0
- package/dist/icons.js +119 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/io/export.js +15 -3
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +43 -0
- package/dist/io/index.d.ts +2 -2
- package/dist/io/index.js +2 -112
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +118 -0
- package/dist/isosurface/Isosurface.svelte +231 -0
- package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
- package/dist/isosurface/IsosurfaceControls.svelte +273 -0
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
- package/dist/isosurface/index.d.ts +5 -0
- package/dist/isosurface/index.js +6 -0
- package/dist/isosurface/parse.d.ts +6 -0
- package/dist/isosurface/parse.js +548 -0
- package/dist/isosurface/slice.d.ts +11 -0
- package/dist/isosurface/slice.js +145 -0
- package/dist/isosurface/types.d.ts +55 -0
- package/dist/isosurface/types.js +178 -0
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +1 -0
- package/dist/layout/InfoTag.svelte +62 -62
- package/dist/layout/SubpageGrid.svelte +74 -0
- package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +226 -53
- package/dist/layout/json-tree/JsonTree.svelte +425 -51
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +218 -97
- package/dist/layout/json-tree/types.d.ts +27 -2
- package/dist/layout/json-tree/utils.d.ts +14 -1
- package/dist/layout/json-tree/utils.js +254 -0
- package/dist/marching-cubes.d.ts +14 -0
- package/dist/marching-cubes.js +519 -0
- package/dist/math.d.ts +8 -0
- package/dist/math.js +374 -7
- package/dist/overlays/ContextMenu.svelte +3 -2
- package/dist/overlays/DraggablePane.svelte +163 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- package/dist/phase-diagram/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +865 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +7 -4
- package/dist/phase-diagram/utils.js +149 -59
- package/dist/plot/AxisLabel.svelte +26 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +473 -228
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +3 -2
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +54 -54
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ElementScatter.svelte +4 -3
- package/dist/plot/FillArea.svelte +4 -1
- package/dist/plot/Histogram.svelte +320 -230
- package/dist/plot/Histogram.svelte.d.ts +2 -2
- package/dist/plot/HistogramControls.svelte +29 -10
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
- package/dist/plot/PlotControls.svelte +109 -27
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +1 -1
- package/dist/plot/PortalSelect.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferencePlane.svelte +1 -3
- package/dist/plot/ScatterPlot.svelte +343 -209
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +203 -250
- package/dist/plot/ScatterPlot3DScene.svelte +4 -7
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +95 -55
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ZeroLines.svelte +44 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +21 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- package/dist/plot/data-cleaning.js +1 -5
- package/dist/plot/index.d.ts +6 -2
- package/dist/plot/index.js +6 -2
- package/dist/plot/interactions.d.ts +8 -10
- package/dist/plot/interactions.js +10 -19
- package/dist/plot/layout.d.ts +7 -1
- package/dist/plot/layout.js +12 -4
- package/dist/plot/reference-line.d.ts +4 -21
- package/dist/plot/reference-line.js +7 -81
- package/dist/plot/types.d.ts +42 -17
- package/dist/plot/types.js +10 -0
- package/dist/plot/utils/label-placement.js +14 -11
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +55 -66
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/index.d.ts +1 -1
- package/dist/rdf/index.js +1 -1
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +37 -3
- package/dist/spectral/Bands.svelte +515 -143
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- package/dist/spectral/helpers.d.ts +23 -1
- package/dist/spectral/helpers.js +65 -9
- package/dist/spectral/types.d.ts +2 -0
- package/dist/structure/AtomLegend.svelte +31 -10
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CellSelect.svelte +92 -22
- package/dist/structure/Lattice.svelte +2 -0
- package/dist/structure/Structure.svelte +716 -173
- package/dist/structure/Structure.svelte.d.ts +7 -2
- package/dist/structure/StructureControls.svelte +26 -14
- package/dist/structure/StructureControls.svelte.d.ts +5 -1
- package/dist/structure/StructureInfoPane.svelte +7 -1
- package/dist/structure/StructureScene.svelte +386 -95
- package/dist/structure/StructureScene.svelte.d.ts +15 -4
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +38 -25
- package/dist/structure/export.js +10 -7
- package/dist/structure/ferrox-wasm-types.d.ts +3 -2
- package/dist/structure/ferrox-wasm-types.js +0 -3
- package/dist/structure/ferrox-wasm.d.ts +3 -2
- package/dist/structure/ferrox-wasm.js +1 -2
- package/dist/structure/index.d.ts +7 -0
- package/dist/structure/index.js +22 -0
- package/dist/structure/parse.js +19 -16
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +102 -0
- package/dist/structure/validation.js +6 -3
- package/dist/symmetry/SymmetryStats.svelte +18 -4
- package/dist/symmetry/WyckoffTable.svelte +18 -10
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +83 -18
- package/dist/table/HeatmapTable.svelte +468 -69
- package/dist/table/HeatmapTable.svelte.d.ts +13 -1
- package/dist/table/ToggleMenu.svelte +291 -44
- package/dist/table/ToggleMenu.svelte.d.ts +4 -1
- package/dist/table/index.d.ts +3 -0
- package/dist/tooltip/index.d.ts +1 -1
- package/dist/tooltip/index.js +1 -0
- package/dist/trajectory/Trajectory.svelte +147 -145
- package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +3 -5
- package/dist/trajectory/format-detect.d.ts +9 -0
- package/dist/trajectory/format-detect.js +76 -0
- package/dist/trajectory/frame-reader.d.ts +17 -0
- package/dist/trajectory/frame-reader.js +339 -0
- package/dist/trajectory/helpers.d.ts +15 -0
- package/dist/trajectory/helpers.js +187 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +11 -4
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +76 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +121 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +304 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +169 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +65 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +109 -0
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/dist/xrd/XrdPlot.svelte +6 -4
- package/dist/xrd/calc-xrd.js +0 -1
- package/package.json +33 -23
- package/readme.md +4 -4
- package/dist/trajectory/parse.d.ts +0 -42
- package/dist/trajectory/parse.js +0 -1267
- /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
<script lang="ts">import { is_color, pick_contrast_color } from '../colors';
|
|
2
|
+
import { format_num } from '../labels';
|
|
3
|
+
import ColorBar from '../plot/ColorBar.svelte';
|
|
4
|
+
import * as d3_sc from 'd3-scale-chromatic';
|
|
5
|
+
import { onDestroy, onMount } from 'svelte';
|
|
6
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
7
|
+
import HeatmapMatrixControls from './HeatmapMatrixControls.svelte';
|
|
8
|
+
import { matrix_to_rows, rows_to_csv } from './index';
|
|
9
|
+
import { make_color_override_key } from './shared';
|
|
10
|
+
let {
|
|
11
|
+
// Data props
|
|
12
|
+
x_items, y_items, values = [], color_scale = $bindable(`interpolateViridis`), color_scale_range = [null, null], color_overrides = {}, missing_color = `transparent`, log = false, value_transform, normalize = `linear`, domain_mode = `auto`, quantile_clip = [0.02, 0.98], show_legend = false, legend_position = `bottom`, legend_label = `Value`, legend_ticks = 5, legend_format = `.3~f`,
|
|
13
|
+
// Interaction props
|
|
14
|
+
active_cell = $bindable(null), selected_cells = $bindable([]), selection_mode = `single`, pinned_cell = $bindable(null), tooltip_mode = `hover`, disabled = false, onclick, ondblclick, onselect, onpin, oncontextmenu, enable_brush = false, onbrush,
|
|
15
|
+
// Display props
|
|
16
|
+
tile_size = `6px`, gap = `0px`, hide_empty = false, show_x_labels = true, show_y_labels = true, stagger_axis_labels = `auto`, symmetric: symmetric_prop = false, symmetric_label_position = `diagonal`, label_style = ``, x_order, y_order, highlight_x_keys = [], highlight_y_keys = [], search_query = ``, sticky_x_labels = false, sticky_y_labels = false, virtualize = false, overscan = 3, export_formats = [`csv`, `json`], onexport, show_gridlines = false, gridline_color = `color-mix(in srgb, currentColor 18%, transparent)`, gridline_width = `1px`, animate_updates = false, animation_duration = `120ms`, show_row_summaries = false, show_col_summaries = false, summary_fn, theme = `default`,
|
|
17
|
+
// Controls pane
|
|
18
|
+
show_controls = false, controls_open = $bindable(false), controls_props = {}, controls_children,
|
|
19
|
+
// Cell value display
|
|
20
|
+
show_values = false,
|
|
21
|
+
// Axis config (label used as axis title)
|
|
22
|
+
x_axis = {}, y_axis = {},
|
|
23
|
+
// Snippet props
|
|
24
|
+
tooltip = false, cell, x_label_cell, y_label_cell, children, ...rest } = $props();
|
|
25
|
+
// Normalize symmetric prop: true→'lower', otherwise pass through
|
|
26
|
+
const symmetric = $derived(symmetric_prop === true ? `lower` : symmetric_prop);
|
|
27
|
+
// Check if a cell should be skipped in symmetric mode
|
|
28
|
+
function is_hidden_cell(x_idx, y_idx) {
|
|
29
|
+
if (symmetric === `lower`)
|
|
30
|
+
return x_idx > y_idx;
|
|
31
|
+
if (symmetric === `upper`)
|
|
32
|
+
return x_idx < y_idx;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// === Value resolution ===
|
|
36
|
+
let x_keys = $derived(x_items.map((item) => item.key ?? item.label));
|
|
37
|
+
let y_keys = $derived(y_items.map((item) => item.key ?? item.label));
|
|
38
|
+
let highlight_x_key_set = $derived(new SvelteSet(highlight_x_keys));
|
|
39
|
+
let highlight_y_key_set = $derived(new SvelteSet(highlight_y_keys));
|
|
40
|
+
let search_query_norm = $derived(search_query.trim().toLowerCase());
|
|
41
|
+
let get_value = $derived.by(() => {
|
|
42
|
+
if (Array.isArray(values)) {
|
|
43
|
+
const matrix_values = values;
|
|
44
|
+
return (x_idx, y_idx) => matrix_values[y_idx]?.[x_idx] ?? null;
|
|
45
|
+
}
|
|
46
|
+
// Record<y_key, Record<x_key, value>>
|
|
47
|
+
const record = values;
|
|
48
|
+
return (x_idx, y_idx) => {
|
|
49
|
+
const y_key = y_keys[y_idx];
|
|
50
|
+
const x_key = x_keys[x_idx];
|
|
51
|
+
return record[y_key]?.[x_key] ?? null;
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
// === Visibility filtering ===
|
|
55
|
+
// Single pass to find which columns and rows have at least one non-null value
|
|
56
|
+
function sort_indices(indices, items, axis_order) {
|
|
57
|
+
if (!axis_order)
|
|
58
|
+
return indices;
|
|
59
|
+
const sorted = [...indices];
|
|
60
|
+
if (typeof axis_order === `function`) {
|
|
61
|
+
sorted.sort((idx_a, idx_b) => axis_order(items[idx_a], items[idx_b]));
|
|
62
|
+
return sorted;
|
|
63
|
+
}
|
|
64
|
+
sorted.sort((idx_a, idx_b) => {
|
|
65
|
+
const item_a = items[idx_a];
|
|
66
|
+
const item_b = items[idx_b];
|
|
67
|
+
if (axis_order === `sort_value`) {
|
|
68
|
+
const a_val = item_a.sort_value ?? Number.POSITIVE_INFINITY;
|
|
69
|
+
const b_val = item_b.sort_value ?? Number.POSITIVE_INFINITY;
|
|
70
|
+
return a_val - b_val;
|
|
71
|
+
}
|
|
72
|
+
if (axis_order === `key`) {
|
|
73
|
+
return (item_a.key ?? item_a.label).localeCompare(item_b.key ?? item_b.label);
|
|
74
|
+
}
|
|
75
|
+
return item_a.label.localeCompare(item_b.label);
|
|
76
|
+
});
|
|
77
|
+
return sorted;
|
|
78
|
+
}
|
|
79
|
+
let { vis_x, vis_y } = $derived.by(() => {
|
|
80
|
+
const all_x = Array.from({ length: x_items.length }, (_, idx) => idx);
|
|
81
|
+
const all_y = Array.from({ length: y_items.length }, (_, idx) => idx);
|
|
82
|
+
const filtered_x = search_query_norm
|
|
83
|
+
? all_x.filter((idx) => {
|
|
84
|
+
const item = x_items[idx];
|
|
85
|
+
const key = item.key ?? item.label;
|
|
86
|
+
return key.toLowerCase().includes(search_query_norm) ||
|
|
87
|
+
item.label.toLowerCase().includes(search_query_norm);
|
|
88
|
+
})
|
|
89
|
+
: all_x;
|
|
90
|
+
const filtered_y = search_query_norm
|
|
91
|
+
? all_y.filter((idx) => {
|
|
92
|
+
const item = y_items[idx];
|
|
93
|
+
const key = item.key ?? item.label;
|
|
94
|
+
return key.toLowerCase().includes(search_query_norm) ||
|
|
95
|
+
item.label.toLowerCase().includes(search_query_norm);
|
|
96
|
+
})
|
|
97
|
+
: all_y;
|
|
98
|
+
if (!hide_empty) {
|
|
99
|
+
return {
|
|
100
|
+
vis_x: sort_indices(filtered_x, x_items, x_order),
|
|
101
|
+
vis_y: sort_indices(filtered_y, y_items, y_order),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const col_has_data = new Array(x_items.length).fill(false);
|
|
105
|
+
const row_has_data = new Array(y_items.length).fill(false);
|
|
106
|
+
for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
|
|
107
|
+
for (let x_idx = 0; x_idx < x_items.length; x_idx++) {
|
|
108
|
+
if (get_value(x_idx, y_idx) !== null) {
|
|
109
|
+
col_has_data[x_idx] = true;
|
|
110
|
+
row_has_data[y_idx] = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
vis_x: sort_indices(filtered_x.filter((idx) => col_has_data[idx]), x_items, x_order),
|
|
116
|
+
vis_y: sort_indices(filtered_y.filter((idx) => row_has_data[idx]), y_items, y_order),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
// === Color computation ===
|
|
120
|
+
let color_scale_fn = $derived.by(() => {
|
|
121
|
+
if (typeof color_scale === `function`)
|
|
122
|
+
return color_scale;
|
|
123
|
+
const named_scale = d3_sc[color_scale];
|
|
124
|
+
return typeof named_scale === `function` ? named_scale : d3_sc.interpolateViridis;
|
|
125
|
+
});
|
|
126
|
+
function get_transformed_value(x_idx, y_idx) {
|
|
127
|
+
const raw_value = get_value(x_idx, y_idx);
|
|
128
|
+
if (typeof raw_value !== `number` || !Number.isFinite(raw_value))
|
|
129
|
+
return null;
|
|
130
|
+
if (!value_transform)
|
|
131
|
+
return raw_value;
|
|
132
|
+
const transformed_value = value_transform(raw_value, {
|
|
133
|
+
x_item: x_items[x_idx],
|
|
134
|
+
y_item: y_items[y_idx],
|
|
135
|
+
x_idx,
|
|
136
|
+
y_idx,
|
|
137
|
+
});
|
|
138
|
+
if (transformed_value === null || !Number.isFinite(transformed_value))
|
|
139
|
+
return null;
|
|
140
|
+
return transformed_value;
|
|
141
|
+
}
|
|
142
|
+
function get_quantile(sorted_values, quantile) {
|
|
143
|
+
if (!sorted_values.length)
|
|
144
|
+
return 0;
|
|
145
|
+
const clipped_quantile = Math.max(0, Math.min(1, quantile));
|
|
146
|
+
const float_idx = (sorted_values.length - 1) * clipped_quantile;
|
|
147
|
+
const low_idx = Math.floor(float_idx);
|
|
148
|
+
const high_idx = Math.ceil(float_idx);
|
|
149
|
+
if (low_idx === high_idx)
|
|
150
|
+
return sorted_values[low_idx];
|
|
151
|
+
const low_weight = high_idx - float_idx;
|
|
152
|
+
const high_weight = float_idx - low_idx;
|
|
153
|
+
return sorted_values[low_idx] * low_weight + sorted_values[high_idx] * high_weight;
|
|
154
|
+
}
|
|
155
|
+
let valid_numeric_values = $derived.by(() => {
|
|
156
|
+
const numeric_values = [];
|
|
157
|
+
for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
|
|
158
|
+
for (let x_idx = 0; x_idx < x_items.length; x_idx++) {
|
|
159
|
+
if (is_hidden_cell(x_idx, y_idx))
|
|
160
|
+
continue;
|
|
161
|
+
const value = get_transformed_value(x_idx, y_idx);
|
|
162
|
+
if (value === null)
|
|
163
|
+
continue;
|
|
164
|
+
numeric_values.push(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return numeric_values;
|
|
168
|
+
});
|
|
169
|
+
// Single-pass min/max to avoid spreading large arrays into Math.min/max
|
|
170
|
+
let [auto_min, auto_max] = $derived.by(() => {
|
|
171
|
+
let min = Infinity;
|
|
172
|
+
let max = -Infinity;
|
|
173
|
+
for (const value of valid_numeric_values) {
|
|
174
|
+
if (value < min)
|
|
175
|
+
min = value;
|
|
176
|
+
if (value > max)
|
|
177
|
+
max = value;
|
|
178
|
+
}
|
|
179
|
+
return min <= max ? [min, max] : [0, 1];
|
|
180
|
+
});
|
|
181
|
+
let [robust_min, robust_max] = $derived.by(() => {
|
|
182
|
+
if (!valid_numeric_values.length)
|
|
183
|
+
return [0, 1];
|
|
184
|
+
const sorted_values = [...valid_numeric_values].sort((value_a, value_b) => value_a - value_b);
|
|
185
|
+
const [q_low, q_high] = quantile_clip;
|
|
186
|
+
const clipped_min = get_quantile(sorted_values, q_low);
|
|
187
|
+
const clipped_max = get_quantile(sorted_values, q_high);
|
|
188
|
+
return clipped_min <= clipped_max
|
|
189
|
+
? [clipped_min, clipped_max]
|
|
190
|
+
: [clipped_max, clipped_min];
|
|
191
|
+
});
|
|
192
|
+
let [domain_min, domain_max] = $derived.by(() => {
|
|
193
|
+
if (domain_mode === `fixed` &&
|
|
194
|
+
color_scale_range[0] !== null &&
|
|
195
|
+
color_scale_range[1] !== null) {
|
|
196
|
+
return [color_scale_range[0], color_scale_range[1]];
|
|
197
|
+
}
|
|
198
|
+
if (domain_mode === `robust`)
|
|
199
|
+
return [robust_min, robust_max];
|
|
200
|
+
return [auto_min, auto_max];
|
|
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
|
+
// Map a single value to a background color
|
|
206
|
+
function value_to_color(val) {
|
|
207
|
+
if (val === null)
|
|
208
|
+
return missing_color || null;
|
|
209
|
+
if (typeof val === `string`) {
|
|
210
|
+
if (is_color(val))
|
|
211
|
+
return val;
|
|
212
|
+
return missing_color || null;
|
|
213
|
+
}
|
|
214
|
+
if (!Number.isFinite(val) || !color_scale_fn)
|
|
215
|
+
return missing_color || null;
|
|
216
|
+
if (use_log_norm && val <= 0)
|
|
217
|
+
return missing_color || null;
|
|
218
|
+
const span = cs_max - cs_min;
|
|
219
|
+
if (!Number.isFinite(span) || span === 0)
|
|
220
|
+
return color_scale_fn(0.5);
|
|
221
|
+
let normalized = typeof normalize === `function`
|
|
222
|
+
? normalize(val, cs_min, cs_max)
|
|
223
|
+
: (val - cs_min) / span;
|
|
224
|
+
if (use_log_norm) {
|
|
225
|
+
const is_descending_range = cs_min > cs_max;
|
|
226
|
+
const lower_bound = Math.min(cs_min, cs_max);
|
|
227
|
+
const upper_bound = Math.max(cs_min, cs_max);
|
|
228
|
+
if (upper_bound <= 0)
|
|
229
|
+
return missing_color || null;
|
|
230
|
+
const safe_lower_bound = Math.max(lower_bound, Number.MIN_VALUE);
|
|
231
|
+
const safe_value = Math.max(val, safe_lower_bound);
|
|
232
|
+
const log_min = Math.log(safe_lower_bound);
|
|
233
|
+
const log_max = Math.log(upper_bound);
|
|
234
|
+
if (!Number.isFinite(log_min) || !Number.isFinite(log_max) || log_max === log_min) {
|
|
235
|
+
return color_scale_fn(0.5);
|
|
236
|
+
}
|
|
237
|
+
const log_normalized = (Math.log(safe_value) - log_min) / (log_max - log_min);
|
|
238
|
+
normalized = is_descending_range ? 1 - log_normalized : log_normalized;
|
|
239
|
+
}
|
|
240
|
+
if (!Number.isFinite(normalized))
|
|
241
|
+
return missing_color || null;
|
|
242
|
+
return color_scale_fn(Math.max(0, Math.min(1, normalized)));
|
|
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);
|
|
250
|
+
for (let y_idx = 0; y_idx < n_y; y_idx++) {
|
|
251
|
+
const row_offset = y_idx * n_x;
|
|
252
|
+
for (let x_idx = 0; x_idx < n_x; x_idx++) {
|
|
253
|
+
if (is_hidden_cell(x_idx, y_idx)) {
|
|
254
|
+
colors[row_offset + x_idx] = null;
|
|
255
|
+
continue;
|
|
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);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return colors;
|
|
268
|
+
});
|
|
269
|
+
function to_contrast_colors(bg_values) {
|
|
270
|
+
return bg_values.map((bg_color) => bg_color ? pick_contrast_color({ bg_color }) : null);
|
|
271
|
+
}
|
|
272
|
+
// Compute text colors when cells render content that needs contrast (cell snippet or show_values)
|
|
273
|
+
let text_flat = $derived.by(() => {
|
|
274
|
+
if (!cell && !show_values)
|
|
275
|
+
return null;
|
|
276
|
+
return to_contrast_colors(bg_flat);
|
|
277
|
+
});
|
|
278
|
+
// Keep selected outlines visible against each cell's background.
|
|
279
|
+
let selected_outline_flat = $derived.by(() => to_contrast_colors(bg_flat));
|
|
280
|
+
function get_flat_idx(x_idx, y_idx) {
|
|
281
|
+
return y_idx * n_x + x_idx;
|
|
282
|
+
}
|
|
283
|
+
// Look up bg color by indices
|
|
284
|
+
function get_bg(x_idx, y_idx) {
|
|
285
|
+
return bg_flat[get_flat_idx(x_idx, y_idx)];
|
|
286
|
+
}
|
|
287
|
+
// === Cell context builder (only called for clicks, not per-hover) ===
|
|
288
|
+
function build_cell_context(x_idx, y_idx) {
|
|
289
|
+
return {
|
|
290
|
+
x_item: x_items[x_idx],
|
|
291
|
+
y_item: y_items[y_idx],
|
|
292
|
+
x_idx,
|
|
293
|
+
y_idx,
|
|
294
|
+
value: get_value(x_idx, y_idx),
|
|
295
|
+
bg_color: get_bg(x_idx, y_idx),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// === Fully imperative hover management ===
|
|
299
|
+
// ZERO $state writes during mouseover — all DOM updates are direct.
|
|
300
|
+
// This avoids Svelte's reactive flush which would re-evaluate effects.
|
|
301
|
+
const is_browser = typeof window !== `undefined`;
|
|
302
|
+
let tooltip_div = $state();
|
|
303
|
+
let active_cell_raf = 0; // rAF handle for deferred active_cell update
|
|
304
|
+
let click_timeout_id = null;
|
|
305
|
+
const dblclick_delay_ms = 250;
|
|
306
|
+
let last_hover_x = -1;
|
|
307
|
+
let last_hover_y = -1;
|
|
308
|
+
let matrix_el = $state();
|
|
309
|
+
let scroll_left = $state(0);
|
|
310
|
+
let scroll_top = $state(0);
|
|
311
|
+
let viewport_width = $state(0);
|
|
312
|
+
let viewport_height = $state(0);
|
|
313
|
+
let grid_offset_left = $state(0);
|
|
314
|
+
let grid_offset_top = $state(0);
|
|
315
|
+
let brush_start = $state(null);
|
|
316
|
+
let brush_end = $state(null);
|
|
317
|
+
let last_selected_cell = $state(null);
|
|
318
|
+
// In symmetric mode, labels can either stay on outer edges ('edge')
|
|
319
|
+
// or move toward the missing triangle and hug the diagonal ('diagonal').
|
|
320
|
+
let use_diagonal_symmetric_labels = $derived(symmetric && symmetric_label_position === `diagonal`);
|
|
321
|
+
let use_staggered_x_labels = $derived(stagger_axis_labels === true ||
|
|
322
|
+
(stagger_axis_labels === `auto` && vis_x.length >= 24));
|
|
323
|
+
let use_staggered_y_labels = $derived(stagger_axis_labels === true ||
|
|
324
|
+
(stagger_axis_labels === `auto` && vis_y.length >= 24));
|
|
325
|
+
let use_side_split_x_labels = $derived(use_staggered_x_labels && !use_diagonal_symmetric_labels);
|
|
326
|
+
// Don't split y-labels to both sides when symmetric -- one side has no cells
|
|
327
|
+
let use_side_split_y_labels = $derived(use_staggered_y_labels && !symmetric);
|
|
328
|
+
// For 'gaps' mode: explicit grid placement to preserve positional alignment
|
|
329
|
+
let gaps_mode = $derived(hide_empty === `gaps`);
|
|
330
|
+
let visible_col_count = $derived(gaps_mode ? x_items.length : vis_x.length);
|
|
331
|
+
let visible_row_count = $derived(gaps_mode ? y_items.length : vis_y.length);
|
|
332
|
+
let show_bottom_summary_row = $derived(show_col_summaries);
|
|
333
|
+
let show_right_summary_col = $derived(show_row_summaries);
|
|
334
|
+
let grid_col_count = $derived(visible_col_count + (show_right_summary_col ? 1 : 0));
|
|
335
|
+
let grid_row_count = $derived(visible_row_count + (show_bottom_summary_row ? 1 : 0));
|
|
336
|
+
function cell_pos_key(x_idx, y_idx) {
|
|
337
|
+
return `${x_idx}:${y_idx}`;
|
|
338
|
+
}
|
|
339
|
+
let selected_cell_key_set = $derived(new SvelteSet(selected_cells.map((cell_pos) => cell_pos_key(cell_pos.x_idx, cell_pos.y_idx))));
|
|
340
|
+
function parse_px_size(size) {
|
|
341
|
+
const parsed = Number.parseFloat(size);
|
|
342
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 12;
|
|
343
|
+
}
|
|
344
|
+
let tile_size_px = $derived(parse_px_size(tile_size));
|
|
345
|
+
let gap_px = $derived(parse_px_size(gap));
|
|
346
|
+
let tile_stride_px = $derived(tile_size_px + gap_px);
|
|
347
|
+
let render_vis_x = $derived.by(() => {
|
|
348
|
+
if (!virtualize)
|
|
349
|
+
return vis_x;
|
|
350
|
+
const raw_start_pos = Math.floor((scroll_left - grid_offset_left) / tile_stride_px) - overscan;
|
|
351
|
+
const start_pos = Math.max(0, raw_start_pos);
|
|
352
|
+
const raw_end_pos = Math.ceil((scroll_left - grid_offset_left + viewport_width) / tile_stride_px) +
|
|
353
|
+
overscan;
|
|
354
|
+
const end_pos = Math.min(vis_x.length, raw_end_pos);
|
|
355
|
+
return vis_x.slice(start_pos, end_pos);
|
|
356
|
+
});
|
|
357
|
+
let render_vis_y = $derived.by(() => {
|
|
358
|
+
if (!virtualize)
|
|
359
|
+
return vis_y;
|
|
360
|
+
const raw_start_pos = Math.floor((scroll_top - grid_offset_top) / tile_stride_px) - overscan;
|
|
361
|
+
const start_pos = Math.max(0, raw_start_pos);
|
|
362
|
+
const raw_end_pos = Math.ceil((scroll_top - grid_offset_top + viewport_height) / tile_stride_px) +
|
|
363
|
+
overscan;
|
|
364
|
+
const end_pos = Math.min(vis_y.length, raw_end_pos);
|
|
365
|
+
return vis_y.slice(start_pos, end_pos);
|
|
366
|
+
});
|
|
367
|
+
function is_selected_cell(x_idx, y_idx) {
|
|
368
|
+
return selected_cell_key_set.has(cell_pos_key(x_idx, y_idx));
|
|
369
|
+
}
|
|
370
|
+
let vis_x_pos_map = $derived.by(() => {
|
|
371
|
+
const position_map = new SvelteMap();
|
|
372
|
+
for (const [vis_pos, item_idx] of vis_x.entries()) {
|
|
373
|
+
position_map.set(item_idx, vis_pos);
|
|
374
|
+
}
|
|
375
|
+
return position_map;
|
|
376
|
+
});
|
|
377
|
+
let vis_y_pos_map = $derived.by(() => {
|
|
378
|
+
const position_map = new SvelteMap();
|
|
379
|
+
for (const [vis_pos, item_idx] of vis_y.entries()) {
|
|
380
|
+
position_map.set(item_idx, vis_pos);
|
|
381
|
+
}
|
|
382
|
+
return position_map;
|
|
383
|
+
});
|
|
384
|
+
let highlight_x_by_idx = $derived(new SvelteSet(vis_x.filter((idx) => highlight_x_key_set.has(x_items[idx].key ?? x_items[idx].label))));
|
|
385
|
+
let highlight_y_by_idx = $derived(new SvelteSet(vis_y.filter((idx) => highlight_y_key_set.has(y_items[idx].key ?? y_items[idx].label))));
|
|
386
|
+
function get_vis_col(item_idx) {
|
|
387
|
+
if (gaps_mode)
|
|
388
|
+
return item_idx;
|
|
389
|
+
return vis_x_pos_map.get(item_idx) ?? null;
|
|
390
|
+
}
|
|
391
|
+
function get_vis_row(item_idx) {
|
|
392
|
+
if (gaps_mode)
|
|
393
|
+
return item_idx;
|
|
394
|
+
return vis_y_pos_map.get(item_idx) ?? null;
|
|
395
|
+
}
|
|
396
|
+
function x_label_diag_grid_row(x_idx) {
|
|
397
|
+
const vis_row = get_vis_row(x_idx);
|
|
398
|
+
if (vis_row === null)
|
|
399
|
+
return undefined;
|
|
400
|
+
if (symmetric === `upper`) {
|
|
401
|
+
// Upper triangle: place x label below diagonal (in empty lower-left area)
|
|
402
|
+
return Math.min(visible_row_count + 1, vis_row + 3);
|
|
403
|
+
}
|
|
404
|
+
// Lower/default: place x label above diagonal (in empty upper-right area)
|
|
405
|
+
return Math.max(1, vis_row + 1);
|
|
406
|
+
}
|
|
407
|
+
function x_label_diag_grid_col(x_idx) {
|
|
408
|
+
const vis_col = get_vis_col(x_idx);
|
|
409
|
+
if (vis_col === null)
|
|
410
|
+
return undefined;
|
|
411
|
+
return vis_col + 2;
|
|
412
|
+
}
|
|
413
|
+
function y_label_edge_grid_row(y_idx) {
|
|
414
|
+
const vis_row = get_vis_row(y_idx);
|
|
415
|
+
if (vis_row === null)
|
|
416
|
+
return undefined;
|
|
417
|
+
return vis_row + 2;
|
|
418
|
+
}
|
|
419
|
+
function x_label_grid_col(x_idx) {
|
|
420
|
+
if (use_diagonal_symmetric_labels)
|
|
421
|
+
return x_label_diag_grid_col(x_idx);
|
|
422
|
+
return cell_grid_col(x_idx);
|
|
423
|
+
}
|
|
424
|
+
function x_label_grid_row(x_idx) {
|
|
425
|
+
if (use_diagonal_symmetric_labels)
|
|
426
|
+
return x_label_diag_grid_row(x_idx);
|
|
427
|
+
if (use_side_split_x_labels && x_idx % 2 !== 0) {
|
|
428
|
+
return visible_row_count + 2 + (show_bottom_summary_row ? 1 : 0);
|
|
429
|
+
}
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
// Upper symmetric or staggered odd labels: place on right side
|
|
433
|
+
function y_label_grid_col(y_idx) {
|
|
434
|
+
if (symmetric === `upper` || (use_side_split_y_labels && y_idx % 2 !== 0)) {
|
|
435
|
+
return visible_col_count + 2 + (show_right_summary_col ? 1 : 0);
|
|
436
|
+
}
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
function cell_grid_col(x_idx) {
|
|
440
|
+
const vis_col = get_vis_col(x_idx);
|
|
441
|
+
if (vis_col === null)
|
|
442
|
+
return undefined;
|
|
443
|
+
return vis_col + 2;
|
|
444
|
+
}
|
|
445
|
+
function cell_grid_row(y_idx) {
|
|
446
|
+
const vis_row = get_vis_row(y_idx);
|
|
447
|
+
if (vis_row === null)
|
|
448
|
+
return undefined;
|
|
449
|
+
return vis_row + 2;
|
|
450
|
+
}
|
|
451
|
+
function schedule_raf(callback) {
|
|
452
|
+
if (!is_browser) {
|
|
453
|
+
callback();
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
return window.requestAnimationFrame(callback);
|
|
457
|
+
}
|
|
458
|
+
function cancel_raf(raf_handle) {
|
|
459
|
+
if (!is_browser || raf_handle === 0)
|
|
460
|
+
return;
|
|
461
|
+
window.cancelAnimationFrame(raf_handle);
|
|
462
|
+
}
|
|
463
|
+
function clear_pending_click() {
|
|
464
|
+
if (click_timeout_id === null)
|
|
465
|
+
return;
|
|
466
|
+
clearTimeout(click_timeout_id);
|
|
467
|
+
click_timeout_id = null;
|
|
468
|
+
}
|
|
469
|
+
function parse_cell_indices(cell_el) {
|
|
470
|
+
const x_value = Number(cell_el.dataset.x);
|
|
471
|
+
const y_value = Number(cell_el.dataset.y);
|
|
472
|
+
if (!Number.isInteger(x_value) || !Number.isInteger(y_value))
|
|
473
|
+
return null;
|
|
474
|
+
return { x_idx: x_value, y_idx: y_value };
|
|
475
|
+
}
|
|
476
|
+
function get_cell_context_from_target(event_target) {
|
|
477
|
+
const cell_el = get_cell_el_from_target(event_target);
|
|
478
|
+
if (!cell_el)
|
|
479
|
+
return null;
|
|
480
|
+
const indices = parse_cell_indices(cell_el);
|
|
481
|
+
if (!indices)
|
|
482
|
+
return null;
|
|
483
|
+
return build_cell_context(indices.x_idx, indices.y_idx);
|
|
484
|
+
}
|
|
485
|
+
function trigger_click(cell_context) {
|
|
486
|
+
if (!onclick)
|
|
487
|
+
return;
|
|
488
|
+
if (!ondblclick) {
|
|
489
|
+
onclick(cell_context);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
clear_pending_click();
|
|
493
|
+
click_timeout_id = setTimeout(() => {
|
|
494
|
+
onclick(cell_context);
|
|
495
|
+
click_timeout_id = null;
|
|
496
|
+
}, dblclick_delay_ms);
|
|
497
|
+
}
|
|
498
|
+
function get_cell_el_from_target(event_target) {
|
|
499
|
+
const target_node = event_target;
|
|
500
|
+
if (!(target_node instanceof Element))
|
|
501
|
+
return null;
|
|
502
|
+
if (target_node instanceof HTMLElement && target_node.dataset.x !== undefined) {
|
|
503
|
+
return target_node;
|
|
504
|
+
}
|
|
505
|
+
const closest_cell = target_node.closest(`[data-x][data-y]`);
|
|
506
|
+
return closest_cell instanceof HTMLElement ? closest_cell : null;
|
|
507
|
+
}
|
|
508
|
+
function update_selected_cells(event, clicked_cell) {
|
|
509
|
+
if (selection_mode === `single`) {
|
|
510
|
+
selected_cells = [clicked_cell];
|
|
511
|
+
last_selected_cell = clicked_cell;
|
|
512
|
+
onselect?.(selected_cells);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (selection_mode === `range` &&
|
|
516
|
+
event.shiftKey &&
|
|
517
|
+
last_selected_cell) {
|
|
518
|
+
const x_min = Math.min(last_selected_cell.x_idx, clicked_cell.x_idx);
|
|
519
|
+
const x_max = Math.max(last_selected_cell.x_idx, clicked_cell.x_idx);
|
|
520
|
+
const y_min = Math.min(last_selected_cell.y_idx, clicked_cell.y_idx);
|
|
521
|
+
const y_max = Math.max(last_selected_cell.y_idx, clicked_cell.y_idx);
|
|
522
|
+
const next_cells = [];
|
|
523
|
+
for (let y_idx = y_min; y_idx <= y_max; y_idx++) {
|
|
524
|
+
for (let x_idx = x_min; x_idx <= x_max; x_idx++) {
|
|
525
|
+
if (is_hidden_cell(x_idx, y_idx))
|
|
526
|
+
continue;
|
|
527
|
+
next_cells.push({ x_idx, y_idx });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
selected_cells = next_cells;
|
|
531
|
+
onselect?.(selected_cells);
|
|
532
|
+
return;
|
|
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);
|
|
546
|
+
}
|
|
547
|
+
selected_cells = next_cells;
|
|
548
|
+
last_selected_cell = clicked_cell;
|
|
549
|
+
onselect?.(selected_cells);
|
|
550
|
+
}
|
|
551
|
+
function update_tooltip_position(client_x, client_y) {
|
|
552
|
+
if (!tooltip_div)
|
|
553
|
+
return;
|
|
554
|
+
tooltip_div.style.left = `${client_x + 10}px`;
|
|
555
|
+
tooltip_div.style.top = `${client_y + 12}px`;
|
|
556
|
+
}
|
|
557
|
+
function set_pinned_cell(next_cell) {
|
|
558
|
+
pinned_cell = next_cell;
|
|
559
|
+
onpin?.(next_cell);
|
|
560
|
+
}
|
|
561
|
+
// Write default tooltip content imperatively (no reactive state)
|
|
562
|
+
function update_tooltip_content(td, x_idx, y_idx) {
|
|
563
|
+
const x_label = x_items[x_idx]?.label ?? ``;
|
|
564
|
+
const y_label = y_items[y_idx]?.label ?? ``;
|
|
565
|
+
const val = get_value(x_idx, y_idx);
|
|
566
|
+
const value_str = val === null || val === undefined
|
|
567
|
+
? ``
|
|
568
|
+
: typeof val === `number`
|
|
569
|
+
? format_num(val)
|
|
570
|
+
: String(val);
|
|
571
|
+
td.textContent = value_str
|
|
572
|
+
? `${x_label} - ${y_label}: ${value_str}`
|
|
573
|
+
: `${x_label} - ${y_label}`;
|
|
574
|
+
}
|
|
575
|
+
function handle_mouseover(event) {
|
|
576
|
+
if (disabled)
|
|
577
|
+
return;
|
|
578
|
+
const cell_el = get_cell_el_from_target(event.target);
|
|
579
|
+
if (!cell_el)
|
|
580
|
+
return;
|
|
581
|
+
const indices = parse_cell_indices(cell_el);
|
|
582
|
+
if (!indices)
|
|
583
|
+
return;
|
|
584
|
+
const { x_idx, y_idx } = indices;
|
|
585
|
+
// Ignore redundant enters on the same cell (can happen with nested children)
|
|
586
|
+
if (last_hover_x === x_idx && last_hover_y === y_idx) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
last_hover_x = x_idx;
|
|
590
|
+
last_hover_y = y_idx;
|
|
591
|
+
// Defer bindable writes out of the hot mouseover path
|
|
592
|
+
cancel_raf(active_cell_raf);
|
|
593
|
+
active_cell_raf = schedule_raf(() => {
|
|
594
|
+
active_cell = { x_idx, y_idx };
|
|
595
|
+
});
|
|
596
|
+
if (enable_brush && brush_start)
|
|
597
|
+
brush_end = { x_idx, y_idx };
|
|
598
|
+
if (tooltip === false || !tooltip_div || tooltip_mode === `pinned`)
|
|
599
|
+
return;
|
|
600
|
+
// 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`);
|
|
603
|
+
if (typeof tooltip === `function`) {
|
|
604
|
+
tooltip_cell = build_cell_context(x_idx, y_idx);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
update_tooltip_content(tooltip_div, x_idx, y_idx);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function handle_mouseout(event) {
|
|
611
|
+
if (disabled)
|
|
612
|
+
return;
|
|
613
|
+
const related = event.relatedTarget;
|
|
614
|
+
if (related?.closest?.(`[data-x][data-y]`))
|
|
615
|
+
return;
|
|
616
|
+
// Clear active state imperatively
|
|
617
|
+
last_hover_x = -1;
|
|
618
|
+
last_hover_y = -1;
|
|
619
|
+
const keep_tooltip_visible = tooltip_mode === `pinned` ||
|
|
620
|
+
(tooltip_mode === `both` && pinned_cell !== null);
|
|
621
|
+
if (!keep_tooltip_visible) {
|
|
622
|
+
tooltip_div?.classList.remove(`visible`);
|
|
623
|
+
}
|
|
624
|
+
// Defer reactive cleanup to rAF
|
|
625
|
+
cancel_raf(active_cell_raf);
|
|
626
|
+
active_cell_raf = schedule_raf(() => {
|
|
627
|
+
active_cell = null;
|
|
628
|
+
if (!keep_tooltip_visible)
|
|
629
|
+
tooltip_cell = null;
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function handle_click(event) {
|
|
633
|
+
if (disabled)
|
|
634
|
+
return;
|
|
635
|
+
const cell_context = get_cell_context_from_target(event.target);
|
|
636
|
+
if (!cell_context)
|
|
637
|
+
return;
|
|
638
|
+
update_selected_cells(event, {
|
|
639
|
+
x_idx: cell_context.x_idx,
|
|
640
|
+
y_idx: cell_context.y_idx,
|
|
641
|
+
});
|
|
642
|
+
if (tooltip_mode === `both` || tooltip_mode === `pinned`) {
|
|
643
|
+
set_pinned_cell({ x_idx: cell_context.x_idx, y_idx: cell_context.y_idx });
|
|
644
|
+
if (tooltip !== false && tooltip_div) {
|
|
645
|
+
update_tooltip_position(event.clientX, event.clientY);
|
|
646
|
+
tooltip_div.classList.add(`visible`);
|
|
647
|
+
if (typeof tooltip === `function`) {
|
|
648
|
+
tooltip_cell = cell_context;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
update_tooltip_content(tooltip_div, cell_context.x_idx, cell_context.y_idx);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!onclick)
|
|
656
|
+
return;
|
|
657
|
+
trigger_click(cell_context);
|
|
658
|
+
}
|
|
659
|
+
function handle_dblclick(event) {
|
|
660
|
+
if (disabled || !ondblclick)
|
|
661
|
+
return;
|
|
662
|
+
const cell_context = get_cell_context_from_target(event.target);
|
|
663
|
+
if (!cell_context)
|
|
664
|
+
return;
|
|
665
|
+
clear_pending_click();
|
|
666
|
+
ondblclick(cell_context);
|
|
667
|
+
}
|
|
668
|
+
function handle_contextmenu(event) {
|
|
669
|
+
if (disabled || !oncontextmenu)
|
|
670
|
+
return;
|
|
671
|
+
const cell_context = get_cell_context_from_target(event.target);
|
|
672
|
+
if (!cell_context)
|
|
673
|
+
return;
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
oncontextmenu(cell_context, event);
|
|
676
|
+
}
|
|
677
|
+
function handle_mousedown(event) {
|
|
678
|
+
if (disabled || !enable_brush)
|
|
679
|
+
return;
|
|
680
|
+
const cell_context = get_cell_context_from_target(event.target);
|
|
681
|
+
if (!cell_context)
|
|
682
|
+
return;
|
|
683
|
+
brush_start = { x_idx: cell_context.x_idx, y_idx: cell_context.y_idx };
|
|
684
|
+
brush_end = { x_idx: cell_context.x_idx, y_idx: cell_context.y_idx };
|
|
685
|
+
}
|
|
686
|
+
function handle_mouseup() {
|
|
687
|
+
if (!enable_brush || !brush_start || !brush_end || !onbrush) {
|
|
688
|
+
brush_start = null;
|
|
689
|
+
brush_end = null;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
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 = [];
|
|
697
|
+
for (let y_idx = y_min; y_idx <= y_max; y_idx++) {
|
|
698
|
+
for (let x_idx = x_min; x_idx <= x_max; x_idx++) {
|
|
699
|
+
if (is_hidden_cell(x_idx, y_idx))
|
|
700
|
+
continue;
|
|
701
|
+
cells.push(build_cell_context(x_idx, y_idx));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
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
|
+
function focus_cell(x_idx, y_idx) {
|
|
709
|
+
const target = matrix_el?.querySelector(`[data-x="${x_idx}"][data-y="${y_idx}"]`);
|
|
710
|
+
if (!(target instanceof HTMLElement))
|
|
711
|
+
return false;
|
|
712
|
+
target.focus();
|
|
713
|
+
active_cell = { x_idx, y_idx };
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
function handle_keydown(event) {
|
|
717
|
+
const active_el = document.activeElement;
|
|
718
|
+
if (!(active_el instanceof HTMLElement))
|
|
719
|
+
return;
|
|
720
|
+
if (!(active_el.dataset.x && active_el.dataset.y))
|
|
721
|
+
return;
|
|
722
|
+
const x_idx = Number(active_el.dataset.x);
|
|
723
|
+
const y_idx = Number(active_el.dataset.y);
|
|
724
|
+
if (!Number.isInteger(x_idx) || !Number.isInteger(y_idx))
|
|
725
|
+
return;
|
|
726
|
+
let x_step = 0;
|
|
727
|
+
let y_step = 0;
|
|
728
|
+
if (event.key === `ArrowRight`)
|
|
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;
|
|
736
|
+
else if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === `e`) {
|
|
737
|
+
const format = export_formats[0];
|
|
738
|
+
if (format && onexport)
|
|
739
|
+
onexport(format, build_export_payload(format));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
else
|
|
743
|
+
return;
|
|
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;
|
|
748
|
+
for (let step_idx = 0; step_idx < max_steps; step_idx++) {
|
|
749
|
+
next_x += x_step;
|
|
750
|
+
next_y += y_step;
|
|
751
|
+
if (next_x < 0 || next_y < 0 || next_x >= x_items.length ||
|
|
752
|
+
next_y >= y_items.length) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (is_hidden_cell(next_x, next_y))
|
|
756
|
+
continue;
|
|
757
|
+
if (focus_cell(next_x, next_y))
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function build_export_payload(format) {
|
|
762
|
+
const rows = matrix_to_rows(vis_x.map((x_idx) => x_items[x_idx]), vis_y.map((y_idx) => y_items[y_idx]), vis_y.map((y_idx) => vis_x.map((x_idx) => get_value(x_idx, y_idx))));
|
|
763
|
+
if (format === `json`)
|
|
764
|
+
return rows;
|
|
765
|
+
return rows_to_csv(rows);
|
|
766
|
+
}
|
|
767
|
+
function update_viewport_state() {
|
|
768
|
+
if (!matrix_el)
|
|
769
|
+
return;
|
|
770
|
+
scroll_left = matrix_el.scrollLeft;
|
|
771
|
+
scroll_top = matrix_el.scrollTop;
|
|
772
|
+
viewport_width = matrix_el.clientWidth;
|
|
773
|
+
viewport_height = matrix_el.clientHeight;
|
|
774
|
+
const first_rendered_cell = matrix_el.querySelector(`.cell[data-x][data-y]`);
|
|
775
|
+
if (!first_rendered_cell)
|
|
776
|
+
return;
|
|
777
|
+
const x_idx = Number(first_rendered_cell.dataset.x);
|
|
778
|
+
const y_idx = Number(first_rendered_cell.dataset.y);
|
|
779
|
+
if (!Number.isInteger(x_idx) || !Number.isInteger(y_idx))
|
|
780
|
+
return;
|
|
781
|
+
const vis_col = get_vis_col(x_idx) ?? 0;
|
|
782
|
+
const vis_row = get_vis_row(y_idx) ?? 0;
|
|
783
|
+
grid_offset_left = first_rendered_cell.offsetLeft - vis_col * tile_stride_px;
|
|
784
|
+
grid_offset_top = first_rendered_cell.offsetTop - vis_row * tile_stride_px;
|
|
785
|
+
}
|
|
786
|
+
function compute_summary(values) {
|
|
787
|
+
if (!values.length)
|
|
788
|
+
return null;
|
|
789
|
+
if (summary_fn)
|
|
790
|
+
return summary_fn(values);
|
|
791
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
792
|
+
return total / values.length;
|
|
793
|
+
}
|
|
794
|
+
function summarize_axis_values(primary_indices, secondary_indices, get_x_idx, get_y_idx) {
|
|
795
|
+
const summary_map = new SvelteMap();
|
|
796
|
+
for (const primary_idx of primary_indices) {
|
|
797
|
+
const values_for_summary = [];
|
|
798
|
+
for (const secondary_idx of secondary_indices) {
|
|
799
|
+
const x_idx = get_x_idx(primary_idx, secondary_idx);
|
|
800
|
+
const y_idx = get_y_idx(primary_idx, secondary_idx);
|
|
801
|
+
if (is_hidden_cell(x_idx, y_idx))
|
|
802
|
+
continue;
|
|
803
|
+
const value = get_value(x_idx, y_idx);
|
|
804
|
+
if (typeof value === `number` && Number.isFinite(value)) {
|
|
805
|
+
values_for_summary.push(value);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
summary_map.set(primary_idx, compute_summary(values_for_summary));
|
|
809
|
+
}
|
|
810
|
+
return summary_map;
|
|
811
|
+
}
|
|
812
|
+
let row_summaries = $derived.by(() => {
|
|
813
|
+
if (!show_row_summaries)
|
|
814
|
+
return new SvelteMap();
|
|
815
|
+
return summarize_axis_values(vis_y, vis_x, (_y_idx, x_idx) => x_idx, (y_idx) => y_idx);
|
|
816
|
+
});
|
|
817
|
+
let col_summaries = $derived.by(() => {
|
|
818
|
+
if (!show_col_summaries)
|
|
819
|
+
return new SvelteMap();
|
|
820
|
+
return summarize_axis_values(vis_x, vis_y, (x_idx) => x_idx, (_x_idx, y_idx) => y_idx);
|
|
821
|
+
});
|
|
822
|
+
let legend_orientation = $derived(legend_position === `right` ? `vertical` : `horizontal`);
|
|
823
|
+
let legend_wrapper_style = $derived.by(() => legend_position === `right`
|
|
824
|
+
? `--cbar-height: 120px; --cbar-min-height: 120px; --cbar-max-height: 120px;`
|
|
825
|
+
: `--cbar-width: 180px;`);
|
|
826
|
+
let has_interaction_handlers = $derived(!disabled &&
|
|
827
|
+
(Boolean(onclick) ||
|
|
828
|
+
Boolean(ondblclick) ||
|
|
829
|
+
Boolean(oncontextmenu) ||
|
|
830
|
+
selection_mode !== `single` ||
|
|
831
|
+
tooltip_mode !== `hover`));
|
|
832
|
+
let cell_tag_name = $derived(has_interaction_handlers ? `button` : `div`);
|
|
833
|
+
let cell_class_name = $derived(has_interaction_handlers ? `cell interactive` : `cell`);
|
|
834
|
+
// Tooltip state: only used for custom tooltip snippets (function tooltips)
|
|
835
|
+
let tooltip_cell = $state(null);
|
|
836
|
+
onMount(() => {
|
|
837
|
+
update_viewport_state();
|
|
838
|
+
if (!is_browser)
|
|
839
|
+
return;
|
|
840
|
+
window.addEventListener(`mouseup`, handle_mouseup);
|
|
841
|
+
return () => {
|
|
842
|
+
window.removeEventListener(`mouseup`, handle_mouseup);
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
onDestroy(() => {
|
|
846
|
+
cancel_raf(active_cell_raf);
|
|
847
|
+
clear_pending_click();
|
|
848
|
+
});
|
|
849
|
+
</script>
|
|
850
|
+
|
|
851
|
+
<div
|
|
852
|
+
class="heatmap legend-{legend_position}"
|
|
853
|
+
style:padding-left={y_axis.label ? `1.8em` : undefined}
|
|
854
|
+
>
|
|
855
|
+
{#if show_controls}
|
|
856
|
+
<HeatmapMatrixControls
|
|
857
|
+
bind:controls_open
|
|
858
|
+
bind:normalize
|
|
859
|
+
bind:domain_mode
|
|
860
|
+
bind:show_legend
|
|
861
|
+
bind:legend_position
|
|
862
|
+
bind:search_query
|
|
863
|
+
{export_formats}
|
|
864
|
+
onexport={onexport
|
|
865
|
+
? (fmt: HeatmapExportFormat) => onexport(fmt, build_export_payload(fmt))
|
|
866
|
+
: undefined}
|
|
867
|
+
toggle_visible
|
|
868
|
+
children={controls_children}
|
|
869
|
+
{...controls_props}
|
|
870
|
+
/>
|
|
871
|
+
{/if}
|
|
872
|
+
<div
|
|
873
|
+
{...rest}
|
|
874
|
+
bind:this={matrix_el}
|
|
875
|
+
class="grid theme-{theme} {rest.class ?? ``}"
|
|
876
|
+
style:--n-cols={gaps_mode ? x_items.length : grid_col_count}
|
|
877
|
+
style:--n-rows={gaps_mode ? y_items.length : grid_row_count}
|
|
878
|
+
style:--extra-right-y={(use_side_split_y_labels || symmetric === `upper`) ? 1 : 0}
|
|
879
|
+
style:--extra-bottom-x={use_side_split_x_labels ? 1 : 0}
|
|
880
|
+
style:--right-y-track={(use_side_split_y_labels || symmetric === `upper`) ? `max-content` : `0`}
|
|
881
|
+
style:--bottom-x-track={use_side_split_x_labels ? `max-content` : `0`}
|
|
882
|
+
style:--tile-size={tile_size}
|
|
883
|
+
style:--heatmap-gridline-color={gridline_color}
|
|
884
|
+
style:--heatmap-gridline-width={gridline_width}
|
|
885
|
+
style:--heatmap-anim-duration={animation_duration}
|
|
886
|
+
style:gap
|
|
887
|
+
onmouseover={handle_mouseover}
|
|
888
|
+
onmouseout={handle_mouseout}
|
|
889
|
+
onmousedown={handle_mousedown}
|
|
890
|
+
onmouseup={handle_mouseup}
|
|
891
|
+
onclick={handle_click}
|
|
892
|
+
ondblclick={handle_dblclick}
|
|
893
|
+
oncontextmenu={handle_contextmenu}
|
|
894
|
+
onkeydown={handle_keydown}
|
|
895
|
+
onscroll={update_viewport_state}
|
|
896
|
+
>
|
|
897
|
+
<!-- Top-left corner spacer (when both axes have labels) -->
|
|
898
|
+
{#if show_x_labels && show_y_labels}
|
|
899
|
+
<div class="corner"></div>
|
|
900
|
+
{/if}
|
|
901
|
+
|
|
902
|
+
<!-- X-axis labels (top row) -->
|
|
903
|
+
{#if show_x_labels}
|
|
904
|
+
{#each vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
|
|
905
|
+
{@const item = x_items[x_idx]}
|
|
906
|
+
<div
|
|
907
|
+
class="x-label"
|
|
908
|
+
class:x-edge-top={use_side_split_x_labels && x_idx % 2 === 0}
|
|
909
|
+
class:x-edge-bottom={use_side_split_x_labels && x_idx % 2 !== 0}
|
|
910
|
+
class:highlighted={highlight_x_by_idx.has(x_idx)}
|
|
911
|
+
class:sticky={sticky_x_labels}
|
|
912
|
+
style={label_style || undefined}
|
|
913
|
+
style:grid-column={x_label_grid_col(x_idx)}
|
|
914
|
+
style:grid-row={x_label_grid_row(x_idx)}
|
|
915
|
+
title={x_label_cell ? undefined : item.label}
|
|
916
|
+
>
|
|
917
|
+
{#if x_label_cell}
|
|
918
|
+
{@render x_label_cell({ item, idx: x_idx })}
|
|
919
|
+
{:else}
|
|
920
|
+
{item.label}
|
|
921
|
+
{/if}
|
|
922
|
+
</div>
|
|
923
|
+
{/each}
|
|
924
|
+
{/if}
|
|
925
|
+
|
|
926
|
+
<!-- Grid rows: y-label + cells -->
|
|
927
|
+
{#each render_vis_y as y_idx (y_items[y_idx].key ?? y_items[y_idx].label)}
|
|
928
|
+
{@const y_item = y_items[y_idx]}
|
|
929
|
+
{#if show_y_labels}
|
|
930
|
+
<div
|
|
931
|
+
class="y-label"
|
|
932
|
+
class:y-edge-left={use_side_split_y_labels && y_idx % 2 === 0}
|
|
933
|
+
class:y-edge-right={use_side_split_y_labels && y_idx % 2 !== 0}
|
|
934
|
+
class:highlighted={highlight_y_by_idx.has(y_idx)}
|
|
935
|
+
class:sticky={sticky_y_labels}
|
|
936
|
+
style={label_style || undefined}
|
|
937
|
+
style:grid-row={y_label_edge_grid_row(y_idx)}
|
|
938
|
+
style:grid-column={y_label_grid_col(y_idx)}
|
|
939
|
+
title={y_label_cell ? undefined : y_item.label}
|
|
940
|
+
>
|
|
941
|
+
{#if y_label_cell}
|
|
942
|
+
{@render y_label_cell({ item: y_item, idx: y_idx })}
|
|
943
|
+
{:else}
|
|
944
|
+
{y_item.label}
|
|
945
|
+
{/if}
|
|
946
|
+
</div>
|
|
947
|
+
{/if}
|
|
948
|
+
|
|
949
|
+
<!-- Cells for this row -->
|
|
950
|
+
{#each render_vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
|
|
951
|
+
{@const flat_idx = get_flat_idx(x_idx, y_idx)}
|
|
952
|
+
{@const bg = bg_flat[flat_idx]}
|
|
953
|
+
{@const should_render = !is_hidden_cell(x_idx, y_idx)}
|
|
954
|
+
{#if should_render}
|
|
955
|
+
<svelte:element
|
|
956
|
+
this={cell_tag_name}
|
|
957
|
+
class={cell_class_name}
|
|
958
|
+
class:selected={is_selected_cell(x_idx, y_idx)}
|
|
959
|
+
class:gridlines={show_gridlines}
|
|
960
|
+
class:animated={animate_updates}
|
|
961
|
+
data-x={x_idx}
|
|
962
|
+
data-y={y_idx}
|
|
963
|
+
style:background-color={bg}
|
|
964
|
+
style:color={text_flat?.[flat_idx]}
|
|
965
|
+
style:--heatmap-selected-outline-color={selected_outline_flat[flat_idx]}
|
|
966
|
+
style:grid-column={cell_grid_col(x_idx)}
|
|
967
|
+
style:grid-row={cell_grid_row(y_idx)}
|
|
968
|
+
>
|
|
969
|
+
{#if cell}
|
|
970
|
+
{@render cell(build_cell_context(x_idx, y_idx))}
|
|
971
|
+
{:else if show_values}
|
|
972
|
+
{@const raw = get_value(x_idx, y_idx)}
|
|
973
|
+
{#if raw !== null}
|
|
974
|
+
<span class="cell-value">{
|
|
975
|
+
typeof raw === `number`
|
|
976
|
+
? format_num(raw, show_values === true ? `.3~g` : show_values)
|
|
977
|
+
: raw
|
|
978
|
+
}</span>
|
|
979
|
+
{/if}
|
|
980
|
+
{/if}
|
|
981
|
+
</svelte:element>
|
|
982
|
+
{:else}
|
|
983
|
+
<div
|
|
984
|
+
class="cell empty"
|
|
985
|
+
style:grid-column={cell_grid_col(x_idx)}
|
|
986
|
+
style:grid-row={cell_grid_row(y_idx)}
|
|
987
|
+
>
|
|
988
|
+
</div>
|
|
989
|
+
{/if}
|
|
990
|
+
{/each}
|
|
991
|
+
{/each}
|
|
992
|
+
|
|
993
|
+
{#if show_row_summaries}
|
|
994
|
+
{#each vis_y as y_idx (y_items[y_idx].key ?? y_items[y_idx].label)}
|
|
995
|
+
<div
|
|
996
|
+
class="summary summary-row"
|
|
997
|
+
style:grid-column={visible_col_count + 2}
|
|
998
|
+
style:grid-row={cell_grid_row(y_idx)}
|
|
999
|
+
>
|
|
1000
|
+
{#if row_summaries.get(y_idx) !== null}
|
|
1001
|
+
{format_num(row_summaries.get(y_idx) ?? 0)}
|
|
1002
|
+
{/if}
|
|
1003
|
+
</div>
|
|
1004
|
+
{/each}
|
|
1005
|
+
{/if}
|
|
1006
|
+
|
|
1007
|
+
{#if show_col_summaries}
|
|
1008
|
+
{#each vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
|
|
1009
|
+
<div
|
|
1010
|
+
class="summary summary-col"
|
|
1011
|
+
style:grid-column={cell_grid_col(x_idx)}
|
|
1012
|
+
style:grid-row={visible_row_count + 2}
|
|
1013
|
+
>
|
|
1014
|
+
{#if col_summaries.get(x_idx) !== null}
|
|
1015
|
+
{format_num(col_summaries.get(x_idx) ?? 0)}
|
|
1016
|
+
{/if}
|
|
1017
|
+
</div>
|
|
1018
|
+
{/each}
|
|
1019
|
+
{/if}
|
|
1020
|
+
|
|
1021
|
+
<!-- Tooltip: always in DOM, visibility toggled imperatively via classList -->
|
|
1022
|
+
{#if tooltip !== false}
|
|
1023
|
+
<div class="tooltip" bind:this={tooltip_div}>
|
|
1024
|
+
{#if typeof tooltip === `function` && tooltip_cell}
|
|
1025
|
+
{@render tooltip(tooltip_cell)}
|
|
1026
|
+
{/if}
|
|
1027
|
+
</div>
|
|
1028
|
+
{/if}
|
|
1029
|
+
|
|
1030
|
+
{@render children?.()}
|
|
1031
|
+
</div>
|
|
1032
|
+
|
|
1033
|
+
{#if show_legend}
|
|
1034
|
+
<ColorBar
|
|
1035
|
+
class="legend legend-{legend_position}"
|
|
1036
|
+
title={legend_label}
|
|
1037
|
+
orientation={legend_orientation}
|
|
1038
|
+
tick_labels={legend_ticks}
|
|
1039
|
+
tick_format={legend_format}
|
|
1040
|
+
range={[cs_min, cs_max]}
|
|
1041
|
+
scale_type={use_log_norm ? `log` : `linear`}
|
|
1042
|
+
{color_scale}
|
|
1043
|
+
wrapper_style={legend_wrapper_style}
|
|
1044
|
+
/>
|
|
1045
|
+
{/if}
|
|
1046
|
+
{#if x_axis.label}<div class="x-title">{x_axis.label}</div>{/if}
|
|
1047
|
+
{#if y_axis.label}<div class="y-title">{y_axis.label}</div>{/if}
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<style>
|
|
1051
|
+
.heatmap {
|
|
1052
|
+
position: relative;
|
|
1053
|
+
width: min(100%, var(--heatmap-max-width, 1200px));
|
|
1054
|
+
max-width: var(--heatmap-max-width, 1200px);
|
|
1055
|
+
box-sizing: border-box;
|
|
1056
|
+
container-type: inline-size;
|
|
1057
|
+
&.legend-bottom {
|
|
1058
|
+
padding-bottom: 44px;
|
|
1059
|
+
}
|
|
1060
|
+
:global(.legend) {
|
|
1061
|
+
position: absolute;
|
|
1062
|
+
background: color-mix(in srgb, var(--bg, #fff) 80%, transparent);
|
|
1063
|
+
padding: 0.3rem 0.4rem;
|
|
1064
|
+
border-radius: var(--border-radius, 3pt);
|
|
1065
|
+
}
|
|
1066
|
+
&.legend-right :global(.legend-right) {
|
|
1067
|
+
right: 8px;
|
|
1068
|
+
top: 8px;
|
|
1069
|
+
}
|
|
1070
|
+
&.legend-bottom :global(.legend-bottom) {
|
|
1071
|
+
left: 50%;
|
|
1072
|
+
bottom: 80px;
|
|
1073
|
+
transform: translateX(-50%);
|
|
1074
|
+
}
|
|
1075
|
+
.x-title {
|
|
1076
|
+
text-align: center;
|
|
1077
|
+
font-size: 0.9em;
|
|
1078
|
+
margin-top: 4px;
|
|
1079
|
+
}
|
|
1080
|
+
.y-title {
|
|
1081
|
+
position: absolute;
|
|
1082
|
+
left: 0;
|
|
1083
|
+
top: 50%;
|
|
1084
|
+
writing-mode: vertical-lr;
|
|
1085
|
+
transform: translateY(-50%) rotate(180deg);
|
|
1086
|
+
font-size: 0.9em;
|
|
1087
|
+
white-space: nowrap;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
.grid {
|
|
1091
|
+
display: grid;
|
|
1092
|
+
grid-template-columns:
|
|
1093
|
+
max-content repeat(
|
|
1094
|
+
var(--n-cols),
|
|
1095
|
+
minmax(var(--tile-size, 6px), 1fr)
|
|
1096
|
+
) var(--right-y-track, 0);
|
|
1097
|
+
grid-template-rows:
|
|
1098
|
+
max-content repeat(
|
|
1099
|
+
var(--n-rows),
|
|
1100
|
+
minmax(var(--tile-size, 6px), 1fr)
|
|
1101
|
+
) var(--bottom-x-track, 0);
|
|
1102
|
+
position: relative;
|
|
1103
|
+
width: min(100%, var(--heatmap-max-width, 1200px));
|
|
1104
|
+
max-width: var(--heatmap-max-width, 1200px);
|
|
1105
|
+
aspect-ratio: calc(
|
|
1106
|
+
(
|
|
1107
|
+
var(--n-cols) + 1 + var(--extra-right-y, 0)
|
|
1108
|
+
)
|
|
1109
|
+
/ (
|
|
1110
|
+
var(--n-rows) + 1 + var(--extra-bottom-x, 0)
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
overflow: auto;
|
|
1114
|
+
&.theme-publication {
|
|
1115
|
+
--tooltip-bg: rgba(255, 255, 255, 0.98);
|
|
1116
|
+
--tooltip-color: #111;
|
|
1117
|
+
}
|
|
1118
|
+
&.theme-dark {
|
|
1119
|
+
--tooltip-bg: rgba(0, 0, 0, 0.9);
|
|
1120
|
+
--tooltip-color: #eee;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
.corner {
|
|
1124
|
+
min-width: 0; /* spacer in top-left when both axes have labels */
|
|
1125
|
+
}
|
|
1126
|
+
.cell {
|
|
1127
|
+
width: 100%;
|
|
1128
|
+
height: 100%;
|
|
1129
|
+
min-width: 0;
|
|
1130
|
+
min-height: 0;
|
|
1131
|
+
border-radius: var(
|
|
1132
|
+
--heatmap-cell-border-radius,
|
|
1133
|
+
calc(var(--tile-size, 6px) * var(--heatmap-cell-radius-ratio, 0.12))
|
|
1134
|
+
);
|
|
1135
|
+
overflow: hidden;
|
|
1136
|
+
display: flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
justify-content: center;
|
|
1139
|
+
cursor: default;
|
|
1140
|
+
&.interactive {
|
|
1141
|
+
border: none;
|
|
1142
|
+
padding: 0;
|
|
1143
|
+
font: inherit;
|
|
1144
|
+
line-height: inherit;
|
|
1145
|
+
cursor: pointer;
|
|
1146
|
+
}
|
|
1147
|
+
&.selected {
|
|
1148
|
+
box-shadow: inset 0 0 0
|
|
1149
|
+
var(
|
|
1150
|
+
--heatmap-selected-outline-width,
|
|
1151
|
+
clamp(1px, calc(var(--tile-size, 6px) * 0.16), 3px)
|
|
1152
|
+
)
|
|
1153
|
+
color-mix(
|
|
1154
|
+
in srgb,
|
|
1155
|
+
var(--heatmap-selected-outline-color, currentColor) 75%,
|
|
1156
|
+
transparent
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
&.gridlines {
|
|
1160
|
+
border: var(--heatmap-gridline-width) solid var(--heatmap-gridline-color);
|
|
1161
|
+
}
|
|
1162
|
+
&.animated {
|
|
1163
|
+
transition: background-color var(--heatmap-anim-duration) ease;
|
|
1164
|
+
}
|
|
1165
|
+
&.empty {
|
|
1166
|
+
pointer-events: none;
|
|
1167
|
+
}
|
|
1168
|
+
.cell-value {
|
|
1169
|
+
font-size: clamp(8px, calc(var(--tile-size, 6px) * 0.45), 14px);
|
|
1170
|
+
user-select: none;
|
|
1171
|
+
white-space: nowrap;
|
|
1172
|
+
overflow: hidden;
|
|
1173
|
+
text-overflow: ellipsis;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
:is(.x-label, .y-label) {
|
|
1177
|
+
font-size: clamp(10px, calc(var(--tile-size, 6px) * 0.75), 24px);
|
|
1178
|
+
overflow: hidden;
|
|
1179
|
+
text-overflow: ellipsis;
|
|
1180
|
+
white-space: nowrap;
|
|
1181
|
+
min-width: 0;
|
|
1182
|
+
display: flex;
|
|
1183
|
+
align-items: center;
|
|
1184
|
+
justify-content: center;
|
|
1185
|
+
text-align: center;
|
|
1186
|
+
&.sticky {
|
|
1187
|
+
position: sticky;
|
|
1188
|
+
z-index: 2;
|
|
1189
|
+
background: var(--bg, transparent);
|
|
1190
|
+
}
|
|
1191
|
+
&.highlighted {
|
|
1192
|
+
font-weight: 700;
|
|
1193
|
+
text-decoration: underline;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
.x-label {
|
|
1197
|
+
overflow: visible;
|
|
1198
|
+
text-overflow: clip;
|
|
1199
|
+
align-items: flex-end;
|
|
1200
|
+
padding: 2px;
|
|
1201
|
+
&.sticky {
|
|
1202
|
+
top: 0;
|
|
1203
|
+
}
|
|
1204
|
+
&.x-edge-top {
|
|
1205
|
+
min-height: 1.6em;
|
|
1206
|
+
align-items: flex-end;
|
|
1207
|
+
}
|
|
1208
|
+
&.x-edge-bottom {
|
|
1209
|
+
min-height: 1.6em;
|
|
1210
|
+
align-items: flex-start;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
.y-label {
|
|
1214
|
+
padding: 0 2px;
|
|
1215
|
+
&.sticky {
|
|
1216
|
+
left: 0;
|
|
1217
|
+
}
|
|
1218
|
+
&:is(.y-edge-left, .y-edge-right) {
|
|
1219
|
+
min-width: 1.6em;
|
|
1220
|
+
}
|
|
1221
|
+
&.y-edge-left {
|
|
1222
|
+
justify-content: flex-end;
|
|
1223
|
+
text-align: right;
|
|
1224
|
+
}
|
|
1225
|
+
&.y-edge-right {
|
|
1226
|
+
justify-content: flex-start;
|
|
1227
|
+
text-align: left;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
.summary {
|
|
1231
|
+
font-size: clamp(9px, calc(var(--tile-size, 6px) * 0.6), 14px);
|
|
1232
|
+
align-self: center;
|
|
1233
|
+
justify-self: center;
|
|
1234
|
+
color: var(--text-color-muted, currentColor);
|
|
1235
|
+
opacity: 0.9;
|
|
1236
|
+
}
|
|
1237
|
+
.tooltip {
|
|
1238
|
+
display: none;
|
|
1239
|
+
position: fixed;
|
|
1240
|
+
transform: none;
|
|
1241
|
+
background: var(
|
|
1242
|
+
--tooltip-bg,
|
|
1243
|
+
light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.85))
|
|
1244
|
+
);
|
|
1245
|
+
color: var(--tooltip-color, light-dark(#222, #eee));
|
|
1246
|
+
padding: var(--tooltip-padding, 4px 6px);
|
|
1247
|
+
border-radius: var(--tooltip-border-radius, var(--border-radius, 3pt));
|
|
1248
|
+
font-size: var(--tooltip-font-size, 12px);
|
|
1249
|
+
text-align: var(--tooltip-text-align, center);
|
|
1250
|
+
line-height: var(--tooltip-line-height, 1.2);
|
|
1251
|
+
z-index: var(--tooltip-z-index, 10);
|
|
1252
|
+
pointer-events: none;
|
|
1253
|
+
box-shadow: var(
|
|
1254
|
+
--tooltip-shadow,
|
|
1255
|
+
light-dark(0 2px 8px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.4))
|
|
1256
|
+
);
|
|
1257
|
+
white-space: nowrap;
|
|
1258
|
+
&.visible {
|
|
1259
|
+
display: block;
|
|
1260
|
+
}
|
|
1261
|
+
&::before {
|
|
1262
|
+
content: '';
|
|
1263
|
+
position: absolute;
|
|
1264
|
+
top: -6px;
|
|
1265
|
+
left: 50%;
|
|
1266
|
+
transform: translateX(-50%);
|
|
1267
|
+
border-left: 6px solid transparent;
|
|
1268
|
+
border-right: 6px solid transparent;
|
|
1269
|
+
border-bottom: 6px solid
|
|
1270
|
+
var(--tooltip-bg, light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.85)));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
</style>
|