matterviz 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,382 +1,479 @@
1
- <script lang="ts">import { get_alphabetical_formula, get_electro_neg_formula, get_reduced_formula, } from '../composition';
2
- import Icon from '../Icon.svelte';
3
- import { format_num } from '../labels';
4
- import Histogram from '../plot/Histogram.svelte';
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();
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);
14
- async function copy_to_clipboard(label, value, key) {
1
+ <script lang="ts">
2
+ import {
3
+ get_alphabetical_formula,
4
+ get_electro_neg_formula,
5
+ get_reduced_formula,
6
+ } from '../composition'
7
+ import Icon from '../Icon.svelte'
8
+ import { format_num } from '../labels'
9
+ import { sanitize_html } from '../sanitize'
10
+ import Histogram from '../plot/Histogram.svelte'
11
+ import type { Label, RowData } from '../table'
12
+ import HeatmapTable from '../table/HeatmapTable.svelte'
13
+ import type { HTMLAttributes } from 'svelte/elements'
14
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
15
+ import type { ConvexHullEntry, PhaseArityField, PhaseStats } from './types'
16
+ import { get_arity, is_on_hull } from './helpers'
17
+
18
+ let {
19
+ phase_stats,
20
+ stable_entries,
21
+ unstable_entries,
22
+ layout = `toggle`,
23
+ on_entry_click,
24
+ highlighted_entry_id,
25
+ min_n_elements = $bindable(1),
26
+ entry_href,
27
+ ...rest
28
+ }:
29
+ & HTMLAttributes<HTMLDivElement>
30
+ & {
31
+ phase_stats: PhaseStats | null
32
+ stable_entries: ConvexHullEntry[]
33
+ unstable_entries: ConvexHullEntry[]
34
+ // 'toggle' shows stats/table with toggle buttons (default)
35
+ // 'side-by-side' shows both stats and table next to each other without toggle
36
+ layout?: `toggle` | `side-by-side`
37
+ // Called when a table row is clicked, with the corresponding entry
38
+ on_entry_click?: (entry: ConvexHullEntry) => void
39
+ // Entry ID to highlight in the table (e.g. current material on detail page)
40
+ highlighted_entry_id?: string
41
+ // Minimum number of elements filter for table (bindable for URL sync)
42
+ min_n_elements?: number
43
+ // Generate URL for an entry (makes ID column a clickable link)
44
+ entry_href?: (entry: ConvexHullEntry) => string | null
45
+ } = $props()
46
+
47
+ let copied_items = new SvelteSet<string>()
48
+ let view_mode = $state<`stats` | `table`>(`stats`)
49
+ // Formula filter: when set, table shows only entries with this reduced formula
50
+ let formula_filter = $state(``)
51
+ let show_export_dropdown = $state(false)
52
+
53
+ async function copy_to_clipboard(label: string, value: string, key: string) {
15
54
  try {
16
- await navigator.clipboard.writeText(`${label}: ${value}`);
17
- copied_items.add(key);
18
- setTimeout(() => copied_items.delete(key), 1000);
55
+ await navigator.clipboard.writeText(`${label}: ${value}`)
56
+ copied_items.add(key)
57
+ setTimeout(() => copied_items.delete(key), 1000)
58
+ } catch (error) {
59
+ console.error(`Failed to copy to clipboard:`, error)
19
60
  }
20
- catch (error) {
21
- console.error(`Failed to copy to clipboard:`, error);
22
- }
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 = [
61
+ }
62
+ function handle_copy_keydown(
63
+ event: KeyboardEvent,
64
+ label: string,
65
+ value: string,
66
+ key: string,
67
+ ): void {
68
+ if (event.key !== `Enter` && event.key !== ` `) return
69
+ event.preventDefault()
70
+ copy_to_clipboard(label, value, key)
71
+ }
72
+
73
+ // Shared concatenation of stable + unstable for histograms
74
+ let all_entries = $derived([...stable_entries, ...unstable_entries])
75
+
76
+ // Static arity labels for phase breakdown display
77
+ const arity_types: [string, PhaseArityField, number][] = [
34
78
  [`Unary`, `unary`, 1],
35
79
  [`Binary`, `binary`, 2],
36
80
  [`Ternary`, `ternary`, 3],
37
81
  [`Quaternary`, `quaternary`, 4],
38
82
  [`Quinary+`, `quinary_plus`, 5],
39
- ];
40
- const histogram_props = {
83
+ ]
84
+
85
+ const histogram_props = {
41
86
  bins: 50,
42
87
  y_axis: { label: ``, ticks: 3 },
43
88
  show_legend: false,
44
89
  show_controls: false,
45
90
  padding: { t: 5, b: 22, l: 35, r: 5 },
46
91
  style: `height: 100px; --histogram-min-height: 100px`,
47
- };
48
- // Prepare histogram data for formation energies and hull distances
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
- }]);
63
- let pane_data = $derived.by(() => {
64
- if (!phase_stats)
65
- return [];
66
- const pct = (count) => phase_stats.total > 0 ? format_num(count / phase_stats.total, `.1~%`) : `0%`;
92
+ } as const
93
+
94
+ // Prepare histogram data for formation energies and hull distances
95
+ let e_form_data = $derived([{
96
+ x: [] as number[],
97
+ y: all_entries
98
+ .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom)
99
+ .filter((val): val is number => val !== undefined && isFinite(val)),
100
+ label: `Formation Energy`,
101
+ }])
102
+
103
+ let hull_distance_data = $derived([{
104
+ x: [] as number[],
105
+ y: all_entries
106
+ .map((entry) => entry.e_above_hull)
107
+ .filter((val): val is number => val !== undefined && isFinite(val)),
108
+ label: `E above hull`,
109
+ }])
110
+
111
+ let pane_data = $derived.by(() => {
112
+ if (!phase_stats) return []
113
+
114
+ const pct = (count: number) =>
115
+ phase_stats.total > 0 ? format_num(count / phase_stats.total, `.1~%`) : `0%`
116
+
67
117
  return [
68
- {
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
- }],
123
- },
124
- ];
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;
118
+ {
119
+ title: ``,
120
+ items: [
121
+ {
122
+ label: `Total entries in ${phase_stats.chemical_system}`,
123
+ value: format_num(phase_stats.total),
124
+ key: `total-entries`,
125
+ },
126
+ // Only show phase types that exist or are within the max_arity
127
+ // used when computing stats (respects zeroed-out counts)
128
+ ...arity_types
129
+ .filter(([, field, arity]) =>
130
+ phase_stats[field] > 0 || phase_stats.max_arity >= arity
131
+ )
132
+ .map(([display, field]) => ({
133
+ label: `${display} phases`,
134
+ value: `${format_num(phase_stats[field])} (${pct(phase_stats[field])})`,
135
+ key: `${field}-phases`,
136
+ })),
137
+ ],
138
+ },
139
+ {
140
+ title: `Stability`,
141
+ items: [
142
+ {
143
+ label: `Stable phases`,
144
+ value: `${format_num(phase_stats.stable)} (${pct(phase_stats.stable)})`,
145
+ key: `stable-phases`,
146
+ },
147
+ {
148
+ label: `Unstable phases`,
149
+ value: `${format_num(phase_stats.unstable)} (${
150
+ pct(phase_stats.unstable)
151
+ })`,
152
+ key: `unstable-phases`,
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ title: `E<sub>form</sub> distribution`,
158
+ items: [{
159
+ label: `Min / avg / max (eV/atom)`,
160
+ value: [
161
+ phase_stats.energy_range.min,
162
+ phase_stats.energy_range.avg,
163
+ phase_stats.energy_range.max,
164
+ ]
165
+ .map((val) => format_num(val, `.3f`)).join(` / `),
166
+ key: `formation-energy`,
167
+ }],
168
+ },
169
+ {
170
+ title: `E<sub>above hull</sub> distribution`,
171
+ items: [{
172
+ label: `Max / avg (eV/atom)`,
173
+ value: [phase_stats.hull_distance.max, phase_stats.hull_distance.avg]
174
+ .map((val) => format_num(val, `.3f`)).join(` / `),
175
+ key: `hull-distance`,
176
+ }],
177
+ },
178
+ ]
179
+ })
180
+
181
+ // Subsystem coverage: count entries per element pair for the stats pane
182
+ let subsystem_coverage = $derived.by(() => {
183
+ if (!phase_stats) return null
184
+ const elements = phase_stats.chemical_system.split(`-`)
185
+ if (elements.length < 3 || elements.length > 10) return null
133
186
  // Count entries containing each pair
134
- const pair_counts = new SvelteMap();
187
+ const pair_counts = new SvelteMap<string, number>()
135
188
  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
- }
189
+ const active =
190
+ (Object.keys(entry.composition) as (keyof typeof entry.composition)[])
191
+ .filter((el) => (entry.composition[el] ?? 0) > 0)
192
+ // Count all pairs present in this entry
193
+ for (let idx_a = 0; idx_a < active.length; idx_a++) {
194
+ for (let idx_b = idx_a + 1; idx_b < active.length; idx_b++) {
195
+ const key = [active[idx_a], active[idx_b]].sort().join(`-`)
196
+ pair_counts.set(key, (pair_counts.get(key) ?? 0) + 1)
144
197
  }
198
+ }
145
199
  }
