matterviz 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,382 +1,479 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
event.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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) =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
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, `&`)
|
|
241
|
+
.replace(/</g, `<`)
|
|
242
|
+
.replace(/>/g, `>`)
|
|
243
|
+
.replace(/"/g, `"`)
|
|
244
|
+
.replace(/'/g, `'`)
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
decoded = next_decoded;
|
|
248
|
+
const next_decoded = decoded
|
|
249
|
+
.replace(/&/g, `&`)
|
|
250
|
+
.replace(/</g, `<`)
|
|
251
|
+
.replace(/>/g, `>`)
|
|
252
|
+
.replace(/"/g, `"`)
|
|
253
|
+
.replace(/'/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) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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(
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
})
|
|
230
|
-
$effect(() => {
|
|
296
|
+
? formula_filter
|
|
297
|
+
: ``
|
|
298
|
+
})
|
|
299
|
+
$effect(() => {
|
|
231
300
|
if (formula_filter && formula_filter !== active_formula_filter) {
|
|
232
|
-
|
|
301
|
+
formula_filter = ``
|
|
233
302
|
}
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
383
|
+
},
|
|
384
|
+
...(has_raw
|
|
307
385
|
? [{
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
392
|
+
...(has_ids
|
|
315
393
|
? [{ label: `ID`, color_scale: null, description: `Entry identifier` }]
|
|
316
394
|
: []),
|
|
317
|
-
|
|
395
|
+
...(has_polymorphs
|
|
318
396
|
? [{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
const temp_el = document.createElement(`div`)
|
|
342
|
-
temp_el.innerHTML = val
|
|
343
|
-
return temp_el.textContent?.trim() ??
|
|
344
|
-
}
|
|
345
|
-
const csv_escape = (val
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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) =>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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"
|