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
@@ -20,6 +20,8 @@
20
20
  import { convex_hull_2d, cross_3d, merge_coplanar_triangles, normalize_vec } from '../math'
21
21
  import DraggablePane from '../overlays/DraggablePane.svelte'
22
22
  import { ColorBar, ScatterPlot3DControls } from '../plot'
23
+ import { page_visibility } from '../scene'
24
+ import { sanitize_html } from '../sanitize'
23
25
  import { constrain_tooltip_position, pad_rect, rects_overlap } from '../plot/core/layout'
24
26
  import type {
25
27
  AxisConfig3D,
@@ -38,7 +40,7 @@
38
40
  import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js'
39
41
  import { compute_chempot_async } from './async-compute.svelte'
40
42
  import ChemPotScene3D from './ChemPotScene3D.svelte'
41
- import { get_chempot_color_bar_config, make_chempot_color_scale } from './color'
43
+ import { ARITY_COLORS, get_chempot_color_bar_config, get_domain_color_data } from './color'
42
44
  import {
43
45
  CHEMPOT_COLOR_MODE_OPTIONS,
44
46
  CHEMPOT_COLOR_SCALE_OPTIONS,
@@ -56,12 +58,10 @@
56
58
  import {
57
59
  apply_element_padding,
58
60
  bbox_diagonal,
59
- best_form_energy_for_formula,
60
61
  build_axis_ranges,
61
62
  dedup_points,
62
- formula_key_from_composition,
63
63
  get_3d_domain_simplexes_and_ann_loc,
64
- get_energy_per_atom,
64
+ get_energy_stats_by_formula,
65
65
  get_min_entries_and_el_refs,
66
66
  get_ternary_combinations,
67
67
  get_visible_domain_labels,
@@ -269,17 +269,14 @@
269
269
  get_projection_source_entries(entries, temp_filtered_entries),
270
270
  )
271
271
 
272
- const all_entry_elements = $derived.by(() =>
273
- Array.from(
274
- new SvelteSet(
275
- projection_source_entries.flatMap((entry) =>
276
- Object.entries(entry.composition)
277
- .filter(([, amount]) => amount > 0)
278
- .map(([element]) => element),
279
- ),
280
- ),
281
- ).sort(),
282
- )
272
+ const all_entry_elements = $derived.by(() => {
273
+ const elements = projection_source_entries.flatMap((entry) =>
274
+ Object.entries(entry.composition)
275
+ .filter(([, amount]) => amount > 0)
276
+ .map(([element]) => element),
277
+ )
278
+ return Array.from(new SvelteSet(elements)).sort()
279
+ })
283
280
  const has_multinary_system = $derived(all_entry_elements.length > 3)
284
281
  let projection_elements_override = $state<string[] | null>(null)
285
282
  const config_projection_elements = $derived(
@@ -287,16 +284,14 @@
287
284
  )
288
285
  const projection_elements = $derived.by(() => {
289
286
  if (all_entry_elements.length < 3) return []
290
- if (!has_multinary_system) {
291
- return config_projection_elements ?? all_entry_elements.slice(0, 3)
292
- }
293
- const override_projection = normalize_projection_triplet(
294
- projection_elements_override ?? undefined,
295
- all_entry_elements,
296
- )
297
- if (override_projection) return override_projection
298
- if (config_projection_elements) return config_projection_elements
299
- return all_entry_elements.slice(0, 3)
287
+ // User-picked projection axes only apply to multinary (4+ element) systems
288
+ const override_projection = has_multinary_system
289
+ ? normalize_projection_triplet(
290
+ projection_elements_override ?? undefined,
291
+ all_entry_elements,
292
+ )
293
+ : null
294
+ return override_projection ?? config_projection_elements ?? all_entry_elements.slice(0, 3)
300
295
  })
301
296
  const effective_config = $derived({
302
297
  ...config,
@@ -361,7 +356,7 @@
361
356
  })
362
357
  const current_projection_key = $derived(plot_elements.join(`|`))
363
358
  let formula_filter_query = $state(``)
364
- const available_formulas = $derived.by(() => Object.keys(diagram_data?.domains ?? {}).sort())
359
+ const available_formulas = $derived(Object.keys(diagram_data?.domains ?? {}).sort())
365
360
  const filtered_formulas = $derived.by(() => {
366
361
  const query = formula_filter_query.trim().toLowerCase()
367
362
  if (!query) return available_formulas
@@ -383,12 +378,6 @@
383
378
  info: ChemPotHoverInfo3D
384
379
  }
385
380
 
386
- interface FormulaEnergyStats {
387
- matching_entry_count: number
388
- min_energy_per_atom: number | null
389
- max_energy_per_atom: number | null
390
- }
391
- type NumericColorMode = Exclude<ChemPotColorMode, `none` | `arity`>
392
381
  const domain_annotation_cache = new Map<string, number[]>()
393
382
 
394
383
  function get_domain_ann_loc(points_3d: number[][]): number[] {
@@ -444,122 +433,27 @@
444
433
  return result
445
434
  })
446
435
 
447
- const entry_energy_stats_by_formula = $derived.by(
448
- (): SvelteMap<string, FormulaEnergyStats> => {
449
- const stats_by_formula = new SvelteMap<string, FormulaEnergyStats>()
450
- for (const entry of temp_filtered_entries) {
451
- const formula_key = formula_key_from_composition(entry.composition)
452
- const energy_per_atom = get_energy_per_atom(entry)
453
- const existing = stats_by_formula.get(formula_key)
454
- if (!existing) {
455
- stats_by_formula.set(formula_key, {
456
- matching_entry_count: 1,
457
- min_energy_per_atom: energy_per_atom,
458
- max_energy_per_atom: energy_per_atom,
459
- })
460
- continue
461
- }
462
- stats_by_formula.set(formula_key, {
463
- matching_entry_count: existing.matching_entry_count + 1,
464
- min_energy_per_atom: Math.min(
465
- existing.min_energy_per_atom ?? energy_per_atom,
466
- energy_per_atom,
467
- ),
468
- max_energy_per_atom: Math.max(
469
- existing.max_energy_per_atom ?? energy_per_atom,
470
- energy_per_atom,
471
- ),
472
- })
473
- }
474
- return stats_by_formula
475
- },
436
+ const entry_energy_stats_by_formula = $derived(
437
+ get_energy_stats_by_formula(temp_filtered_entries),
476
438
  )
477
439
 
478
440
  // === Region coloring ===
479
- // Categorical palette for arity mode (element count)
480
- const arity_colors = [`#3498db`, `#2ecc71`, `#e67e22`, `#9b59b6`] as const
481
-
482
441
  // Original (non-renormalized) elemental references for formation energy computation.
483
442
  // diagram_data.el_refs may be renormalized to zero when formal_chempots is true,
484
443
  // so we compute our own from the raw entries to get true DFT reference energies.
485
444
  const raw_el_refs = $derived(get_min_entries_and_el_refs(temp_filtered_entries).el_refs)
486
445
 
487
- const color_mode_labels: Record<NumericColorMode, string> = {
488
- energy: `Energy per atom (eV)`,
489
- formation_energy: `Formation energy (eV/atom)`,
490
- entries: `Entry count`,
491
- }
492
- function get_numeric_color_value(
493
- formula: string,
494
- active_color_mode: NumericColorMode,
495
- ): number | null {
496
- if (active_color_mode === `energy`) {
497
- return entry_energy_stats_by_formula.get(formula)?.min_energy_per_atom ?? null
498
- }
499
- if (active_color_mode === `formation_energy`) {
500
- return best_form_energy_for_formula(temp_filtered_entries, formula, raw_el_refs) ?? null
501
- }
502
- return entry_energy_stats_by_formula.get(formula)?.matching_entry_count ?? 0
503
- }
504
- const domain_color_values = $derived.by(
505
- (): { value_by_formula: SvelteMap<string, number>; values: number[] } | null => {
506
- if (color_mode === `none` || color_mode === `arity`) return null
507
- const active_color_mode = color_mode as NumericColorMode
508
- const value_by_formula = new SvelteMap<string, number>()
509
- const values: number[] = []
510
- for (const domain of render_domains) {
511
- const value = get_numeric_color_value(domain.formula, active_color_mode)
512
- if (value == null || !Number.isFinite(value)) continue
513
- values.push(value)
514
- value_by_formula.set(domain.formula, value)
515
- }
516
- return { value_by_formula, values }
517
- },
518
- )
519
-
520
- // Per-domain color map keyed by formula
521
- const domain_colors = $derived.by((): SvelteMap<string, string> => {
522
- const colors = new SvelteMap<string, string>()
523
- if (color_mode === `none`) return colors
524
-
525
- if (color_mode === `arity`) {
526
- for (const domain of render_domains) {
527
- const n_elements = extract_formula_elements(domain.formula).length
528
- const idx = Math.min(n_elements, arity_colors.length) - 1
529
- colors.set(domain.formula, arity_colors[Math.max(0, idx)])
530
- }
531
- return colors
532
- }
533
- const values_payload = domain_color_values
534
- const scale = make_chempot_color_scale(
535
- values_payload?.values ?? [],
446
+ const { colors: domain_colors, color_range } = $derived(
447
+ get_domain_color_data({
448
+ formulas: render_domains.map((domain) => domain.formula),
449
+ color_mode,
536
450
  color_scale,
537
451
  reverse_color_scale,
538
- )
539
- for (const domain of render_domains) {
540
- const value = values_payload?.value_by_formula.get(domain.formula)
541
- colors.set(domain.formula, value != null && scale ? scale(value) : `#999`)
542
- }
543
- return colors
544
- })
545
-
546
- // Range and label for the color bar (null for none/arity which are categorical)
547
- const color_range = $derived.by((): { min: number; max: number; label: string } | null => {
548
- const values = domain_color_values?.values ?? []
549
- if (values.length === 0) return null
550
- let lo = values[0],
551
- hi = values[0]
552
- for (let idx = 1; idx < values.length; idx++) {
553
- if (values[idx] < lo) lo = values[idx]
554
- if (values[idx] > hi) hi = values[idx]
555
- }
556
- return {
557
- min: lo,
558
- max: Math.max(hi, lo + 1e-6),
559
- label:
560
- color_mode === `none` || color_mode === `arity` ? `` : color_mode_labels[color_mode],
561
- }
562
- })
452
+ entries: temp_filtered_entries,
453
+ el_refs: raw_el_refs,
454
+ energy_stats: entry_energy_stats_by_formula,
455
+ }),
456
+ )
563
457
 
564
458
  const arity_legend_labels = $derived.by((): string[] => {
565
459
  let has_four_plus_regions = false
@@ -604,10 +498,7 @@
604
498
  ]
605
499
  })
606
500
 
607
- function to_render_xyz(point: number[]): Vec3 {
608
- const [scale_x, scale_y, scale_z] = render_axis_scale
609
- return [point[1] * scale_x, point[2] * scale_y, point[0] * scale_z]
610
- }
501
+ const to_render_xyz = (point: number[]): Vec3 => swiz(point[0], point[1], point[2])
611
502
 
612
503
  // Compute data center and extent for camera positioning (in swizzled coords)
613
504
  const { data_center, data_extent } = $derived.by(() => {
@@ -752,20 +643,8 @@
752
643
  const point_b_idx = selected_coord_to_idx.get(
753
644
  `${point_b[0].toFixed(6)},${point_b[1].toFixed(6)}`,
754
645
  )
755
- if (
756
- point_a_idx == null ||
757
- point_b_idx == null ||
758
- point_a_idx >= pts.length ||
759
- point_b_idx >= pts.length
760
- ) {
761
- console.warn(`get_2d_hull_edges: invalid edge`, {
762
- point_a,
763
- point_b,
764
- point_a_idx,
765
- point_b_idx,
766
- })
767
- continue
768
- }
646
+ // Hull vertices come from pts_2d, so lookups always succeed
647
+ if (point_a_idx == null || point_b_idx == null) continue
769
648
  edges.push([pts[point_a_idx], pts[point_b_idx]])
770
649
  }
771
650
 
@@ -1069,23 +948,21 @@
1069
948
  return geom
1070
949
  })
1071
950
 
951
+ const domain_label = (domain: DomainRenderData) => ({
952
+ formula: domain.formula,
953
+ position: swiz(domain.ann_loc[0], domain.ann_loc[1], domain.ann_loc[2]),
954
+ label_font_size: domain.label_font_size,
955
+ })
956
+
1072
957
  const visible_domain_labels = $derived.by(() => {
1073
958
  if (!hull_base_geometry || face_domain_map.length === 0) {
1074
- return render_domains.map((domain) => ({
1075
- formula: domain.formula,
1076
- position: swiz(domain.ann_loc[0], domain.ann_loc[1], domain.ann_loc[2]),
1077
- label_font_size: domain.label_font_size,
1078
- }))
959
+ return render_domains.map(domain_label)
1079
960
  }
1080
961
 
1081
962
  const pos = hull_base_geometry.getAttribute(`position`)
1082
963
  const pinned_labels = render_domains
1083
964
  .filter((domain) => domain.is_draw_formula)
1084
- .map((domain) => ({
1085
- formula: domain.formula,
1086
- position: swiz(domain.ann_loc[0], domain.ann_loc[1], domain.ann_loc[2]),
1087
- label_font_size: domain.label_font_size,
1088
- }))
965
+ .map(domain_label)
1089
966
  const font_size_by_formula = new SvelteMap(
1090
967
  render_domains.map((domain) => [domain.formula, domain.label_font_size]),
1091
968
  )
@@ -1097,16 +974,24 @@
1097
974
  )
1098
975
  })