146
200
  // 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;
201
+ return elements.flatMap((el_a, idx_a) =>
202
+ elements.slice(idx_a + 1).map((el_b) => {
203
+ const key = [el_a, el_b].sort().join(`-`)
204
+ return { pair: key, count: pair_counts.get(key) ?? 0 }
205
+ })
206
+ )
207
+ })
208
+ let subsystem_coverage_summary = $derived(
209
+ subsystem_coverage?.map(({ pair, count }) => `${pair}: ${count}`).join(` | `) ??
210
+ null,
211
+ )
212
+
213
+ // Table view: visible entries filtered by min element count and formula
214
+ let visible_entries = $derived(
215
+ all_entries.filter((entry) => {
216
+ if (!entry.visible) return false
217
+ if (min_n_elements > 1 && get_arity(entry) < min_n_elements) return false
218
+ if (
219
+ active_formula_filter &&
220
+ composition_key(entry.composition) !== active_formula_filter
221
+ ) return false
222
+ return true
223
+ }),
224
+ )
225
+ let has_raw = $derived(
226
+ visible_entries.some((entry) => entry.energy_per_atom !== undefined),
227
+ )
228
+ let has_ids = $derived(visible_entries.some((entry) => entry.entry_id))
229
+ let max_n_el = $derived(
230
+ all_entries.reduce((max, entry) => Math.max(max, get_arity(entry)), 1),
231
+ )
232
+
233
+ // Sortable HTML cell with a hidden data-sort-value for HeatmapTable sorting
234
+ const sort_span = (sort_val: number | string, display: string, attrs = ``) =>
235
+ `<span data-sort-value="${sort_val}"${attrs ? ` ${attrs}` : ``}>${display}</span>`
236
+
237
+ // Escape HTML special chars to prevent XSS when rendering user-supplied strings via {@html}
238
+ const escape_html = (str: string): string =>
239
+ str
240
+ .replace(/&/g, `&amp;`)
241
+ .replace(/</g, `&lt;`)
242
+ .replace(/>/g, `&gt;`)
243
+ .replace(/"/g, `&quot;`)
244
+ .replace(/'/g, `&#39;`)
245
+ const unescape_html = (str: string, max_rounds = 5): string => {
246
+ let decoded = str
179
247
  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;
248
+ const next_decoded = decoded
249
+ .replace(/&amp;/g, `&`)
250
+ .replace(/&lt;/g, `<`)
251
+ .replace(/&gt;/g, `>`)
252
+ .replace(/&quot;/g, `"`)
253
+ .replace(/&#39;/g, `'`)
254
+ if (next_decoded === decoded) break
255
+ decoded = next_decoded
189
256
  }
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();
257
+ return decoded
258
+ }
259
+ // Convert legacy/html formula strings like Fe<sub>2</sub>O<sub>3</sub> back to plain
260
+ // stoichiometric input before parsing/reordering.
261
+ const normalize_formula_markup = (formula: string): string =>
262
+ unescape_html(formula)
263
+ .replaceAll(/<sub>\s*([^<]+?)\s*<\/sub>/gi, `$1`)
264
+ .replaceAll(/<[^>]+>/g, ``)
265
+ .replaceAll(/\s+/g, ``)
266
+ const sanitize_href = (href: string | null | undefined): string | null => {
267
+ const trimmed_href = href?.trim()
268
+ if (!trimmed_href) return null
269
+ const lower_href = trimmed_href.toLowerCase()
270
+ const blocked_schemes = [`javascript:`, `data:`, `vbscript:`]
271
+ if (blocked_schemes.some((scheme) => lower_href.startsWith(scheme))) return null
272
+ return trimmed_href
273
+ }
274
+ // Serialize reduced composition to a stable string key for polymorph counting
275
+ const composition_key = (comp: Record<string, number>): string =>
276
+ get_alphabetical_formula(get_reduced_formula(comp), true, ``)
277
+
278
+ // Count polymorphs per reduced formula across all entries
279
+ let polymorph_counts = $derived.by(() => {
280
+ const counts = new SvelteMap<string, number>()
213
281
  for (const entry of all_entries) {
214
- const key = composition_key(entry.composition);
215
- counts.set(key, (counts.get(key) ?? 0) + 1);
282
+ const key = composition_key(entry.composition)
283
+ counts.set(key, (counts.get(key) ?? 0) + 1)
216
284
  }
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 ``;
285
+ return counts
286
+ })
287
+ let poly_formulas = $derived(
288
+ [...polymorph_counts.entries()]
289
+ .filter(([, count]) => count > 1)
290
+ .sort(([, count_a], [, count_b]) => count_b - count_a),
291
+ )
292
+ let has_polymorphs = $derived(poly_formulas.length > 0)
293
+ let active_formula_filter = $derived.by(() => {
294
+ if (!formula_filter || !has_polymorphs) return ``
226
295
  return poly_formulas.some(([formula]) => formula === formula_filter)
227
- ? formula_filter
228
- : ``;
229
- });
230
- $effect(() => {
296
+ ? formula_filter
297
+ : ``
298
+ })
299
+ $effect(() => {
231
300
  if (formula_filter && formula_filter !== active_formula_filter) {
232
- formula_filter = ``;
301
+ formula_filter = ``
233
302
  }
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();
303
+ })
304
+
305
+ // Build table rows and a WeakMap from row→entry for the click handler
306
+ let { table_data, entry_by_row } = $derived.by(() => {
307
+ const map = new WeakMap<RowData, ConvexHullEntry>()
238
308
  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;
281
- });
282
- return { table_data: rows, entry_by_row: map };
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
- {
309
+ const n_atoms = Object.values(entry.composition).reduce(
310
+ (sum, count) => sum + count,
311
+ 0,
312
+ )
313
+ const on_hull = is_on_hull(entry)
314
+ const formula_source = entry.reduced_formula ?? entry.name ??
315
+ get_alphabetical_formula(entry.composition, true, ``)
316
+ const normalized_formula = normalize_formula_markup(formula_source)
317
+ const formatted_formula = get_electro_neg_formula(normalized_formula)
318
+ const formula_html = formatted_formula || escape_html(normalized_formula)
319
+ // Match by entry_id or common data fields (mat_id, structure_id)
320
+ // since entry_id may be wrapped in HTML (e.g. <a> tags)
321
+ const entry_data = entry.data as Record<string, unknown> | undefined
322
+ const is_highlighted = !!(highlighted_entry_id && (
323
+ entry.entry_id === highlighted_entry_id ||
324
+ entry_data?.mat_id === highlighted_entry_id ||
325
+ entry_data?.structure_id === highlighted_entry_id
326
+ ))
327
+ const row: RowData = {
328
+ '#': sort_span(idx + 1, `${idx + 1}`),
329
+ Formula: on_hull ? `<strong>${formula_html}</strong>` : formula_html,
330
+ 'E<sub>hull</sub>': entry.e_above_hull ?? null,
331
+ 'E<sub>form</sub>': entry.e_form_per_atom ?? entry.energy_per_atom ?? null,
332
+ }
333
+ if (has_raw) row[`E<sub>raw</sub>`] = entry.energy_per_atom
334
+ if (has_ids) {
335
+ const safe_href = sanitize_href(entry_href?.(entry))
336
+ const safe_id = entry.entry_id ? escape_html(entry.entry_id) : undefined
337
+ row.ID = safe_href && safe_id
338
+ ? `<a href="${
339
+ escape_html(safe_href)
340
+ }" target="_blank" rel="noopener">${safe_id}</a>`
341
+ : safe_id
342
+ }
343
+ if (has_polymorphs) {
344
+ const comp_key = composition_key(entry.composition)
345
+ const poly_count = polymorph_counts.get(comp_key) ?? 1
346
+ row.Poly = poly_count
347
+ }
348
+ row[`N<sub>el</sub>`] = get_arity(entry)
349
+ row[`N<sub>at</sub>`] = n_atoms
350
+ // Highlight row for current material
351
+ if (is_highlighted) {
352
+ row.style =
353
+ `background: color-mix(in srgb, var(--hull-stable-color, #22c55e) 15%, transparent)`
354
+ }
355
+ map.set(row, entry)
356
+ return row
357
+ })
358
+ return { table_data: rows, entry_by_row: map }
359
+ })
360
+
361
+ function handle_row_click(_event: KeyboardEvent | MouseEvent, row: RowData): void {
362
+ const entry = entry_by_row.get(row)
363
+ if (entry) on_entry_click?.(entry)
364
+ }
365
+
366
+ let table_columns: Label[] = $derived(
367
+ [
368
+ { label: `#`, color_scale: null, description: `Row number` },
369
+ { label: `Formula`, color_scale: null },
370
+ {
293
371
  label: `E<sub>hull</sub>`,
294
372
  better: `lower`,
295
373
  color_scale: `interpolateRdYlGn`,
296
374
  format: `.4f`,
297
375
  description: `Energy above convex hull (eV/atom)`,
298
- },
299
- {
376
+ },
377
+ {
300
378
  label: `E<sub>form</sub>`,
301
379
  better: `lower`,
302
380
  color_scale: `interpolateBlues`,
303
381
  format: `.4f`,
304
382
  description: `Formation energy (eV/atom)`,
305
- },
306
- ...(has_raw
383
+ },
384
+ ...(has_raw
307
385
  ? [{
308
- label: `E<sub>raw</sub>`,
309
- color_scale: `interpolateCool`,
310
- format: `.4f`,
311
- description: `Raw energy per atom (eV/atom)`,
312
- }]
386
+ label: `E<sub>raw</sub>`,
387
+ color_scale: `interpolateCool` as const,
388
+ format: `.4f`,
389
+ description: `Raw energy per atom (eV/atom)`,
390
+ }]
313
391
  : []),
