matterviz 0.3.2 → 0.3.4

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.
Files changed (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,851 +1,1105 @@
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(() => {
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
- const matrix_values = values;
44
- return (x_idx, y_idx) => matrix_values[y_idx]?.[x_idx] ?? null;
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
- 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];
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
- sorted.sort((idx_a, idx_b) => axis_order(items[idx_a], items[idx_b]));
62
- return sorted;
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
- 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);
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
- ? 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;
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
- ? 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;
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
- return {
100
- vis_x: sort_indices(filtered_x, x_items, x_order),
101
- vis_y: sort_indices(filtered_y, y_items, y_order),
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
- const col_has_data = new Array(x_items.length).fill(false);
105
- const row_has_data = new Array(y_items.length).fill(false);
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
- 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
- }
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
- 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;
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
- 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 = [];
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
- 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
- }
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
- // 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;
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
- if (value < min)
175
- min = value;
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
- 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);
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
- ? [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]];
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
- 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;
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
- if (is_color(val))
211
- return val;
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
- 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);
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
- ? normalize(val, cs_min, cs_max)
223
- : (val - cs_min) / span;
412
+ ? normalize(val, cs_min, cs_max)
413
+ : (val - cs_min) / span
224
414
  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;
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
- 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);
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
- 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);
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
- 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) {
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
- 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();
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
- position_map.set(item_idx, vis_pos);
587
+ position_map.set(item_idx, vis_pos)
374
588
  }
375
- return position_map;
376
- });
377
- let vis_y_pos_map = $derived.by(() => {
378
- const position_map = new SvelteMap();
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
- position_map.set(item_idx, vis_pos);
595
+ position_map.set(item_idx, vis_pos)
381
596
  }
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;
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
- // Upper triangle: place x label below diagonal (in empty lower-left area)
402
- return Math.min(visible_row_count + 1, vis_row + 3);
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
- 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);
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
- return visible_row_count + 2 + (show_bottom_summary_row ? 1 : 0);
655
+ return visible_row_count + 2 + (show_bottom_summary_row ? 1 : 0)
429
656
  }
430
- return 1;
431
- }
432
- // Upper symmetric or staggered odd labels: place on right side
433
- function y_label_grid_col(y_idx) {
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
- return visible_col_count + 2 + (show_right_summary_col ? 1 : 0);
663
+ return visible_col_count + 2 + (show_right_summary_col ? 1 : 0)
436
664
  }
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) {
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
- callback();
454
- return 0;
682
+ callback()
683
+ return 0
455
684
  }
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;
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
- onclick(cell_context);
490
- return;
721
+ onclick(cell_context)
722
+ return
491
723
  }
492
- clear_pending_click();
724
+ clear_pending_click()
493
725
  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;
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
- return target_node;
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
- function update_selected_cells(event, clicked_cell) {
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
- selected_cells = [clicked_cell];
511
- last_selected_cell = clicked_cell;
512
- onselect?.(selected_cells);
513
- return;
748
+ selected_cells = [clicked_cell]
749
+ last_selected_cell = clicked_cell
750
+ onselect?.(selected_cells)
751
+ return
514
752
  }
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
- }
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
- 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);
768
+ }
769
+ selected_cells = next_cells
770
+ onselect?.(selected_cells)
771
+ return
546
772
  }
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);
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
- ? `${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;
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
- return;
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
- 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;
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
- tooltip_cell = build_cell_context(x_idx, y_idx);
605
- }
606
- else {
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
- function handle_mouseout(event) {
611
- if (disabled)
612
- return;
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
- (tooltip_mode === `both` && pinned_cell !== null);
865
+ (tooltip_mode === `both` && pinned_cell !== null)
621
866
  if (!keep_tooltip_visible) {
622
- tooltip_div?.classList.remove(`visible`);
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
- 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
- });
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
- 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
- }
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
- 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() {
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
- brush_start = null;
689
- brush_end = null;
690
- return;
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
- 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
- }
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
- 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;
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
- 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;
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
- 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;
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
- 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();
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
- 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
- }
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
- summary_map.set(primary_idx, compute_summary(values_for_summary));
1037
+ }
1038
+ summary_map.set(primary_idx, compute_summary(values_for_summary))
809
1039
  }
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) ||
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
- 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);
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
- window.removeEventListener(`mouseup`, handle_mouseup);
843
- };
844
- });
845
- onDestroy(() => {
846
- cancel_raf(active_cell_raf);
847
- clear_pending_click();
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