matterviz 0.3.5 → 0.3.7

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 (229) hide show
  1. package/dist/MillerIndexInput.svelte +5 -5
  2. package/dist/api/optimade.js +3 -3
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
  8. package/dist/brillouin/compute.js +21 -21
  9. package/dist/brillouin/index.d.ts +1 -1
  10. package/dist/brillouin/index.js +0 -1
  11. package/dist/brillouin/types.d.ts +8 -13
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
  15. package/dist/chempot-diagram/compute.js +1 -7
  16. package/dist/chempot-diagram/temperature.d.ts +1 -1
  17. package/dist/chempot-diagram/temperature.js +1 -3
  18. package/dist/chempot-diagram/types.d.ts +4 -9
  19. package/dist/colors/index.js +5 -5
  20. package/dist/composition/Composition.svelte +2 -1
  21. package/dist/composition/Formula.svelte +7 -4
  22. package/dist/composition/FormulaFilter.svelte +1 -3
  23. package/dist/composition/format.js +4 -4
  24. package/dist/composition/parse.d.ts +2 -1
  25. package/dist/composition/parse.js +61 -46
  26. package/dist/convex-hull/ConvexHull2D.svelte +62 -51
  27. package/dist/convex-hull/ConvexHull3D.svelte +101 -90
  28. package/dist/convex-hull/ConvexHull4D.svelte +70 -58
  29. package/dist/convex-hull/ConvexHullControls.svelte +24 -35
  30. package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
  31. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
  32. package/dist/convex-hull/ConvexHullStats.svelte +9 -2
  33. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
  34. package/dist/convex-hull/GasPressureControls.svelte +7 -7
  35. package/dist/convex-hull/StructurePopup.svelte +65 -30
  36. package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
  37. package/dist/convex-hull/TemperatureSlider.svelte +8 -5
  38. package/dist/convex-hull/barycentric-coords.d.ts +2 -2
  39. package/dist/convex-hull/barycentric-coords.js +2 -2
  40. package/dist/convex-hull/gas-thermodynamics.js +2 -4
  41. package/dist/convex-hull/helpers.d.ts +13 -2
  42. package/dist/convex-hull/helpers.js +37 -16
  43. package/dist/convex-hull/index.d.ts +1 -0
  44. package/dist/convex-hull/index.js +1 -0
  45. package/dist/convex-hull/thermodynamics.d.ts +2 -1
  46. package/dist/convex-hull/thermodynamics.js +7 -7
  47. package/dist/convex-hull/types.d.ts +15 -15
  48. package/dist/effects.svelte.d.ts +12 -0
  49. package/dist/effects.svelte.js +37 -0
  50. package/dist/element/BohrAtom.svelte +4 -4
  51. package/dist/element/data.json.gz.d.ts +3 -1
  52. package/dist/element/index.d.ts +1 -1
  53. package/dist/element/index.js +0 -1
  54. package/dist/fermi-surface/FermiSurface.svelte +4 -4
  55. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  56. package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
  57. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  58. package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
  59. package/dist/fermi-surface/compute.js +2 -2
  60. package/dist/fermi-surface/export.js +13 -26
  61. package/dist/fermi-surface/parse.js +8 -12
  62. package/dist/fermi-surface/types.d.ts +2 -5
  63. package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
  64. package/dist/heatmap-matrix/index.js +6 -6
  65. package/dist/io/decompress.d.ts +2 -1
  66. package/dist/io/decompress.js +1 -1
  67. package/dist/io/export.js +1 -1
  68. package/dist/io/index.d.ts +1 -1
  69. package/dist/io/index.js +0 -1
  70. package/dist/io/url-drop.js +7 -1
  71. package/dist/isosurface/IsosurfaceControls.svelte +11 -25
  72. package/dist/isosurface/slice.js +1 -1
  73. package/dist/isosurface/types.js +12 -12
  74. package/dist/labels.d.ts +1 -1
  75. package/dist/labels.js +14 -11
  76. package/dist/layout/InfoTag.svelte +6 -4
  77. package/dist/layout/PropertyFilter.svelte +4 -2
  78. package/dist/layout/json-tree/JsonTree.svelte +22 -14
  79. package/dist/layout/json-tree/JsonValue.svelte +2 -2
  80. package/dist/layout/json-tree/types.d.ts +3 -2
  81. package/dist/layout/json-tree/types.js +0 -1
  82. package/dist/layout/json-tree/utils.d.ts +4 -4
  83. package/dist/layout/json-tree/utils.js +12 -20
  84. package/dist/marching-cubes.js +13 -15
  85. package/dist/math.d.ts +11 -1
  86. package/dist/math.js +15 -6
  87. package/dist/overlays/DragControlTab.svelte +98 -0
  88. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  89. package/dist/overlays/DraggablePane.svelte +7 -84
  90. package/dist/overlays/index.d.ts +1 -0
  91. package/dist/overlays/index.js +1 -0
  92. package/dist/periodic-table/PeriodicTable.svelte +11 -11
  93. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
  94. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  95. package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
  96. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  97. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
  98. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
  99. package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
  100. package/dist/phase-diagram/build-diagram.js +11 -18
  101. package/dist/phase-diagram/diagram-input.d.ts +5 -9
  102. package/dist/phase-diagram/index.d.ts +2 -2
  103. package/dist/phase-diagram/index.js +0 -2
  104. package/dist/phase-diagram/parse.d.ts +2 -2
  105. package/dist/phase-diagram/parse.js +6 -10
  106. package/dist/phase-diagram/svg-to-diagram.js +15 -15
  107. package/dist/phase-diagram/types.d.ts +5 -11
  108. package/dist/phase-diagram/utils.d.ts +2 -2
  109. package/dist/phase-diagram/utils.js +9 -11
  110. package/dist/plot/BarPlot.svelte +162 -314
  111. package/dist/plot/BarPlot.svelte.d.ts +5 -4
  112. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  113. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  114. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  115. package/dist/plot/ColorBar.svelte +19 -17
  116. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  117. package/dist/plot/FillArea.svelte +2 -4
  118. package/dist/plot/FillArea.svelte.d.ts +1 -1
  119. package/dist/plot/Histogram.svelte +167 -281
  120. package/dist/plot/Histogram.svelte.d.ts +1 -1
  121. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  122. package/dist/plot/InteractiveAxisLabel.svelte +5 -3
  123. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  124. package/dist/plot/PlotAxis.svelte +169 -0
  125. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  126. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  127. package/dist/plot/ReferenceLine3D.svelte +53 -51
  128. package/dist/plot/ReferencePlane.svelte +39 -42
  129. package/dist/plot/ScatterPlot.svelte +300 -367
  130. package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
  131. package/dist/plot/ScatterPlot3D.svelte +33 -6
  132. package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
  133. package/dist/plot/ScatterPlot3DControls.svelte +9 -9
  134. package/dist/plot/ScatterPlotControls.svelte +3 -4
  135. package/dist/plot/ScatterPoint.svelte +18 -27
  136. package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
  137. package/dist/plot/Surface3D.svelte +4 -7
  138. package/dist/plot/ZeroLines.svelte +2 -1
  139. package/dist/plot/ZeroLines.svelte.d.ts +2 -1
  140. package/dist/plot/ZoomRect.svelte +2 -2
  141. package/dist/plot/ZoomRect.svelte.d.ts +3 -3
  142. package/dist/plot/adaptive-density.d.ts +69 -0
  143. package/dist/plot/adaptive-density.js +191 -0
  144. package/dist/plot/auto-place.d.ts +43 -0
  145. package/dist/plot/auto-place.js +122 -0
  146. package/dist/plot/axis-utils.js +3 -5
  147. package/dist/plot/binned-scatter-types.d.ts +59 -0
  148. package/dist/plot/binned-scatter-types.js +1 -0
  149. package/dist/plot/data-cleaning.js +1 -1
  150. package/dist/plot/data-transform.js +1 -1
  151. package/dist/plot/fill-utils.d.ts +4 -9
  152. package/dist/plot/fill-utils.js +29 -44
  153. package/dist/plot/index.d.ts +4 -0
  154. package/dist/plot/index.js +2 -0
  155. package/dist/plot/interactions.d.ts +4 -4
  156. package/dist/plot/interactions.js +4 -3
  157. package/dist/plot/layout.d.ts +20 -2
  158. package/dist/plot/layout.js +59 -16
  159. package/dist/plot/reference-line.d.ts +1 -1
  160. package/dist/plot/reference-line.js +9 -11
  161. package/dist/plot/scales.d.ts +1 -1
  162. package/dist/plot/scales.js +20 -23
  163. package/dist/plot/types.d.ts +30 -58
  164. package/dist/plot/types.js +2 -6
  165. package/dist/plot/utils/label-placement.d.ts +24 -3
  166. package/dist/plot/utils/label-placement.js +82 -12
  167. package/dist/plot/utils/series-visibility.d.ts +8 -2
  168. package/dist/plot/utils/series-visibility.js +23 -5
  169. package/dist/rdf/RdfPlot.svelte +5 -5
  170. package/dist/rdf/calc-rdf.js +3 -3
  171. package/dist/sanitize.d.ts +2 -0
  172. package/dist/sanitize.js +2 -0
  173. package/dist/spectral/Bands.svelte +1 -1
  174. package/dist/spectral/BandsAndDos.svelte +22 -16
  175. package/dist/spectral/BrillouinBandsDos.svelte +20 -16
  176. package/dist/spectral/Dos.svelte +1 -1
  177. package/dist/spectral/helpers.d.ts +4 -2
  178. package/dist/spectral/helpers.js +44 -35
  179. package/dist/spectral/index.d.ts +1 -1
  180. package/dist/spectral/index.js +0 -1
  181. package/dist/structure/AtomLegend.svelte +23 -6
  182. package/dist/structure/AtomLegend.svelte.d.ts +1 -0
  183. package/dist/structure/CanvasTooltip.svelte +9 -9
  184. package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
  185. package/dist/structure/CellSelect.svelte +14 -16
  186. package/dist/structure/Structure.svelte +317 -68
  187. package/dist/structure/Structure.svelte.d.ts +4 -2
  188. package/dist/structure/StructureControls.svelte +20 -45
  189. package/dist/structure/StructureExportPane.svelte +2 -1
  190. package/dist/structure/StructureInfoPane.svelte +10 -8
  191. package/dist/structure/StructureScene.svelte +527 -177
  192. package/dist/structure/StructureScene.svelte.d.ts +5 -2
  193. package/dist/structure/atom-properties.js +4 -4
  194. package/dist/structure/bond-order-perception.js +115 -98
  195. package/dist/structure/bonding.d.ts +27 -1
  196. package/dist/structure/bonding.js +187 -16
  197. package/dist/structure/export.js +1 -1
  198. package/dist/structure/index.d.ts +3 -2
  199. package/dist/structure/index.js +0 -2
  200. package/dist/structure/parse.js +88 -59
  201. package/dist/symmetry/WyckoffTable.svelte +7 -0
  202. package/dist/symmetry/index.js +13 -14
  203. package/dist/table/HeatmapTable.svelte +45 -66
  204. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  205. package/dist/table/ToggleMenu.svelte +19 -10
  206. package/dist/theme/themes.mjs +12 -0
  207. package/dist/tooltip/index.d.ts +1 -1
  208. package/dist/tooltip/index.js +0 -1
  209. package/dist/trajectory/Trajectory.svelte +43 -15
  210. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
  211. package/dist/trajectory/extract.js +1 -1
  212. package/dist/trajectory/frame-reader.js +4 -4
  213. package/dist/trajectory/helpers.d.ts +5 -4
  214. package/dist/trajectory/helpers.js +9 -17
  215. package/dist/trajectory/index.d.ts +2 -2
  216. package/dist/trajectory/index.js +2 -2
  217. package/dist/trajectory/parse/ase.js +4 -4
  218. package/dist/trajectory/parse/hdf5.js +1 -1
  219. package/dist/trajectory/parse/index.js +2 -3
  220. package/dist/trajectory/parse/lammps.js +1 -1
  221. package/dist/trajectory/parse/vasp.js +1 -1
  222. package/dist/trajectory/plotting.d.ts +1 -1
  223. package/dist/trajectory/plotting.js +38 -38
  224. package/dist/trajectory/types.d.ts +1 -1
  225. package/dist/utils.d.ts +1 -0
  226. package/dist/utils.js +9 -0
  227. package/dist/xrd/calc-xrd.js +3 -4
  228. package/dist/xrd/parse.js +1 -1
  229. package/package.json +42 -22
