matterviz 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +154 -96
  3. package/dist/Icon.svelte +20 -14
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -178
  7. package/dist/brillouin/BrillouinZone.svelte +299 -198
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
  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 +327 -0
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  23. package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
  24. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  25. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  26. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  27. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  28. package/dist/chempot-diagram/chempot-worker.js +11 -0
  29. package/dist/chempot-diagram/color.d.ts +10 -0
  30. package/dist/chempot-diagram/color.js +32 -0
  31. package/dist/chempot-diagram/compute.d.ts +48 -0
  32. package/dist/chempot-diagram/compute.js +812 -0
  33. package/dist/chempot-diagram/index.d.ts +6 -0
  34. package/dist/chempot-diagram/index.js +6 -0
  35. package/dist/chempot-diagram/pointer.d.ts +16 -0
  36. package/dist/chempot-diagram/pointer.js +40 -0
  37. package/dist/chempot-diagram/temperature.d.ts +15 -0
  38. package/dist/chempot-diagram/temperature.js +36 -0
  39. package/dist/chempot-diagram/types.d.ts +86 -0
  40. package/dist/chempot-diagram/types.js +28 -0
  41. package/dist/colors/index.d.ts +3 -1
  42. package/dist/colors/index.js +9 -3
  43. package/dist/composition/BarChart.svelte +141 -77
  44. package/dist/composition/BubbleChart.svelte +107 -52
  45. package/dist/composition/Composition.svelte +100 -79
  46. package/dist/composition/Formula.svelte +108 -62
  47. package/dist/composition/FormulaFilter.svelte +973 -353
  48. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  49. package/dist/composition/PieChart.svelte +199 -99
  50. package/dist/composition/PieChart.svelte.d.ts +1 -1
  51. package/dist/composition/format.d.ts +5 -0
  52. package/dist/composition/format.js +20 -3
  53. package/dist/composition/parse.js +14 -9
  54. package/dist/convex-hull/ConvexHull.svelte +93 -38
  55. package/dist/convex-hull/ConvexHull2D.svelte +551 -393
  56. package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
  57. package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
  58. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  59. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  60. package/dist/convex-hull/ConvexHullStats.svelte +821 -249
  61. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  62. package/dist/convex-hull/ConvexHullTooltip.svelte +41 -16
  63. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  64. package/dist/convex-hull/StructurePopup.svelte +25 -4
  65. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  66. package/dist/convex-hull/barycentric-coords.js +13 -7
  67. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  68. package/dist/convex-hull/demo-temperature.js +40 -0
  69. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  70. package/dist/convex-hull/helpers.d.ts +10 -1
  71. package/dist/convex-hull/helpers.js +79 -38
  72. package/dist/convex-hull/index.d.ts +1 -0
  73. package/dist/convex-hull/index.js +1 -0
  74. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  75. package/dist/convex-hull/thermodynamics.js +163 -69
  76. package/dist/convex-hull/types.d.ts +12 -12
  77. package/dist/convex-hull/types.js +0 -12
  78. package/dist/coordination/CoordinationBarPlot.svelte +232 -176
  79. package/dist/element/BohrAtom.svelte +56 -13
  80. package/dist/element/ElementHeading.svelte +7 -2
  81. package/dist/element/ElementPhoto.svelte +15 -9
  82. package/dist/element/ElementStats.svelte +10 -4
  83. package/dist/element/ElementTile.svelte +137 -73
  84. package/dist/element/Nucleus.svelte +39 -11
  85. package/dist/element/data.js +2 -14
  86. package/dist/element/data.json.gz +0 -0
  87. package/dist/element/types.d.ts +1 -0
  88. package/dist/feedback/ClickFeedback.svelte +16 -5
  89. package/dist/feedback/DragOverlay.svelte +10 -2
  90. package/dist/feedback/Spinner.svelte +4 -2
  91. package/dist/feedback/StatusMessage.svelte +8 -2
  92. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  93. package/dist/fermi-surface/FermiSurface.svelte +336 -239
  94. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  95. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  96. package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
  97. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  98. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  99. package/dist/fermi-surface/compute.js +16 -20
  100. package/dist/fermi-surface/parse.js +37 -33
  101. package/dist/fermi-surface/symmetry.js +2 -7
  102. package/dist/fermi-surface/types.d.ts +3 -5
  103. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
  104. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  105. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  106. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
  107. package/dist/heatmap-matrix/index.d.ts +53 -0
  108. package/dist/heatmap-matrix/index.js +100 -0
  109. package/dist/heatmap-matrix/shared.d.ts +2 -0
  110. package/dist/heatmap-matrix/shared.js +4 -0
  111. package/dist/icons.d.ts +111 -0
  112. package/dist/icons.js +158 -0
  113. package/dist/index.d.ts +5 -2
  114. package/dist/index.js +5 -2
  115. package/dist/io/decompress.js +1 -1
  116. package/dist/io/export.d.ts +3 -0
  117. package/dist/io/export.js +138 -140
  118. package/dist/io/file-drop.d.ts +7 -0
  119. package/dist/io/file-drop.js +43 -0
  120. package/dist/io/index.d.ts +2 -2
  121. package/dist/io/index.js +2 -112
  122. package/dist/io/is-binary.js +2 -3
  123. package/dist/io/types.d.ts +1 -0
  124. package/dist/io/url-drop.d.ts +2 -0
  125. package/dist/io/url-drop.js +117 -0
  126. package/dist/isosurface/Isosurface.svelte +220 -110
  127. package/dist/isosurface/IsosurfaceControls.svelte +65 -28
  128. package/dist/isosurface/parse.js +104 -56
  129. package/dist/isosurface/slice.d.ts +2 -1
  130. package/dist/isosurface/slice.js +8 -13
  131. package/dist/isosurface/types.d.ts +14 -1
  132. package/dist/isosurface/types.js +152 -5
  133. package/dist/labels.d.ts +2 -1
  134. package/dist/labels.js +12 -8
  135. package/dist/layout/FullscreenToggle.svelte +11 -2
  136. package/dist/layout/InfoCard.svelte +38 -6
  137. package/dist/layout/InfoTag.svelte +125 -94
  138. package/dist/layout/PropertyFilter.svelte +82 -37
  139. package/dist/layout/SettingsSection.svelte +85 -55
  140. package/dist/layout/SubpageGrid.svelte +82 -0
  141. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  142. package/dist/layout/index.d.ts +1 -0
  143. package/dist/layout/index.js +1 -0
  144. package/dist/layout/json-tree/JsonNode.svelte +266 -223
  145. package/dist/layout/json-tree/JsonTree.svelte +516 -429
  146. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  147. package/dist/layout/json-tree/JsonValue.svelte +281 -173
  148. package/dist/layout/json-tree/types.d.ts +10 -2
  149. package/dist/layout/json-tree/utils.d.ts +2 -0
  150. package/dist/layout/json-tree/utils.js +37 -2
  151. package/dist/marching-cubes.js +25 -2
  152. package/dist/math.d.ts +20 -17
  153. package/dist/math.js +474 -57
  154. package/dist/overlays/ContextMenu.svelte +66 -40
  155. package/dist/overlays/DraggablePane.svelte +331 -154
  156. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  157. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  158. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  159. package/dist/periodic-table/PropertySelect.svelte +25 -7
  160. package/dist/periodic-table/TableInset.svelte +8 -3
  161. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +559 -267
  162. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  163. package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
  164. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  165. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  166. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  167. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
  168. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
  169. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
  170. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  171. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  172. package/dist/phase-diagram/build-diagram.js +9 -9
  173. package/dist/phase-diagram/colors.js +1 -3
  174. package/dist/phase-diagram/index.d.ts +2 -0
  175. package/dist/phase-diagram/index.js +2 -0
  176. package/dist/phase-diagram/parse.js +10 -9
  177. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  178. package/dist/phase-diagram/svg-to-diagram.js +869 -0
  179. package/dist/phase-diagram/types.d.ts +10 -0
  180. package/dist/phase-diagram/utils.d.ts +8 -4
  181. package/dist/phase-diagram/utils.js +219 -74
  182. package/dist/plot/AxisLabel.svelte +51 -0
  183. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  184. package/dist/plot/BarPlot.svelte +1461 -768
  185. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  186. package/dist/plot/BarPlotControls.svelte +33 -6
  187. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  188. package/dist/plot/ColorBar.svelte +533 -383
  189. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  190. package/dist/plot/ColorScaleSelect.svelte +28 -7
  191. package/dist/plot/ElementScatter.svelte +38 -16
  192. package/dist/plot/FillArea.svelte +152 -92
  193. package/dist/plot/Histogram.svelte +1162 -709
  194. package/dist/plot/Histogram.svelte.d.ts +1 -1
  195. package/dist/plot/HistogramControls.svelte +81 -18
  196. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  197. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  198. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  199. package/dist/plot/Line.svelte +63 -28
  200. package/dist/plot/PlotControls.svelte +221 -96
  201. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  202. package/dist/plot/PlotLegend.svelte +174 -91
  203. package/dist/plot/PlotTooltip.svelte +45 -6
  204. package/dist/plot/PortalSelect.svelte +175 -146
  205. package/dist/plot/ReferenceLine.svelte +77 -22
  206. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  207. package/dist/plot/ReferenceLine3D.svelte +132 -107
  208. package/dist/plot/ReferencePlane.svelte +146 -123
  209. package/dist/plot/ScatterPlot.svelte +1880 -1156
  210. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  211. package/dist/plot/ScatterPlot3D.svelte +256 -131
  212. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  213. package/dist/plot/ScatterPlot3DControls.svelte +300 -297
  214. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  215. package/dist/plot/ScatterPlot3DScene.svelte +608 -406
  216. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  217. package/dist/plot/ScatterPlotControls.svelte +150 -70
  218. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  219. package/dist/plot/ScatterPoint.svelte +98 -26
  220. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  221. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  222. package/dist/plot/Surface3D.svelte +159 -108
  223. package/dist/plot/ZeroLines.svelte +96 -0
  224. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  225. package/dist/plot/ZoomRect.svelte +23 -0
  226. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  227. package/dist/plot/axis-utils.d.ts +1 -1
  228. package/dist/plot/axis-utils.js +1 -3
  229. package/dist/plot/data-cleaning.js +12 -28
  230. package/dist/plot/data-transform.js +2 -1
  231. package/dist/plot/fill-utils.js +2 -0
  232. package/dist/plot/index.d.ts +6 -2
  233. package/dist/plot/index.js +6 -2
  234. package/dist/plot/interactions.d.ts +8 -10
  235. package/dist/plot/interactions.js +2 -3
  236. package/dist/plot/layout.d.ts +11 -2
  237. package/dist/plot/layout.js +44 -17
  238. package/dist/plot/reference-line.d.ts +5 -22
  239. package/dist/plot/reference-line.js +12 -84
  240. package/dist/plot/scales.js +24 -36
  241. package/dist/plot/types.d.ts +53 -40
  242. package/dist/plot/types.js +12 -7
  243. package/dist/plot/utils/label-placement.d.ts +32 -15
  244. package/dist/plot/utils/label-placement.js +227 -63
  245. package/dist/plot/utils/series-visibility.js +2 -3
  246. package/dist/plot/utils.d.ts +1 -0
  247. package/dist/plot/utils.js +14 -0
  248. package/dist/rdf/RdfPlot.svelte +173 -132
  249. package/dist/rdf/calc-rdf.js +4 -5
  250. package/dist/sanitize.d.ts +4 -0
  251. package/dist/sanitize.js +107 -0
  252. package/dist/settings.d.ts +21 -6
  253. package/dist/settings.js +63 -19
  254. package/dist/spectral/Bands.svelte +963 -412
  255. package/dist/spectral/Bands.svelte.d.ts +22 -2
  256. package/dist/spectral/BandsAndDos.svelte +90 -49
  257. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  258. package/dist/spectral/Dos.svelte +389 -258
  259. package/dist/spectral/helpers.d.ts +23 -1
  260. package/dist/spectral/helpers.js +119 -51
  261. package/dist/spectral/types.d.ts +2 -0
  262. package/dist/state.svelte.d.ts +1 -1
  263. package/dist/state.svelte.js +3 -2
  264. package/dist/structure/Arrow.svelte +59 -20
  265. package/dist/structure/AtomLegend.svelte +231 -129
  266. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  267. package/dist/structure/Bond.svelte +73 -47
  268. package/dist/structure/CanvasTooltip.svelte +10 -2
  269. package/dist/structure/CellSelect.svelte +148 -51
  270. package/dist/structure/Cylinder.svelte +33 -17
  271. package/dist/structure/Lattice.svelte +88 -33
  272. package/dist/structure/Structure.svelte +1077 -821
  273. package/dist/structure/Structure.svelte.d.ts +1 -1
  274. package/dist/structure/StructureControls.svelte +373 -139
  275. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  276. package/dist/structure/StructureExportPane.svelte +124 -89
  277. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  278. package/dist/structure/StructureInfoPane.svelte +304 -231
  279. package/dist/structure/StructureScene.svelte +919 -445
  280. package/dist/structure/StructureScene.svelte.d.ts +16 -7
  281. package/dist/structure/atom-properties.d.ts +6 -2
  282. package/dist/structure/atom-properties.js +42 -29
  283. package/dist/structure/bonding.js +6 -7
  284. package/dist/structure/export.js +22 -34
  285. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  286. package/dist/structure/ferrox-wasm-types.js +0 -3
  287. package/dist/structure/ferrox-wasm.d.ts +3 -2
  288. package/dist/structure/ferrox-wasm.js +2 -3
  289. package/dist/structure/index.d.ts +16 -0
  290. package/dist/structure/index.js +88 -6
  291. package/dist/structure/measure.d.ts +2 -2
  292. package/dist/structure/measure.js +4 -44
  293. package/dist/structure/parse.js +130 -155
  294. package/dist/structure/partial-occupancy.d.ts +25 -0
  295. package/dist/structure/partial-occupancy.js +99 -0
  296. package/dist/structure/pbc.d.ts +1 -0
  297. package/dist/structure/pbc.js +16 -6
  298. package/dist/structure/supercell.d.ts +2 -2
  299. package/dist/structure/supercell.js +12 -22
  300. package/dist/structure/validation.js +5 -3
  301. package/dist/symmetry/SymmetryStats.svelte +94 -37
  302. package/dist/symmetry/WyckoffTable.svelte +42 -14
  303. package/dist/symmetry/cell-transform.js +5 -3
  304. package/dist/symmetry/index.d.ts +7 -4
  305. package/dist/symmetry/index.js +87 -21
  306. package/dist/symmetry/spacegroups.js +148 -148
  307. package/dist/table/HeatmapTable.svelte +1112 -516
  308. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  309. package/dist/table/ToggleMenu.svelte +125 -90
  310. package/dist/table/index.d.ts +2 -0
  311. package/dist/table/index.js +2 -4
  312. package/dist/theme/ThemeControl.svelte +21 -12
  313. package/dist/time.js +4 -1
  314. package/dist/tooltip/TooltipContent.svelte +33 -8
  315. package/dist/trajectory/Trajectory.svelte +889 -687
  316. package/dist/trajectory/TrajectoryError.svelte +14 -3
  317. package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
  318. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  319. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  320. package/dist/trajectory/constants.d.ts +6 -0
  321. package/dist/trajectory/constants.js +7 -0
  322. package/dist/trajectory/extract.js +13 -31
  323. package/dist/trajectory/format-detect.d.ts +9 -0
  324. package/dist/trajectory/format-detect.js +76 -0
  325. package/dist/trajectory/frame-reader.d.ts +17 -0
  326. package/dist/trajectory/frame-reader.js +332 -0
  327. package/dist/trajectory/helpers.d.ts +14 -0
  328. package/dist/trajectory/helpers.js +172 -0
  329. package/dist/trajectory/index.d.ts +1 -0
  330. package/dist/trajectory/index.js +23 -14
  331. package/dist/trajectory/parse/ase.d.ts +2 -0
  332. package/dist/trajectory/parse/ase.js +77 -0
  333. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  334. package/dist/trajectory/parse/hdf5.js +129 -0
  335. package/dist/trajectory/parse/index.d.ts +12 -0
  336. package/dist/trajectory/parse/index.js +299 -0
  337. package/dist/trajectory/parse/lammps.d.ts +5 -0
  338. package/dist/trajectory/parse/lammps.js +179 -0
  339. package/dist/trajectory/parse/vasp.d.ts +2 -0
  340. package/dist/trajectory/parse/vasp.js +68 -0
  341. package/dist/trajectory/parse/xyz.d.ts +2 -0
  342. package/dist/trajectory/parse/xyz.js +110 -0
  343. package/dist/trajectory/plotting.js +13 -8
  344. package/dist/trajectory/types.d.ts +11 -0
  345. package/dist/trajectory/types.js +1 -0
  346. package/dist/utils.d.ts +3 -0
  347. package/dist/utils.js +17 -0
  348. package/dist/xrd/XrdPlot.svelte +337 -245
  349. package/dist/xrd/broadening.js +14 -9
  350. package/dist/xrd/calc-xrd.js +12 -19
  351. package/dist/xrd/parse.d.ts +1 -1
  352. package/dist/xrd/parse.js +17 -17
  353. package/package.json +103 -101
  354. package/readme.md +4 -4
  355. package/dist/trajectory/parse.d.ts +0 -42
  356. package/dist/trajectory/parse.js +0 -1267
  357. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
  358. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -0,0 +1,1527 @@
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(() => {
220
+ if (Array.isArray(values)) {
221
+ const matrix_values = values as CellValue[][]
222
+ return (x_idx: number, y_idx: number): CellValue =>
223
+ matrix_values[y_idx]?.[x_idx] ?? null
224
+ }
225
+ // Record<y_key, Record<x_key, value>>
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]
243
+ if (typeof axis_order === `function`) {
244
+ sorted.sort((idx_a, idx_b) => axis_order(items[idx_a], items[idx_b]))
245
+ return sorted
246
+ }
247
+ sorted.sort((idx_a, idx_b) => {
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)
266
+ const filtered_x = search_query_norm
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
274
+ const filtered_y = search_query_norm
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
282
+ if (!hide_empty) {
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
+ }
287
+ }
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)
291
+ for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
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
296
+ }
297
+ }
298
+ }
299
+ return {
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
324
+ const transformed_value = value_transform(raw_value, {
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[] = []
348
+ for (let y_idx = 0; y_idx < y_items.length; y_idx++) {
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
+ }
355
+ }
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]
362
+ for (const value of valid_numeric_values) {
363
+ if (value < min) min = value
364
+ if (value > max) max = value
365
+ }
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)
377
+ return clipped_min <= clipped_max
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
389
+ }
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
401
+ if (typeof val === `string`) {
402
+ if (is_color(val)) return val
403
+ return missing_color || null
404
+ }
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
+
411
+ let normalized = typeof normalize === `function`
412
+ ? normalize(val, cs_min, cs_max)
413
+ : (val - cs_min) / span
414
+ if (use_log_norm) {
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
430
+ }
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)
441
+ for (let y_idx = 0; y_idx < n_y; y_idx++) {
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
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
+ }
457
+ }
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 {
483
+ return {
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>()
586
+ for (const [vis_pos, item_idx] of vis_x.entries()) {
587
+ position_map.set(item_idx, vis_pos)
588
+ }
589
+ return position_map
590
+ })
591
+
592
+ let vis_y_pos_map = $derived.by(() => {
593
+ const position_map = new SvelteMap<number, number>()
594
+ for (const [vis_pos, item_idx] of vis_y.entries()) {
595
+ position_map.set(item_idx, vis_pos)
596
+ }
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
627
+ if (symmetric === `upper`) {
628
+ // Upper triangle: place x label below diagonal (in empty lower-left area)
629
+ return Math.min(visible_row_count + 1, vis_row + 3)
630
+ }
631
+ // Lower/default: place x label above diagonal (in empty upper-right area)
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)
654
+ if (use_side_split_x_labels && x_idx % 2 !== 0) {
655
+ return visible_row_count + 2 + (show_bottom_summary_row ? 1 : 0)
656
+ }
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 {
662
+ if (symmetric === `upper` || (use_side_split_y_labels && y_idx % 2 !== 0)) {
663
+ return visible_col_count + 2 + (show_right_summary_col ? 1 : 0)
664
+ }
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 {
681
+ if (!is_browser) {
682
+ callback()
683
+ return 0
684
+ }
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
720
+ if (!ondblclick) {
721
+ onclick(cell_context)
722
+ return
723
+ }
724
+ clear_pending_click()
725
+ click_timeout_id = setTimeout(() => {
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
736
+ if (target_node instanceof HTMLElement && target_node.dataset.x !== undefined) {
737
+ return target_node
738
+ }
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 {
747
+ if (selection_mode === `single`) {
748
+ selected_cells = [clicked_cell]
749
+ last_selected_cell = clicked_cell
750
+ onselect?.(selected_cells)
751
+ return
752
+ }
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 })
767
+ }
768
+ }
769
+ selected_cells = next_cells
770
+ onselect?.(selected_cells)
771
+ return
772
+ }
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)
817
+ td.textContent = value_str
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
+
830
+ // Ignore redundant enters on the same cell (can happen with nested children)
831
+ if (last_hover_x === x_idx && last_hover_y === y_idx) {
832
+ return
833
+ }
834
+ last_hover_x = x_idx
835
+ last_hover_y = y_idx
836
+
837
+ // Defer bindable writes out of the hot mouseover path
838
+ cancel_raf(active_cell_raf)
839
+ active_cell_raf = schedule_raf(() => {
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
+
846
+ // Use viewport coordinates to avoid forced layout reads on large grids
847
+ update_tooltip_position(event.clientX, event.clientY)
848
+ tooltip_div.classList.add(`visible`)
849
+
850
+ if (typeof tooltip === `function`) {
851
+ tooltip_cell = build_cell_context(x_idx, y_idx)
852
+ } else {
853
+ update_tooltip_content(tooltip_div, x_idx, y_idx)
854
+ }
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
861
+ // Clear active state imperatively
862
+ last_hover_x = -1
863
+ last_hover_y = -1
864
+ const keep_tooltip_visible = tooltip_mode === `pinned` ||
865
+ (tooltip_mode === `both` && pinned_cell !== null)
866
+ if (!keep_tooltip_visible) {
867
+ tooltip_div?.classList.remove(`visible`)
868
+ }
869
+ // Defer reactive cleanup to rAF
870
+ cancel_raf(active_cell_raf)
871
+ active_cell_raf = schedule_raf(() => {
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 })
883
+ if (tooltip_mode === `both` || tooltip_mode === `pinned`) {
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
+ }
891
+ }
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 {
921
+ if (!enable_brush || !brush_start || !brush_end || !onbrush) {
922
+ brush_start = null
923
+ brush_end = null
924
+ return
925
+ }
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[] = []
931
+ for (let y_idx = y_min; y_idx <= y_max; y_idx++) {
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
+ }
936
+ }
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
962
+ else if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === `e`) {
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
970
+ for (let step_idx = 0; step_idx < max_steps; step_idx++) {
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
981
+ }
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>()
1027
+ for (const primary_idx of primary_indices) {
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)
1036
+ }
1037
+ }
1038
+ summary_map.set(primary_idx, compute_summary(values_for_summary))
1039
+ }
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) ||
1076
+ Boolean(ondblclick) ||
1077
+ Boolean(oncontextmenu) ||
1078
+ selection_mode !== `single` ||
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)
1094
+ return () => {
1095
+ globalThis.removeEventListener(`mouseup`, handle_mouseup)
1096
+ }
1097
+ })
1098
+
1099
+ onDestroy(() => {
1100
+ cancel_raf(active_cell_raf)
1101
+ clear_pending_click()
1102
+ })
1103
+ </script>
1104
+
1105
+ <div
1106
+ class="heatmap legend-{legend_position}"
1107
+ style:padding-left={y_axis.label ? `1.8em` : undefined}
1108
+ >
1109
+ {#if show_controls}
1110
+ <HeatmapMatrixControls
1111
+ bind:controls_open
1112
+ bind:normalize
1113
+ bind:domain_mode
1114
+ bind:show_legend
1115
+ bind:legend_position
1116
+ bind:search_query
1117
+ {export_formats}
1118
+ onexport={onexport
1119
+ ? (fmt: HeatmapExportFormat) => onexport(fmt, build_export_payload(fmt))
1120
+ : undefined}
1121
+ toggle_visible
1122
+ children={controls_children}
1123
+ {...controls_props}
1124
+ />
1125
+ {/if}
1126
+ <div
1127
+ {...rest}
1128
+ bind:this={matrix_el}
1129
+ class="grid theme-{theme} {rest.class ?? ``}"
1130
+ style:--n-cols={gaps_mode ? x_items.length : grid_col_count}
1131
+ style:--n-rows={gaps_mode ? y_items.length : grid_row_count}
1132
+ style:--extra-right-y={(use_side_split_y_labels || symmetric === `upper`) ? 1 : 0}
1133
+ style:--extra-bottom-x={use_side_split_x_labels ? 1 : 0}
1134
+ style:--right-y-track={(use_side_split_y_labels || symmetric === `upper`) ? `max-content` : `0`}
1135
+ style:--bottom-x-track={use_side_split_x_labels ? `max-content` : `0`}
1136
+ style:--tile-size={tile_size}
1137
+ style:--heatmap-gridline-color={gridline_color}
1138
+ style:--heatmap-gridline-width={gridline_width}
1139
+ style:--heatmap-anim-duration={animation_duration}
1140
+ style:gap
1141
+ onmouseover={handle_mouseover}
1142
+ onmouseout={handle_mouseout}
1143
+ onmousedown={handle_mousedown}
1144
+ onmouseup={handle_mouseup}
1145
+ onclick={handle_click}
1146
+ ondblclick={handle_dblclick}
1147
+ oncontextmenu={handle_contextmenu}
1148
+ onkeydown={handle_keydown}
1149
+ onscroll={update_viewport_state}
1150
+ >
1151
+ <!-- Top-left corner spacer (when both axes have labels) -->
1152
+ {#if show_x_labels && show_y_labels}
1153
+ <div class="corner"></div>
1154
+ {/if}
1155
+
1156
+ <!-- X-axis labels (top row) -->
1157
+ {#if show_x_labels}
1158
+ {#each vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
1159
+ {@const item = x_items[x_idx]}
1160
+ <div
1161
+ class="x-label"
1162
+ class:x-edge-top={use_side_split_x_labels && x_idx % 2 === 0}
1163
+ class:x-edge-bottom={use_side_split_x_labels && x_idx % 2 !== 0}
1164
+ class:highlighted={highlight_x_by_idx.has(x_idx)}
1165
+ class:sticky={sticky_x_labels}
1166
+ style={label_style || undefined}
1167
+ style:grid-column={x_label_grid_col(x_idx)}
1168
+ style:grid-row={x_label_grid_row(x_idx)}
1169
+ title={x_label_cell ? undefined : item.label}
1170
+ >
1171
+ {#if x_label_cell}
1172
+ {@render x_label_cell({ item, idx: x_idx })}
1173
+ {:else}
1174
+ {item.label}
1175
+ {/if}
1176
+ </div>
1177
+ {/each}
1178
+ {/if}
1179
+
1180
+ <!-- Grid rows: y-label + cells -->
1181
+ {#each render_vis_y as y_idx (y_items[y_idx].key ?? y_items[y_idx].label)}
1182
+ {@const y_item = y_items[y_idx]}
1183
+ {#if show_y_labels}
1184
+ <div
1185
+ class="y-label"
1186
+ class:y-edge-left={use_side_split_y_labels && y_idx % 2 === 0}
1187
+ class:y-edge-right={use_side_split_y_labels && y_idx % 2 !== 0}
1188
+ class:highlighted={highlight_y_by_idx.has(y_idx)}
1189
+ class:sticky={sticky_y_labels}
1190
+ style={label_style || undefined}
1191
+ style:grid-row={y_label_edge_grid_row(y_idx)}
1192
+ style:grid-column={y_label_grid_col(y_idx)}
1193
+ title={y_label_cell ? undefined : y_item.label}
1194
+ >
1195
+ {#if y_label_cell}
1196
+ {@render y_label_cell({ item: y_item, idx: y_idx })}
1197
+ {:else}
1198
+ {y_item.label}
1199
+ {/if}
1200
+ </div>
1201
+ {/if}
1202
+
1203
+ <!-- Cells for this row -->
1204
+ {#each render_vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
1205
+ {@const flat_idx = get_flat_idx(x_idx, y_idx)}
1206
+ {@const bg = bg_flat[flat_idx]}
1207
+ {@const should_render = !is_hidden_cell(x_idx, y_idx)}
1208
+ {#if should_render}
1209
+ <svelte:element
1210
+ this={cell_tag_name}
1211
+ class={cell_class_name}
1212
+ class:selected={is_selected_cell(x_idx, y_idx)}
1213
+ class:gridlines={show_gridlines}
1214
+ class:animated={animate_updates}
1215
+ data-x={x_idx}
1216
+ data-y={y_idx}
1217
+ style:background-color={bg}
1218
+ style:color={text_flat?.[flat_idx]}
1219
+ style:--heatmap-selected-outline-color={selected_outline_flat[flat_idx]}
1220
+ style:grid-column={cell_grid_col(x_idx)}
1221
+ style:grid-row={cell_grid_row(y_idx)}
1222
+ >
1223
+ {#if cell}
1224
+ {@render cell(build_cell_context(x_idx, y_idx))}
1225
+ {:else if show_values}
1226
+ {@const raw = get_value(x_idx, y_idx)}
1227
+ {#if raw !== null}
1228
+ <span class="cell-value">{
1229
+ typeof raw === `number`
1230
+ ? format_num(raw, show_values === true ? `.3~g` : show_values)
1231
+ : raw
1232
+ }</span>
1233
+ {/if}
1234
+ {/if}
1235
+ </svelte:element>
1236
+ {:else}
1237
+ <div
1238
+ class="cell empty"
1239
+ style:grid-column={cell_grid_col(x_idx)}
1240
+ style:grid-row={cell_grid_row(y_idx)}
1241
+ >
1242
+ </div>
1243
+ {/if}
1244
+ {/each}
1245
+ {/each}
1246
+
1247
+ {#if show_row_summaries}
1248
+ {#each vis_y as y_idx (y_items[y_idx].key ?? y_items[y_idx].label)}
1249
+ <div
1250
+ class="summary summary-row"
1251
+ style:grid-column={visible_col_count + 2}
1252
+ style:grid-row={cell_grid_row(y_idx)}
1253
+ >
1254
+ {#if row_summaries.get(y_idx) !== null}
1255
+ {format_num(row_summaries.get(y_idx) ?? 0)}
1256
+ {/if}
1257
+ </div>
1258
+ {/each}
1259
+ {/if}
1260
+
1261
+ {#if show_col_summaries}
1262
+ {#each vis_x as x_idx (x_items[x_idx].key ?? x_items[x_idx].label)}
1263
+ <div
1264
+ class="summary summary-col"
1265
+ style:grid-column={cell_grid_col(x_idx)}
1266
+ style:grid-row={visible_row_count + 2}
1267
+ >
1268
+ {#if col_summaries.get(x_idx) !== null}
1269
+ {format_num(col_summaries.get(x_idx) ?? 0)}
1270
+ {/if}
1271
+ </div>
1272
+ {/each}
1273
+ {/if}
1274
+
1275
+ <!-- Tooltip: always in DOM, visibility toggled imperatively via classList -->
1276
+ {#if tooltip !== false}
1277
+ <div class="tooltip" bind:this={tooltip_div}>
1278
+ {#if typeof tooltip === `function` && tooltip_cell}
1279
+ {@render tooltip(tooltip_cell)}
1280
+ {/if}
1281
+ </div>
1282
+ {/if}
1283
+
1284
+ {@render children?.()}
1285
+ </div>
1286
+
1287
+ {#if show_legend}
1288
+ <ColorBar
1289
+ class="legend legend-{legend_position}"
1290
+ title={legend_label}
1291
+ orientation={legend_orientation}
1292
+ tick_labels={legend_ticks}
1293
+ tick_format={legend_format}
1294
+ range={[cs_min, cs_max]}
1295
+ scale_type={use_log_norm ? `log` : `linear`}
1296
+ {color_scale}
1297
+ wrapper_style={legend_wrapper_style}
1298
+ />
1299
+ {/if}
1300
+ {#if x_axis.label}<div class="x-title">{x_axis.label}</div>{/if}
1301
+ {#if y_axis.label}<div class="y-title">{y_axis.label}</div>{/if}
1302
+ </div>
1303
+
1304
+ <style>
1305
+ .heatmap {
1306
+ position: relative;
1307
+ width: min(100%, var(--heatmap-max-width, 1200px));
1308
+ max-width: var(--heatmap-max-width, 1200px);
1309
+ box-sizing: border-box;
1310
+ container-type: inline-size;
1311
+ &.legend-bottom {
1312
+ padding-bottom: 44px;
1313
+ }
1314
+ :global(.legend) {
1315
+ position: absolute;
1316
+ background: color-mix(in srgb, var(--bg, #fff) 80%, transparent);
1317
+ padding: 0.3rem 0.4rem;
1318
+ border-radius: var(--border-radius, 3pt);
1319
+ }
1320
+ &.legend-right :global(.legend-right) {
1321
+ right: 8px;
1322
+ top: 8px;
1323
+ }
1324
+ &.legend-bottom :global(.legend-bottom) {
1325
+ left: 50%;
1326
+ bottom: 80px;
1327
+ transform: translateX(-50%);
1328
+ }
1329
+ .x-title {
1330
+ text-align: center;
1331
+ font-size: 0.9em;
1332
+ margin-top: 4px;
1333
+ }
1334
+ .y-title {
1335
+ position: absolute;
1336
+ left: 0;
1337
+ top: 50%;
1338
+ writing-mode: vertical-lr;
1339
+ transform: translateY(-50%) rotate(180deg);
1340
+ font-size: 0.9em;
1341
+ white-space: nowrap;
1342
+ }
1343
+ }
1344
+ .grid {
1345
+ display: grid;
1346
+ grid-template-columns:
1347
+ max-content repeat(
1348
+ var(--n-cols),
1349
+ minmax(var(--tile-size, 6px), 1fr)
1350
+ ) var(--right-y-track, 0);
1351
+ grid-template-rows:
1352
+ max-content repeat(
1353
+ var(--n-rows),
1354
+ minmax(var(--tile-size, 6px), 1fr)
1355
+ ) var(--bottom-x-track, 0);
1356
+ position: relative;
1357
+ width: min(100%, var(--heatmap-max-width, 1200px));
1358
+ max-width: var(--heatmap-max-width, 1200px);
1359
+ aspect-ratio: calc(
1360
+ (
1361
+ var(--n-cols) + 1 + var(--extra-right-y, 0)
1362
+ )
1363
+ / (
1364
+ var(--n-rows) + 1 + var(--extra-bottom-x, 0)
1365
+ )
1366
+ );
1367
+ overflow: auto;
1368
+ &.theme-publication {
1369
+ --tooltip-bg: rgba(255, 255, 255, 0.98);
1370
+ --tooltip-color: #111;
1371
+ }
1372
+ &.theme-dark {
1373
+ --tooltip-bg: rgba(0, 0, 0, 0.9);
1374
+ --tooltip-color: #eee;
1375
+ }
1376
+ }
1377
+ .corner {
1378
+ min-width: 0; /* spacer in top-left when both axes have labels */
1379
+ }
1380
+ .cell {
1381
+ width: 100%;
1382
+ height: 100%;
1383
+ min-width: 0;
1384
+ min-height: 0;
1385
+ border-radius: var(
1386
+ --heatmap-cell-border-radius,
1387
+ calc(var(--tile-size, 6px) * var(--heatmap-cell-radius-ratio, 0.12))
1388
+ );
1389
+ overflow: hidden;
1390
+ display: flex;
1391
+ align-items: center;
1392
+ justify-content: center;
1393
+ cursor: default;
1394
+ &.interactive {
1395
+ border: none;
1396
+ padding: 0;
1397
+ font: inherit;
1398
+ line-height: inherit;
1399
+ cursor: pointer;
1400
+ }
1401
+ &.selected {
1402
+ box-shadow: inset 0 0 0
1403
+ var(
1404
+ --heatmap-selected-outline-width,
1405
+ clamp(1px, calc(var(--tile-size, 6px) * 0.16), 3px)
1406
+ )
1407
+ color-mix(
1408
+ in srgb,
1409
+ var(--heatmap-selected-outline-color, currentColor) 75%,
1410
+ transparent
1411
+ );
1412
+ }
1413
+ &.gridlines {
1414
+ border: var(--heatmap-gridline-width) solid var(--heatmap-gridline-color);
1415
+ }
1416
+ &.animated {
1417
+ transition: background-color var(--heatmap-anim-duration) ease;
1418
+ }
1419
+ &.empty {
1420
+ pointer-events: none;
1421
+ }
1422
+ .cell-value {
1423
+ font-size: clamp(8px, calc(var(--tile-size, 6px) * 0.45), 14px);
1424
+ user-select: none;
1425
+ white-space: nowrap;
1426
+ overflow: hidden;
1427
+ text-overflow: ellipsis;
1428
+ }
1429
+ }
1430
+ :is(.x-label, .y-label) {
1431
+ font-size: clamp(10px, calc(var(--tile-size, 6px) * 0.75), 24px);
1432
+ overflow: hidden;
1433
+ text-overflow: ellipsis;
1434
+ white-space: nowrap;
1435
+ min-width: 0;
1436
+ display: flex;
1437
+ align-items: center;
1438
+ justify-content: center;
1439
+ text-align: center;
1440
+ &.sticky {
1441
+ position: sticky;
1442
+ z-index: 2;
1443
+ background: var(--bg, transparent);
1444
+ }
1445
+ &.highlighted {
1446
+ font-weight: 700;
1447
+ text-decoration: underline;
1448
+ }
1449
+ }
1450
+ .x-label {
1451
+ overflow: visible;
1452
+ text-overflow: clip;
1453
+ align-items: flex-end;
1454
+ padding: 2px;
1455
+ &.sticky {
1456
+ top: 0;
1457
+ }
1458
+ &.x-edge-top {
1459
+ min-height: 1.6em;
1460
+ align-items: flex-end;
1461
+ }
1462
+ &.x-edge-bottom {
1463
+ min-height: 1.6em;
1464
+ align-items: flex-start;
1465
+ }
1466
+ }
1467
+ .y-label {
1468
+ padding: 0 2px;
1469
+ &.sticky {
1470
+ left: 0;
1471
+ }
1472
+ &:is(.y-edge-left, .y-edge-right) {
1473
+ min-width: 1.6em;
1474
+ }
1475
+ &.y-edge-left {
1476
+ justify-content: flex-end;
1477
+ text-align: right;
1478
+ }
1479
+ &.y-edge-right {
1480
+ justify-content: flex-start;
1481
+ text-align: left;
1482
+ }
1483
+ }
1484
+ .summary {
1485
+ font-size: clamp(9px, calc(var(--tile-size, 6px) * 0.6), 14px);
1486
+ align-self: center;
1487
+ justify-self: center;
1488
+ color: var(--text-color-muted, currentColor);
1489
+ opacity: 0.9;
1490
+ }
1491
+ .tooltip {
1492
+ display: none;
1493
+ position: fixed;
1494
+ transform: none;
1495
+ background: var(
1496
+ --tooltip-bg,
1497
+ light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.85))
1498
+ );
1499
+ color: var(--tooltip-color, light-dark(#222, #eee));
1500
+ padding: var(--tooltip-padding, 4px 6px);
1501
+ border-radius: var(--tooltip-border-radius, var(--border-radius, 3pt));
1502
+ font-size: var(--tooltip-font-size, 12px);
1503
+ text-align: var(--tooltip-text-align, center);
1504
+ line-height: var(--tooltip-line-height, 1.2);
1505
+ z-index: var(--tooltip-z-index, 10);
1506
+ pointer-events: none;
1507
+ box-shadow: var(
1508
+ --tooltip-shadow,
1509
+ light-dark(0 2px 8px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.4))
1510
+ );
1511
+ white-space: nowrap;
1512
+ &.visible {
1513
+ display: block;
1514
+ }
1515
+ &::before {
1516
+ content: '';
1517
+ position: absolute;
1518
+ top: -6px;
1519
+ left: 50%;
1520
+ transform: translateX(-50%);
1521
+ border-left: 6px solid transparent;
1522
+ border-right: 6px solid transparent;
1523
+ border-bottom: 6px solid
1524
+ var(--tooltip-bg, light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.85)));
1525
+ }
1526
+ }
1527
+ </style>