1099
976
 
1100
- $effect(() => {
1101
- const geom = hull_base_geometry
1102
- return () => dispose_geometry(geom)
1103
- })
977
+ // Register an effect that disposes the given geometries whenever they are
978
+ // recomputed or the component unmounts.
979
+ function dispose_on_change(
980
+ get_geometries: () => (THREE.BufferGeometry | null | undefined)[],
981
+ ): void {
982
+ $effect(() => {
983
+ const geometries = get_geometries()
984
+ return () => {
985
+ for (const geometry of geometries) geometry?.dispose()
986
+ }
987
+ })
988
+ }
1104
989
 
1105
- $effect(() => {
1106
- const geom = colored_hull_geometry
1107
- // Don't dispose if it's the same object as hull_base_geometry (no clone was made)
1108
- if (geom && geom !== hull_base_geometry) return () => dispose_geometry(geom)
1109
- })
990
+ dispose_on_change(() => [hull_base_geometry])
991
+ // Don't dispose colored hull if it's the same object as hull_base_geometry (no clone made)
992
+ dispose_on_change(() =>
993
+ colored_hull_geometry !== hull_base_geometry ? [colored_hull_geometry] : [],
994
+ )
1110
995
 
1111
996
  // Domains on the outer surface (used by the "Surface" formula overlay quick-select).