@@ -89,11 +89,11 @@
89
89
  .join(` `),
90
90
  )
91
91
 
92
- function handle_copy(event: ClipboardEvent) {
92
+ function handle_copy(event: ClipboardEvent & { currentTarget: HTMLElement }) {
93
93
  const selection = window.getSelection()
94
94
  // Only intercept if user selected text fully within this formula
95
95
  if (!selection || selection.isCollapsed) return
96
- const formula_el = event.currentTarget as HTMLElement
96
+ const formula_el = event.currentTarget
97
97
  if (
98
98
  !formula_el.contains(selection.anchorNode) ||
99
99
  !formula_el.contains(selection.focusNode)
@@ -102,8 +102,11 @@
102
102
  event.clipboardData?.setData(`text/plain`, plain_text_formula)
103
103
  }
104
104
 
105
- function show_tooltip(element: ElementSymbol, event: MouseEvent) {
106
- const { left, width, top, bottom, right, height } = (event.target as HTMLElement)
105
+ function show_tooltip(
106
+ element: ElementSymbol,
107
+ event: MouseEvent & { currentTarget: HTMLElement },
108
+ ) {
109
+ const { left, width, top, bottom, right, height } = event.currentTarget
107
110
  .getBoundingClientRect()
108
111
  hovered_element = element
109
112
 
@@ -243,9 +243,7 @@
243
243
  close_history()
244
244
  }
245
245
 
246
- function is_pinned(entry: string): boolean {
247
- return pinned_history.includes(entry)
248
- }
246
+ const is_pinned = (entry: string): boolean => pinned_history.includes(entry)
249
247
 
250
248
  // Filtered history: exclude current value to avoid redundant suggestion
251
249
  let visible_history = $derived.by(() => {
@@ -1,5 +1,5 @@
1
1
  import { format_num } from '../labels';
2
- import { ELEMENT_ELECTRONEGATIVITY_MAP, parse_composition } from './parse';
2
+ import { ELEMENT_ELECTRONEGATIVITY_MAP, is_valid_element, parse_composition } from './parse';
3
3
  // Extract composition from structure object
4
4
  const structure_to_composition = (structure) => {
5
5
  if (!structure.sites || !Array.isArray(structure.sites)) {
@@ -17,9 +17,10 @@ const structure_to_composition = (structure) => {
17
17
  }
18
18
  return composition;
19
19
  };
20
+ const is_structure_like = (input) => `sites` in input || `lattice` in input;
20
21
  // Format composition into chemical formula string
21
22
  export const format_composition_formula = (composition, sort_fn, plain_text = false, delim = ` `, amount_format = `.3~s`) => {
22
- const symbols = Object.keys(composition);
23
+ const symbols = Object.keys(composition).filter(is_valid_element);
23
24
  return sort_fn(symbols)
24
25
  .filter((el) => composition[el] && composition[el] > 0)
25
26
  .map((el) => {
@@ -37,9 +38,8 @@ const format_formula_generic = (input, sort_fn, plain_text = false, delim = ` `,
37
38
  let composition;
38
39
  if (typeof input === `string`)
39
40
  composition = parse_composition(input);
40
- else if (`sites` in input || `lattice` in input) {
41
+ else if (is_structure_like(input))
41
42
  composition = structure_to_composition(input);
42
- }
43
43
  else
44
44
  composition = input;
45
45
  return format_composition_formula(composition, sort_fn, plain_text, delim, amount_format);
@@ -34,7 +34,8 @@ export declare function extract_formula_elements(formula: string, { unique, sort
34
34
  sorted?: boolean;
35
35
  }): ElementSymbol[];
36
36
  export declare function generate_chem_sys_subspaces(input: string | CompositionType | ElementSymbol[]): string[];
37
- export declare const normalize_element_symbols: <T extends string>(csv: string, all_symbols?: T[]) => T[];
37
+ export declare function normalize_element_symbols(csv: string): ElementSymbol[];
38
+ export declare function normalize_element_symbols<T extends string>(csv: string, all_symbols: T[]): T[];
38
39
  export type WildcardFormulaToken = {
39
40
  element: ElementSymbol | null;
40
41
  count: number;
@@ -21,8 +21,19 @@ export const is_valid_element = (sym) => ELEM_SYMBOLS.includes(sym);
21
21
  // Check if object has atomic numbers as keys (1-118)
22
22
  const is_atomic_number_composition = (obj) => {
23
23
  const keys = Object.keys(obj);
24
+ const atomic_nums = keys.map(Number);
24
25
  return (keys.length > 0 &&
25
- keys.map(Number).every((num) => Number.isInteger(num) && num >= 1 && num <= 118));
26
+ atomic_nums.every((atomic_num) => Number.isInteger(atomic_num) && Object.hasOwn(ATOMIC_NUMBER_TO_SYMBOL, atomic_num)));
27
+ };
28
+ const format_state = (state) => (state > 0 ? `+` : ``) + state;
29
+ const parse_count = (count) => (count ? parseFloat(count) : 1);
30
+ const format_count = (count) => {
31
+ if (!Number.isFinite(count))
32
+ return `${count}`;
33
+ return Number(count.toPrecision(12)).toLocaleString(`en-US`, {
34
+ maximumSignificantDigits: 12,
35
+ useGrouping: false,
36
+ });
26
37
  };
27
38
  // Convert atomic numbers to element symbols
28
39
  export const atomic_num_to_symbols = (atomic_composition) => {
@@ -32,7 +43,7 @@ export const atomic_num_to_symbols = (atomic_composition) => {
32
43
  if (!symbol)
33
44
  throw new Error(`Invalid atomic number: ${atomic_num_str}`);
34
45
  if (amount > 0)
35
- composition[symbol] = (composition[symbol] || 0) + amount;
46
+ composition[symbol] = (composition[symbol] ?? 0) + amount;
36
47
  }
37
48
  return composition;
38
49
  };
@@ -40,6 +51,8 @@ export const atomic_num_to_symbols = (atomic_composition) => {
40
51
  export const atomic_symbol_to_num = (symbol_composition) => {
41
52
  const atomic_composition = {};
42
53
  for (const [symbol, amount] of Object.entries(symbol_composition)) {
54
+ if (!is_valid_element(symbol))
55
+ throw new Error(`Invalid element symbol: ${symbol}`);
43
56
  const atomic_num = SYMBOL_TO_ATOMIC_NUMBER[symbol];
44
57
  if (!atomic_num)
45
58
  throw new Error(`Invalid element symbol: ${symbol}`);
@@ -52,11 +65,11 @@ export const atomic_symbol_to_num = (symbol_composition) => {
52
65
  // Expand parentheses in chemical formulas
53
66
  const expand_parentheses = (formula) => {
54
67
  while (formula.includes(`(`)) {
55
- formula = formula.replace(/\(([^()]+)\)(\d*)/g, (_match, group, multiplier) => {
56
- const mult = multiplier ? parseInt(multiplier, 10) : 1;
57
- return group.replace(/([A-Z][a-z]?)(\d*)/g, (_m, element, count) => {
58
- const num = (count ? parseInt(count, 10) : 1) * mult;
59
- return element + (num > 1 ? num : ``);
68
+ formula = formula.replace(/\(([^()]+)\)(\d+(?:\.\d+)?|\.\d+)?/g, (_match, group, multiplier) => {
69
+ const mult = parse_count(multiplier);
70
+ return group.replace(/([A-Z][a-z]?)(\d+(?:\.\d+)?|\.\d+)?/g, (_m, element, count) => {
71
+ const count_str = format_count(parse_count(count) * mult);
72
+ return element + (count_str === `1` ? `` : count_str);
60
73
  });
61
74
  });
62
75
  }
@@ -66,12 +79,12 @@ const expand_parentheses = (formula) => {
66
79
  export const parse_formula = (formula) => {
67
80
  const composition = {};
68
81
  const cleaned_formula = expand_parentheses(formula.replace(/\s/g, ``));
69
- for (const match of cleaned_formula.matchAll(/([A-Z][a-z]?)(\d*)/g)) {
82
+ for (const match of cleaned_formula.matchAll(/([A-Z][a-z]?)(\d+(?:\.\d+)?|\.\d+)?/g)) {
70
83
  const element = match[1];
71
- const count = match[2] ? parseInt(match[2], 10) : 1;
84
+ const count = parse_count(match[2]);
72
85
  if (!is_valid_element(element))
73
86
  throw new Error(`Invalid element symbol: ${element}`);
74
- composition[element] = (composition[element] || 0) + count;
87
+ composition[element] = (composition[element] ?? 0) + count;
75
88
  }
76
89
  return composition;
77
90
  };
@@ -82,7 +95,7 @@ export const normalize_composition = (composition) => {
82
95
  }
83
96
  const normalized = {};
84
97
  for (const [element, amount] of Object.entries(composition)) {
85
- if (typeof amount === `number` && amount > 0) {
98
+ if (typeof amount === `number` && amount > 0 && is_valid_element(element)) {
86
99
  normalized[element] = amount;
87
100
  }
88
101
  }
@@ -100,7 +113,7 @@ export const sanitize_composition_keys = (composition) => {
100
113
  if (typeof amount !== `number` || amount <= 0)
101
114
  continue;
102
115
  // Extract first valid element symbol from key (e.g. "B0." -> "B", "Fe2+" -> "Fe")
103
- const elem = (key.match(/[A-Z][a-z]?/g) || []).find(is_valid_element);
116
+ const elem = (key.match(/[A-Z][a-z]?/g) ?? []).find(is_valid_element);
104
117
  if (elem)
105
118
  sanitized[elem] = (sanitized[elem] || 0) + amount;
106
119
  }
@@ -114,6 +127,8 @@ export const fractional_composition = (composition, by_weight = false) => {
114
127
  const filtered = Object.fromEntries(Object.entries(composition).filter(([, amount]) => amount > 0));
115
128
  if (by_weight) {
116
129
  const element_weights = Object.fromEntries(Object.entries(filtered).map(([element, amount]) => {
130
+ if (!is_valid_element(element))
131
+ throw new Error(`Unknown element: ${element}`);
117
132
  const atomic_mass = ATOMIC_WEIGHTS.get(element);
118
133
  if (!atomic_mass)
119
134
  throw new Error(`Unknown element: ${element}`);
@@ -162,11 +177,16 @@ export const get_reduced_formula = (composition) => {
162
177
  const divisor = amounts.reduce((acc, amt) => gcd(acc, amt));
163
178
  if (divisor <= 1)
164
179
  return composition;
165
- return Object.fromEntries(Object.entries(composition).map(([elem, amt]) => [elem, amt / divisor]));
180
+ const reduced = {};
181
+ for (const [elem, amt] of Object.entries(composition)) {
182
+ if (is_valid_element(elem))
183
+ reduced[elem] = amt / divisor;
184
+ }
185
+ return reduced;
166
186
  };
167
187
  // Calculate molecular weight (sum of atomic masses * amounts)
168
188
  export const get_molecular_weight = (composition) => Object.entries(composition).reduce((total, [elem, amount]) => {
169
- const mass = ATOMIC_WEIGHTS.get(elem) ?? 0;
189
+ const mass = is_valid_element(elem) ? (ATOMIC_WEIGHTS.get(elem) ?? 0) : 0;
170
190
  return total + mass * amount;
171
191
  }, 0);
172
192
  // Parse oxidation state string (e.g. "+2", "2+", "-", "[2-]") to number
@@ -194,12 +214,12 @@ export const parse_formula_with_oxidation = (formula, strict = false) => {
194
214
  // Regex to match: Element, optional oxidation state and/or count in either order
195
215
  // Pattern: ([A-Z][a-z]?) - element symbol
196
216
  // Followed by one of:
197
- // - oxidation then optional count: (?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])(\d*)
198
- // - count then optional oxidation: (\d+)(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])?
217
+ // - oxidation then optional count: (?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])(count?)
218
+ // - count then optional oxidation: count(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])?
199
219
  // - just oxidation: (?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])
200
- // - just count: (\d+)
220
+ // - just count: count
201
221
  // - neither
202
- const regex = /([A-Z][a-z]?)(?:(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])(\d*)|(\d+)(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])?)?/g;
222
+ const regex = /([A-Z][a-z]?)(?:(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])((?:\d+(?:\.\d+)?|\.\d+)?)|((?:\d+(?:\.\d+)?|\.\d+))(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])?)?/g;
203
223
  let match;
204
224
  let orig_idx = 0;
205
225
  while ((match = regex.exec(cleaned_formula)) !== null) {
@@ -208,7 +228,7 @@ export const parse_formula_with_oxidation = (formula, strict = false) => {
208
228
  // Count can be in group 4 (after oxidation) or 5 (before oxidation)
209
229
  const oxidation_str = match[2] || match[3] || match[6] || match[7];
210
230
  const count_str = match[4] || match[5];
211
- const count = count_str ? parseInt(count_str, 10) : 1;
231
+ const count = parse_count(count_str);
212
232
  if (!is_valid_element(element))
213
233
  throw new Error(`Invalid element symbol: ${element}`);
214
234
  const oxidation_state = oxidation_str ? parse_oxidation_state(oxidation_str) : undefined;
@@ -226,7 +246,6 @@ export const parse_formula_with_oxidation = (formula, strict = false) => {
226
246
  }
227
247
  else if (strict && existing.oxidation_state !== oxidation_state) {
228
248
  // In strict mode, throw on conflicting oxidation states
229
- const format_state = (state) => (state > 0 ? `+` : ``) + state;
230
249
  throw new Error(`Conflicting oxidation states for ${element}: ${format_state(existing.oxidation_state)} and ${format_state(oxidation_state)}`);
231
250
  }
232
251
  }
@@ -238,12 +257,7 @@ export const parse_formula_with_oxidation = (formula, strict = false) => {
238
257
  };
239
258
  // Convert OxiComposition to ElementWithOxidation array
240
259
  // Does not preserve original order since objects don't have a defined order
241
- export const oxi_composition_to_elements = (composition) => Object.entries(composition).map(([element, data], idx) => ({
242
- element: element,
243
- amount: data.amount,
244
- oxidation_state: data.oxidation_state,
245
- orig_idx: idx,
246
- }));
260
+ export const oxi_composition_to_elements = (composition) => Object.entries(composition).flatMap(([element, { amount, oxidation_state }], idx) => is_valid_element(element) ? [{ element, amount, oxidation_state, orig_idx: idx }] : []);
247
261
  // Extract element symbols from a chemical formula.
248
262
  // Default (unique=true, sorted=true): "NbZr2Nb" -> ["Nb", "Zr"]
249
263
  // unique=false: Fast token extraction preserving order without parentheses expansion
@@ -254,10 +268,15 @@ export const oxi_composition_to_elements = (composition) => Object.entries(compo
254
268
  export function extract_formula_elements(formula, { unique = true, sorted = true } = {}) {
255
269
  if (!unique) {
256
270
  // Fast path: regex token extraction without parentheses expansion
257
- const matches = formula.match(/[A-Z][a-z]?/g) || [];
258
- return matches.filter(is_valid_element);
271
+ const matches = formula.match(/[A-Z][a-z]?/g) ?? [];
272
+ const elements = [];
273
+ for (const match of matches) {
274
+ if (is_valid_element(match))
275
+ elements.push(match);
276
+ }
277
+ return elements;
259
278
  }
260
- const symbols = Object.keys(parse_formula(formula));
279
+ const symbols = Object.keys(parse_formula(formula)).filter(is_valid_element);
261
280
  return sorted ? symbols.sort() : symbols;
262
281
  }
263
282
  // Generate all non-empty subsets of a chemical system as hyphenated strings.
@@ -277,12 +296,12 @@ export function generate_chem_sys_subspaces(input) {
277
296
  elements = uniq;
278
297
  }
279
298
  else {
280
- const keys = Object.keys(input);
281
- for (const elem of keys) {
299
+ elements = [];
300
+ for (const elem of Object.keys(input)) {
282
301
  if (!is_valid_element(elem))
283
302
  throw new Error(`Invalid element symbol: ${elem}`);
303
+ elements.push(elem);
284
304
  }
285
- elements = keys;
286
305
  }
287
306
  const sorted = [...elements].sort();
288
307
  const subspaces = [];
@@ -297,18 +316,14 @@ export function generate_chem_sys_subspaces(input) {
297
316
  }
298
317
  return subspaces;
299
318
  }
300
- // Normalize CSV of element symbols to valid symbols in periodic order.
301
- // Filters invalid symbols, removes duplicates, trims whitespace.
302
- // Example: "Zr, Nb, InvalidElement, H" -> ["H", "Nb", "Zr"]
303
- // Note: Matching is case-sensitive. Use all_symbols to filter against a subset.
304
- export const normalize_element_symbols = (csv, all_symbols) => {
319
+ export function normalize_element_symbols(csv, all_symbols) {
305
320
  const input_set = new Set(csv
306
321
  .split(`,`)
307
322
  .map((sym) => sym.trim())
308
323
  .filter(Boolean));
309
- // Cast needed: ELEM_SYMBOLS is readonly const tuple, T is generic string subtype
310
- return (all_symbols ?? ELEM_SYMBOLS).filter((sym) => input_set.has(sym));
311
- };
324
+ const symbols = all_symbols ?? ELEM_SYMBOLS;
325
+ return symbols.filter((sym) => input_set.has(sym));
326
+ }
312
327
  // Check if input contains wildcard elements (*).
313
328
  // Works for both chemsys format (Li-Fe-*-*) and exact formula format (LiFe*2*).
314
329
  export const has_wildcards = (input) => input.includes(`*`);
@@ -360,20 +375,20 @@ export function parse_formula_with_wildcards(formula) {
360
375
  // Restore wildcards
361
376
  cleaned = cleaned.replace(ELEM_WILDCARD.from_placeholder, `*`);
362
377
  // Regex to match either:
363
- // 1. Standard element symbol with optional count: ([A-Z][a-z]?)(\d*)
364
- // 2. Wildcard with optional count: \*(\d*)
365
- const regex = /([A-Z][a-z]?)(\d*)|(\*)(\d*)/g;
378
+ // 1. Standard element symbol with optional decimal count
379
+ // 2. Wildcard with optional decimal count
380
+ const regex = /([A-Z][a-z]?)((?:\d+(?:\.\d+)?|\.\d+)?)|(\*)((?:\d+(?:\.\d+)?|\.\d+)?)/g;
366
381
  let match;
367
382
  while ((match = regex.exec(cleaned)) !== null) {
368
383
  if (match[3] === `*`) {
369
384
  // Wildcard match
370
- const count = match[4] ? parseInt(match[4], 10) : 1;
385
+ const count = parse_count(match[4]);
371
386
  tokens.push({ element: null, count });
372
387
  }
373
388
  else if (match[1]) {
374
389
  // Element symbol match
375
390
  const element = match[1];
376
- const count = match[2] ? parseInt(match[2], 10) : 1;
391
+ const count = parse_count(match[2]);
377
392
  if (!is_valid_element(element)) {
378
393
  throw new Error(`Invalid element symbol: ${element}`);
379
394
  }
@@ -395,7 +410,7 @@ export function matches_chemsys_wildcard(formula, explicit_elements, wildcard_co
395
410
  // Must contain all explicit elements
396
411
  const formula_set = new Set(formula_elements);
397
412
  for (const elem of explicit_elements) {
398
- if (!formula_set.has(elem))
413
+ if (!is_valid_element(elem) || !formula_set.has(elem))
399
414
  return false;
400
415
  }
401
416
  return true;
@@ -206,39 +206,26 @@
206
206
  // Require formation energy per atom to place along y
207
207
  const e_form = entry.e_form_per_atom
208
208
  if (typeof e_form !== `number`) continue
209
- const total = Object.values(entry.composition).reduce((s, v) => s + v, 0)
209
+ const total = Object.values(entry.composition).reduce((sum, amount) => sum + amount, 0)
210
210
  if (total <= 0) continue
211
211
  const frac_b = (entry.composition[el2] || 0) / total
212
212
  const is_element = is_unary_entry(entry)
213
- coords.push({ ...entry, x: frac_b, y: e_form, z: 0, is_element, visible: true })
213
+ coords.push({ ...entry, x: frac_b, y: e_form, z: 0, is_element })
214
214
  }
215
215
  // Ensure elemental references at x=0 and x=1 with y=0 to close the hull
216
- const el_a: ConvexHullEntry | undefined = coords.find((e) =>
217
- e.is_element && e.x === 0
218
- )
219
- const el_b: ConvexHullEntry | undefined = coords.find((e) =>
220
- e.is_element && e.x === 1
221
- )
222
- if (!el_a) {
216
+ for (const [element, x_coord] of [
217
+ [el1, 0],
218
+ [el2, 1],
219
+ ] as const) {
220
+ if (coords.some((entry) => entry.is_element && entry.x === x_coord)) continue
223
221
  coords.push({
224
- composition: { [el1]: 1 } as CompositionType,
222
+ composition: { [element]: 1 } as CompositionType,
225
223
  energy: 0,
226
- x: 0,
224
+ entry_id: `synthetic-element:${element}`,
225
+ x: x_coord,
227
226
  y: 0,
228
227
  z: 0,
229
228
  is_element: true,
230
- visible: true,
231
- })
232
- }
233
- if (!el_b) {
234
- coords.push({
235
- composition: { [el2]: 1 } as CompositionType,
236
- energy: 0,
237
- x: 1,
238
- y: 0,
239
- z: 0,
240
- is_element: true,
241
- visible: true,
242
229
  })
243
230
  }
244
231
  return coords
@@ -280,9 +267,7 @@
280
267
  const enriched_entries = coords_entries.map((entry) => {
281
268
  const y_hull = thermo.interpolate_hull_2d(computed_hull_points, entry.x)
282
269
  const raw_dist = y_hull == null ? 0 : entry.y - y_hull
283
- return {
284
- ...entry, ...compute_hull_stability(raw_dist, entry.exclude_from_hull), visible: true,
285
- }
270
+ return { ...entry, ...compute_hull_stability(raw_dist, entry.exclude_from_hull) }
286
271
  })
287
272
  return { all_enriched_entries: enriched_entries, hull_points: computed_hull_points }
288
273
  })
@@ -297,25 +282,23 @@
297
282
  DEFAULTS.convex_hull.binary.max_hull_dist_show_phases,
298
283
  ))
299
284
 
300
- // Initialize threshold to auto value on first load
301
- let initialized = $state(false)
285
+ const next_auto_threshold = helpers.auto_threshold_reset(
286
+ DEFAULTS.convex_hull.binary.max_hull_dist_show_phases,
287
+ )
302
288
  $effect(() => {
303
- if (!initialized && all_enriched_entries.length > 0) {
304
- initialized = true
305
- max_hull_dist_show_phases = auto_default_threshold
306
- }
289
+ max_hull_dist_show_phases = next_auto_threshold(
290
+ entries,
291
+ max_hull_dist_show_phases,
292
+ auto_default_threshold,
293
+ ) ?? max_hull_dist_show_phases
307
294
  })
308
295
 
309
- // Filter by threshold and compute visibility
296
+ // Filter by threshold; visibility is a view predicate, not entry state.
310
297
  const plot_entries = $derived(
311
- all_enriched_entries
312
- .filter((e) =>
313
- e.is_stable || (e.e_above_hull ?? 0) <= max_hull_dist_show_phases
314
- )
315
- .map((e) => ({
316
- ...e,
317
- visible: (e.is_stable && show_stable) || (!e.is_stable && show_unstable),
318
- })),
298
+ all_enriched_entries.filter((entry) =>
299
+ helpers.entry_is_stable(entry) ||
300
+ (entry.e_above_hull ?? 0) <= max_hull_dist_show_phases
301
+ ),
319
302
  )
320
303
 
321
304
  // Update bindable entries arrays when plot_entries change (single pass)
@@ -323,7 +306,7 @@
323
306
  const stable: ConvexHullEntry[] = []
324
307
  const unstable: ConvexHullEntry[] = []
325
308
  for (const entry of plot_entries) {
326
- if (entry.is_stable) stable.push(entry)
309
+ if (helpers.entry_is_stable(entry)) stable.push(entry)
327
310
  else unstable.push(entry)
328
311
  }
329
312
  stable_entries = stable
@@ -371,7 +354,11 @@
371
354
  }
372
355
 
373
356
  // Pre-compute visible entries to avoid redundant filtering
374
- const visible_entries = $derived(plot_entries.filter((e) => e.visible))
357
+ const visible_entries = $derived(helpers.visible_entries(
358
+ plot_entries,
359
+ show_stable,
360
+ show_unstable,
361
+ ))
375
362
 
376
363
  const scatter_points_series = $derived.by(() => {
377
364
  const is_energy_mode = color_mode === `energy`
@@ -389,7 +376,7 @@
389
376
  y_vals[idx] = entry.y
390
377
  if (is_energy_mode) color_values[idx] = entry.e_above_hull ?? 0
391
378
 
392
- const is_stable = entry.is_stable || entry.e_above_hull === 0
379
+ const is_stable = helpers.entry_is_stable(entry)
393
380
  const base_radius = entry.size || (is_stable ? 6 : 4)
394
381
  const hl = is_highlighted(entry) ? merged_highlight_style : null
395
382
 
@@ -444,10 +431,8 @@
444
431
 
445
432
  const scatter_series = $derived([scatter_points_series, ...hull_segments_series])
446
433
 
447
- // Map selected_entry to ScatterPlot point index (series_idx: 0 = points series)
448
- // Use object identity comparison (e === entry) instead of entry_id comparison
449
- // because synthetic elemental entries lack entry_id, and undefined === undefined
450
- // would incorrectly match the first entry with undefined entry_id
434
+ // Map selected_entry to ScatterPlot point index (series_idx: 0 = points series).
435
+ // current_entry() keeps selections pointing at the current plot entry object.
451
436
  const selected_scatter_point = $derived.by(() => {
452
437
  const entry = selected_entry
453
438
  if (!entry) return null
@@ -505,9 +490,33 @@
505
490
 
506
491
  // Custom hover tooltip state used with ScatterPlot events
507
492
  let hover_data = $state<HoverData3D<ConvexHullEntry> | null>(null)
493
+ $effect(() => {
494
+ const current_selection = helpers.current_entry(selected_entry, plot_entries)
495
+ if (selected_entry && !current_selection) selected_entry = null
496
+ else if (current_selection && current_selection !== selected_entry) {
497
+ selected_entry = current_selection
498
+ }
499
+ const current_hover = helpers.current_entry(hover_data?.entry, plot_entries)
500
+ if (hover_data?.entry && !current_hover) {
501
+ hover_data = null
502
+ on_point_hover?.(null)
503
+ } else if (hover_data && current_hover && current_hover !== hover_data.entry) {
504
+ hover_data = { ...hover_data, entry: current_hover }
505
+ }
506
+ const current_popup = helpers.current_entry(structure_popup.entry, plot_entries)
507
+ if (structure_popup.open) {
508
+ const structure = current_popup && extract_structure_from_entry(current_popup)
509
+ if (!structure) structure_popup = { open: false, structure: null, entry: null, place_right: true }
510
+ else if (
511
+ current_popup !== structure_popup.entry ||
512
+ structure !== structure_popup.structure
513
+ ) structure_popup = { ...structure_popup, entry: current_popup, structure }
514
+ }
515
+ })
508
516
 
509
517
  const handle_keydown = (event: KeyboardEvent) => {
510
- if ((event.target as HTMLElement).tagName.match(/INPUT|TEXTAREA/)) return
518
+ const target = event.target
519
+ if (target instanceof HTMLElement && target.tagName.match(/INPUT|TEXTAREA/)) return
511
520
  const actions: Record<string, () => void> = {
512
521
  b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
513
522
  s: () => show_stable = !show_stable,
@@ -637,6 +646,8 @@
637
646
  {phase_stats}
638
647
  {stable_entries}
639
648
  {unstable_entries}
649
+ {show_stable}
650
+ {show_unstable}
640
651
  {max_hull_dist_show_phases}
641
652
  {max_hull_dist_show_labels}
642
653
  {label_threshold}
@@ -695,11 +706,11 @@
695
706
  tabindex={-1}
696
707
  onkeydown={handle_keydown}
697
708
  ondrop={handle_file_drop}
698
- ondragover={(event) => {
709
+ ondragover={(event: DragEvent) => {
699
710
  event.preventDefault()
700
711
  drag_over = true
701
712
  }}
702
- ondragleave={(event) => {
713
+ ondragleave={(event: DragEvent) => {
703
714
  event.preventDefault()
704
715
  drag_over = false
705
716
  }}