matterviz 0.3.4 → 0.3.5

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 (125) hide show
  1. package/dist/FilePicker.svelte +1 -1
  2. package/dist/app.css +7 -0
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/compute.js +8 -4
  5. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
  6. package/dist/chempot-diagram/async-compute.svelte.js +5 -4
  7. package/dist/chempot-diagram/chempot-worker.js +2 -2
  8. package/dist/chempot-diagram/compute.js +16 -16
  9. package/dist/composition/FormulaFilter.svelte +3 -3
  10. package/dist/constants.js +2 -8
  11. package/dist/convex-hull/ConvexHull.svelte +2 -2
  12. package/dist/convex-hull/ConvexHull2D.svelte +11 -10
  13. package/dist/convex-hull/ConvexHull3D.svelte +16 -14
  14. package/dist/convex-hull/ConvexHull4D.svelte +26 -14
  15. package/dist/convex-hull/ConvexHullControls.svelte +1 -1
  16. package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
  17. package/dist/convex-hull/ConvexHullStats.svelte +23 -6
  18. package/dist/convex-hull/GasPressureControls.svelte +3 -3
  19. package/dist/convex-hull/TemperatureSlider.svelte +1 -1
  20. package/dist/convex-hull/barycentric-coords.js +2 -2
  21. package/dist/convex-hull/helpers.js +45 -27
  22. package/dist/convex-hull/thermodynamics.js +2 -2
  23. package/dist/element/BohrAtom.svelte +25 -27
  24. package/dist/element/BohrAtom.svelte.d.ts +2 -2
  25. package/dist/element/data.d.ts +2 -3
  26. package/dist/fermi-surface/FermiSurface.svelte +5 -2
  27. package/dist/fermi-surface/compute.js +3 -3
  28. package/dist/fermi-surface/parse.js +2 -2
  29. package/dist/fermi-surface/symmetry.js +1 -1
  30. package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
  31. package/dist/icons.d.ts +6 -6
  32. package/dist/icons.js +6 -6
  33. package/dist/io/decompress.js +12 -7
  34. package/dist/io/export.js +20 -16
  35. package/dist/io/is-binary.js +19 -4
  36. package/dist/isosurface/parse.js +8 -8
  37. package/dist/isosurface/types.js +9 -9
  38. package/dist/layout/InfoTag.svelte +1 -1
  39. package/dist/layout/json-tree/JsonNode.svelte +1 -0
  40. package/dist/layout/json-tree/utils.js +2 -1
  41. package/dist/marching-cubes.js +1 -1
  42. package/dist/math.js +1 -1
  43. package/dist/overlays/CopyButton.svelte +45 -0
  44. package/dist/overlays/CopyButton.svelte.d.ts +8 -0
  45. package/dist/overlays/InfoPaneCards.svelte +149 -0
  46. package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
  47. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
  48. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
  49. package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
  50. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
  51. package/dist/phase-diagram/parse.js +3 -3
  52. package/dist/phase-diagram/svg-to-diagram.js +10 -12
  53. package/dist/plot/BarPlot.svelte +24 -15
  54. package/dist/plot/BarPlot.svelte.d.ts +3 -2
  55. package/dist/plot/FillArea.svelte +2 -3
  56. package/dist/plot/FillArea.svelte.d.ts +3 -2
  57. package/dist/plot/Histogram.svelte +37 -19
  58. package/dist/plot/Line.svelte +2 -3
  59. package/dist/plot/Line.svelte.d.ts +2 -2
  60. package/dist/plot/PlotLegend.svelte +79 -8
  61. package/dist/plot/PlotLegend.svelte.d.ts +4 -0
  62. package/dist/plot/PortalSelect.svelte +5 -5
  63. package/dist/plot/ScatterPlot.svelte +47 -33
  64. package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
  65. package/dist/plot/ScatterPlot3D.svelte +6 -3
  66. package/dist/plot/ScatterPoint.svelte +10 -4
  67. package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
  68. package/dist/plot/SpacegroupBarPlot.svelte +5 -4
  69. package/dist/plot/data-cleaning.js +9 -9
  70. package/dist/plot/index.d.ts +0 -6
  71. package/dist/plot/scales.d.ts +3 -3
  72. package/dist/plot/scales.js +29 -29
  73. package/dist/plot/types.d.ts +5 -9
  74. package/dist/rdf/calc-rdf.js +1 -1
  75. package/dist/sanitize.js +22 -15
  76. package/dist/settings.d.ts +2 -0
  77. package/dist/settings.js +12 -3
  78. package/dist/spectral/Bands.svelte +6 -6
  79. package/dist/spectral/BandsAndDos.svelte +4 -4
  80. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  81. package/dist/spectral/Dos.svelte +2 -2
  82. package/dist/spectral/helpers.js +1 -1
  83. package/dist/structure/AtomLegend.svelte +4 -4
  84. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  85. package/dist/structure/Cylinder.svelte +7 -7
  86. package/dist/structure/Structure.svelte +169 -27
  87. package/dist/structure/Structure.svelte.d.ts +6 -2
  88. package/dist/structure/StructureControls.svelte +130 -16
  89. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  90. package/dist/structure/StructureInfoPane.svelte +519 -218
  91. package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
  92. package/dist/structure/StructureScene.svelte +399 -68
  93. package/dist/structure/StructureScene.svelte.d.ts +8 -4
  94. package/dist/structure/atom-properties.js +3 -1
  95. package/dist/structure/bond-order-perception.d.ts +13 -0
  96. package/dist/structure/bond-order-perception.js +367 -0
  97. package/dist/structure/bonding.d.ts +10 -1
  98. package/dist/structure/bonding.js +232 -11
  99. package/dist/structure/export.js +6 -4
  100. package/dist/structure/index.d.ts +19 -4
  101. package/dist/structure/index.js +3 -0
  102. package/dist/structure/label-placement.d.ts +14 -0
  103. package/dist/structure/label-placement.js +72 -0
  104. package/dist/structure/parse.d.ts +2 -1
  105. package/dist/structure/parse.js +25 -36
  106. package/dist/structure/supercell.js +35 -2
  107. package/dist/symmetry/SymmetryStats.svelte +1 -1
  108. package/dist/symmetry/cell-transform.js +15 -1
  109. package/dist/symmetry/index.js +3 -3
  110. package/dist/table/HeatmapTable.svelte +3 -3
  111. package/dist/table/ToggleMenu.svelte +1 -1
  112. package/dist/trajectory/Trajectory.svelte +2 -2
  113. package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
  114. package/dist/trajectory/extract.js +4 -4
  115. package/dist/trajectory/frame-reader.js +2 -2
  116. package/dist/trajectory/parse/ase.js +2 -6
  117. package/dist/trajectory/parse/hdf5.js +1 -3
  118. package/dist/trajectory/plotting.js +1 -1
  119. package/dist/utils.js +1 -1
  120. package/dist/xrd/calc-xrd.js +1 -1
  121. package/package.json +22 -37
  122. package/dist/structure/ferrox-wasm-types.d.ts +0 -46
  123. package/dist/structure/ferrox-wasm-types.js +0 -18
  124. package/dist/structure/ferrox-wasm.d.ts +0 -94
  125. package/dist/structure/ferrox-wasm.js +0 -249
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { FileInfo } from './io'
3
- import { tooltip } from 'svelte-multiselect'
3
+ import { tooltip } from 'svelte-multiselect/attachments'
4
4
  import type { HTMLAttributes } from 'svelte/elements'