1112
997
  const surface_formulas = $derived.by((): SvelteSet<string> => {
@@ -1343,44 +1228,12 @@
1343
1228
  return result
1344
1229
  })
1345
1230
 
1346
- function dispose_geometry(geometry: THREE.BufferGeometry | null | undefined): void {
1347
- if (!geometry) return
1348
- geometry.dispose()
1349
- }
1350
-
1351
- function dispose_geometries(geometries: (THREE.BufferGeometry | null | undefined)[]): void {
1352
- for (const geometry of geometries) dispose_geometry(geometry)
1353
- }
1354
-
1355
- $effect(() => {
1356
- const geometry = edge_geometry
1357
- return () => dispose_geometry(geometry)
1358
- })
1359
-
1360
- $effect(() => {
1361
- const geometry = occlusion_hull_geometry
1362
- return () => dispose_geometry(geometry)
1363
- })
1364
-
1365
- $effect(() => {
1366
- const geometry = bounding_box_geometry
1367
- return () => dispose_geometry(geometry)
1368
- })
1369
-
1370
- $effect(() => {
1371
- const geometries = formula_edge_data.map((data) => data.geometry)
1372
- return () => dispose_geometries(geometries)
1373
- })
1374
-
1375
- $effect(() => {
1376
- const geometries = formula_mesh_data.map((data) => data.geometry)
1377
- return () => dispose_geometries(geometries)
1378
- })
1379
-
1380
- $effect(() => {
1381
- const geometries = hover_mesh_data.map((data) => data.geometry)
1382
- return () => dispose_geometries(geometries)
1383
- })
1231
+ dispose_on_change(() => [edge_geometry])
1232
+ dispose_on_change(() => [occlusion_hull_geometry])
1233
+ dispose_on_change(() => [bounding_box_geometry])
1234
+ dispose_on_change(() => formula_edge_data.map((data) => data.geometry))
1235
+ dispose_on_change(() => formula_mesh_data.map((data) => data.geometry))
1236
+ dispose_on_change(() => hover_mesh_data.map((data) => data.geometry))
1384
1237
 
