matterviz 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/MillerIndexInput.svelte +60 -0
  4. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  5. package/dist/app.css +38 -2
  6. package/dist/brillouin/BrillouinZone.svelte +20 -62
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  9. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  10. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  17. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  18. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  19. package/dist/chempot-diagram/color.d.ts +10 -0
  20. package/dist/chempot-diagram/color.js +33 -0
  21. package/dist/chempot-diagram/compute.d.ts +38 -0
  22. package/dist/chempot-diagram/compute.js +650 -0
  23. package/dist/chempot-diagram/index.d.ts +5 -0
  24. package/dist/chempot-diagram/index.js +5 -0
  25. package/dist/chempot-diagram/pointer.d.ts +16 -0
  26. package/dist/chempot-diagram/pointer.js +40 -0
  27. package/dist/chempot-diagram/temperature.d.ts +15 -0
  28. package/dist/chempot-diagram/temperature.js +37 -0
  29. package/dist/chempot-diagram/types.d.ts +83 -0
  30. package/dist/chempot-diagram/types.js +27 -0
  31. package/dist/colors/index.d.ts +3 -1
  32. package/dist/colors/index.js +4 -0
  33. package/dist/composition/BarChart.svelte +13 -22
  34. package/dist/composition/BubbleChart.svelte +5 -3
  35. package/dist/composition/FormulaFilter.svelte +770 -90
  36. package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
  37. package/dist/composition/PieChart.svelte +43 -18
  38. package/dist/composition/PieChart.svelte.d.ts +1 -1
  39. package/dist/constants.d.ts +1 -0
  40. package/dist/constants.js +2 -0
  41. package/dist/convex-hull/ConvexHull.svelte +14 -1
  42. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull2D.svelte +14 -45
  44. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHull3D.svelte +396 -134
  46. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  47. package/dist/convex-hull/ConvexHull4D.svelte +93 -42
  48. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHullControls.svelte +94 -31
  50. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
  51. package/dist/convex-hull/ConvexHullStats.svelte +697 -128
  52. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  53. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  54. package/dist/convex-hull/GasPressureControls.svelte +72 -38
  55. package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
  56. package/dist/convex-hull/TemperatureSlider.svelte +46 -19
  57. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
  58. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  59. package/dist/convex-hull/demo-temperature.js +36 -0
  60. package/dist/convex-hull/gas-thermodynamics.js +16 -5
  61. package/dist/convex-hull/helpers.d.ts +7 -1
  62. package/dist/convex-hull/helpers.js +45 -15
  63. package/dist/convex-hull/index.d.ts +15 -1
  64. package/dist/convex-hull/index.js +1 -0
  65. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  66. package/dist/convex-hull/thermodynamics.js +106 -17
  67. package/dist/convex-hull/types.d.ts +7 -0
  68. package/dist/convex-hull/types.js +11 -0
  69. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  70. package/dist/element/BohrAtom.svelte +1 -1
  71. package/dist/element/data.js +2 -14
  72. package/dist/element/data.json.gz +0 -0
  73. package/dist/element/index.d.ts +1 -1
  74. package/dist/element/index.js +1 -0
  75. package/dist/element/types.d.ts +1 -0
  76. package/dist/fermi-surface/FermiSurface.svelte +21 -65
  77. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  80. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  81. package/dist/fermi-surface/compute.js +1 -21
  82. package/dist/fermi-surface/marching-cubes.d.ts +2 -13
  83. package/dist/fermi-surface/marching-cubes.js +2 -519
  84. package/dist/fermi-surface/parse.js +17 -23
  85. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  86. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  87. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  88. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  89. package/dist/heatmap-matrix/index.d.ts +53 -0
  90. package/dist/heatmap-matrix/index.js +100 -0
  91. package/dist/heatmap-matrix/shared.d.ts +2 -0
  92. package/dist/heatmap-matrix/shared.js +4 -0
  93. package/dist/icons.d.ts +119 -0
  94. package/dist/icons.js +119 -0
  95. package/dist/index.d.ts +6 -1
  96. package/dist/index.js +6 -1
  97. package/dist/io/export.js +15 -3
  98. package/dist/io/file-drop.d.ts +7 -0
  99. package/dist/io/file-drop.js +43 -0
  100. package/dist/io/index.d.ts +2 -2
  101. package/dist/io/index.js +2 -112
  102. package/dist/io/types.d.ts +1 -0
  103. package/dist/io/url-drop.d.ts +2 -0
  104. package/dist/io/url-drop.js +118 -0
  105. package/dist/isosurface/Isosurface.svelte +231 -0
  106. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  107. package/dist/isosurface/IsosurfaceControls.svelte +273 -0
  108. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  109. package/dist/isosurface/index.d.ts +5 -0
  110. package/dist/isosurface/index.js +6 -0
  111. package/dist/isosurface/parse.d.ts +6 -0
  112. package/dist/isosurface/parse.js +548 -0
  113. package/dist/isosurface/slice.d.ts +11 -0
  114. package/dist/isosurface/slice.js +145 -0
  115. package/dist/isosurface/types.d.ts +55 -0
  116. package/dist/isosurface/types.js +178 -0
  117. package/dist/labels.d.ts +2 -1
  118. package/dist/labels.js +1 -0
  119. package/dist/layout/InfoTag.svelte +62 -62
  120. package/dist/layout/SubpageGrid.svelte +74 -0
  121. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +226 -53
  125. package/dist/layout/json-tree/JsonTree.svelte +425 -51
  126. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  127. package/dist/layout/json-tree/JsonValue.svelte +218 -97
  128. package/dist/layout/json-tree/types.d.ts +27 -2
  129. package/dist/layout/json-tree/utils.d.ts +14 -1
  130. package/dist/layout/json-tree/utils.js +254 -0
  131. package/dist/marching-cubes.d.ts +14 -0
  132. package/dist/marching-cubes.js +519 -0
  133. package/dist/math.d.ts +8 -0
  134. package/dist/math.js +374 -7
  135. package/dist/overlays/ContextMenu.svelte +3 -2
  136. package/dist/overlays/DraggablePane.svelte +163 -58
  137. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  138. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  139. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  140. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  141. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  142. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  143. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  144. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  145. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  146. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  147. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  148. package/dist/phase-diagram/index.d.ts +2 -0
  149. package/dist/phase-diagram/index.js +2 -0
  150. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  151. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  152. package/dist/phase-diagram/types.d.ts +10 -0
  153. package/dist/phase-diagram/utils.d.ts +7 -4
  154. package/dist/phase-diagram/utils.js +149 -59
  155. package/dist/plot/AxisLabel.svelte +26 -0
  156. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  157. package/dist/plot/BarPlot.svelte +473 -228
  158. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  159. package/dist/plot/BarPlotControls.svelte +3 -2
  160. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  161. package/dist/plot/ColorBar.svelte +54 -54
  162. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  163. package/dist/plot/ElementScatter.svelte +4 -3
  164. package/dist/plot/FillArea.svelte +4 -1
  165. package/dist/plot/Histogram.svelte +320 -230
  166. package/dist/plot/Histogram.svelte.d.ts +2 -2
  167. package/dist/plot/HistogramControls.svelte +29 -10
  168. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  169. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  170. package/dist/plot/PlotControls.svelte +109 -27
  171. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  172. package/dist/plot/PlotLegend.svelte +1 -1
  173. package/dist/plot/PortalSelect.svelte +2 -1
  174. package/dist/plot/ReferenceLine.svelte +2 -1
  175. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  176. package/dist/plot/ReferencePlane.svelte +1 -3
  177. package/dist/plot/ScatterPlot.svelte +343 -209
  178. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  179. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  180. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  181. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  182. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  183. package/dist/plot/ScatterPlotControls.svelte +95 -55
  184. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  185. package/dist/plot/ZeroLines.svelte +44 -0
  186. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  187. package/dist/plot/ZoomRect.svelte +21 -0
  188. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  189. package/dist/plot/axis-utils.d.ts +1 -1
  190. package/dist/plot/data-cleaning.js +1 -5
  191. package/dist/plot/index.d.ts +6 -2
  192. package/dist/plot/index.js +6 -2
  193. package/dist/plot/interactions.d.ts +8 -10
  194. package/dist/plot/interactions.js +10 -19
  195. package/dist/plot/layout.d.ts +7 -1
  196. package/dist/plot/layout.js +12 -4
  197. package/dist/plot/reference-line.d.ts +4 -21
  198. package/dist/plot/reference-line.js +7 -81
  199. package/dist/plot/types.d.ts +42 -17
  200. package/dist/plot/types.js +10 -0
  201. package/dist/plot/utils/label-placement.js +14 -11
  202. package/dist/plot/utils.d.ts +1 -0
  203. package/dist/plot/utils.js +14 -0
  204. package/dist/rdf/RdfPlot.svelte +55 -66
  205. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  206. package/dist/rdf/index.d.ts +1 -1
  207. package/dist/rdf/index.js +1 -1
  208. package/dist/settings.d.ts +5 -0
  209. package/dist/settings.js +37 -3
  210. package/dist/spectral/Bands.svelte +515 -143
  211. package/dist/spectral/Bands.svelte.d.ts +22 -2
  212. package/dist/spectral/helpers.d.ts +23 -1
  213. package/dist/spectral/helpers.js +65 -9
  214. package/dist/spectral/types.d.ts +2 -0
  215. package/dist/structure/AtomLegend.svelte +31 -10
  216. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  217. package/dist/structure/CellSelect.svelte +92 -22
  218. package/dist/structure/Lattice.svelte +2 -0
  219. package/dist/structure/Structure.svelte +716 -173
  220. package/dist/structure/Structure.svelte.d.ts +7 -2
  221. package/dist/structure/StructureControls.svelte +26 -14
  222. package/dist/structure/StructureControls.svelte.d.ts +5 -1
  223. package/dist/structure/StructureInfoPane.svelte +7 -1
  224. package/dist/structure/StructureScene.svelte +386 -95
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -4
  226. package/dist/structure/atom-properties.d.ts +6 -2
  227. package/dist/structure/atom-properties.js +38 -25
  228. package/dist/structure/export.js +10 -7
  229. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  230. package/dist/structure/ferrox-wasm-types.js +0 -3
  231. package/dist/structure/ferrox-wasm.d.ts +3 -2
  232. package/dist/structure/ferrox-wasm.js +1 -2
  233. package/dist/structure/index.d.ts +7 -0
  234. package/dist/structure/index.js +22 -0
  235. package/dist/structure/parse.js +19 -16
  236. package/dist/structure/partial-occupancy.d.ts +25 -0
  237. package/dist/structure/partial-occupancy.js +102 -0
  238. package/dist/structure/validation.js +6 -3
  239. package/dist/symmetry/SymmetryStats.svelte +18 -4
  240. package/dist/symmetry/WyckoffTable.svelte +18 -10
  241. package/dist/symmetry/index.d.ts +7 -4
  242. package/dist/symmetry/index.js +83 -18
  243. package/dist/table/HeatmapTable.svelte +468 -69
  244. package/dist/table/HeatmapTable.svelte.d.ts +13 -1
  245. package/dist/table/ToggleMenu.svelte +291 -44
  246. package/dist/table/ToggleMenu.svelte.d.ts +4 -1
  247. package/dist/table/index.d.ts +3 -0
  248. package/dist/tooltip/index.d.ts +1 -1
  249. package/dist/tooltip/index.js +1 -0
  250. package/dist/trajectory/Trajectory.svelte +147 -145
  251. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  252. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  253. package/dist/trajectory/constants.d.ts +6 -0
  254. package/dist/trajectory/constants.js +7 -0
  255. package/dist/trajectory/extract.js +3 -5
  256. package/dist/trajectory/format-detect.d.ts +9 -0
  257. package/dist/trajectory/format-detect.js +76 -0
  258. package/dist/trajectory/frame-reader.d.ts +17 -0
  259. package/dist/trajectory/frame-reader.js +339 -0
  260. package/dist/trajectory/helpers.d.ts +15 -0
  261. package/dist/trajectory/helpers.js +187 -0
  262. package/dist/trajectory/index.d.ts +1 -0
  263. package/dist/trajectory/index.js +11 -4
  264. package/dist/trajectory/parse/ase.d.ts +2 -0
  265. package/dist/trajectory/parse/ase.js +76 -0
  266. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  267. package/dist/trajectory/parse/hdf5.js +121 -0
  268. package/dist/trajectory/parse/index.d.ts +12 -0
  269. package/dist/trajectory/parse/index.js +304 -0
  270. package/dist/trajectory/parse/lammps.d.ts +5 -0
  271. package/dist/trajectory/parse/lammps.js +169 -0
  272. package/dist/trajectory/parse/vasp.d.ts +2 -0
  273. package/dist/trajectory/parse/vasp.js +65 -0
  274. package/dist/trajectory/parse/xyz.d.ts +2 -0
  275. package/dist/trajectory/parse/xyz.js +109 -0
  276. package/dist/trajectory/types.d.ts +11 -0
  277. package/dist/trajectory/types.js +1 -0
  278. package/dist/utils.d.ts +2 -0
  279. package/dist/utils.js +4 -0
  280. package/dist/xrd/XrdPlot.svelte +6 -4
  281. package/dist/xrd/calc-xrd.js +0 -1
  282. package/package.json +33 -23
  283. package/readme.md +4 -4
  284. package/dist/trajectory/parse.d.ts +0 -42
  285. package/dist/trajectory/parse.js +0 -1267
  286. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,9 +1,16 @@