314
- ...(has_ids
392
+ ...(has_ids
315
393
  ? [{ label: `ID`, color_scale: null, description: `Entry identifier` }]
316
394
  : []),
317
- ...(has_polymorphs
395
+ ...(has_polymorphs
318
396
  ? [{
319
- label: `Poly`,
320
- color_scale: null,
321
- description: `Number of polymorphs (same reduced formula)`,
322
- }]
397
+ label: `Poly`,
398
+ color_scale: null,
399
+ description: `Number of polymorphs (same reduced formula)`,
400
+ }]
323
401
  : []),
324
- {
402
+ {
325
403
  label: `N<sub>el</sub>`,
326
404
  color_scale: null,
327
405
  description: `Number of elements`,
328
- },
329
- {
406
+ },
407
+ {
330
408
  label: `N<sub>at</sub>`,
331
409
  color_scale: null,
332
410
  format: `d`,
333
411
  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) => {
412
+ },
413
+ ] satisfies Label[],
414
+ )
415
+
416
+ const html_to_text = (val: unknown): string => {
417
+ if (val == null) return ``
418
+ if (typeof val !== `string`) return String(val)
419
+ const temp_el = document.createElement(`div`)
420
+ temp_el.innerHTML = val
421
+ return temp_el.textContent?.trim() ?? ``
422
+ }
423
+ const csv_escape = (val: string): string =>
424
+ /[",\n]/.test(val) ? `"${val.replaceAll(`"`, `""`)}"` : val
425
+ const get_export_filename = (format: `csv` | `json`): string => {
347
426
  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();
427
+ .toLowerCase()
428
+ .replaceAll(/\s+/g, `-`)
429
+ return `${system}.${format}`
430
+ }
431
+ const build_export_rows = () => {
432
+ const column_labels = table_columns.map((col) => col.label)
433
+ return table_data.map((row) =>
434
+ Object.fromEntries(
435
+ column_labels.map((label) => [html_to_text(label), html_to_text(row[label])]),
436
+ )
437
+ )
438
+ }
439
+ const download_file = (
440
+ content: string,
441
+ filename: string,
442
+ mime_type: string,
443
+ ): void => {
444
+ const blob = new Blob([content], { type: mime_type })
445
+ const object_url = URL.createObjectURL(blob)
446
+ const link_el = document.createElement(`a`)
447
+ link_el.href = object_url
448
+ link_el.download = filename
449
+ document.body.append(link_el)
450
+ link_el.click()
451
+ link_el.remove()
452
+ URL.revokeObjectURL(object_url)
453
+ }
454
+ function export_table(format: `csv` | `json`): void {
455
+ const rows = build_export_rows()
369
456
  if (format === `json`) {
370
- download_file(JSON.stringify(rows, null, 2), get_export_filename(`json`), `application/json;charset=utf-8`);
371
- return;
457
+ download_file(
458
+ JSON.stringify(rows, null, 2),
459
+ get_export_filename(`json`),
460
+ `application/json;charset=utf-8`,
461
+ )
462
+ return
372
463
  }
373
- const headers = rows.length > 0 ? Object.keys(rows[0]) : [];
464
+ const headers = rows.length > 0 ? Object.keys(rows[0]) : []
374
465
  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
- }
466
+ headers.map(csv_escape).join(`,`),
467
+ ...rows.map((row) =>
468
+ headers.map((header) => csv_escape(row[header] ?? ``)).join(`,`)
469
+ ),
470
+ ]
471
+ download_file(
472
+ csv_lines.join(`\n`),
473
+ get_export_filename(`csv`),
474
+ `text/csv;charset=utf-8`,
475
+ )
476
+ }
380
477
  </script>
381
478
 
382
479
  {#snippet stats_panel()}
@@ -384,7 +481,7 @@ function export_table(format) {
384
481
  {#if sec_idx > 0}<hr />{/if}
385
482
  <section>
386
483
  {#if section.title}
387
- <h5>{@html section.title}</h5>
484
+ <h5>{@html sanitize_html(section.title)}</h5>
388
485
  {/if}
389
486
  {#each section.items as item (item.key ?? item.label)}
390
487
  {@const { key, label, value } = item}
@@ -403,8 +500,8 @@ function export_table(format) {
403
500
  key ?? item.label,
404
501
  )}
405
502
  >
406
- <span>{@html label}:</span>
407
- <span>{@html value}</span>
503
+ <span>{@html sanitize_html(label)}:</span>
504
+ <span>{@html sanitize_html(value)}</span>
408
505
  {#if key && copied_items.has(key)}
409
506
  <Icon
410
507
  icon="Check"