matterviz 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -1,12 +1,24 @@
1
1
  import { get_d3_interpolator } from '../colors';
2
+ import { extract_formula_elements } from '../composition/parse';
3
+ import { group } from 'd3-array';
2
4
  import { scaleSequential } from 'd3-scale';
5
+ import { best_form_energy_for_formula as best_form_energy, formula_key_from_composition, } from './compute';
6
+ // Categorical palette for arity mode (element count)
7
+ export const ARITY_COLORS = [`#3498db`, `#2ecc71`, `#e67e22`, `#9b59b6`];
8
+ // Exhaustively typed over the numeric color modes so adding a new mode
9
+ // fails compilation here instead of rendering an undefined colorbar title
10
+ const COLOR_MODE_LABELS = {
11
+ energy: `Energy per atom (eV)`,
12
+ formation_energy: `Formation energy (eV/atom)`,
13
+ entries: `Entry count`,
14
+ };
3
15
  // Resolve D3 interpolator with optional reverse for chempot color scales.
4
16
  function get_chempot_interpolator(name, reverse) {
5
17
  const raw = get_d3_interpolator(name);
6
18
  return reverse ? (frac) => raw(1 - frac) : raw;
7
19
  }
8
20
  // Build sequential color scale from values and D3 interpolator name.
9
- export function make_chempot_color_scale(values, interpolator_name, reverse) {
21
+ function make_chempot_color_scale(values, interpolator_name, reverse) {
10
22
  const finite_values = values.filter(Number.isFinite);
11
23
  if (finite_values.length === 0)
12
24
  return null;
@@ -30,3 +42,60 @@ export function get_chempot_color_bar_config(color_scale, reverse) {
30
42
  color_scale_domain: [0, 1],
31
43
  };
32
44
  }
45
+ // Per-formula domain colors plus color-bar range for the active color mode.
46
+ // Shared by ChemPotDiagram2D and ChemPotDiagram3D.
47
+ export function get_domain_color_data(opts) {
48
+ const { formulas, color_mode, entries, el_refs, energy_stats } = opts;
49
+ const colors = new Map();
50
+ if (color_mode === `none`)
51
+ return { colors, color_range: null };
52
+ if (color_mode === `arity`) {
53
+ for (const formula of formulas) {
54
+ const n_elements = extract_formula_elements(formula).length;
55
+ const color_idx = Math.min(n_elements, ARITY_COLORS.length) - 1;
56
+ colors.set(formula, ARITY_COLORS[Math.max(0, color_idx)]);
57
+ }
58
+ return { colors, color_range: null };
59
+ }
60
+ const entries_by_formula = color_mode === `formation_energy`
61
+ ? group(entries, (entry) => formula_key_from_composition(entry.composition))
62
+ : new Map();
63
+ const get_value = (formula) => {
64
+ if (color_mode === `energy`)
65
+ return energy_stats.get(formula)?.min_energy_per_atom ?? null;
66
+ if (color_mode === `formation_energy`) {
67
+ return best_form_energy(entries_by_formula.get(formula) ?? [], formula, el_refs) ?? null;
68
+ }
69
+ return energy_stats.get(formula)?.matching_entry_count ?? 0;
70
+ };
71
+ const value_by_formula = new Map();
72
+ for (const formula of formulas) {
73
+ const value = get_value(formula);
74
+ if (value == null || !Number.isFinite(value))
75
+ continue;
76
+ value_by_formula.set(formula, value);
77
+ }
78
+ const values = [...value_by_formula.values()];
79
+ const scale = make_chempot_color_scale(values, opts.color_scale, opts.reverse_color_scale);
80
+ for (const formula of formulas) {
81
+ const value = value_by_formula.get(formula);
82
+ colors.set(formula, value != null && scale ? scale(value) : `#999`);
83
+ }
84
+ if (values.length === 0)
85
+ return { colors, color_range: null };
86
+ let [min_val, max_val] = [values[0], values[0]];
87
+ for (const value of values) {
88
+ if (value < min_val)
89
+ min_val = value;
90
+ if (value > max_val)
91
+ max_val = value;
92
+ }
93
+ return {
94
+ colors,
95
+ color_range: {
96
+ min: min_val,
97
+ max: Math.max(max_val, min_val + 1e-6),
98
+ label: COLOR_MODE_LABELS[color_mode],
99
+ },
100
+ };
101
+ }
@@ -8,6 +8,12 @@ export declare function get_min_entries_and_el_refs(entries: PhaseData[]): {
8
8
  el_refs: Record<string, PhaseData>;
9
9
  };
10
10
  export declare function best_form_energy_for_formula(entries: PhaseData[], formula: string, el_refs: Record<string, PhaseData>): number | undefined;
11
+ export interface FormulaEnergyStats {
12
+ matching_entry_count: number;
13
+ min_energy_per_atom: number | null;
14
+ max_energy_per_atom: number | null;
15
+ }
16
+ export declare function get_energy_stats_by_formula(entries: PhaseData[]): Map<string, FormulaEnergyStats>;
11
17
  export declare const renormalize_entries: (entries: PhaseData[], el_refs: Record<string, PhaseData>, elements: string[]) => PhaseData[];
12
18
  export declare function build_hyperplanes(min_entries: PhaseData[], el_refs: Record<string, PhaseData>, elements: string[]): {
13
19
  hyperplanes: number[][];
@@ -44,5 +50,6 @@ export interface VisibleDomainLabel {
44
50
  }
45
51
  export declare function get_visible_domain_labels(face_positions: ArrayLike<number>, face_domain_map: string[], label_font_size_by_formula: ReadonlyMap<string, number>, pinned_labels?: VisibleDomainLabel[]): VisibleDomainLabel[];
46
52
  export declare function get_ternary_combinations(elements: string[]): string[][];
53
+ export declare function entry_fingerprint(entry: PhaseData): string;
47
54
  export declare function make_nd_cache_key(entries: PhaseData[], formal_chempots: boolean, default_min_limit: number, limits: ChemPotDiagramConfig[`limits`]): string;
48
55
  export declare function compute_chempot_diagram(entries: PhaseData[], config?: ChemPotDiagramConfig): ChemPotDiagramData;
@@ -34,16 +34,28 @@ const get_reduced_formula = (composition) => {
34
34
  .map(([elem, amt]) => [elem, Math.round(amt * factor)]));
35
35
  };
36
36
  // === Entry Helpers ===
37
- // Get energy per atom for a PhaseData entry
37
+ // Energy per atom; NaN for invalid/non-finite so UI can skip without throwing.
38
38
  export function get_energy_per_atom(entry) {
39
- if (typeof entry.energy_per_atom === `number`)
40
- return entry.energy_per_atom;
41
- const atoms = count_atoms_in_composition(entry.composition);
42
- if (atoms <= 0) {
43
- throw new Error(`Invalid composition with non-positive atom count: ${JSON.stringify(entry.composition)}`);
39
+ if (typeof entry.energy_per_atom === `number`) {
40
+ return Number.isFinite(entry.energy_per_atom) ? entry.energy_per_atom : Number.NaN;
44
41
  }
42
+ const atoms = count_atoms_in_composition(entry.composition);
43
+ if (atoms <= 0 || !Number.isFinite(entry.energy))
44
+ return Number.NaN;
45
45
  return entry.energy / atoms;
46
46
  }
47
+ // Same-formula EPA total order (keep aligned with make_nd_cache_key / entry_fingerprint).
48
+ function prefer_min_entry(candidate, candidate_epa, existing, existing_epa) {
49
+ if (candidate_epa !== existing_epa)
50
+ return candidate_epa < existing_epa;
51
+ if (Boolean(candidate.exclude_from_hull) !== Boolean(existing.exclude_from_hull)) {
52
+ return !candidate.exclude_from_hull;
53
+ }
54
+ if ((candidate.is_stable === true) !== (existing.is_stable === true)) {
55
+ return candidate.is_stable === true;
56
+ }
57
+ return (candidate.e_above_hull ?? Infinity) < (existing.e_above_hull ?? Infinity);
58
+ }
47
59
  // Cache for reduced formula strings -- avoids recomputing get_reduced_formula
48
60
  // in hot loops. Key is object identity (WeakMap), value is the formula string.
49
61
  const formula_cache = new WeakMap();
@@ -69,8 +81,10 @@ export function get_min_entries_and_el_refs(entries) {
69
81
  for (const entry of entries) {
70
82
  const key = formula_key_from_composition(entry.composition);
71
83
  const epa = get_energy_per_atom(entry);
84
+ if (!Number.isFinite(epa))
85
+ continue;
72
86
  const existing = by_formula.get(key);
73
- if (!existing || epa < existing.epa) {
87
+ if (!existing || prefer_min_entry(entry, epa, existing.entry, existing.epa)) {
74
88
  by_formula.set(key, { entry, epa });
75
89
  }
76
90
  }
@@ -89,14 +103,20 @@ export function get_min_entries_and_el_refs(entries) {
89
103
  function compute_form_energy_per_atom(entry, el_refs) {
90
104
  const atom_count = count_atoms_in_composition(entry.composition);
91
105
  const energy_per_atom = get_energy_per_atom(entry);
106
+ if (!(atom_count > 0) || !Number.isFinite(energy_per_atom))
107
+ return Number.NaN;
92
108
  let ref_energy = 0;
93
109
  for (const [element, amount] of Object.entries(entry.composition)) {
94
110
  if (amount <= 0)
95
111
  continue;
96
112
  const fraction = amount / atom_count;
97
113
  const ref_entry = el_refs[element];
98
- if (ref_entry)
99
- ref_energy += fraction * get_energy_per_atom(ref_entry);
114
+ if (ref_entry) {
115
+ const ref_epa = get_energy_per_atom(ref_entry);
116
+ if (!Number.isFinite(ref_epa))
117
+ return Number.NaN;
118
+ ref_energy += fraction * ref_epa;
119
+ }
100
120
  }
101
121
  return energy_per_atom - ref_energy;
102
122
  }
@@ -107,23 +127,57 @@ export function best_form_energy_for_formula(entries, formula, el_refs) {
107
127
  if (formula_key_from_composition(entry.composition) !== formula)
108
128
  continue;
109
129
  const e_form = entry.e_form_per_atom ?? compute_form_energy_per_atom(entry, el_refs);
130
+ if (!Number.isFinite(e_form))
131
+ continue;
110
132
  if (best_value === undefined || e_form < best_value)
111
133
  best_value = e_form;
112
134
  }
113
135
  return best_value;
114
136
  }
137
+ // Aggregate per-formula entry count and min/max energy per atom (used for
138
+ // region coloring and hover tooltips in the 2D/3D diagram components).
139
+ export function get_energy_stats_by_formula(entries) {
140
+ const stats = new Map();
141
+ for (const entry of entries) {
142
+ const energy_per_atom = get_energy_per_atom(entry);
143
+ if (!Number.isFinite(energy_per_atom))
144
+ continue;
145
+ const formula_key = formula_key_from_composition(entry.composition);
146
+ const existing = stats.get(formula_key);
147
+ if (existing) {
148
+ existing.matching_entry_count += 1;
149
+ existing.min_energy_per_atom = Math.min(existing.min_energy_per_atom ?? energy_per_atom, energy_per_atom);
150
+ existing.max_energy_per_atom = Math.max(existing.max_energy_per_atom ?? energy_per_atom, energy_per_atom);
151
+ }
152
+ else {
153
+ stats.set(formula_key, {
154
+ matching_entry_count: 1,
155
+ min_energy_per_atom: energy_per_atom,
156
+ max_energy_per_atom: energy_per_atom,
157
+ });
158
+ }
159
+ }
160
+ return stats;
161
+ }
115
162
  // Renormalize entry energies to be relative to elemental references (formal chemical potentials).
116
163
  // For each entry, subtracts sum(x_i * E_ref_i) from its energy per atom.
117
164
  export const renormalize_entries = (entries, el_refs, elements) => entries.map((entry) => {
118
165
  const atoms = count_atoms_in_composition(entry.composition);
166
+ const base_epa = get_energy_per_atom(entry);
167
+ if (!(atoms > 0) || !Number.isFinite(base_epa))
168
+ return entry;
119
169
  let renorm_energy = 0;
120
170
  for (const el of elements) {
121
- const frac = atoms > 0 ? (entry.composition[el] ?? 0) / atoms : 0;
171
+ const frac = (entry.composition[el] ?? 0) / atoms;
122
172
  const ref = el_refs[el];
123
- if (ref)
124
- renorm_energy += frac * get_energy_per_atom(ref);
173
+ if (!ref)
174
+ continue;
175
+ const ref_epa = get_energy_per_atom(ref);
176
+ if (!Number.isFinite(ref_epa))
177
+ return entry;
178
+ renorm_energy += frac * ref_epa;
125
179
  }
126
- const new_energy_per_atom = get_energy_per_atom(entry) - renorm_energy;
180
+ const new_energy_per_atom = base_epa - renorm_energy;
127
181
  return {
128
182
  ...entry,
129
183
  energy: new_energy_per_atom * atoms,
@@ -137,7 +191,10 @@ export function build_hyperplanes(min_entries, el_refs, elements) {
137
191
  const n_elems = elements.length;
138
192
  const element_ref_energies = elements.map((element) => {
139
193
  const ref_entry = el_refs[element];
140
- return ref_entry ? get_energy_per_atom(ref_entry) : 0;
194
+ if (!ref_entry)
195
+ return 0;
196
+ const epa = get_energy_per_atom(ref_entry);
197
+ return Number.isFinite(epa) ? epa : 0;
141
198
  });
142
199
  const always_include = new Set(Object.values(el_refs));
143
200
  const tol = 1e-6; // PhaseDiagram.formation_energy_tol
@@ -148,6 +205,8 @@ export function build_hyperplanes(min_entries, el_refs, elements) {
148
205
  const atom_count = count_atoms_in_composition(entry.composition);
149
206
  const composition = entry.composition;
150
207
  const energy_per_atom = get_energy_per_atom(entry);
208
+ if (!(atom_count > 0) || !Number.isFinite(energy_per_atom))
209
+ continue;
151
210
  const row = Array(n_elems + 1).fill(0);
152
211
  let ref_energy = 0;
153
212
  for (let elem_idx = 0; elem_idx < n_elems; elem_idx++) {
@@ -670,19 +729,19 @@ export function get_ternary_combinations(elements) {
670
729
  return combos;
671
730
  }
672
731
  let nd_cache = null;
673
- // Content-based fingerprint for N-D result caching. Uses sorted formula keys
674
- // so deserialized Web Worker copies match and different compositions never collide.
675
- export function make_nd_cache_key(entries, formal_chempots, default_min_limit, limits) {
676
- const keyed = entries
677
- .map((entry) => [
732
+ // Fingerprint for N-D / async-compute cache keys (computed EPA; NaN → null).
733
+ export function entry_fingerprint(entry) {
734
+ const epa = get_energy_per_atom(entry);
735
+ return JSON.stringify([
678
736
  formula_key_from_composition(entry.composition),
679
- entry.energy,
680
- entry.is_stable ?? ``,
681
- entry.e_above_hull ?? ``,
682
- entry.exclude_from_hull ?? ``,
683
- ].join(`:`))
684
- .sort();
685
- return `${keyed.join(`,`)}|${formal_chempots}|${default_min_limit}|${JSON.stringify(limits ?? {})}`;
737
+ Number.isFinite(epa) ? epa : null,
738
+ entry.is_stable ?? null,
739
+ entry.e_above_hull ?? null,
740
+ entry.exclude_from_hull ?? null,
741
+ ]);
742
+ }
743
+ export function make_nd_cache_key(entries, formal_chempots, default_min_limit, limits) {
744
+ return `${entries.map(entry_fingerprint).sort().join(`,`)}|${formal_chempots}|${default_min_limit}|${JSON.stringify(limits ?? {})}`;
686
745
  }
687
746
  // === Main Pipeline ===
688
747
  // Compute the full chemical potential diagram from entries and config.
@@ -1,6 +1,7 @@
1
1
  // Export helpers for chemical potential diagrams (shared between 2D and 3D views).
2
2
  import { dpi_to_scale } from '../io/export';
3
3
  import { download } from '../io/fetch';
4
+ import { escape_html as xml_escape } from '../utils';
4
5
  import * as THREE from 'three';
5
6
  import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
6
7
  export const get_json_string = (payload) => JSON.stringify(payload, null, 2);
@@ -77,12 +78,6 @@ export function export_png_file(wrapper, basename, png_dpi) {
77
78
  download(blob, `${basename}.png`, `image/png`);
78
79
  }, `image/png`);
79
80
  }
80
- const xml_escape = (text) => text
81
- .replaceAll(`&`, `&amp;`)
82
- .replaceAll(`<`, `&lt;`)
83
- .replaceAll(`>`, `&gt;`)
84
- .replaceAll(`"`, `&quot;`)
85
- .replaceAll(`'`, `&#39;`);
86
81
  // SVG snapshot: rasterized canvas as embedded image + overlay labels as real text nodes
87
82
  export function export_svg_file(wrapper, basename, view_settings) {
88
83
  const gl_canvas = get_gl_canvas(wrapper);
@@ -1,21 +1,20 @@
1
1
  import { analyze_temperature_data, filter_entries_at_temperature, } from '../convex-hull/helpers';
2
2
  import { CHEMPOT_DEFAULTS } from './types';
3
3
  export const get_projection_source_entries = (entries, temp_filtered_entries) => (temp_filtered_entries.length > 0 ? temp_filtered_entries : entries);
4
- const resolve_temp_filter_options = (config, props) => ({
5
- interpolate: config.interpolate_temperature ??
6
- props.interpolate_temperature ??
7
- CHEMPOT_DEFAULTS.interpolate_temperature,
8
- max_interpolation_gap: config.max_interpolation_gap ??
9
- props.max_interpolation_gap ??
10
- CHEMPOT_DEFAULTS.max_interpolation_gap,
11
- });
12
4
  export function get_temp_filter_payload(entries, temperature, config, props) {
13
5
  const { has_temp_data, available_temperatures } = analyze_temperature_data(entries);
14
6
  if (!has_temp_data || temperature === undefined) {
15
7
  return { has_temp_data, available_temperatures, temp_filtered_entries: entries };
16
8
  }
17
- const filter_options = resolve_temp_filter_options(config, props);
18
- const temp_filtered_entries = filter_entries_at_temperature(entries, temperature, filter_options);
9
+ // Option resolution order: config beats props beats defaults
10
+ const temp_filtered_entries = filter_entries_at_temperature(entries, temperature, {
11
+ interpolate: config.interpolate_temperature ??
12
+ props.interpolate_temperature ??
13
+ CHEMPOT_DEFAULTS.interpolate_temperature,
14
+ max_interpolation_gap: config.max_interpolation_gap ??
15
+ props.max_interpolation_gap ??
16
+ CHEMPOT_DEFAULTS.max_interpolation_gap,
17
+ });
19
18
  return { has_temp_data, available_temperatures, temp_filtered_entries };
20
19
  }
21
20
  export function get_valid_temperature(temperature, has_temp_data, available_temperatures) {
@@ -210,15 +210,13 @@
210
210
  class:hovered={hovered_element === segment.element}
211
211
  onmouseenter={() => interactive && (hovered_element = segment.element)}
212
212
  onmouseleave={() => interactive && (hovered_element = null)}
213
- {...interactive
214
- ? {
215
- role: `button`,
216
- tabindex: 0,
217
- 'aria-label': `${segment.element}: ${segment.amount} ${
218
- segment.amount === 1 ? `atom` : `atoms`
219
- } (${format_num(segment.fraction, `.1~%`)})`,
220
- }
221
- : {}}
213
+ {...interactive && {
214
+ role: `button`,
215
+ tabindex: 0,
216
+ 'aria-label': `${segment.element}: ${segment.amount} ${
217
+ segment.amount === 1 ? `atom` : `atoms`
218
+ } (${format_num(segment.fraction, `.1~%`)})`,
219
+ }}
222
220
  >
223
221
  <title>
224
222
  {segment.element}: {segment.amount}
@@ -60,14 +60,9 @@
60
60
  { value: `bar`, icon: `Graph`, label: `Bar Chart` },
61
61
  ] as const
62
62
 
63
- const color_scheme_options = [
64
- { value: `Vesta`, icon: `ColorPalette`, label: `Vesta` },
65
- { value: `Jmol`, icon: `ColorPalette`, label: `Jmol` },
66
- { value: `Alloy`, icon: `ColorPalette`, label: `Alloy` },
67
- { value: `Pastel`, icon: `ColorPalette`, label: `Pastel` },
68
- { value: `Muted`, icon: `ColorPalette`, label: `Muted` },
69
- { value: `Dark Mode`, icon: `ColorPalette`, label: `Dark Mode` },
70
- ] as const
63
+ const color_scheme_options = (
64
+ [`Vesta`, `Jmol`, `Alloy`, `Pastel`, `Muted`, `Dark Mode`] as const
65
+ ).map((value) => ({ value, icon: `ColorPalette`, label: value }))
71
66
 
72
67
  const export_options = [
73
68
  { value: `copy_formula`, icon: `Copy`, label: `Copy Formula` },
@@ -2,7 +2,7 @@
2
2
  import type { ColorSchemeName } from '../colors'
3
3
  import { ELEMENT_COLOR_SCHEMES, luminance } from '../colors'
4
4
  import type { ElementSymbol } from '../element'
5
- import { element_data } from '../element'
5
+ import { element_by_symbol } from '../element'
6
6
  import ElementTile from '../element/ElementTile.svelte'
7
7
  import { format_num } from '../labels'
8
8
  import type { HTMLAttributes } from 'svelte/elements'
@@ -65,7 +65,7 @@
65
65
  let tooltip_pos = $state({ x: 0, y: 0 })
66
66
 
67
67
  const hovered_elem_data = $derived(
68
- hovered_element ? element_data.find((el) => el.symbol === hovered_element) : null,
68
+ hovered_element ? (element_by_symbol.get(hovered_element) ?? null) : null,
69
69
  )
70
70
 
71
71
  // Format formula as plain text for clipboard: "Li2 O" or "Fe(+3)2 O(-2)3"
@@ -61,32 +61,12 @@
61
61
  },
62
62
  ]
63
63
 
64
- const SUBSCRIPT_TO_ASCII: Record<string, string> = {
65
- [`\u2080`]: `0`,
66
- [`\u2081`]: `1`,
67
- [`\u2082`]: `2`,
68
- [`\u2083`]: `3`,
69
- [`\u2084`]: `4`,
70
- [`\u2085`]: `5`,
71
- [`\u2086`]: `6`,
72
- [`\u2087`]: `7`,
73
- [`\u2088`]: `8`,
74
- [`\u2089`]: `9`,
75
- }
76
-
77
- const SUPERSCRIPT_TO_ASCII: Record<string, string> = {
78
- [`\u2070`]: `0`,
79
- [`\u00B9`]: `1`,
80
- [`\u00B2`]: `2`,
81
- [`\u00B3`]: `3`,
82
- [`\u2074`]: `4`,
83
- [`\u2075`]: `5`,
84
- [`\u2076`]: `6`,
85
- [`\u2077`]: `7`,
86
- [`\u2078`]: `8`,
87
- [`\u2079`]: `9`,
88
- [`\u207A`]: `+`,
89
- [`\u207B`]: `-`,
64
+ // Unicode subscript/superscript digits and signs -> ASCII equivalents
65
+ const UNICODE_TO_ASCII: Record<string, string> = {
66
+ ...Object.fromEntries([...`₀₁₂₃₄₅₆₇₈₉`].map((char, digit) => [char, `${digit}`])),
67
+ ...Object.fromEntries([...`⁰¹²³⁴⁵⁶⁷⁸⁹`].map((char, digit) => [char, `${digit}`])),
68
+ [`⁺`]: `+`,
69
+ [`⁻`]: `-`,
90
70
  }
91
71
 
92
72
  let {
@@ -152,51 +132,31 @@
152
132
  const has_storage = typeof localStorage !== `undefined`
153
133
  const history_pins_key = $derived(`${history_key}-pins`)
154
134
 
155
- function load_history(): string[] {
135
+ function load_entries(key: string): string[] {
156
136
  if (max_history <= 0 || !has_storage) return []
157
137
  try {
158
- const raw = localStorage.getItem(history_key)
159
- if (!raw) return []
160
- const parsed: unknown = JSON.parse(raw)
138
+ const parsed: unknown = JSON.parse(localStorage.getItem(key) ?? `null`)
161
139
  if (!Array.isArray(parsed)) return []
162
- return parsed
163
- .filter((item): item is string => typeof item === `string`)
164
- .slice(0, max_history)
140
+ return parsed.filter((item): item is string => typeof item === `string`)
165
141
  } catch {
166
142
  return []
167
143
  }
168
144
  }
169
145
 
170
- function save_history(entries: string[]): void {
146
+ function save_entries(key: string, entries: string[]): void {
171
147
  if (max_history <= 0 || !has_storage) return
172
148
  try {
173
- localStorage.setItem(history_key, JSON.stringify(entries.slice(0, max_history)))
149
+ localStorage.setItem(key, JSON.stringify(entries))
174
150
  } catch {
175
151
  // localStorage may be unavailable (e.g. private browsing)
176
152
  }
177
153
  }
178
154
 
179
- function load_pinned(): string[] {
180
- if (max_history <= 0 || !has_storage) return []
181
- try {
182
- const raw = localStorage.getItem(history_pins_key)
183
- if (!raw) return []
184
- const parsed: unknown = JSON.parse(raw)
185
- if (!Array.isArray(parsed)) return []
186
- return parsed.filter((item): item is string => typeof item === `string`)
187
- } catch {
188
- return []
189
- }
190
- }
191
-
192
- function save_pinned(entries: string[]): void {
193
- if (max_history <= 0 || !has_storage) return
194
- try {
195
- localStorage.setItem(history_pins_key, JSON.stringify(entries))
196
- } catch {
197
- // localStorage may be unavailable
198
- }
199
- }
155
+ const load_history = () => load_entries(history_key).slice(0, max_history)
156
+ const load_pinned = () => load_entries(history_pins_key)
157
+ const save_history = (entries: string[]) =>
158
+ save_entries(history_key, entries.slice(0, max_history))
159
+ const save_pinned = (entries: string[]) => save_entries(history_pins_key, entries)
200
160
 
201
161
  let history = $state<string[]>(load_history())
202
162
  let pinned_history = $state<string[]>(load_pinned())
@@ -325,11 +285,8 @@
325
285
 
326
286
  function normalize_unicode_formula(input: string): string {
327
287
  let normalized = input
328
- for (const [subscript, ascii] of Object.entries(SUBSCRIPT_TO_ASCII)) {
329
- normalized = normalized.replaceAll(subscript, ascii)
330
- }
331
- for (const [superscript, ascii] of Object.entries(SUPERSCRIPT_TO_ASCII)) {
332
- normalized = normalized.replaceAll(superscript, ascii)
288
+ for (const [unicode_char, ascii] of Object.entries(UNICODE_TO_ASCII)) {
289
+ normalized = normalized.replaceAll(unicode_char, ascii)
333
290
  }
334
291
  return (
335
292
  normalized
@@ -410,13 +367,10 @@
410
367
  return `${prefix}${token.element}${suffix}`
411
368
  }
412
369
 
413
- function token_chip_label(
370
+ // Chip labels show an explicit + prefix for include tokens; serialized values omit it
371
+ const token_chip_label = (
414
372
  token: Pick<FormulaFilterToken, `operator` | `element` | `constraint`>,
415
- ): string {
416
- const prefix = token.operator === `exclude` ? `-` : `+`
417
- const suffix = token.constraint ? `:${token.constraint}` : ``
418
- return `${prefix}${token.element}${suffix}`
419
- }
373
+ ): string => (token.operator === `include` ? `+` : ``) + serialize_token(token)
420
374
 
421
375
  function parse_token(raw_token: string): FormulaFilterToken {
422
376
  const token = raw_token.trim()
@@ -571,15 +525,8 @@
571
525
  // Otherwise parse as formula (already returns sorted by default)
572
526
  // For formulas with wildcards, we can't parse them normally
573
527
  if (has_wildcards(trimmed)) {
574
- // Use shared utility and extract unique elements
575
528
  const tokens = parse_formula_with_wildcards(trimmed)
576
- const unique_elements: string[] = []
577
- for (const token of tokens) {
578
- if (token.element !== null && !unique_elements.includes(token.element)) {
579
- unique_elements.push(token.element)
580
- }
581
- }
582
- const elements = unique_elements.sort()
529
+ const elements = [...new Set(tokens.flatMap((token) => token.element ?? []))].sort()
583
530
  const wildcards = tokens.filter((token) => token.element === null).map(() => `*`)
584
531
  return [...elements, ...wildcards]
585
532
  }
@@ -1124,24 +1071,7 @@
1124
1071
  font-size: 0.88em;
1125
1072
  color: inherit;
1126
1073
  }
1127
- .history-remove {
1128
- min-width: 24px;
1129
- min-height: 24px;
1130
- display: flex;
1131
- align-items: center;
1132
- justify-content: center;
1133
- background: none;
1134
- border: none;
1135
- cursor: pointer;
1136
- padding: 3pt;
1137
- border-radius: 50%;
1138
- opacity: 0.3;
1139
- color: inherit;
1140
- &:hover {
1141
- opacity: 0.8;
1142
- background: rgba(128, 128, 128, 0.15);
1143
- }
1144
- }
1074
+ .history-remove,
1145
1075
  .history-pin {
1146
1076
  display: flex;
1147
1077
  align-items: center;
@@ -1158,6 +1088,10 @@
1158
1088
  background: rgba(128, 128, 128, 0.15);
1159
1089
  }
1160
1090
  }
1091
+ .history-remove {
1092
+ min-width: 24px;
1093
+ min-height: 24px;
1094
+ }
1161
1095
  .examples-wrapper {
1162
1096
  position: relative;
1163
1097
  }
@@ -3,17 +3,15 @@ import { is_elem_symbol } from '../element';
3
3
  import { ELEMENT_ELECTRONEGATIVITY_MAP, parse_composition } from './parse';
4
4
  // Extract composition from structure object
5
5
  const structure_to_composition = (structure) => {
6
- if (!structure.sites || !Array.isArray(structure.sites)) {
7
- throw new Error(`Invalid structure object`);
6
+ if (!Array.isArray(structure.sites)) {
7
+ throw new TypeError(`Invalid structure object`);
8
8
  }
9
9
  const composition = {};
10
10
  for (const site of structure.sites) {
11
- if (site.species && Array.isArray(site.species)) {
12
- for (const species of site.species) {
13
- const element = species.element;
14
- const occu = species.occu ?? 1;
15
- composition[element] = (composition[element] ?? 0) + occu;
16
- }
11
+ if (!Array.isArray(site.species))
12
+ continue;
13
+ for (const species of site.species) {
14
+ composition[species.element] = (composition[species.element] ?? 0) + (species.occu ?? 1);
17
15
  }
18
16
  }
19
17
  return composition;