1
- <script lang="ts">import Icon from '../Icon.svelte';
1
+ <script lang="ts">import { get_alphabetical_formula, get_electro_neg_formula, get_reduced_formula, } from '../composition';
2
+ import Icon from '../Icon.svelte';
2
3
  import { format_num } from '../labels';
3
4
  import Histogram from '../plot/Histogram.svelte';
4
- import { SvelteSet } from 'svelte/reactivity';
5
- let { phase_stats, stable_entries, unstable_entries, ...rest } = $props();
5
+ import HeatmapTable from '../table/HeatmapTable.svelte';
6
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
7
+ import { get_arity, is_on_hull } from './types';
8
+ let { phase_stats, stable_entries, unstable_entries, layout = `toggle`, on_entry_click, highlighted_entry_id, min_n_elements = $bindable(1), entry_href, ...rest } = $props();
6
9
  let copied_items = new SvelteSet();
10
+ let view_mode = $state(`stats`);
11
+ // Formula filter: when set, table shows only entries with this reduced formula
12
+ let formula_filter = $state(``);
13
+ let show_export_dropdown = $state(false);
7
14
  async function copy_to_clipboard(label, value, key) {
8
15
  try {
9
16
  await navigator.clipboard.writeText(`${label}: ${value}`);
@@ -14,119 +21,365 @@ async function copy_to_clipboard(label, value, key) {
14
21
  console.error(`Failed to copy to clipboard:`, error);
15
22
  }
16
23
  }
24
+ function handle_copy_keydown(event, label, value, key) {
25
+ if (event.key !== `Enter` && event.key !== ` `)
26
+ return;
27
+ event.preventDefault();
28
+ copy_to_clipboard(label, value, key);
29
+ }
30
+ // Shared concatenation of stable + unstable for histograms
31
+ let all_entries = $derived([...stable_entries, ...unstable_entries]);
32
+ // Static arity labels for phase breakdown display
33
+ const arity_types = [
34
+ [`Unary`, `unary`, 1],
35
+ [`Binary`, `binary`, 2],
36
+ [`Ternary`, `ternary`, 3],
37
+ [`Quaternary`, `quaternary`, 4],
38
+ [`Quinary+`, `quinary_plus`, 5],
39
+ ];
40
+ const histogram_props = {
41
+ bins: 50,
42
+ y_axis: { label: ``, ticks: 3 },
43
+ show_legend: false,
44
+ show_controls: false,
45
+ padding: { t: 5, b: 22, l: 35, r: 5 },
46
+ style: `height: 100px; --histogram-min-height: 100px`,
47
+ };
17
48
  // Prepare histogram data for formation energies and hull distances
18
- let e_form_data = $derived.by(() => {
19
- const all_entries = [...stable_entries, ...unstable_entries];
20
- const energies = all_entries
21
- .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom)
22
- .filter((val) => val !== undefined && isFinite(val));
23
- return [{
24
- x: [],
25
- y: energies,
26
- label: `Formation Energy`,
27
- line_style: { stroke: `steelblue` },
28
- }];
29
- });
30
- let hull_distance_data = $derived.by(() => {
31
- const all_entries = [...stable_entries, ...unstable_entries];
32
- const distances = all_entries
33
- .map((entry) => entry.e_above_hull)
34
- .filter((val) => val !== undefined && isFinite(val));
35
- return [{
36
- x: [],
37
- y: distances,
38
- label: `E above hull`,
39
- line_style: { stroke: `coral` },
40
- }];
41
- });
49
+ let e_form_data = $derived([{
50
+ x: [],
51
+ y: all_entries
52
+ .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom)
53
+ .filter((val) => val !== undefined && isFinite(val)),
54
+ label: `Formation Energy`,
55
+ }]);
56
+ let hull_distance_data = $derived([{
57
+ x: [],
58
+ y: all_entries
59
+ .map((entry) => entry.e_above_hull)
60
+ .filter((val) => val !== undefined && isFinite(val)),
61
+ label: `E above hull`,
62
+ }]);
42
63
  let pane_data = $derived.by(() => {
43
64
  if (!phase_stats)
44
65
  return [];
45
- const sections = [];
46
- // Determine system dimensionality from chemical_system string (count elements)
47
- const num_elements = phase_stats.chemical_system.split(`-`).length;
48
- const max_arity = Math.max(num_elements, phase_stats.quaternary > 0
49
- ? 4
50
- : phase_stats.ternary > 0
51
- ? 3
52
- : phase_stats.binary > 0
53
- ? 2
54
- : 1);
55
- const phase_items = [
66
+ const pct = (count) => phase_stats.total > 0 ? format_num(count / phase_stats.total, `.1~%`) : `0%`;
67
+ return [
56
68
  {
57
- label: `Total entries in ${phase_stats.chemical_system}`,
58
- value: format_num(phase_stats.total),
59
- key: `total-entries`,
69
+ title: ``,
70
+ items: [
71
+ {
72
+ label: `Total entries in ${phase_stats.chemical_system}`,
73
+ value: format_num(phase_stats.total),
74
+ key: `total-entries`,
75
+ },
76
+ // Only show phase types that exist or are within the max_arity
77
+ // used when computing stats (respects zeroed-out counts)
78
+ ...arity_types
79
+ .filter(([, field, arity]) => phase_stats[field] > 0 || phase_stats.max_arity >= arity)
80
+ .map(([display, field]) => ({
81
+ label: `${display} phases`,
82
+ value: `${format_num(phase_stats[field])} (${pct(phase_stats[field])})`,
83
+ key: `${field}-phases`,
84
+ })),
85
+ ],
86
+ },
87
+ {
88
+ title: `Stability`,
89
+ items: [
90
+ {
91
+ label: `Stable phases`,
92
+ value: `${format_num(phase_stats.stable)} (${pct(phase_stats.stable)})`,
93
+ key: `stable-phases`,
94
+ },
95
+ {
96
+ label: `Unstable phases`,
97
+ value: `${format_num(phase_stats.unstable)} (${pct(phase_stats.unstable)})`,
98
+ key: `unstable-phases`,
99
+ },
100
+ ],
101
+ },
102
+ {
103
+ title: `E<sub>form</sub> distribution`,
104
+ items: [{
105
+ label: `Min / avg / max (eV/atom)`,
106
+ value: [
107
+ phase_stats.energy_range.min,
108
+ phase_stats.energy_range.avg,
109
+ phase_stats.energy_range.max,
110
+ ]
111
+ .map((val) => format_num(val, `.3f`)).join(` / `),
112
+ key: `formation-energy`,
113
+ }],
114
+ },
115
+ {
116
+ title: `E<sub>above hull</sub> distribution`,
117
+ items: [{
118
+ label: `Max / avg (eV/atom)`,
119
+ value: [phase_stats.hull_distance.max, phase_stats.hull_distance.avg]
120
+ .map((val) => format_num(val, `.3f`)).join(` / `),
121
+ key: `hull-distance`,
122
+ }],
60
123
  },
61
124
  ];
62
- // Only show phase types that exist or are within expected dimensionality
63
- if (phase_stats.unary > 0 || max_arity >= 1) {
64
- phase_items.push({
65
- label: `Unary phases`,
66
- value: `${format_num(phase_stats.unary)} (${format_num(phase_stats.unary / phase_stats.total, `.1~%`)})`,
67
- key: `unary-phases`,
68
- });
69
- }
70
- if (phase_stats.binary > 0 || max_arity >= 2) {
71
- phase_items.push({
72
- label: `Binary phases`,
73
- value: `${format_num(phase_stats.binary)} (${format_num(phase_stats.binary / phase_stats.total, `.1~%`)})`,
74
- key: `binary-phases`,
75
- });
76
- }
77
- if (phase_stats.ternary > 0 || max_arity >= 3) {
78
- phase_items.push({
79
- label: `Ternary phases`,
80
- value: `${format_num(phase_stats.ternary)} (${format_num(phase_stats.ternary / phase_stats.total, `.1~%`)})`,
81
- key: `ternary-phases`,
82
- });
83
- }
84
- if (phase_stats.quaternary > 0 || max_arity >= 4) {
85
- phase_items.push({
86
- label: `Quaternary phases`,
87
- value: `${format_num(phase_stats.quaternary)} (${format_num(phase_stats.quaternary / phase_stats.total, `.1~%`)})`,
88
- key: `quaternary-phases`,
89
- });
90
- }
91
- sections.push({ title: ``, items: phase_items });
92
- // Stability
93
- const stable_item = {
94
- label: `Stable phases`,
95
- value: `${format_num(phase_stats.stable)} (${format_num(phase_stats.stable / phase_stats.total, `.1~%`)})`,
96
- key: `stable-phases`,
97
- };
98
- const unstable_item = {
99
- label: `Unstable phases`,
100
- value: `${format_num(phase_stats.unstable)} (${format_num(phase_stats.unstable / phase_stats.total, `.1~%`)})`,
101
- key: `unstable-phases`,
102
- };
103
- sections.push({ title: `Stability`, items: [stable_item, unstable_item] });
104
- // Energy Statistics
105
- const energy_item = {
106
- label: `Min / avg / max (eV/atom)`,
107
- value: `${format_num(phase_stats.energy_range.min, `.3f`)} / ${format_num(phase_stats.energy_range.avg, `.3f`)} / ${format_num(phase_stats.energy_range.max, `.3f`)}`,
108
- key: `formation-energy`,
109
- };
110
- sections.push({
111
- title: `E<sub>form</sub> distribution`,
112
- items: [energy_item],
113
- });
114
- // Hull Distance
115
- const hull_distance_item = {
116
- label: `Max / avg (eV/atom)`,
117
- value: `${format_num(phase_stats.hull_distance.max, `.3f`)} / ${format_num(phase_stats.hull_distance.avg, `.3f`)}`,
118
- key: `hull-distance`,
119
- };
120
- sections.push({
121
- title: `E<sub>above hull</sub> distribution`,
122
- items: [hull_distance_item],
125
+ });
126
+ // Subsystem coverage: count entries per element pair for the stats pane
127
+ let subsystem_coverage = $derived.by(() => {
128
+ if (!phase_stats)
129
+ return null;
130
+ const elements = phase_stats.chemical_system.split(`-`);
131
+ if (elements.length < 3 || elements.length > 10)
132
+ return null;
133
+ // Count entries containing each pair
134
+ const pair_counts = new SvelteMap();
135
+ for (const entry of all_entries) {
136
+ const active = Object.keys(entry.composition)
137
+ .filter((el) => (entry.composition[el] ?? 0) > 0);
138
+ // Count all pairs present in this entry
139
+ for (let idx_a = 0; idx_a < active.length; idx_a++) {
140
+ for (let idx_b = idx_a + 1; idx_b < active.length; idx_b++) {
141
+ const key = [active[idx_a], active[idx_b]].sort().join(`-`);
142
+ pair_counts.set(key, (pair_counts.get(key) ?? 0) + 1);
143
+ }
144
+ }
145
+ }
146
+ // Build pairs list sorted by element order in chemical_system
147
+ return elements.flatMap((el_a, idx_a) => elements.slice(idx_a + 1).map((el_b) => {
148
+ const key = [el_a, el_b].sort().join(`-`);
149
+ return { pair: key, count: pair_counts.get(key) ?? 0 };
150
+ }));
151
+ });
152
+ let subsystem_coverage_summary = $derived(subsystem_coverage?.map(({ pair, count }) => `${pair}: ${count}`).join(` | `) ??
153
+ null);
154
+ // Table view: visible entries filtered by min element count and formula
155
+ let visible_entries = $derived(all_entries.filter((entry) => {
156
+ if (!entry.visible)
157
+ return false;
158
+ if (min_n_elements > 1 && get_arity(entry) < min_n_elements)
159
+ return false;
160
+ if (active_formula_filter &&
161
+ composition_key(entry.composition) !== active_formula_filter)
162
+ return false;
163
+ return true;
164
+ }));
165
+ let has_raw = $derived(visible_entries.some((entry) => entry.energy_per_atom !== undefined));
166
+ let has_ids = $derived(visible_entries.some((entry) => entry.entry_id));
167
+ let max_n_el = $derived(all_entries.reduce((max, entry) => Math.max(max, get_arity(entry)), 1));
168
+ // Sortable HTML cell with a hidden data-sort-value for HeatmapTable sorting
169
+ const sort_span = (sort_val, display, attrs = ``) => `<span data-sort-value="${sort_val}"${attrs ? ` ${attrs}` : ``}>${display}</span>`;
170
+ // Escape HTML special chars to prevent XSS when rendering user-supplied strings via {@html}
171
+ const escape_html = (str) => str
172
+ .replace(/&/g, `&amp;`)
173
+ .replace(/</g, `&lt;`)
174
+ .replace(/>/g, `&gt;`)
175
+ .replace(/"/g, `&quot;`)
176
+ .replace(/'/g, `&#39;`);
177
+ const unescape_html = (str, max_rounds = 5) => {
178
+ let decoded = str;
179
+ for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
180
+ const next_decoded = decoded
181
+ .replace(/&amp;/g, `&`)
182
+ .replace(/&lt;/g, `<`)
183
+ .replace(/&gt;/g, `>`)
184
+ .replace(/&quot;/g, `"`)
185
+ .replace(/&#39;/g, `'`);
186
+ if (next_decoded === decoded)
187
+ break;
188
+ decoded = next_decoded;
189
+ }
190
+ return decoded;
191
+ };
192
+ // Convert legacy/html formula strings like Fe<sub>2</sub>O<sub>3</sub> back to plain
193
+ // stoichiometric input before parsing/reordering.
194
+ const normalize_formula_markup = (formula) => unescape_html(formula)
195
+ .replaceAll(/<sub>\s*([^<]+?)\s*<\/sub>/gi, `$1`)
196
+ .replaceAll(/<[^>]+>/g, ``)
197
+ .replaceAll(/\s+/g, ``);
198
+ const sanitize_href = (href) => {
199
+ const trimmed_href = href?.trim();
200
+ if (!trimmed_href)
201
+ return null;
202
+ const lower_href = trimmed_href.toLowerCase();
203
+ const blocked_schemes = [`javascript:`, `data:`, `vbscript:`];
204
+ if (blocked_schemes.some((scheme) => lower_href.startsWith(scheme)))
205
+ return null;
206
+ return trimmed_href;
207
+ };
208
+ // Serialize reduced composition to a stable string key for polymorph counting
209
+ const composition_key = (comp) => get_alphabetical_formula(get_reduced_formula(comp), true, ``);
210
+ // Count polymorphs per reduced formula across all entries
211
+ let polymorph_counts = $derived.by(() => {
212
+ const counts = new SvelteMap();
213
+ for (const entry of all_entries) {
214
+ const key = composition_key(entry.composition);
215
+ counts.set(key, (counts.get(key) ?? 0) + 1);
216
+ }
217
+ return counts;
218
+ });
219
+ let poly_formulas = $derived([...polymorph_counts.entries()]
220
+ .filter(([, count]) => count > 1)
221
+ .sort(([, count_a], [, count_b]) => count_b - count_a));
222
+ let has_polymorphs = $derived(poly_formulas.length > 0);
223
+ let active_formula_filter = $derived.by(() => {
224
+ if (!formula_filter || !has_polymorphs)
225
+ return ``;
226
+ return poly_formulas.some(([formula]) => formula === formula_filter)
227
+ ? formula_filter
228
+ : ``;
229
+ });
230
+ $effect(() => {
231
+ if (formula_filter && formula_filter !== active_formula_filter) {
232
+ formula_filter = ``;
233
+ }
234
+ });
235
+ // Build table rows and a WeakMap from row→entry for the click handler
236
+ let { table_data, entry_by_row } = $derived.by(() => {
237
+ const map = new WeakMap();
238
+ const rows = visible_entries.map((entry, idx) => {
239
+ const n_atoms = Object.values(entry.composition).reduce((sum, count) => sum + count, 0);
240
+ const on_hull = is_on_hull(entry);
241
+ const formula_source = entry.reduced_formula ?? entry.name ??
242
+ get_alphabetical_formula(entry.composition, true, ``);
243
+ const normalized_formula = normalize_formula_markup(formula_source);
244
+ const formatted_formula = get_electro_neg_formula(normalized_formula);
245
+ const formula_html = formatted_formula || escape_html(normalized_formula);
246
+ // Match by entry_id or common data fields (mat_id, structure_id)
247
+ // since entry_id may be wrapped in HTML (e.g. <a> tags)
248
+ const entry_data = entry.data;
249
+ const is_highlighted = !!(highlighted_entry_id && (entry.entry_id === highlighted_entry_id ||
250
+ entry_data?.mat_id === highlighted_entry_id ||
251
+ entry_data?.structure_id === highlighted_entry_id));
252
+ const row = {
253
+ '#': sort_span(idx + 1, `${idx + 1}`),
254
+ Formula: on_hull ? `<strong>${formula_html}</strong>` : formula_html,
255
+ 'E<sub>hull</sub>': entry.e_above_hull ?? null,
256
+ 'E<sub>form</sub>': entry.e_form_per_atom ?? entry.energy_per_atom ?? null,
257
+ };
258
+ if (has_raw)
259
+ row[`E<sub>raw</sub>`] = entry.energy_per_atom;
260
+ if (has_ids) {
261
+ const safe_href = sanitize_href(entry_href?.(entry));
262
+ const safe_id = entry.entry_id ? escape_html(entry.entry_id) : undefined;
263
+ row.ID = safe_href && safe_id
264
+ ? `<a href="${escape_html(safe_href)}" target="_blank" rel="noopener">${safe_id}</a>`
265
+ : safe_id;
266
+ }
267
+ if (has_polymorphs) {
268
+ const comp_key = composition_key(entry.composition);
269
+ const poly_count = polymorph_counts.get(comp_key) ?? 1;
270
+ row.Poly = poly_count;
271
+ }
272
+ row[`N<sub>el</sub>`] = get_arity(entry);
273
+ row[`N<sub>at</sub>`] = n_atoms;
274
+ // Highlight row for current material
275
+ if (is_highlighted) {
276
+ row.style =
277
+ `background: color-mix(in srgb, var(--hull-stable-color, #22c55e) 15%, transparent)`;
278
+ }
279
+ map.set(row, entry);
280
+ return row;
123
281
  });
124
- return sections;
282
+ return { table_data: rows, entry_by_row: map };
125
283
  });
284
+ function handle_row_click(_event, row) {
285
+ const entry = entry_by_row.get(row);
286
+ if (entry)
287
+ on_entry_click?.(entry);
288
+ }
289
+ let table_columns = $derived([
290
+ { label: `#`, color_scale: null, description: `Row number` },
291
+ { label: `Formula`, color_scale: null },
292
+ {
293
+ label: `E<sub>hull</sub>`,
294
+ better: `lower`,
295
+ color_scale: `interpolateRdYlGn`,
296
+ format: `.4f`,
297
+ description: `Energy above convex hull (eV/atom)`,
298
+ },
299
+ {
300
+ label: `E<sub>form</sub>`,
301
+ better: `lower`,
302
+ color_scale: `interpolateBlues`,
303
+ format: `.4f`,
304
+ description: `Formation energy (eV/atom)`,
305
+ },
306
+ ...(has_raw
307
+ ? [{
308
+ label: `E<sub>raw</sub>`,
309
+ color_scale: `interpolateCool`,
310
+ format: `.4f`,
311
+ description: `Raw energy per atom (eV/atom)`,
312
+ }]
313
+ : []),
314
+ ...(has_ids
315
+ ? [{ label: `ID`, color_scale: null, description: `Entry identifier` }]
316
+ : []),
317
+ ...(has_polymorphs
318
+ ? [{
319
+ label: `Poly`,
320
+ color_scale: null,
321
+ description: `Number of polymorphs (same reduced formula)`,
322
+ }]
323
+ : []),
324
+ {
325
+ label: `N<sub>el</sub>`,
326
+ color_scale: null,
327
+ description: `Number of elements`,
328
+ },
329
+ {
330
+ label: `N<sub>at</sub>`,
331
+ color_scale: null,
332
+ format: `d`,
333
+ description: `Number of atoms in unit cell`,
334
+ },
335
+ ]);
336
+ const html_to_text = (val) => {
337
+ if (val == null)
338
+ return ``;
339
+ if (typeof val !== `string`)
340
+ return String(val);
341
+ const temp_el = document.createElement(`div`);
342
+ temp_el.innerHTML = val;
343
+ return temp_el.textContent?.trim() ?? ``;
344
+ };
345
+ const csv_escape = (val) => /[",\n]/.test(val) ? `"${val.replaceAll(`"`, `""`)}"` : val;
346
+ const get_export_filename = (format) => {
347
+ const system = (phase_stats?.chemical_system ?? `convex-hull-stats`)
348
+ .toLowerCase()
349
+ .replaceAll(/\s+/g, `-`);
350
+ return `${system}.${format}`;
351
+ };
352
+ const build_export_rows = () => {
353
+ const column_labels = table_columns.map((col) => col.label);
354
+ return table_data.map((row) => Object.fromEntries(column_labels.map((label) => [html_to_text(label), html_to_text(row[label])])));
355
+ };
356
+ const download_file = (content, filename, mime_type) => {
357
+ const blob = new Blob([content], { type: mime_type });
358
+ const object_url = URL.createObjectURL(blob);
359
+ const link_el = document.createElement(`a`);
360
+ link_el.href = object_url;
361
+ link_el.download = filename;
362
+ document.body.append(link_el);
363
+ link_el.click();
364
+ link_el.remove();
365
+ URL.revokeObjectURL(object_url);
366
+ };
367
+ function export_table(format) {
368
+ const rows = build_export_rows();
369
+ if (format === `json`) {
370
+ download_file(JSON.stringify(rows, null, 2), get_export_filename(`json`), `application/json;charset=utf-8`);
371
+ return;
372
+ }
373
+ const headers = rows.length > 0 ? Object.keys(rows[0]) : [];
374
+ const csv_lines = [
375
+ headers.map(csv_escape).join(`,`),
376
+ ...rows.map((row) => headers.map((header) => csv_escape(row[header] ?? ``)).join(`,`)),
377
+ ];
378
+ download_file(csv_lines.join(`\n`), get_export_filename(`csv`), `text/csv;charset=utf-8`);
379
+ }
126
380
  </script>
127
381
 
128
- <div {...rest} class="convex-hull-stats {rest.class ?? ``}">
129
- <h4 id="convex-hull-stats" style="margin-top: 0">Convex Hull Stats</h4>
382
+ {#snippet stats_panel()}
130
383
  {#each pane_data as section, sec_idx (sec_idx)}
131
384
  {#if sec_idx > 0}<hr />{/if}
132
385
  <section>
@@ -142,12 +395,13 @@ let pane_data = $derived.by(() => {
142
395
  onclick={() => copy_to_clipboard(item.label, String(item.value), key ?? item.label)}
143
396
  role="button"
144
397
  tabindex="0"
145
- onkeydown={(event) => {
146
- if ([`Enter`, ` `].includes(event.key)) {
147
- event.preventDefault()
148
- copy_to_clipboard(item.label, String(item.value), key ?? item.label)
149
- }
150
- }}
398
+ onkeydown={(event) =>
399
+ handle_copy_keydown(
400
+ event,
401
+ item.label,
402
+ String(item.value),
403
+ key ?? item.label,
404
+ )}
151
405
  >
152
406
  <span>{@html label}:</span>
153
407
  <span>{@html value}</span>
@@ -161,44 +415,201 @@ let pane_data = $derived.by(() => {
161
415
  </div>
162
416
  {/each}
163
417
 
418
+ {#if sec_idx === 0 && subsystem_coverage}
419
+ <div
420
+ class="clickable stat-item subsystem-coverage-row"
421
+ data-testid="pd-binary-subsystem-coverage"
422
+ title="Click to copy: Binary subsystem coverage: {subsystem_coverage_summary ?? ``}"
423
+ onclick={() =>
424
+ copy_to_clipboard(
425
+ `Binary subsystem coverage`,
426
+ subsystem_coverage_summary ?? ``,
427
+ `binary-subsystem-coverage`,
428
+ )}
429
+ role="button"
430
+ tabindex="0"
431
+ onkeydown={(event) =>
432
+ handle_copy_keydown(
433
+ event,
434
+ `Binary subsystem coverage`,
435
+ subsystem_coverage_summary ?? ``,
436
+ `binary-subsystem-coverage`,
437
+ )}
438
+ >
439
+ <span class="subsystem-label"
440
+ >Binary subsystem coverage ({subsystem_coverage.length} pairs)</span>
441
+ <span class="subsystem-chips">
442
+ {#each subsystem_coverage as { pair, count } (pair)}
443
+ <span class="subsystem-chip" class:has-entries={count > 0}>
444
+ <span class="pair">{pair}</span>
445
+ <span class="count">{count}</span>
446
+ </span>
447
+ {/each}
448
+ </span>
449
+ {#if copied_items.has(`binary-subsystem-coverage`)}
450
+ <Icon
451
+ icon="Check"
452
+ style="color: var(--success-color, #10b981); width: 12px; height: 12px"
453
+ class="copy-checkmark"
454
+ />
455
+ {/if}
456
+ </div>
457
+ {/if}
458
+
164
459
  {#if section.title === `E<sub>form</sub> distribution` &&
165
- e_form_data[0].y.length > 0}
460
+ e_form_data[0].y.length > 0}
166
461
  <Histogram
462
+ {...histogram_props}
167
463
  series={e_form_data}
168
- bins={50}
169
464
  x_axis={{ label: ``, format: `.2f` }}
170
- y_axis={{ label: ``, ticks: 3 }}
171
- show_legend={false}
172
- show_controls={false}
173
- padding={{ t: 5, b: 25, l: 35, r: 5 }}
174
- style="height: 100px; --histogram-min-height: 100px"
175
465
  bar={{ color: `steelblue`, opacity: 0.7 }}
176
466
  />
177
467
  {/if}
178
468
 
179
469
  {#if section.title === `E<sub>above hull</sub> distribution` &&
180
- hull_distance_data[0].y.length > 0}
470
+ hull_distance_data[0].y.length > 0}
181
471
  <Histogram
472
+ {...histogram_props}
182
473
  series={hull_distance_data}
183
- bins={50}
184
474
  x_axis={{ label: ``, format: `.2f`, range: [0, null] }}
185
- y_axis={{ label: ``, ticks: 3 }}
186
- show_legend={false}
187
- show_controls={false}
188
- padding={{ t: 5, b: 25, l: 35, r: 5 }}
189
- style="height: 100px; --histogram-min-height: 100px"
190
475
  bar={{ color: `coral`, opacity: 0.7 }}
191
476
  />
192
477
  {/if}
193
478
  </section>
194
479
  {/each}
195
- </div>
480
+ {/snippet}
481
+
482
+ {#snippet table_panel()}
483
+ <div class="table-filters">
484
+ {#if max_n_el > 2}
485
+ <label>
486
+ Min N<sub>el</sub>:
487
+ <select bind:value={min_n_elements}>
488
+ {#each Array.from({ length: max_n_el }, (_, idx) => idx + 1) as nel (nel)}
489
+ <option value={nel}>{nel}{nel === 1 ? ` (all)` : ``}</option>
490
+ {/each}
491
+ </select>
492
+ </label>
493
+ {/if}
494
+ {#if has_polymorphs}
495
+ <label>
496
+ Polymorphs:
497
+ <select bind:value={formula_filter}>
498
+ <option value="">all</option>
499
+ {#each poly_formulas as [formula, count] (formula)}
500
+ <option value={formula}>{formula} ({count})</option>
501
+ {/each}
502
+ </select>
503
+ </label>
504
+ {/if}
505
+ <span class="filter-count">{visible_entries.length} entries</span>
506
+ <span class="filter-spacer"></span>
507
+ <div class="export-actions">
508
+ <button
509
+ class="icon-btn"
510
+ class:active={show_export_dropdown}
511
+ title="Export"
512
+ onclick={() => show_export_dropdown = !show_export_dropdown}
513
+ >
514
+ <Icon icon="Export" style="width: 14px" />
515
+ </button>
516
+ {#if show_export_dropdown}
517
+ <div class="export-dropdown">
518
+ <button
519
+ class="dropdown-option"
520
+ onclick={() => {
521
+ export_table(`csv`)
522
+ show_export_dropdown = false
523
+ }}
524
+ >
525
+ <Icon icon="Download" style="width: 12px" /> CSV
526
+ </button>
527
+ <button
528
+ class="dropdown-option"
529
+ onclick={() => {
530
+ export_table(`json`)
531
+ show_export_dropdown = false
532
+ }}
533
+ >
534
+ <Icon icon="Download" style="width: 12px" /> JSON
535
+ </button>
536
+ </div>
537
+ {/if}
538
+ </div>
539
+ </div>
540
+ <HeatmapTable
541
+ data={table_data}
542
+ columns={table_columns}
543
+ initial_sort={{ column: `E<sub>hull</sub>`, direction: `asc` }}
544
+ scroll_style={layout === `side-by-side`
545
+ ? `flex: 1 1 0; max-width: 100%; overflow: auto`
546
+ : `max-height: var(--hull-stats-max-height, 500px)`}
547
+ style="width: 100%"
548
+ root_style={layout === `side-by-side`
549
+ ? `flex: 1 1 0; min-height: 0; margin-inline: 0`
550
+ : undefined}
551
+ onrowclick={on_entry_click ? handle_row_click : undefined}
552
+ export_data={false}
553
+ />
554
+ {/snippet}
555
+
556
+ {#if layout === `side-by-side`}
557
+ <div {...rest} class="convex-hull-stats side-by-side {rest.class ?? ``}">
558
+ <div class="stats-pane">
559
+ {@render stats_panel()}
560
+ </div>
561
+ <div class="table-pane">
562
+ {@render table_panel()}
563
+ </div>
564
+ </div>
565
+ {:else}
566
+ <div {...rest} class="convex-hull-stats {rest.class ?? ``}">
567
+ <div class="view-toggle">
568
+ <button class:active={view_mode === `stats`} onclick={() => view_mode = `stats`}>
569
+ Stats
570
+ </button>
571
+ <button class:active={view_mode === `table`} onclick={() => view_mode = `table`}>
572
+ Table
573
+ </button>
574
+ </div>
575
+ {#if view_mode === `stats`}
576
+ {@render stats_panel()}
577
+ {:else}
578
+ {@render table_panel()}
579
+ {/if}
580
+ </div>
581
+ {/if}
196
582
 
197
583
  <style>
198
584
  .convex-hull-stats {
199
585
  background: var(--hull-stats-bg, var(--hull-bg));
200
586
  border-radius: var(--hull-border-radius, var(--border-radius, 3pt));
201
- padding: 0 1em 1em;
587
+ padding: var(--hull-stats-padding, 1em);
588
+ }
589
+ .convex-hull-stats.side-by-side {
590
+ display: flex;
591
+ gap: var(--hull-stats-gap, 1.5em);
592
+ align-items: stretch;
593
+ width: fit-content;
594
+ max-width: 100%;
595
+ margin-inline: auto;
596
+ }
597
+ .stats-pane {
598
+ flex: 0 0 auto;
599
+ width: fit-content;
600
+ min-width: var(--hull-stats-pane-min-width, 200px);
601
+ max-width: var(--hull-stats-pane-max-width, 320px);
602
+ }
603
+ .table-pane {
604
+ flex: 1 1 0;
605
+ max-width: 100%;
606
+ min-width: 0;
607
+ overflow: auto;
608
+ display: flex;
609
+ flex-direction: column;
610
+ }
611
+ .convex-hull-stats :global(tbody tr[onclick]) {
612
+ cursor: pointer;
202
613
  }
203
614
  section div {
204
615
  display: flex;
@@ -231,9 +642,167 @@ let pane_data = $derived.by(() => {
231
642
  }
232
643
  }
233
644
  .stat-item span:first-child {
234
- color: var(--text-color-muted, #666);
645
+ color: var(--text-color-muted, light-dark(#666, #bbb));
235
646
  }
236
647
  section h5 {
237
648
  margin: 0 0 6px 0;
238
649
  }
650
+ .view-toggle {
651
+ display: flex;
652
+ margin-bottom: 8pt;
653
+ }
654
+ .view-toggle button {
655
+ flex: 1;
656
+ padding: 2pt 8pt;
657
+ border: 1px solid
658
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
659
+ background: transparent;
660
+ color: inherit;
661
+ cursor: pointer;
662
+ font-size: 0.85em;
663
+ }
664
+ .view-toggle button:first-child {
665
+ border-radius: 4pt 0 0 4pt;
666
+ }
667
+ .view-toggle button:last-child {
668
+ border-radius: 0 4pt 4pt 0;
669
+ border-left: none;
670
+ }
671
+ .view-toggle button.active {
672
+ background: var(
673
+ --hull-stats-toggle-active-bg,
674
+ light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.15))
675
+ );
676
+ font-weight: 500;
677
+ }
678
+ .table-filters {
679
+ display: flex;
680
+ align-items: center;
681
+ flex-wrap: wrap;
682
+ gap: 0.75em;
683
+ margin-bottom: 6pt;
684
+ font-size: 0.85em;
685
+ label {
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 0.4em;
689
+ sub {
690
+ margin-left: -0.2em;
691
+ font-size: 0.72em;
692
+ line-height: 0;
693
+ vertical-align: baseline;
694
+ position: relative;
695
+ top: 0.33em;
696
+ }
697
+ }
698
+ select {
699
+ padding: 2pt 4pt;
700
+ border: 1px solid
701
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
702
+ border-radius: 3pt;
703
+ background: transparent;
704
+ color: inherit;
705
+ font-size: inherit;
706
+ }
707
+ }
708
+ .filter-spacer {
709
+ flex: 1 1 auto;
710
+ }
711
+ .export-actions {
712
+ position: relative;
713
+ .icon-btn {
714
+ padding: 2pt 6pt;
715
+ border: 1px solid
716
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
717
+ border-radius: 3pt;
718
+ background: transparent;
719
+ color: inherit;
720
+ cursor: pointer;
721
+ display: inline-flex;
722
+ align-items: center;
723
+ justify-content: center;
724
+ }
725
+ .icon-btn:hover {
726
+ background: color-mix(in srgb, currentColor 8%, transparent);
727
+ }
728
+ .icon-btn.active {
729
+ background: color-mix(in srgb, currentColor 12%, transparent);
730
+ }
731
+ }
732
+ .export-dropdown {
733
+ position: absolute;
734
+ right: 0;
735
+ top: calc(100% + 4px);
736
+ display: flex;
737
+ flex-direction: column;
738
+ min-width: 88px;
739
+ padding: 3pt;
740
+ border: 1px solid
741
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
742
+ border-radius: 4pt;
743
+ background: var(--page-bg, Canvas);
744
+ z-index: 4;
745
+ box-shadow: 0 2px 8px color-mix(in srgb, black 20%, transparent);
746
+ .dropdown-option {
747
+ display: inline-flex;
748
+ align-items: center;
749
+ gap: 5px;
750
+ border: none;
751
+ border-radius: 3pt;
752
+ background: transparent;
753
+ color: inherit;
754
+ cursor: pointer;
755
+ text-align: left;
756
+ padding: 3pt 6pt;
757
+ }
758
+ .dropdown-option:hover {
759
+ background: color-mix(in srgb, currentColor 8%, transparent);
760
+ }
761
+ }
762
+ .table-pane :global(.control-buttons) {
763
+ display: none;
764
+ margin: 0;
765
+ }
766
+ .filter-count {
767
+ color: var(--text-color-muted, light-dark(#666, #bbb));
768
+ font-size: 0.9em;
769
+ }
770
+ .subsystem-coverage-row {
771
+ flex-wrap: wrap;
772
+ gap: 4pt 1em;
773
+ justify-content: flex-start;
774
+ .subsystem-label {
775
+ color: var(--text-color-muted, light-dark(#666, #bbb));
776
+ font-size: 0.9em;
777
+ }
778
+ .subsystem-chips {
779
+ display: flex;
780
+ flex-wrap: wrap;
781
+ gap: 4pt;
782
+ }
783
+ }
784
+ .subsystem-chip {
785
+ display: inline-flex;
786
+ align-items: center;
787
+ gap: 0;
788
+ padding: 1pt 5pt;
789
+ border-radius: 3pt;
790
+ font-size: 0.78em;
791
+ line-height: 1.2;
792
+ background: color-mix(in srgb, currentColor 5%, transparent);
793
+ color: var(--text-color-muted, light-dark(#666, #bbb));
794
+ .pair {
795
+ font-weight: 500;
796
+ }
797
+ .count {
798
+ margin-left: 3pt;
799
+ font-size: 0.9em;
800
+ font-weight: 600;
801
+ color: color-mix(in srgb, currentColor 70%, transparent);
802
+ }
803
+ }
804
+ .subsystem-chip.has-entries {
805
+ background: color-mix(in srgb, var(--hull-stable-color, #22c55e) 15%, transparent);
806
+ color: inherit;
807
+ }
239
808
  </style>