1385
1238
  // === Grid, axes, ticks (matching ScatterPlot3D style) ===
1386
1239
 
@@ -1437,8 +1290,8 @@
1437
1290
  // Niced ranges (from ticks) padded so the grid extends beyond the diagram.
1438
1291
  // For horizontal axes (0,1): pad both sides.
1439
1292
  // For vertical axis (2): use actual data range and round min down to an integer.
1440
- const niced_range = $derived.by(() => {
1441
- return [0, 1, 2].map((axis): Vec2 => {
1293
+ const niced_range = $derived(
1294
+ [0, 1, 2].map((axis): Vec2 => {
1442
1295
  const ticks = data_ticks[axis]
1443
1296
  const lo = ticks[0]
1444
1297
  const hi = ticks.at(-1) ?? lo
@@ -1448,8 +1301,8 @@
1448
1301
  return [Math.floor(min_data), hi]
1449
1302
  }
1450
1303
  return [lo - step, hi + step]
1451
- })
1452
- })
1304
+ }),
1305
+ )
1453
1306
 
1454
1307
  // Helper to create a line geometry from two Vec3 arrays
1455
1308
  function make_line_geom(start: Vec3, end: Vec3): THREE.BufferGeometry {
@@ -1618,11 +1471,11 @@
1618
1471
  tick_label.style.visibility = ``
1619
1472
  }