5
5
 
6
6
  // Delay for distinguishing click from double-click (ms)
package/dist/app.css CHANGED
@@ -20,6 +20,13 @@
20
20
  --sms-li-active-bg: light-dark(rgba(100, 149, 237, 0.25), cornflowerblue);
21
21
  --sms-selected-bg: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.15));
22
22
  --border-radius: 3pt;
23
+
24
+ /* App-wide overlay stack: controls < fullscreen/nav < dialogs < floating options.
25
+ These must exceed Threlte HTML overlays, which use very high z-index values. */
26
+ --z-index-overlay-controls: 100000000;
27
+ --z-index-overlay-nav: 100000001;
28
+ --z-index-overlay-dialog: 100000002;
29
+ --z-index-overlay-options: 100000003;
23
30
  }
24
31
 
25
32
  /* App-shell styles: page layout, typography, headings, navigation.
@@ -444,7 +444,7 @@
444
444
  y={hover_data.screen_position.y}
445
445
  bg_color={hover_data.is_ibz ? ibz_color : surface_color}
446
446
  fixed
447
- style="z-index: calc(var(--bz-buttons-z-index, 100000000) + 1); backdrop-filter: blur(4px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3)"
447
+ style="z-index: calc(var(--bz-buttons-z-index, var(--z-index-overlay-controls, 100000000)) + 1); backdrop-filter: blur(4px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3)"
448
448
  >
449
449
  <BrillouinZoneTooltip {hover_data} tooltip={tooltip_config} />
450
450
  </PlotTooltip>
@@ -497,7 +497,10 @@
497
497
  top: var(--bz-buttons-top, var(--ctrl-btn-top, 1ex));
498
498
  right: var(--bz-buttons-right, var(--ctrl-btn-right, 1ex));
499
499
  gap: clamp(6pt, 1cqmin, 9pt);
500
- z-index: var(--bz-buttons-z-index, 100000000);
500
+ z-index: var(
501
+ --bz-buttons-z-index,
502
+ var(--z-index-overlay-controls, 100000000)
503
+ );
501
504
  opacity: 0;
502
505
  pointer-events: none;
503
506
  transition: opacity 0.2s ease;
@@ -205,7 +205,7 @@ export function compute_convex_hull(vertices, edge_sharp_angle_deg = 5) {
205
205
  }
206
206
  const geometry = new ConvexGeometry(vertices.map((cert) => new Vector3(...cert)));
207
207
  const pos = geometry.getAttribute(`position`);
208
- const idx = geometry.index;
208
+ const geometry_index = geometry.index;
209
209
  // Deduplicate vertices from Three.js geometry
210
210
  const unique_verts = [];
211
211
  const vert_map = new Map();
@@ -218,10 +218,14 @@ export function compute_convex_hull(vertices, edge_sharp_angle_deg = 5) {
218
218
  }
219
219
  // Build faces with deduplicated vertex indices
220
220
  const faces = [];
221
- const n_faces = idx ? idx.count / 3 : pos.count / 3;
221
+ const n_faces = geometry_index ? geometry_index.count / 3 : pos.count / 3;
222
222
  for (let idx_face = 0; idx_face < n_faces; idx_face++) {
223
- const tri = idx
224
- ? [idx.getX(idx_face * 3), idx.getX(idx_face * 3 + 1), idx.getX(idx_face * 3 + 2)]
223
+ const tri = geometry_index
224
+ ? [
225
+ geometry_index.getX(idx_face * 3),
226
+ geometry_index.getX(idx_face * 3 + 1),
227
+ geometry_index.getX(idx_face * 3 + 2),
228
+ ]
225
229
  : [idx_face * 3, idx_face * 3 + 1, idx_face * 3 + 2];
226
230
  faces.push(tri.map((j) => {
227
231
  const mapped = vert_map.get(j);
@@ -1931,8 +1931,8 @@
1931
1931
  const export_basename = $derived(`chempot-${plot_elements.join(`-`)}`)
1932
1932
 
1933
1933
  function get_view_settings(): Record<string, unknown> {
1934
- const camera_position = orbit_controls_ref?.object?.position
1935
- const camera_target = orbit_controls_ref?.target
1934
+ const view_camera_position = orbit_controls_ref?.object?.position
1935
+ const view_camera_target = orbit_controls_ref?.target
1936
1936
  return {
1937
1937
  elements: plot_elements,
1938
1938
  camera_projection,
@@ -1940,11 +1940,11 @@
1940
1940
  color_mode,
1941
1941
  color_scale,
1942
1942
  reverse_color_scale,
1943
- camera_position: camera_position
1944
- ? [camera_position.x, camera_position.y, camera_position.z]
1943
+ camera_position: view_camera_position
1944
+ ? [view_camera_position.x, view_camera_position.y, view_camera_position.z]
1945
1945
  : null,
1946
- camera_target: camera_target
1947
- ? [camera_target.x, camera_target.y, camera_target.z]
1946
+ camera_target: view_camera_target
1947
+ ? [view_camera_target.x, view_camera_target.y, view_camera_target.z]
1948
1948
  : null,
1949
1949
  }
1950
1950
  }
@@ -29,7 +29,7 @@ function get_worker() {
29
29
  return null;
30
30
  if (!worker) {
31
31
  worker = new Worker(new URL(`./chempot-worker.js`, import.meta.url), { type: `module` });
32
- worker.onmessage = ({ data: { id, result, error } }) => {
32
+ worker.addEventListener(`message`, ({ data: { id, result, error } }) => {
33
33
  const req = pending.get(id);
34
34
  if (!req)
35
35
  return;
@@ -38,15 +38,15 @@ function get_worker() {
38
38
  req.reject(new Error(error ?? `Worker returned null`));
39
39
  else
40
40
  req.resolve(result);
41
- };
42
- worker.onerror = (event) => {
41
+ });
42
+ worker.addEventListener(`error`, (event) => {
43
43
  event.preventDefault();
44
44
  const err = new Error(event.message || `Worker initialization error`);
45
45
  for (const req of pending.values())
46
46
  req.reject(err);
47
47
  pending.clear();
48
48
  worker = null;
49
- };
49
+ });
50
50
  }
51
51
  return worker;
52
52
  }
@@ -66,6 +66,7 @@ export function compute_chempot_async(entries, config = {}) {
66
66
  pending.set(id, { resolve, reject });
67
67
  try {
68
68
  // $state.snapshot strips Svelte $state proxies (not structured-cloneable)
69
+ // eslint-disable-next-line unicorn/require-post-message-target-origin
69
70
  wkr.postMessage($state.snapshot({ id, entries, config }));
70
71
  }
71
72
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { compute_chempot_diagram } from './compute';
2
- self.onmessage = (event) => {
2
+ self.addEventListener(`message`, (event) => {
3
3
  const { id, entries, config } = event.data;
4
4
  try {
5
5
  const result = compute_chempot_diagram(entries, config);
@@ -8,4 +8,4 @@ self.onmessage = (event) => {
8
8
  catch (err) {
9
9
  postMessage({ id, result: null, error: err instanceof Error ? err.message : String(err) });
10
10
  }
11
- };
11
+ });
@@ -149,7 +149,7 @@ export function build_hyperplanes(min_entries, el_refs, elements) {
149
149
  const atom_count = count_atoms_in_composition(entry.composition);
150
150
  const composition = entry.composition;
151
151
  const energy_per_atom = get_energy_per_atom(entry);
152
- const row = new Array(n_elems + 1).fill(0);
152
+ const row = Array(n_elems + 1).fill(0);
153
153
  let ref_energy = 0;
154
154
  for (let elem_idx = 0; elem_idx < n_elems; elem_idx++) {
155
155
  const element = elements[elem_idx];
@@ -180,12 +180,12 @@ export function build_border_hyperplanes(lims) {
180
180
  const borders = [];
181
181
  for (let idx = 0; idx < dim; idx++) {
182
182
  // Lower bound: -mu_i + lo <= 0 → [-1, 0, ..., lo]
183
- const lower = new Array(dim + 1).fill(0);
183
+ const lower = Array(dim + 1).fill(0);
184
184
  lower[idx] = -1;
185
185
  lower[dim] = lims[idx][0];
186
186
  borders.push(lower);
187
187
  // Upper bound: mu_i - hi <= 0 → [1, 0, ..., -hi]
188
- const upper = new Array(dim + 1).fill(0);
188
+ const upper = Array(dim + 1).fill(0);
189
189
  upper[idx] = 1;
190
190
  upper[dim] = -lims[idx][1];
191
191
  borders.push(upper);
@@ -246,12 +246,12 @@ export function compute_domains(hyperplanes, border_hyperplanes, hyperplane_entr
246
246
  domains[formula] = [];
247
247
  }
248
248
  // Pre-allocate reusable buffers to avoid GC pressure in the combo loop
249
- const mu = new Array(dim).fill(0);
250
- const offsets = new Array(dim).fill(0);
249
+ const mu = Array(dim).fill(0);
250
+ const offsets = Array(dim).fill(0);
251
251
  // For dim <= 3, use inline solvers; for larger dims, build A on the fly
252
- const A_rows = dim > 3 ? Array.from({ length: dim }, () => new Array(dim).fill(0)) : [];
252
+ const A_rows = dim > 3 ? Array.from({ length: dim }, () => Array(dim).fill(0)) : [];
253
253
  // Generate all combinations of dim indices from n_total halfspaces
254
- const combo = new Array(dim).fill(0);
254
+ const combo = Array(dim).fill(0);
255
255
  for (let idx = 0; idx < dim; idx++)
256
256
  combo[idx] = idx;
257
257
  function advance_combo() {
@@ -434,7 +434,7 @@ export function simple_pca(data, k = 2) {
434
434
  return { scores: [], eigenvectors: [] };
435
435
  }
436
436
  // Center the data
437
- const means = new Array(n_cols).fill(0);
437
+ const means = Array(n_cols).fill(0);
438
438
  for (const row of data) {
439
439
  for (let col = 0; col < n_cols; col++)
440
440
  means[col] += row[col];
@@ -443,7 +443,7 @@ export function simple_pca(data, k = 2) {
443
443
  means[col] /= n_rows;
444
444
  const centered = data.map((row) => row.map((val, col) => val - means[col]));
445
445
  // Covariance matrix
446
- const cov = Array.from({ length: n_cols }, () => new Array(n_cols).fill(0));
446
+ const cov = Array.from({ length: n_cols }, () => Array(n_cols).fill(0));
447
447
  for (const row of centered) {
448
448
  for (let idx = 0; idx < n_cols; idx++) {
449
449
  for (let jdx = idx; jdx < n_cols; jdx++) {
@@ -462,11 +462,11 @@ export function simple_pca(data, k = 2) {
462
462
  const eigenvectors = [];
463
463
  const work_cov = cov.map((row) => [...row]);
464
464
  for (let comp = 0; comp < k; comp++) {
465
- let vec = new Array(n_cols).fill(0);
465
+ let vec = Array(n_cols).fill(0);
466
466
  vec[comp % n_cols] = 1; // initial guess
467
467
  for (let iter = 0; iter < 100; iter++) {
468
468
  // Matrix-vector multiply
469
- const new_vec = new Array(n_cols).fill(0);
469
+ const new_vec = Array(n_cols).fill(0);
470
470
  for (let idx = 0; idx < n_cols; idx++) {
471
471
  for (let jdx = 0; jdx < n_cols; jdx++) {
472
472
  new_vec[idx] += work_cov[idx][jdx] * vec[jdx];
@@ -548,8 +548,8 @@ export function get_3d_domain_simplexes_and_ann_loc(points_3d) {
548
548
  const mean_3d = Array.from({ length: n_dims }, (_, dim) => unique.reduce((sum, pt) => sum + pt[dim], 0) / unique.length);
549
549
  const centroid_x = centroid[0] ?? 0;
550
550
  const centroid_y = centroid[1] ?? 0;
551
- const first_eigenvector = eigenvectors[0] ?? new Array(n_dims).fill(0);
552
- const second_eigenvector = eigenvectors[1] ?? new Array(n_dims).fill(0);
551
+ const first_eigenvector = eigenvectors[0] ?? Array(n_dims).fill(0);
552
+ const second_eigenvector = eigenvectors[1] ?? Array(n_dims).fill(0);
553
553
  const ann_loc = mean_3d.map((m, dim) => m + centroid_x * first_eigenvector[dim] + centroid_y * second_eigenvector[dim]);
554
554
  // Map hull vertices back to original point indices using nearest projected
555
555
  // vertex instead of stringified coordinates to avoid precision aliasing.
@@ -675,7 +675,7 @@ export function get_ternary_combinations(elements) {
675
675
  }
676
676
  return combos;
677
677
  }
678
- let _nd_cache = null;
678
+ let nd_cache = null;
679
679
  // Content-based fingerprint for N-D result caching. Uses sorted formula keys
680
680
  // so deserialized Web Worker copies match and different compositions never collide.
681
681
  export function make_nd_cache_key(entries, formal_chempots, default_min_limit, limits) {
@@ -723,7 +723,7 @@ export function compute_chempot_diagram(entries, config = {}) {
723
723
  const cache_key = is_projection
724
724
  ? make_nd_cache_key(entries, formal_chempots, default_min_limit, limits)
725
725
  : ``;
726
- let nd_result = is_projection && _nd_cache?.key === cache_key ? _nd_cache.result : null;
726
+ let nd_result = is_projection && nd_cache?.key === cache_key ? nd_cache.result : null;
727
727
  if (!nd_result) {
728
728
  // In subsystem mode, filter entries to only those within the element set
729
729
  let working_entries = entries;
@@ -779,7 +779,7 @@ export function compute_chempot_diagram(entries, config = {}) {
779
779
  };
780
780
  // Cache for projection mode reuse
781
781
  if (is_projection) {
782
- _nd_cache = { key: cache_key, result: nd_result };
782
+ nd_cache = { key: cache_key, result: nd_result };
783
783
  }
784
784
  }
785
785
  let domains = nd_result.domains;
@@ -2,7 +2,7 @@
2
2
  import Icon from '../Icon.svelte'
3
3
  import { get_alphabetical_formula } from './format'
4
4
  import { ELEM_SYMBOLS } from '../labels'
5
- import { tooltip } from 'svelte-multiselect'
5
+ import { tooltip } from 'svelte-multiselect/attachments'
6
6
  import type { HTMLAttributes } from 'svelte/elements'
7
7
  import type { FormulaSearchMode } from './index'
8
8
  import {
@@ -398,11 +398,11 @@
398
398
  const operator = token.startsWith(`-`) || token.startsWith(`!`)
399
399
  ? `exclude`
400
400
  : `include`
401
- const value =
401
+ const stripped_value =
402
402
  token.startsWith(`+`) || token.startsWith(`-`) || token.startsWith(`!`)
403
403
  ? token.slice(1)
404
404
  : token
405
- return { operator, value }
405
+ return { operator, value: stripped_value }
406
406
  }
407
407
 
408
408
  function serialize_token(
package/dist/constants.js CHANGED
@@ -9,9 +9,7 @@ export const COMPRESSION_FORMATS = {
9
9
  bz2: [`.bz2`], // Browser DecompressionStream doesn't support BZ2
10
10
  };
11
11
  // All detectable compression extensions
12
- export const COMPRESSION_EXTENSIONS = Object.freeze([
13
- ...Object.values(COMPRESSION_FORMATS).flat(),
14
- ]);
12
+ export const COMPRESSION_EXTENSIONS = Object.freeze(Object.values(COMPRESSION_FORMATS).flat());
15
13
  // Keywords that indicate a file is likely a trajectory file
16
14
  export const TRAJ_KEYWORDS = Object.freeze([
17
15
  `trajectory`,
@@ -64,11 +62,7 @@ export const STRUCT_KEYWORDS_REGEX = new RegExp(`(${STRUCT_KEYWORDS.join(`|`)})`
64
62
  export const STRUCT_KEYWORDS_STRICT_REGEX = new RegExp(`(${STRUCT_KEYWORDS_STRICT.join(`|`)})`, `i`);
65
63
  export const TRAJ_KEYWORDS_SIMPLE_REGEX = new RegExp(`(${TRAJ_KEYWORDS.join(`|`)})`, `i`);
66
64
  // File extensions for different file types
67
- export const TRAJ_EXTENSIONS = Object.freeze([
68
- `.traj`,
69
- `.xtc`,
70
- `.lammpstrj`,
71
- ]);
65
+ export const TRAJ_EXTENSIONS = Object.freeze([`.traj`, `.xtc`, `.lammpstrj`]);
72
66
  export const TRAJ_EXTENSIONS_REGEX = new RegExp(`\\.(${TRAJ_EXTENSIONS.map((ext) => ext.slice(1)).join(`|`)})$`, `i`);
73
67
  export const STRUCTURE_EXTENSIONS = Object.freeze([
74
68
  `.cif`,
@@ -55,10 +55,10 @@
55
55
  // Lightweight element extraction - count unique elements, stripping oxidation states
56
56
  // (e.g. "V4+" -> "V") to avoid counting the same element multiple times
57
57
  function extract_unique_elements(
58
- entries: { composition: Record<string, number> }[],
58
+ hull_entries: { composition: Record<string, number> }[],
59
59
  ): string[] {
60
60
  const elements = new SvelteSet<string>()
61
- for (const entry of entries) {
61
+ for (const entry of hull_entries) {
62
62
  for (const key of Object.keys(entry.composition)) {
63
63
  // Extract valid element symbols, stripping oxidation states
64
64
  for (const elem of extract_formula_elements(key, { unique: false })) {
@@ -11,6 +11,7 @@
11
11
  import { set_fullscreen_bg, setup_fullscreen_effect } from '../layout'
12
12
  import type {
13
13
  AxisConfig,
14
+ PointStyle,
14
15
  ScatterHandlerEvent,
15
16
  ScatterHandlerProps,
16
17
  UserContentProps,
@@ -99,8 +100,8 @@
99
100
  ...default_hull_config,
100
101
  point_size: 6, // Binary diagrams use slightly smaller points
101
102
  ...config,
102
- colors: { ...default_hull_config.colors, ...(config.colors || {}) },
103
- margin: { t: 40, r: 40, b: 60, l: 60, ...(config.margin || {}) },
103
+ colors: { ...default_hull_config.colors, ...config.colors },
104
+ margin: { t: 40, r: 40, b: 60, l: 60, ...config.margin },
104
105
  })
105
106
 
106
107
  // Merge highlight style with defaults (consistent with 3D/4D)
@@ -274,16 +275,16 @@
274
275
  x: x_coord,
275
276
  y: min_y,
276
277
  }))
277
- const hull_points = thermo.compute_lower_hull_2d(hull_input)
278
+ const computed_hull_points = thermo.compute_lower_hull_2d(hull_input)
278
279
 
279
- const all_enriched_entries = coords_entries.map((entry) => {
280
- const y_hull = thermo.interpolate_hull_2d(hull_points, entry.x)
280
+ const enriched_entries = coords_entries.map((entry) => {
281
+ const y_hull = thermo.interpolate_hull_2d(computed_hull_points, entry.x)
281
282
  const raw_dist = y_hull == null ? 0 : entry.y - y_hull
282
283
  return {
283
284
  ...entry, ...compute_hull_stability(raw_dist, entry.exclude_from_hull), visible: true,
284
285
  }
285
286
  })
286
- return { all_enriched_entries, hull_points }
287
+ return { all_enriched_entries: enriched_entries, hull_points: computed_hull_points }
287
288
  })
288
289
 
289
290
  // Auto threshold: show all for few entries, use default for many, interpolate between
@@ -377,10 +378,10 @@
377
378
  const count = visible_entries.length
378
379
 
379
380
  // Single pass: extract x, y, color_values, and point_style simultaneously
380
- const x_vals: number[] = new Array(count)
381
- const y_vals: number[] = new Array(count)
382
- const color_values: number[] = is_energy_mode ? new Array(count) : []
383
- const point_style = new Array(count)
381
+ const x_vals: number[] = Array(count)
382
+ const y_vals: number[] = Array(count)
383
+ const color_values: number[] = is_energy_mode ? Array(count) : []
384
+ const point_style: PointStyle[] = Array(count)
384
385
 
385
386
  for (let idx = 0; idx < count; idx++) {
386
387
  const entry = visible_entries[idx]
@@ -130,8 +130,8 @@
130
130
  const merged_config = $derived({
131
131
  ...default_hull_config,
132
132
  ...config,
133
- colors: { ...default_hull_config.colors, ...(config.colors || {}) },
134
- margin: { t: 40, r: 40, b: 60, l: 60, ...(config.margin || {}) },
133
+ colors: { ...default_hull_config.colors, ...config.colors },
134
+ margin: { t: 40, r: 40, b: 60, l: 60, ...config.margin },
135
135
  })
136
136
 
137
137
  // Temperature-dependent free energy support
@@ -306,7 +306,7 @@
306
306
  })
307
307
 
308
308
  // Canvas rendering
309
- let canvas: HTMLCanvasElement
309
+ let canvas: HTMLCanvasElement | undefined = undefined
310
310
  let ctx: CanvasRenderingContext2D | null = null
311
311
 
312
312
  // Performance optimization
@@ -632,7 +632,7 @@
632
632
  }
633
633
 
634
634
  function draw_structure_outline(): void {
635
- if (!ctx) return
635
+ if (!ctx || !canvas) return
636
636
 
637
637
  // Set consistent style for all triangle structure lines
638
638
  ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
@@ -644,12 +644,11 @@
644
644
  }
645
645
 
646
646
  function draw_triangle_structure(): void {
647
- if (!ctx) return
647
+ if (!ctx || !canvas) return
648
648
 
649
649
  // Get formation energy range for vertical edges
650
- const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
651
- const e_form_min = Math.min(0, ...formation_energies) // Include 0 for elemental references
652
- const e_form_max = Math.max(0, ...formation_energies) // Include 0 for elemental references
650
+ const e_form_min = energy_range.min // Includes 0 for elemental references
651
+ const e_form_max = energy_range.max // Includes 0 for elemental references
653
652
 
654
653
  // Draw base triangle edges (top triangle at formation energy = 0)
655
654
  const triangle_edges = get_triangle_edges()
@@ -799,8 +798,7 @@
799
798
  // Lazy computation for uniform mode: normalize alpha by formation energy
800
799
  let norm_alpha: ((z: number) => number) | null = null
801
800
  if (hull_face_color_mode === `uniform`) {
802
- const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
803
- const min_fe = Math.min(0, ...formation_energies)
801
+ const min_fe = energy_range.min
804
802
  norm_alpha = (z: number) => {
805
803
  const t = Math.max(0, Math.min(1, (0 - z) / Math.max(1e-6, 0 - min_fe)))
806
804
  return t * hull_face_opacity
@@ -955,8 +953,7 @@
955
953
 
956
954
  // Formation energy color bar helpers
957
955
  const e_form_range = $derived.by((): [number, number] => {
958
- const energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
959
- const min_fe = energies.length ? Math.min(0, ...energies) : -1
956
+ const min_fe = plot_entries.length ? energy_range.min : -1
960
957
  return [min_fe, 0]
961
958
  })
962
959
 
@@ -1387,8 +1384,13 @@
1387
1384
  // Performance: Cache canvas dimensions and formation energy range
1388
1385
  let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1389
1386
  const energy_range = $derived.by(() => {
1390
- const energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
1391
- const [min, max] = [Math.min(0, ...energies), Math.max(0, ...energies)]
1387
+ let min = 0
1388
+ let max = 0
1389
+ for (const entry of plot_entries) {
1390
+ const energy = entry.e_form_per_atom ?? 0
1391
+ min = Math.min(min, energy)
1392
+ max = Math.max(max, energy)
1393
+ }
1392
1394
  const z_scale = 0.75 / Math.max(max - min, 0.001)
1393
1395
  return { min, max, center: (min + max) / 2, z_scale }
1394
1396
  })
@@ -107,8 +107,8 @@
107
107
  const merged_config = $derived({
108
108
  ...default_hull_config,
109
109
  ...config,
110
- colors: { ...default_hull_config.colors, ...(config.colors || {}) },
111
- margin: { t: 60, r: 60, b: 60, l: 60, ...(config.margin || {}) },
110
+ colors: { ...default_hull_config.colors, ...config.colors },
111
+ margin: { t: 60, r: 60, b: 60, l: 60, ...config.margin },
112
112
  })
113
113
 
114
114
  // Reactive dark mode detection for canvas text color
@@ -311,7 +311,7 @@
311
311
  )
312
312
  })
313
313
 
314
- let canvas: HTMLCanvasElement
314
+ let canvas: HTMLCanvasElement | undefined = undefined
315
315
  let ctx: CanvasRenderingContext2D | null = null
316
316
  let frame_id = 0 // Performance optimization
317
317
 
@@ -554,7 +554,7 @@
554
554
  }
555
555
 
556
556
  function draw_structure_outline(): void {
557
- if (!ctx) return
557
+ if (!ctx || !canvas) return
558
558
 
559
559
  const styles = getComputedStyle(canvas)
560
560
  // Match gray dashed structure lines used in 3D
@@ -732,10 +732,11 @@
732
732
  // Lazy computation for uniform mode: normalize alpha by formation energy
733
733
  let norm_alpha: ((w: number) => number) | null = null
734
734
  if (hull_face_color_mode === `uniform`) {
735
- const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
736
- const min_fe = Math.min(0, ...formation_energies)
737
735
  norm_alpha = (energy: number) => {
738
- const t = Math.max(0, Math.min(1, (0 - energy) / Math.max(1e-6, 0 - min_fe)))
736
+ const t = Math.max(
737
+ 0,
738
+ Math.min(1, (0 - energy) / Math.max(1e-6, 0 - formation_energy_min)),
739
+ )
739
740
  return t * hull_face_opacity
740
741
  }
741
742
  }
@@ -1032,15 +1033,19 @@
1032
1033
  const rect = container?.getBoundingClientRect()
1033
1034
  const [width, height] = rect ? [rect.width, rect.height] : [400, 400]
1034
1035
 
1035
- canvas.width = Math.max(0, Math.round(width * dpr))
1036
- canvas.height = Math.max(0, Math.round(height * dpr))
1036
+ const new_width = Math.max(0, Math.round(width * dpr))
1037
+ const new_height = Math.max(0, Math.round(height * dpr))
1037
1038
  canvas_dims = { width, height, scale: Math.min(width, height) / 600 }
1038
1039
 
1039
- ctx = canvas.getContext(`2d`)
1040
- if (ctx) {
1041
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1042
- ctx.imageSmoothingEnabled = true
1043
- ctx.imageSmoothingQuality = `high`
1040
+ if (!ctx || canvas.width !== new_width || canvas.height !== new_height) {
1041
+ canvas.width = new_width
1042
+ canvas.height = new_height
1043
+ ctx = canvas.getContext(`2d`)
1044
+ if (ctx) {
1045
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1046
+ ctx.imageSmoothingEnabled = true
1047
+ ctx.imageSmoothingQuality = `high`
1048
+ }
1044
1049
  }
1045
1050
  render_once()
1046
1051
  }
@@ -1078,6 +1083,13 @@
1078
1083
 
1079
1084
  // Performance: Cache canvas dimensions and pre-compute sorted point projections
1080
1085
  let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1086
+ const formation_energy_min = $derived.by(() => {
1087
+ let min_energy = 0
1088
+ for (const entry of plot_entries) {
1089
+ min_energy = Math.min(min_energy, entry.e_form_per_atom ?? 0)
1090
+ }
1091
+ return min_energy
1092
+ })
1081
1093
  const sorted_points_cache = $derived.by(() => {
1082
1094
  if (!canvas || plot_entries.length === 0) return []
1083
1095
  return plot_entries
@@ -5,7 +5,7 @@
5
5
  import { sanitize_html } from '../sanitize'
6
6
  import { ColorScaleSelect } from '../plot'
7
7
  import type { ComponentProps } from 'svelte'
8
- import { tooltip } from 'svelte-multiselect'
8
+ import { tooltip } from 'svelte-multiselect/attachments'
9
9
  import type { HTMLAttributes } from 'svelte/elements'
10
10
  import type {
11
11
  ConvexHullControlsType,