1620
1473
  const domain_rects = Array.from(wrapper.querySelectorAll<HTMLElement>(`.domain-label`))
1621
- .filter((domain_label) => {
1622
- const style = getComputedStyle(domain_label)
1474
+ .filter((label_el) => {
1475
+ const style = getComputedStyle(label_el)
1623
1476
  return style.display !== `none` && style.visibility !== `hidden`
1624
1477
  })
1625
- .map((domain_label) => pad_rect(domain_label.getBoundingClientRect(), 1))
1478
+ .map((label_el) => pad_rect(label_el.getBoundingClientRect(), 1))
1626
1479
  if (domain_rects.length === 0) return
1627
1480
 
1628
1481
  for (const tick_label of tick_labels) {
@@ -1750,20 +1603,13 @@
1750
1603
  bg_css_var: `--chempot-3d-bg-fullscreen`,
1751
1604
  })
1752
1605
 
1753
- $effect(() => {
1754
- const grid_geometries = grid_config
1755
- return () => {
1756
- for (const grid_item of grid_geometries) {
1757
- dispose_geometry(grid_item.line_geom)
1758
- for (const tick_geometry of grid_item.tick_geoms) {
1759
- dispose_geometry(tick_geometry)
1760
- }
1761
- for (const line_geometry of grid_item.grid_geoms) {
1762
- dispose_geometry(line_geometry)
1763
- }
1764
- }
1765
- }
1766
- })
1606
+ dispose_on_change(() =>
1607
+ grid_config.flatMap((grid_item) => [
1608
+ grid_item.line_geom,
1609
+ ...grid_item.tick_geoms,
1610
+ ...grid_item.grid_geoms,
1611
+ ]),
1612
+ )
1767
1613
 
1768
1614
  const projection_planes = $derived.by(() => {
1769
1615
  const projections = display.projections
@@ -1997,15 +1843,12 @@
1997
1843
  }
1998
1844
 
1999
1845
  function stop_phase_pointer_event(raw_event: unknown): void {
2000
- if (!raw_event || typeof raw_event !== `object`) return
2001
-
2002
- const event = raw_event as { nativeEvent?: unknown; stopPropagation?: () => void }
2003
- event.stopPropagation?.()
2004
-
2005
- const native_event = event.nativeEvent
2006
- if (!native_event || typeof native_event !== `object`) return
2007
- const native_pointer_event = native_event as { stopPropagation?: () => void }
2008
- native_pointer_event.stopPropagation?.()
1846
+ const event = raw_event as
1847
+ | { nativeEvent?: { stopPropagation?: () => void }; stopPropagation?: () => void }
1848
+ | null
1849
+ | undefined
1850
+ event?.stopPropagation?.()
1851
+ event?.nativeEvent?.stopPropagation?.()
2009
1852
  }
2010
1853
 
2011
1854
  function handle_phase_hover(domain_data: HoverMeshData, raw_event: unknown): void {
@@ -2307,6 +2150,17 @@
2307
2150
  <p>Need at least 2 elements with elemental reference entries.</p>
2308
2151
  </div>
2309
2152
  {:else if mounted && typeof WebGLRenderingContext !== `undefined`}
2153
+ {#snippet orbit_controls()}
2154
+ <extras.OrbitControls
2155
+ bind:ref={orbit_controls_ref}
2156
+ enableRotate
2157
+ enableZoom
2158
+ enablePan
2159
+ autoRotate={auto_rotate > 0 && page_visibility.visible}
2160
+ autoRotateSpeed={auto_rotate}
2161
+ target={camera_target}
2162
+ />
2163
+ {/snippet}
2310
2164
  <Canvas
2311
2165
  createRenderer={(cvs) =>
2312
2166
  new THREE.WebGLRenderer({
@@ -2326,15 +2180,7 @@
2326
2180
  near={0.1}
2327
2181
  far={data_extent * 10}
2328
2182
  >
2329
- <extras.OrbitControls
2330
- bind:ref={orbit_controls_ref}
2331
- enableRotate
2332
- enableZoom
2333
- enablePan
2334
- autoRotate={auto_rotate > 0}
2335
- autoRotateSpeed={auto_rotate}
2336
- target={camera_target}
2337
- />
2183
+ {@render orbit_controls()}
2338
2184
  </T.OrthographicCamera>
2339
2185
  {:else}
2340
2186
  <T.PerspectiveCamera
@@ -2344,15 +2190,7 @@
2344
2190
  near={0.1}
2345
2191
  far={data_extent * 10}
2346
2192
  >
2347
- <extras.OrbitControls
2348
- bind:ref={orbit_controls_ref}
2349
- enableRotate
2350
- enableZoom
2351
- enablePan
2352
- autoRotate={auto_rotate > 0}
2353
- autoRotateSpeed={auto_rotate}
2354
- target={camera_target}
2355
- />
2193
+ {@render orbit_controls()}
2356
2194
  </T.PerspectiveCamera>
2357
2195
  {/if}
2358
2196
 
@@ -2484,7 +2322,9 @@
2484
2322
  portal={wrapper}
2485
2323
  zIndexRange={[1, 0]}
2486
2324
  >
2487
- <span class="axis-label" style:color={gc.color}>{@html gc.label}</span>
2325
+ <span class="axis-label" style:color={gc.color}
2326
+ >{@html sanitize_html(gc.label)}</span
2327
+ >
2488
2328
  </extras.HTML>
2489
2329
  {/if}
2490
2330
  {/each}
@@ -2512,7 +2352,7 @@
2512
2352
  </ChemPotScene3D>
2513
2353
  </Canvas>
2514
2354
  <!-- Color bar for continuous modes -->
2515
- {#if color_mode !== `none` && color_mode !== `arity` && color_range}
2355
+ {#if color_range}
2516
2356
  {@const color_bar_config = get_chempot_color_bar_config(
2517
2357
  color_scale,
2518
2358
  reverse_color_scale,
@@ -2532,7 +2372,7 @@
2532
2372
  <div class="arity-legend">
2533
2373
  {#each arity_legend_labels as label, idx (label)}
2534
2374
  <span>
2535
- <span style:background={arity_colors[idx]}></span>
2375
+ <span style:background={ARITY_COLORS[idx]}></span>
2536
2376
  {label}
2537
2377
  </span>
2538
2378
  {/each}
@@ -1,24 +1,13 @@
1
1
  // Async wrapper for compute_chempot_diagram via Web Worker.
2
2
  // Falls back to synchronous main-thread computation during SSR.
3
- import { compute_chempot_diagram, formula_key_from_composition } from './compute';
3
+ import { compute_chempot_diagram, entry_fingerprint } from './compute';
4
4
  import { to_error } from '../utils';
5
5
  let worker = null;
6
6
  let next_id = 0;
7
7
  const pending = new Map();
8
8
  const pending_by_key = new Map();
9
9
  function make_compute_request_key(entries, config) {
10
- const keyed_entries = entries
11
- .map((entry) => [
12
- formula_key_from_composition(entry.composition),
13
- entry.energy,
14
- entry.energy_per_atom ?? ``,
15
- entry.e_form_per_atom ?? ``,
16
- entry.is_stable ?? ``,
17
- entry.e_above_hull ?? ``,
18
- entry.exclude_from_hull ?? ``,
19
- ].join(`:`))
20
- .sort();
21
- return `${keyed_entries.join(`,`)}|${JSON.stringify(config)}`;
10
+ return `${entries.map(entry_fingerprint).sort().join(`,`)}|${JSON.stringify(config)}`;
22
11
  }
23
12
  function track_pending(request_key, promise) {
24
13
  pending_by_key.set(request_key, promise);
@@ -53,6 +42,16 @@ function get_worker() {
53
42
  return worker;
54
43
  }
55
44
  export function compute_chempot_async(entries, config = {}) {
45
+ // Never throw synchronously: callers handle errors via .catch() only, so key
46
+ // construction or Worker instantiation failures (e.g. CSP) must reject instead
47
+ try {
48
+ return compute_chempot_async_unsafe(entries, config);
49
+ }
50
+ catch (err) {
51
+ return Promise.reject(to_error(err));
52
+ }
53
+ }
54
+ function compute_chempot_async_unsafe(entries, config) {
56
55
  const request_key = make_compute_request_key(entries, config);
57
56
  const existing = pending_by_key.get(request_key);
58
57
  if (existing)
@@ -1,7 +1,27 @@
1
1
  import { type D3InterpolateName } from '../colors';
2
+ import type { PhaseData } from '../convex-hull/types';
2
3
  import type { Vec2 } from '../math';
3
- export declare function make_chempot_color_scale(values: number[], interpolator_name: D3InterpolateName, reverse: boolean): ((val: number) => string) | null;
4
+ import { type FormulaEnergyStats } from './compute';
5
+ import type { ChemPotColorMode } from './types';
6
+ export declare const ARITY_COLORS: readonly ["#3498db", "#2ecc71", "#e67e22", "#9b59b6"];
4
7
  export declare function get_chempot_color_bar_config(color_scale: D3InterpolateName, reverse: boolean): {
5
8
  color_scale_fn: (frac: number) => string;
6
9
  color_scale_domain: Vec2;
7
10
  };
11
+ export interface ChemPotDomainColorData {
12
+ colors: Map<string, string>;
13
+ color_range: {
14
+ min: number;
15
+ max: number;
16
+ label: string;
17
+ } | null;
18
+ }
19
+ export declare function get_domain_color_data(opts: {
20
+ formulas: string[];
21
+ color_mode: ChemPotColorMode;
22
+ color_scale: D3InterpolateName;
23
+ reverse_color_scale: boolean;
24
+ entries: PhaseData[];
25
+ el_refs: Record<string, PhaseData>;
26
+ energy_stats: Map<string, FormulaEnergyStats>;
27
+ }): ChemPotDomainColorData;