matterviz 0.3.2 → 0.3.4

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 (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,786 +1,1102 @@
1
- <script lang="ts">import { add_alpha, is_dark_mode, PLOT_COLORS, vesta_hex, watch_dark_mode, } from '../colors';
2
- import { normalize_show_controls } from '../controls';
3
- import { ClickFeedback, DragOverlay, Spinner } from '../feedback';
4
- import Icon from '../Icon.svelte';
5
- import { set_fullscreen_bg, setup_fullscreen_effect, toggle_fullscreen, } from '../layout';
6
- import { ColorBar, PlotTooltip } from '../plot';
7
- import { DEFAULTS } from '../settings';
8
- import { barycentric_to_tetrahedral, compute_4d_coords, TETRAHEDRON_VERTICES, } from './barycentric-coords';
9
- import ConvexHullControls from './ConvexHullControls.svelte';
10
- import ConvexHullInfoPane from './ConvexHullInfoPane.svelte';
11
- import ConvexHullTooltip from './ConvexHullTooltip.svelte';
12
- import GasPressureControls from './GasPressureControls.svelte';
13
- import * as helpers from './helpers';
14
- import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index';
15
- import StructurePopup from './StructurePopup.svelte';
16
- import TemperatureSlider from './TemperatureSlider.svelte';
17
- import * as thermo from './thermodynamics';
18
- let { entries = [], controls = {}, config = {}, on_point_click, on_point_hover, fullscreen = $bindable(DEFAULTS.convex_hull.quaternary.fullscreen), enable_fullscreen = true, enable_info_pane = true, wrapper = $bindable(), label_threshold = 50, show_stable = $bindable(DEFAULTS.convex_hull.quaternary.show_stable), show_unstable = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable), show_hull_faces = $bindable(DEFAULTS.convex_hull.quaternary.show_hull_faces), hull_face_opacity = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_opacity), hull_face_color_mode = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_color_mode), element_colors = vesta_hex, color_mode = $bindable(DEFAULTS.convex_hull.quaternary.color_mode), color_scale = $bindable(DEFAULTS.convex_hull.quaternary.color_scale), info_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.info_pane_open), legend_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open), max_hull_dist_show_phases = $bindable(DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases), max_hull_dist_show_labels = $bindable(DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels), show_stable_labels = $bindable(DEFAULTS.convex_hull.quaternary.show_stable_labels), show_unstable_labels = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable_labels), on_file_drop, enable_click_selection = true, enable_structure_preview = true, energy_source_mode = $bindable(`precomputed`), phase_stats = $bindable(null), stable_entries = $bindable([]), unstable_entries = $bindable([]), highlighted_entries = $bindable([]), highlight_style = {}, selected_entry = $bindable(null), temperature = $bindable(), interpolate_temperature = true, max_interpolation_gap = 500, gas_config, gas_pressures = $bindable({}), children, tooltip, ...rest } = $props();
19
- const merged_controls = $derived({ ...default_controls, ...controls });
20
- const controls_config = $derived(normalize_show_controls(merged_controls.show));
21
- const merged_config = $derived({
1
+ <script lang="ts">
2
+ import type { D3InterpolateName } from '../colors'
3
+ import {
4
+ add_alpha,
5
+ is_dark_mode,
6
+ PLOT_COLORS,
7
+ vesta_hex,
8
+ watch_dark_mode,
9
+ } from '../colors'
10
+ import { normalize_show_controls } from '../controls'
11
+ import { sanitize_html } from '../sanitize'
12
+ import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
13
+ import Icon from '../Icon.svelte'
14
+ import {
15
+ set_fullscreen_bg,
16
+ setup_fullscreen_effect,
17
+ toggle_fullscreen,
18
+ } from '../layout'
19
+ import { ColorBar, PlotTooltip } from '../plot'
20
+ import { DEFAULTS } from '../settings'
21
+ import type { AnyStructure } from '../structure'
22
+ import {
23
+ barycentric_to_tetrahedral,
24
+ compute_4d_coords,
25
+ TETRAHEDRON_VERTICES,
26
+ } from './barycentric-coords'
27
+ import ConvexHullControls from './ConvexHullControls.svelte'
28
+ import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
29
+ import ConvexHullTooltip from './ConvexHullTooltip.svelte'
30
+ import GasPressureControls from './GasPressureControls.svelte'
31
+ import * as helpers from './helpers'
32
+ import type { BaseConvexHullProps, Hull3DProps } from './index'
33
+ import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
34
+ import StructurePopup from './StructurePopup.svelte'
35
+ import TemperatureSlider from './TemperatureSlider.svelte'
36
+ import type { Point4D } from './thermodynamics'
37
+ import * as thermo from './thermodynamics'
38
+ import type {
39
+ ConvexHullEntry,
40
+ HighlightStyle,
41
+ HoverData3D,
42
+ HullFaceColorMode,
43
+ } from './types'
44
+ import { compute_hull_stability } from './helpers'
45
+
46
+ let {
47
+ entries = [],
48
+ controls = {},
49
+ config = {},
50
+ on_point_click,
51
+ on_point_hover,
52
+ fullscreen = $bindable(DEFAULTS.convex_hull.quaternary.fullscreen),
53
+ enable_fullscreen = true,
54
+ enable_info_pane = true,
55
+ wrapper = $bindable(),
56
+ label_threshold = 50,
57
+ show_stable = $bindable(DEFAULTS.convex_hull.quaternary.show_stable),
58
+ show_unstable = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable),
59
+ show_hull_faces = $bindable(DEFAULTS.convex_hull.quaternary.show_hull_faces),
60
+ hull_face_opacity = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_opacity),
61
+ hull_face_color_mode = $bindable(
62
+ DEFAULTS.convex_hull.quaternary.hull_face_color_mode as HullFaceColorMode,
63
+ ),
64
+ element_colors = vesta_hex,
65
+ color_mode = $bindable(DEFAULTS.convex_hull.quaternary.color_mode),
66
+ color_scale = $bindable(
67
+ DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName,
68
+ ),
69
+ info_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.info_pane_open),
70
+ legend_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
71
+ max_hull_dist_show_phases = $bindable(
72
+ DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
73
+ ),
74
+ max_hull_dist_show_labels = $bindable(
75
+ DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels,
76
+ ),
77
+ show_stable_labels = $bindable(
78
+ DEFAULTS.convex_hull.quaternary.show_stable_labels,
79
+ ),
80
+ show_unstable_labels = $bindable(
81
+ DEFAULTS.convex_hull.quaternary.show_unstable_labels,
82
+ ),
83
+ on_file_drop,
84
+ enable_click_selection = true,
85
+ enable_structure_preview = true,
86
+ energy_source_mode = $bindable(`precomputed`),
87
+ phase_stats = $bindable(null),
88
+ stable_entries = $bindable([]),
89
+ unstable_entries = $bindable([]),
90
+ highlighted_entries = $bindable([]),
91
+ highlight_style = {},
92
+ selected_entry = $bindable(null),
93
+ temperature = $bindable(),
94
+ interpolate_temperature = true,
95
+ max_interpolation_gap = 500,
96
+ gas_config,
97
+ gas_pressures = $bindable({}),
98
+ children,
99
+ tooltip,
100
+ ...rest
101
+ }: BaseConvexHullProps<ConvexHullEntry> & Hull3DProps & {
102
+ highlight_style?: HighlightStyle
103
+ } = $props()
104
+
105
+ const merged_controls = $derived({ ...default_controls, ...controls })
106
+ const controls_config = $derived(normalize_show_controls(merged_controls.show))
107
+ const merged_config = $derived({
22
108
  ...default_hull_config,
23
109
  ...config,
24
110
  colors: { ...default_hull_config.colors, ...(config.colors || {}) },
25
111
  margin: { t: 60, r: 60, b: 60, l: 60, ...(config.margin || {}) },
26
- });
27
- // Reactive dark mode detection for canvas text color
28
- let dark_mode = $state(is_dark_mode());
29
- $effect(() => watch_dark_mode((dark) => dark_mode = dark));
30
- const text_color = $derived(helpers.get_canvas_text_color(dark_mode));
31
- // Temperature-dependent free energy support
32
- const { has_temp_data, available_temperatures } = $derived(helpers.analyze_temperature_data(entries));
33
- // Initialize or reset temperature when it's undefined or no longer valid
34
- $effect(() => {
35
- if (has_temp_data &&
36
- available_temperatures.length > 0 &&
37
- (temperature === undefined || !available_temperatures.includes(temperature)))
38
- temperature = available_temperatures[0];
39
- });
40
- // Filter entries by temperature when in temperature mode
41
- const temp_filtered_entries = $derived(has_temp_data && temperature !== undefined
42
- ? helpers.filter_entries_at_temperature(entries, temperature, {
112
+ })
113
+
114
+ // Reactive dark mode detection for canvas text color
115
+ let dark_mode = $state(is_dark_mode())
116
+ $effect(() => watch_dark_mode((dark) => dark_mode = dark))
117
+ const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
118
+
119
+ // Temperature-dependent free energy support
120
+ const { has_temp_data, available_temperatures } = $derived(
121
+ helpers.analyze_temperature_data(entries),
122
+ )
123
+
124
+ // Initialize or reset temperature when it's undefined or no longer valid
125
+ $effect(() => {
126
+ if (
127
+ has_temp_data &&
128
+ available_temperatures.length > 0 &&
129
+ (temperature === undefined || !available_temperatures.includes(temperature))
130
+ ) temperature = available_temperatures[0]
131
+ })
132
+
133
+ // Filter entries by temperature when in temperature mode
134
+ const temp_filtered_entries = $derived(
135
+ has_temp_data && temperature !== undefined
136
+ ? helpers.filter_entries_at_temperature(entries, temperature, {
43
137
  interpolate: interpolate_temperature,
44
138
  max_interpolation_gap,
45
- })
46
- : entries);
47
- // Gas-dependent chemical potential support (corrections based on T, P)
48
- const { entries: gas_corrected_entries, analysis: gas_analysis, merged_config: merged_gas_config, } = $derived(helpers.get_gas_corrected_entries(temp_filtered_entries, gas_config, gas_pressures, temperature ?? helpers.DEFAULT_GAS_TEMP));
49
- let { // Compute energy mode information
50
- has_precomputed_e_form, has_precomputed_hull, can_compute_e_form, can_compute_hull, energy_mode, unary_refs, } = $derived(helpers.compute_energy_mode_info(gas_corrected_entries, thermo.find_lowest_energy_unary_refs, energy_source_mode));
51
- const effective_entries = $derived(helpers.get_effective_entries(gas_corrected_entries, energy_mode, unary_refs, thermo.compute_e_form_per_atom));
52
- // Process convex hull data with unified PhaseData interface using effective entries
53
- const pd_data = $derived(thermo.process_hull_entries(effective_entries));
54
- // Pre-compute polymorph stats once for O(1) tooltip lookups
55
- const polymorph_stats_map = $derived(helpers.compute_all_polymorph_stats(effective_entries));
56
- const elements = $derived.by(() => {
139
+ })
140
+ : entries,
141
+ )
142
+
143
+ // Gas-dependent chemical potential support (corrections based on T, P)
144
+ const {
145
+ entries: gas_corrected_entries,
146
+ analysis: gas_analysis,
147
+ merged_config: merged_gas_config,
148
+ } = $derived(
149
+ helpers.get_gas_corrected_entries(
150
+ temp_filtered_entries,
151
+ gas_config,
152
+ gas_pressures,
153
+ temperature ?? helpers.DEFAULT_GAS_TEMP,
154
+ ),
155
+ )
156
+
157
+ let { // Compute energy mode information
158
+ has_precomputed_e_form,
159
+ has_precomputed_hull,
160
+ can_compute_e_form,
161
+ can_compute_hull,
162
+ energy_mode,
163
+ unary_refs,
164
+ } = $derived(
165
+ helpers.compute_energy_mode_info(
166
+ gas_corrected_entries,
167
+ thermo.find_lowest_energy_unary_refs,
168
+ energy_source_mode,
169
+ ),
170
+ )
171
+
172
+ const effective_entries = $derived(
173
+ helpers.get_effective_entries(
174
+ gas_corrected_entries,
175
+ energy_mode,
176
+ unary_refs,
177
+ thermo.compute_e_form_per_atom,
178
+ ),
179
+ )
180
+
181
+ // Process convex hull data with unified PhaseData interface using effective entries
182
+ const pd_data = $derived(thermo.process_hull_entries(effective_entries))
183
+
184
+ // Pre-compute polymorph stats once for O(1) tooltip lookups
185
+ const polymorph_stats_map = $derived(
186
+ helpers.compute_all_polymorph_stats(effective_entries),
187
+ )
188
+
189
+ const elements = $derived.by(() => {
57
190
  if (pd_data.elements.length > 4) {
58
- console.error(`ConvexHull4D: Dataset contains ${pd_data.elements.length} elements, but quaternary diagrams require exactly 4. Found: [${pd_data.elements.join(`, `)}]`);
59
- return [];
191
+ console.error(
192
+ `ConvexHull4D: Dataset contains ${pd_data.elements.length} elements, but quaternary diagrams require exactly 4. Found: [${
193
+ pd_data.elements.join(`, `)
194
+ }]`,
195
+ )
196
+ return []
60
197
  }
61
- return pd_data.elements;
62
- });
63
- // Compute 4D hull for visualization (always compute when we have formation energies)
64
- const hull_4d = $derived.by(() => {
65
- if (elements.length !== 4)
66
- return [];
198
+ return pd_data.elements
199
+ })
200
+
201
+ // Compute 4D hull for visualization (always compute when we have formation energies)
202
+ const hull_4d = $derived.by(() => {
203
+ if (elements.length !== 4) return []
204
+
67
205
  try {
68
- // Get coords with formation energies
69
- const coords = compute_4d_coords(pd_data.entries, elements);
70
- // Convert to 4D points for hull computation using barycentric coordinates (composition fractions)
71
- const points_4d = coords
72
- .filter((ent) => Number.isFinite(ent.e_form_per_atom) &&
73
- [ent.x, ent.y, ent.z].every(Number.isFinite))
74
- .map((ent) => {
75
- const amounts = elements.map((el) => ent.composition[el] || 0);
76
- const total = amounts.reduce((sum, amt) => sum + amt, 0);
77
- if (!(total > 0))
78
- return { x: NaN, y: NaN, z: NaN, w: NaN };
79
- const [x, y, z] = amounts.map((amt) => amt / total);
80
- return { x, y, z, w: ent.e_form_per_atom };
206
+ // Get coords with formation energies, excluding entries that don't participate in hull
207
+ const coords = compute_4d_coords(pd_data.entries, elements)
208
+ .filter((ent) => !ent.exclude_from_hull)
209
+
210
+ // Convert to 4D points for hull computation using barycentric coordinates (composition fractions)
211
+ const points_4d: Point4D[] = coords
212
+ .filter(
213
+ (ent) =>
214
+ Number.isFinite(ent.e_form_per_atom) &&
215
+ [ent.x, ent.y, ent.z].every(Number.isFinite),
216
+ )
217
+ .map((ent) => {
218
+ const amounts = elements.map((el) => ent.composition[el] || 0)
219
+ const total = amounts.reduce((sum, amt) => sum + amt, 0)
220
+ if (!(total > 0)) return { x: NaN, y: NaN, z: NaN, w: NaN }
221
+ const [x, y, z] = amounts.map((amt) => amt / total)
222
+ return { x, y, z, w: ent.e_form_per_atom ?? NaN }
81
223
  })
82
- .filter((p) => [p.x, p.y, p.z, p.w].every(Number.isFinite));
83
- if (points_4d.length < 5)
84
- return []; // Need at least 5 points for 4D hull
85
- return thermo.compute_lower_hull_4d(points_4d);
86
- }
87
- catch (error) {
88
- console.error(`Error computing 4D hull:`, error);
89
- return [];
224
+ .filter((point) => [point.x, point.y, point.z, point.w].every(Number.isFinite))
225
+
226
+ if (points_4d.length < 5) return [] // Need at least 5 points for 4D hull
227
+
228
+ return thermo.compute_lower_hull_4d(points_4d)
229
+ } catch (error) {
230
+ console.error(`Error computing 4D hull:`, error)
231
+ return []
90
232
  }
91
- });
92
- // Enrich coords with e_above_hull (before filtering)
93
- const all_enriched_entries = $derived.by(() => {
94
- if (elements.length !== 4)
95
- return [];
233
+ })
234
+
235
+ // Enrich coords with e_above_hull (before filtering)
236
+ const all_enriched_entries = $derived.by(() => {
237
+ if (elements.length !== 4) return []
96
238
  try {
97
- const coords = compute_4d_coords(pd_data.entries, elements);
98
- if (energy_mode !== `on-the-fly` || hull_4d.length === 0)
99
- return coords;
100
- // Build 4D points, tracking original indices for mapping hull distances back
101
- const valid = coords.flatMap((entry, idx) => {
102
- if (!Number.isFinite(entry.e_form_per_atom) ||
103
- ![entry.x, entry.y, entry.z].every(Number.isFinite))
104
- return [];
105
- const amounts = elements.map((el) => entry.composition[el] || 0);
106
- const total = amounts.reduce((s, a) => s + a, 0);
107
- if (!(total > 0))
108
- return [];
109
- const [x, y, z] = amounts.map((a) => a / total);
110
- return [x, y, z].every(Number.isFinite)
111
- ? [{ idx, pt: { x, y, z, w: entry.e_form_per_atom } }]
112
- : [];
113
- });
114
- const e_hulls = thermo.compute_e_above_hull_4d(valid.map((v) => v.pt), hull_4d);
115
- const hull_map = new Map(valid.map((v, hi) => [v.idx, e_hulls[hi]]));
116
- return coords.map((entry, idx) => ({
117
- ...entry,
118
- e_above_hull: hull_map.get(idx),
119
- }));
120
- }
121
- catch (err) {
122
- console.error(`Error computing quaternary coordinates:`, err);
123
- return [];
239
+ const coords = compute_4d_coords(pd_data.entries, elements)
240
+ if (energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
241
+
242
+ // Build 4D points, tracking original indices for mapping hull distances back
243
+ const valid = coords.flatMap((entry, idx) => {
244
+ if (
245
+ !Number.isFinite(entry.e_form_per_atom) ||
246
+ ![entry.x, entry.y, entry.z].every(Number.isFinite)
247
+ ) return []
248
+ const amounts = elements.map((el) => entry.composition[el] || 0)
249
+ const total = amounts.reduce((sum, amt) => sum + amt, 0)
250
+ if (!(total > 0)) return []
251
+ const [x, y, z] = amounts.map((amt) => amt / total)
252
+ return [x, y, z].every(Number.isFinite)
253
+ ? [{ idx, pt: { x, y, z, w: entry.e_form_per_atom ?? NaN } }]
254
+ : []
255
+ })
256
+ const raw_dists = thermo.compute_e_above_hull_4d(valid.map((item) => item.pt), hull_4d)
257
+ const hull_map = new Map(valid.map((item, hull_idx) => [item.idx, raw_dists[hull_idx]]))
258
+ return coords.map((entry, idx) => {
259
+ const raw = hull_map.get(idx)
260
+ if (raw === undefined) return { ...entry, e_above_hull: raw, is_stable: false }
261
+ return { ...entry, ...compute_hull_stability(raw, entry.exclude_from_hull) }
262
+ })
263
+ } catch (err) {
264
+ console.error(`Error computing quaternary coordinates:`, err)
265
+ return []
124
266
  }
125
- });
126
- // Auto threshold: show all for few entries, use default for many, interpolate between
127
- const max_hull_dist_in_data = $derived(helpers.calc_max_hull_dist_in_data(all_enriched_entries));
128
- const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(all_enriched_entries.length, max_hull_dist_in_data, DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases));
129
- // Initialize threshold to auto value on first load
130
- let initialized = $state(false);
131
- $effect(() => {
267
+ })
268
+
269
+ // Auto threshold: show all for few entries, use default for many, interpolate between
270
+ const max_hull_dist_in_data = $derived(
271
+ helpers.calc_max_hull_dist_in_data(all_enriched_entries),
272
+ )
273
+ const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
274
+ all_enriched_entries.length,
275
+ max_hull_dist_in_data,
276
+ DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
277
+ ))
278
+
279
+ // Initialize threshold to auto value on first load
280
+ let initialized = $state(false)
281
+ $effect(() => {
132
282
  if (!initialized && all_enriched_entries.length > 0) {
133
- initialized = true;
134
- max_hull_dist_show_phases = auto_default_threshold;
283
+ initialized = true
284
+ max_hull_dist_show_phases = auto_default_threshold
135
285
  }
136
- });
137
- // Filter by threshold and compute visibility
138
- const plot_entries = $derived(all_enriched_entries.filter((entry) => {
139
- // Always include stable entries and elemental reference points
140
- if (entry.is_stable || (entry.e_above_hull ?? Infinity) <= 1e-6)
141
- return true;
142
- return typeof entry.e_above_hull === `number` &&
143
- entry.e_above_hull <= max_hull_dist_show_phases;
144
- }).map((entry) => {
145
- const is_stable = entry.is_stable || entry.e_above_hull === 0;
146
- return {
286
+ })
287
+
288
+ // Filter by threshold and compute visibility
289
+ const plot_entries = $derived(
290
+ all_enriched_entries.filter((entry) => {
291
+ // Always include stable entries and elemental reference points
292
+ if (entry.is_stable || (entry.e_above_hull ?? Infinity) <= 1e-6) return true
293
+ return typeof entry.e_above_hull === `number` &&
294
+ entry.e_above_hull <= max_hull_dist_show_phases
295
+ }).map((entry) => {
296
+ const is_stable = entry.is_stable || entry.e_above_hull === 0
297
+ return {
147
298
  ...entry,
148
299
  visible: (is_stable && show_stable) || (!is_stable && show_unstable),
149
- };
150
- }));
151
- // Stable and unstable entries exposed as bindable props
152
- $effect(() => {
153
- stable_entries = plot_entries.filter((entry) => entry.is_stable || entry.e_above_hull === 0);
154
- unstable_entries = plot_entries.filter((entry) => (entry.e_above_hull ?? 0) > 0 && !entry.is_stable);
155
- });
156
- let canvas;
157
- let ctx = null;
158
- let frame_id = 0; // Performance optimization
159
- // Camera state - following Materials Project's 3D camera setup
160
- let camera = $state({
300
+ }
301
+ }),
302
+ )
303
+
304
+ // Stable and unstable entries exposed as bindable props
305
+ $effect(() => {
306
+ stable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
307
+ entry.is_stable || entry.e_above_hull === 0
308
+ )
309
+ unstable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
310
+ (entry.e_above_hull ?? 0) > 0 && !entry.is_stable
311
+ )
312
+ })
313
+
314
+ let canvas: HTMLCanvasElement
315
+ let ctx: CanvasRenderingContext2D | null = null
316
+ let frame_id = 0 // Performance optimization
317
+
318
+ // Camera state - following Materials Project's 3D camera setup
319
+ let camera = $state({
161
320
  rotation_x: DEFAULTS.convex_hull.quaternary.camera_rotation_x,
162
321
  rotation_y: DEFAULTS.convex_hull.quaternary.camera_rotation_y,
163
322
  zoom: DEFAULTS.convex_hull.quaternary.camera_zoom,
164
323
  center_x: 0,
165
324
  center_y: 20, // Slight offset to avoid legend overlap
166
- });
167
- // Interaction state
168
- let is_dragging = $state(false);
169
- let drag_started = $state(false);
170
- let last_mouse = $state({ x: 0, y: 0 });
171
- let hover_data = $state(null);
172
- let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } });
173
- // Drag and drop state
174
- let drag_over = $state(false);
175
- // Structure popup state
176
- let modal_open = $state(false);
177
- let selected_structure = $state(null);
178
- let modal_place_right = $state(true);
179
- // Hull face color (customizable via controls)
180
- let hull_face_color = $state(`#4caf50`);
181
- // Pulsating highlight for selected point and highlighted entries
182
- let pulse_time = $state(0);
183
- let pulse_opacity = $derived(0.3 + 0.4 * Math.sin(pulse_time * 4));
184
- let pulse_frame_id = 0;
185
- // Merge highlight style with defaults
186
- const merged_highlight_style = $derived(helpers.merge_highlight_style(highlight_style));
187
- // Helper to check if entry is highlighted
188
- const is_highlighted = (entry) => helpers.is_entry_highlighted(entry, highlighted_entries);
189
- $effect(() => {
190
- if (!selected_entry && !highlighted_entries.length)
191
- return;
192
- const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches;
193
- if (reduce)
194
- return;
325
+ })
326
+
327
+ // Interaction state
328
+ let is_dragging = $state(false)
329
+ let drag_started = $state(false)
330
+ let last_mouse = $state({ x: 0, y: 0 })
331
+ let hover_data = $state<HoverData3D<ConvexHullEntry> | null>(null)
332
+ let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
333
+
334
+ // Drag and drop state
335
+ let drag_over = $state(false)
336
+
337
+ // Structure popup state
338
+ let modal_open = $state(false)
339
+ let selected_structure = $state<AnyStructure | null>(null)
340
+ let modal_place_right = $state(true)
341
+
342
+ // Hull face color (customizable via controls)
343
+ let hull_face_color = $state(`#4caf50`)
344
+
345
+ // Pulsating highlight for selected point and highlighted entries
346
+ let pulse_time = $state(0)
347
+ let pulse_opacity = $derived(0.3 + 0.4 * Math.sin(pulse_time * 4))
348
+ let pulse_frame_id = 0
349
+
350
+ // Merge highlight style with defaults
351
+ const merged_highlight_style = $derived(
352
+ helpers.merge_highlight_style(highlight_style),
353
+ )
354
+
355
+ // Helper to check if entry is highlighted
356
+ const is_highlighted = (entry: ConvexHullEntry): boolean =>
357
+ helpers.is_entry_highlighted(entry, highlighted_entries)
358
+
359
+ $effect(() => {
360
+ if (!selected_entry && !highlighted_entries.length) return
361
+ const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches
362
+ if (reduce) return
195
363
  const animate = () => {
196
- pulse_time += 0.02;
197
- render_once();
198
- pulse_frame_id = requestAnimationFrame(animate);
199
- };
200
- pulse_frame_id = requestAnimationFrame(animate);
364
+ pulse_time += 0.02
365
+ render_once()
366
+ pulse_frame_id = requestAnimationFrame(animate)
367
+ }
368
+ pulse_frame_id = requestAnimationFrame(animate)
201
369
  return () => {
202
- if (pulse_frame_id)
203
- cancelAnimationFrame(pulse_frame_id);
204
- };
205
- });
206
- // Re-render when important state changes
207
- $effect(() => {
208
- // deno-fmt-ignore
209
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
210
- [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements];
211
- render_once();
212
- });
213
- // Visibility toggles are now bindable props
214
- // Smart label defaults - hide labels if too many entries
215
- $effect(() => {
216
- const total_entries = effective_entries.length;
217
- if (total_entries > label_threshold) {
218
- show_stable_labels = false;
219
- show_unstable_labels = false;
370
+ if (pulse_frame_id) cancelAnimationFrame(pulse_frame_id)
220
371
  }
221
- else {
222
- // For smaller datasets, show stable labels by default
223
- show_stable_labels = true;
224
- show_unstable_labels = false;
372
+ })
373
+
374
+ // Re-render when important state changes
375
+ $effect(() => {
376
+ // oxfmt-ignore
377
+ void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
378
+
379
+ render_once()
380
+ })
381
+
382
+ // Visibility toggles are now bindable props
383
+
384
+ // Smart label defaults - hide labels if too many entries
385
+ $effect(() => {
386
+ const total_entries = effective_entries.length
387
+ if (total_entries > label_threshold) {
388
+ show_stable_labels = false
389
+ show_unstable_labels = false
390
+ } else {
391
+ // For smaller datasets, show stable labels by default
392
+ show_stable_labels = true
393
+ show_unstable_labels = false
225
394
  }
226
- });
227
- // Function to extract structure data from a convex hull entry
228
- function extract_structure_from_entry(entry) {
229
- const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id);
230
- return orig_entry?.structure || null;
231
- }
232
- const reset_camera = () => {
233
- camera.rotation_x = DEFAULTS.convex_hull.quaternary.camera_rotation_x;
234
- camera.rotation_y = DEFAULTS.convex_hull.quaternary.camera_rotation_y;
235
- camera.zoom = DEFAULTS.convex_hull.quaternary.camera_zoom;
236
- camera.center_x = 0;
237
- camera.center_y = 20; // Slight offset to avoid legend overlap
238
- };
239
- function reset_all() {
240
- reset_camera();
241
- fullscreen = DEFAULTS.convex_hull.quaternary.fullscreen;
242
- info_pane_open = DEFAULTS.convex_hull.quaternary.info_pane_open;
243
- legend_pane_open = DEFAULTS.convex_hull.quaternary.legend_pane_open;
244
- color_mode = DEFAULTS.convex_hull.quaternary.color_mode;
245
- color_scale = DEFAULTS.convex_hull.quaternary.color_scale;
246
- show_stable = DEFAULTS.convex_hull.quaternary.show_stable;
247
- show_unstable = DEFAULTS.convex_hull.quaternary.show_unstable;
248
- show_stable_labels = DEFAULTS.convex_hull.quaternary.show_stable_labels;
249
- show_unstable_labels = DEFAULTS.convex_hull.quaternary.show_unstable_labels;
395
+ })
396
+
397
+ // Function to extract structure data from a convex hull entry
398
+ function extract_structure_from_entry(
399
+ entry: ConvexHullEntry,
400
+ ): AnyStructure | null {
401
+ const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
402
+ return orig_entry?.structure as AnyStructure || null
403
+ }
404
+
405
+ const reset_camera = () => {
406
+ camera.rotation_x = DEFAULTS.convex_hull.quaternary.camera_rotation_x
407
+ camera.rotation_y = DEFAULTS.convex_hull.quaternary.camera_rotation_y
408
+ camera.zoom = DEFAULTS.convex_hull.quaternary.camera_zoom
409
+ camera.center_x = 0
410
+ camera.center_y = 20 // Slight offset to avoid legend overlap
411
+ }
412
+ function reset_all() {
413
+ reset_camera()
414
+ fullscreen = DEFAULTS.convex_hull.quaternary.fullscreen
415
+ info_pane_open = DEFAULTS.convex_hull.quaternary.info_pane_open
416
+ legend_pane_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
417
+ color_mode = DEFAULTS.convex_hull.quaternary.color_mode
418
+ color_scale = DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName
419
+ show_stable = DEFAULTS.convex_hull.quaternary.show_stable
420
+ show_unstable = DEFAULTS.convex_hull.quaternary.show_unstable
421
+ show_stable_labels = DEFAULTS.convex_hull.quaternary.show_stable_labels
422
+ show_unstable_labels = DEFAULTS.convex_hull.quaternary.show_unstable_labels
250
423
  // Use auto-computed threshold based on entry count instead of static default
251
- max_hull_dist_show_phases = auto_default_threshold;
424
+ max_hull_dist_show_phases = auto_default_threshold
252
425
  max_hull_dist_show_labels =
253
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels;
254
- show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces;
255
- hull_face_color = DEFAULTS.convex_hull.quaternary.hull_face_color;
256
- hull_face_opacity = DEFAULTS.convex_hull.quaternary.hull_face_opacity;
426
+ DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels
427
+ show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces
428
+ hull_face_color = DEFAULTS.convex_hull.quaternary.hull_face_color
429
+ hull_face_opacity = DEFAULTS.convex_hull.quaternary.hull_face_opacity
257
430
  hull_face_color_mode = DEFAULTS.convex_hull.quaternary
258
- .hull_face_color_mode;
259
- }
260
- const handle_keydown = (event) => {
261
- const target = event.target;
431
+ .hull_face_color_mode as HullFaceColorMode
432
+ }
433
+
434
+ const handle_keydown = (event: KeyboardEvent) => {
435
+ const target = event.target as HTMLElement
262
436
  // Skip if focus is on an interactive element that handles Enter natively
263
- const interactive_selector = `input,textarea,select,button,a,[contenteditable="true"],[role="button"],[tabindex]:not([tabindex="-1"])`;
264
- if (target.matches(interactive_selector) && target !== canvas)
265
- return;
437
+ const interactive_selector =
438
+ `input,textarea,select,button,a,[contenteditable="true"],[role="button"],[tabindex]:not([tabindex="-1"])`
439
+ if (target.matches(interactive_selector) && target !== canvas) return
440
+
266
441
  // Prevent double handling from canvas + wrapper bubbling
267
- if (event.target !== event.currentTarget && event.currentTarget !== canvas)
268
- return;
442
+ if (event.target !== event.currentTarget && event.currentTarget !== canvas) return
443
+
269
444
  // Handle Enter for keyboard accessibility - select hovered entry
270
445
  if (event.key === `Enter`) {
271
- const entry = hover_data?.entry;
272
- if (entry) {
273
- on_point_click?.(entry);
274
- if (enable_click_selection) {
275
- selected_entry = entry;
276
- if (enable_structure_preview) {
277
- const structure = extract_structure_from_entry(entry);
278
- if (structure) {
279
- selected_structure = structure;
280
- modal_place_right = helpers.calculate_modal_side(wrapper);
281
- modal_open = true;
282
- }
283
- }
446
+ const entry = hover_data?.entry
447
+ if (entry) {
448
+ on_point_click?.(entry)
449
+ if (enable_click_selection) {
450
+ selected_entry = entry
451
+ if (enable_structure_preview) {
452
+ const structure = extract_structure_from_entry(entry)
453
+ if (structure) {
454
+ selected_structure = structure
455
+ modal_place_right = helpers.calculate_modal_side(wrapper)
456
+ modal_open = true
284
457
  }
458
+ }
285
459
  }
286
- else if (modal_open) {
287
- close_structure_popup();
288
- }
289
- return;
460
+ } else if (modal_open) {
461
+ close_structure_popup()
462
+ }
463
+ return
464
+ }
465
+
466
+ const actions: Record<string, () => void> = {
467
+ r: reset_camera,
468
+ b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
469
+ s: () => show_stable = !show_stable,
470
+ u: () => show_unstable = !show_unstable,
471
+ h: () => show_hull_faces = !show_hull_faces,
472
+ l: () => show_stable_labels = !show_stable_labels,
290
473
  }
291
- const actions = {
292
- r: reset_camera,
293
- b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
294
- s: () => show_stable = !show_stable,
295
- u: () => show_unstable = !show_unstable,
296
- h: () => show_hull_faces = !show_hull_faces,
297
- l: () => show_stable_labels = !show_stable_labels,
298
- };
299
- actions[event.key.toLowerCase()]?.();
300
- };
301
- async function handle_file_drop(event) {
302
- drag_over = false;
303
- const data = await helpers.parse_hull_entries_from_drop(event);
304
- if (data)
305
- on_file_drop?.(data);
306
- }
307
- async function copy_entry_data(entry, position) {
474
+ actions[event.key.toLowerCase()]?.()
475
+ }
476
+
477
+ async function handle_file_drop(event: DragEvent): Promise<void> {
478
+ drag_over = false
479
+ const data = await helpers.parse_hull_entries_from_drop(event)
480
+ if (data) on_file_drop?.(data)
481
+ }
482
+
483
+ async function copy_entry_data(
484
+ entry: ConvexHullEntry,
485
+ position: { x: number; y: number },
486
+ ) {
308
487
  await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
309
- copy_feedback.visible = visible;
310
- copy_feedback.position = pos;
311
- });
312
- }
313
- const get_point_color = (entry) => helpers.get_point_color_for_entry(entry, color_mode, merged_config.colors, energy_color_scale);
314
- // Cache energy color scale per frame/setting
315
- const energy_color_scale = $derived.by(() => helpers.get_energy_color_scale(color_mode, color_scale, plot_entries));
316
- // Convex hull statistics - compute internally and expose via bindable prop
317
- $effect(() => {
318
- phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 4);
319
- });
320
- // 3D to 2D projection following Materials Project approach
321
- function project_3d_point(x, y, z) {
322
- if (!canvas)
323
- return { x: 0, y: 0, depth: 0 };
488
+ copy_feedback.visible = visible
489
+ copy_feedback.position = pos
490
+ })
491
+ }
492
+
493
+ const get_point_color = (entry: ConvexHullEntry): string =>
494
+ helpers.get_point_color_for_entry(
495
+ entry,
496
+ color_mode,
497
+ merged_config.colors,
498
+ energy_color_scale,
499
+ )
500
+
501
+ // Cache energy color scale per frame/setting
502
+ const energy_color_scale = $derived.by(() =>
503
+ helpers.get_energy_color_scale(color_mode, color_scale, plot_entries)
504
+ )
505
+
506
+ // Convex hull statistics - compute internally and expose via bindable prop
507
+ $effect(() => {
508
+ phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 4)
509
+ })
510
+
511
+ // 3D to 2D projection following Materials Project approach
512
+ function project_3d_point(
513
+ x: number,
514
+ y: number,
515
+ z: number,
516
+ ): { x: number; y: number; depth: number } {
517
+ if (!canvas) return { x: 0, y: 0, depth: 0 }
518
+
324
519
  // Center coordinates around tetrahedron/triangle centroid
325
- let centered_x = x;
326
- let centered_y = y;
327
- let centered_z = z;
520
+ let [centered_x, centered_y, centered_z] = [x, y, z]
521
+
328
522
  // Tetrahedron centroid: average of vertices (1,0,0), (0.5,√3/2,0), (0.5,√3/6,√6/3), (0,0,0)
329
- const centroid_x = (1 + 0.5 + 0.5 + 0) / 4; // = 0.5
330
- const centroid_y = (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4; // = √3/6
331
- const centroid_z = (0 + 0 + Math.sqrt(6) / 3 + 0) / 4; // = √6/12
332
- centered_x = x - centroid_x;
333
- centered_y = y - centroid_y;
334
- centered_z = z - centroid_z;
523
+ const centroid_x = (1 + 0.5 + 0.5 + 0) / 4 // = 0.5
524
+ const centroid_y = (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4 // = √3/6
525
+ const centroid_z = (0 + 0 + Math.sqrt(6) / 3 + 0) / 4 // = √6/12
526
+ centered_x = x - centroid_x
527
+ centered_y = y - centroid_y
528
+ centered_z = z - centroid_z
529
+
335
530
  // Apply 3D transformations around the centered coordinates
336
- const cos_x = Math.cos(camera.rotation_x);
337
- const sin_x = Math.sin(camera.rotation_x);
338
- const cos_y = Math.cos(camera.rotation_y);
339
- const sin_y = Math.sin(camera.rotation_y);
531
+ const cos_x = Math.cos(camera.rotation_x)
532
+ const sin_x = Math.sin(camera.rotation_x)
533
+ const cos_y = Math.cos(camera.rotation_y)
534
+ const sin_y = Math.sin(camera.rotation_y)
535
+
340
536
  // Rotate around Y axis first
341
- const x1 = centered_x * cos_y - centered_z * sin_y;
342
- const z1 = centered_x * sin_y + centered_z * cos_y;
537
+ const x1 = centered_x * cos_y - centered_z * sin_y
538
+ const z1 = centered_x * sin_y + centered_z * cos_y
539
+
343
540
  // Then rotate around X axis
344
- const y2 = centered_y * cos_x - z1 * sin_x;
345
- const z2 = centered_y * sin_x + z1 * cos_x;
541
+ const y2 = centered_y * cos_x - z1 * sin_x
542
+ const z2 = centered_y * sin_x + z1 * cos_x
543
+
346
544
  // Apply perspective projection using cached canvas dimensions for consistency
347
- const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom;
348
- const center_x = canvas_dims.width / 2 + camera.center_x;
349
- const center_y = canvas_dims.height / 2 + camera.center_y;
545
+ const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
546
+ const center_x = canvas_dims.width / 2 + camera.center_x
547
+ const center_y = canvas_dims.height / 2 + camera.center_y
548
+
350
549
  return {
351
- x: center_x + x1 * scale,
352
- y: center_y - y2 * scale, // Flip Y for canvas coordinates
353
- depth: z2, // For depth sorting
354
- };
355
- }
356
- function draw_structure_outline() {
357
- if (!ctx)
358
- return;
359
- const styles = getComputedStyle(canvas);
550
+ x: center_x + x1 * scale,
551
+ y: center_y - y2 * scale, // Flip Y for canvas coordinates
552
+ depth: z2, // For depth sorting
553
+ }
554
+ }
555
+
556
+ function draw_structure_outline(): void {
557
+ if (!ctx) return
558
+
559
+ const styles = getComputedStyle(canvas)
360
560
  // Match gray dashed structure lines used in 3D
361
- ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color;
362
- ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width;
363
- ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash);
561
+ ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
562
+ ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
563
+ ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
564
+
364
565
  // Draw tetrahedron edges
365
- draw_tetrahedron();
566
+ draw_tetrahedron()
567
+
366
568
  // Reset dash and stroke for subsequent drawings
367
- ctx.setLineDash([]);
368
- ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121`;
369
- }
370
- function draw_tetrahedron() {
371
- if (!ctx)
372
- return;
569
+ ctx.setLineDash([])
570
+ ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121`
571
+ }
572
+
573
+ function draw_tetrahedron(): void {
574
+ if (!ctx) return
575
+
373
576
  // Convert vertices to Point3D objects
374
- const vertices = TETRAHEDRON_VERTICES.map(([x, y, z]) => ({ x, y, z }));
577
+ const vertices = TETRAHEDRON_VERTICES.map(([x, y, z]) => ({ x, y, z }))
578
+
375
579
  // Tetrahedron edges (connecting vertices)
376
580
  const edges = [
377
- [0, 1],
378
- [0, 2],
379
- [0, 3], // From vertex 0
380
- [1, 2],
381
- [1, 3], // From vertex 1
382
- [2, 3], // From vertex 2
383
- ];
581
+ [0, 1],
582
+ [0, 2],
583
+ [0, 3], // From vertex 0
584
+ [1, 2],
585
+ [1, 3], // From vertex 1
586
+ [2, 3], // From vertex 2
587
+ ]
588
+
384
589
  // Draw edges
385
- ctx.beginPath();
386
- for (const [i, j] of edges) {
387
- const v1 = vertices[i];
388
- const v2 = vertices[j];
389
- const proj1 = project_3d_point(v1.x, v1.y, v1.z);
390
- const proj2 = project_3d_point(v2.x, v2.y, v2.z);
391
- ctx.moveTo(proj1.x, proj1.y);
392
- ctx.lineTo(proj2.x, proj2.y);
590
+ ctx.beginPath()
591
+ for (const [start, end] of edges) {
592
+ const v1 = vertices[start]
593
+ const v2 = vertices[end]
594
+
595
+ const proj1 = project_3d_point(v1.x, v1.y, v1.z)
596
+ const proj2 = project_3d_point(v2.x, v2.y, v2.z)
597
+
598
+ ctx.moveTo(proj1.x, proj1.y)
599
+ ctx.lineTo(proj2.x, proj2.y)
393
600
  }
394
- ctx.stroke();
601
+ ctx.stroke()
602
+
395
603
  // Corner element labels: place just outside along line towards tetrahedron centroid
396
604
  if (elements.length === 4) {
397
- // Tetrahedron centroid in barycentric space maps to average of vertices
398
- const centroid = {
399
- x: (vertices[0].x + vertices[1].x + vertices[2].x + vertices[3].x) / 4,
400
- y: (vertices[0].y + vertices[1].y + vertices[2].y + vertices[3].y) / 4,
401
- z: (vertices[0].z + vertices[1].z + vertices[2].z + vertices[3].z) / 4,
402
- };
403
- ctx.fillStyle = text_color;
404
- ctx.font = `bold 18px Arial`;
405
- ctx.textAlign = `center`;
406
- ctx.textBaseline = `middle`;
407
- const distance = 0.06;
408
- for (let idx = 0; idx < 4; idx++) {
409
- const vx = vertices[idx];
410
- // Direction from centroid to vertex
411
- const dir = {
412
- x: vx.x - centroid.x,
413
- y: vx.y - centroid.y,
414
- z: vx.z - centroid.z,
415
- };
416
- const len = Math.hypot(dir.x, dir.y, dir.z) || 1;
417
- const label_pos = {
418
- x: vx.x + (dir.x / len) * distance,
419
- y: vx.y + (dir.y / len) * distance,
420
- z: vx.z + (dir.z / len) * distance,
421
- };
422
- const proj = project_3d_point(label_pos.x, label_pos.y, label_pos.z);
423
- ctx.fillText(elements[idx], proj.x, proj.y);
605
+ // Tetrahedron centroid in barycentric space maps to average of vertices
606
+ const centroid = {
607
+ x: (vertices[0].x + vertices[1].x + vertices[2].x + vertices[3].x) / 4,
608
+ y: (vertices[0].y + vertices[1].y + vertices[2].y + vertices[3].y) / 4,
609
+ z: (vertices[0].z + vertices[1].z + vertices[2].z + vertices[3].z) / 4,
610
+ }
611
+
612
+ ctx.fillStyle = text_color
613
+ ctx.font = `bold 18px Arial`
614
+ ctx.textAlign = `center`
615
+ ctx.textBaseline = `middle`
616
+
617
+ const distance = 0.06
618
+ for (let idx = 0; idx < 4; idx++) {
619
+ const vx = vertices[idx]
620
+ // Direction from centroid to vertex
621
+ const dir = {
622
+ x: vx.x - centroid.x,
623
+ y: vx.y - centroid.y,
624
+ z: vx.z - centroid.z,
625
+ }
626
+ const len = Math.hypot(dir.x, dir.y, dir.z) || 1
627
+ const label_pos = {
628
+ x: vx.x + (dir.x / len) * distance,
629
+ y: vx.y + (dir.y / len) * distance,
630
+ z: vx.z + (dir.z / len) * distance,
424
631
  }
632
+ const proj = project_3d_point(label_pos.x, label_pos.y, label_pos.z)
633
+ ctx.fillText(elements[idx], proj.x, proj.y)
634
+ }
425
635
  }
426
- }
427
- // Draw convex hull faces connecting stable points
428
- function draw_convex_hull_faces() {
429
- if (!ctx || !show_hull_faces || hull_4d.length === 0)
430
- return;
636
+ }
637
+
638
+ // Draw convex hull faces connecting stable points
639
+ function draw_convex_hull_faces(): void {
640
+ if (!ctx || !show_hull_faces || hull_4d.length === 0) return
641
+
431
642
  // Get stable points to determine which hull facets to draw
432
- const stable_points = plot_entries.filter((e) => e.is_stable || e.e_above_hull === 0);
433
- if (stable_points.length === 0)
434
- return;
435
- const triangles = [];
643
+ const stable_points = plot_entries.filter((entry) =>
644
+ entry.is_stable || entry.e_above_hull === 0
645
+ )
646
+ if (stable_points.length === 0) return
647
+
648
+ // Each tetrahedral facet has 4 triangular faces - we need to draw these
649
+ // Collect all triangular faces with depth for sorting
650
+ type TriangleFace = {
651
+ vertices: [
652
+ { x: number; y: number; depth: number },
653
+ { x: number; y: number; depth: number },
654
+ { x: number; y: number; depth: number },
655
+ ]
656
+ avg_depth: number
657
+ avg_w: number // Average formation energy for coloring
658
+ tet_idx: number // Tetrahedron index for facet_index mode
659
+ centroid_bary: number[] // Barycentric centroid [el0, el1, el2, el3] for dominant_element mode
660
+ }
661
+
662
+ const triangles: TriangleFace[] = []
663
+
436
664
  for (let tet_idx = 0; tet_idx < hull_4d.length; tet_idx++) {
437
- const tet = hull_4d[tet_idx];
438
- const [p0, p1, p2, p3] = tet.vertices;
439
- // Convert barycentric coordinates to tetrahedral 3D coordinates
440
- const tet0 = barycentric_to_tetrahedral([
441
- p0.x,
442
- p0.y,
443
- p0.z,
444
- 1 - p0.x - p0.y - p0.z,
445
- ]);
446
- const tet1 = barycentric_to_tetrahedral([
447
- p1.x,
448
- p1.y,
449
- p1.z,
450
- 1 - p1.x - p1.y - p1.z,
451
- ]);
452
- const tet2 = barycentric_to_tetrahedral([
453
- p2.x,
454
- p2.y,
455
- p2.z,
456
- 1 - p2.x - p2.y - p2.z,
457
- ]);
458
- const tet3 = barycentric_to_tetrahedral([
459
- p3.x,
460
- p3.y,
461
- p3.z,
462
- 1 - p3.x - p3.y - p3.z,
463
- ]);
464
- // Project to 2D screen space
465
- const proj0 = project_3d_point(tet0.x, tet0.y, tet0.z);
466
- const proj1 = project_3d_point(tet1.x, tet1.y, tet1.z);
467
- const proj2 = project_3d_point(tet2.x, tet2.y, tet2.z);
468
- const proj3 = project_3d_point(tet3.x, tet3.y, tet3.z);
469
- // Compute tetrahedron centroid in barycentric coords (for dominant_element mode)
470
- // All 4 faces share the same tetrahedron, so they get the same color for facet_index
471
- const tet_centroid_bary = [
472
- (p0.x + p1.x + p2.x + p3.x) / 4,
473
- (p0.y + p1.y + p2.y + p3.y) / 4,
474
- (p0.z + p1.z + p2.z + p3.z) / 4,
475
- ((1 - p0.x - p0.y - p0.z) + (1 - p1.x - p1.y - p1.z) +
476
- (1 - p2.x - p2.y - p2.z) + (1 - p3.x - p3.y - p3.z)) / 4,
477
- ];
478
- // Each tetrahedron has 4 triangular faces
479
- const faces = [
480
- [proj0, proj1, proj2, (p0.w + p1.w + p2.w) / 3],
481
- [proj0, proj1, proj3, (p0.w + p1.w + p3.w) / 3],
482
- [proj0, proj2, proj3, (p0.w + p2.w + p3.w) / 3],
483
- [proj1, proj2, proj3, (p1.w + p2.w + p3.w) / 3],
484
- ];
485
- for (const [v0, v1, v2, avg_w] of faces) {
486
- triangles.push({
487
- vertices: [v0, v1, v2],
488
- avg_depth: (v0.depth + v1.depth + v2.depth) / 3,
489
- avg_w,
490
- tet_idx,
491
- centroid_bary: tet_centroid_bary,
492
- });
493
- }
665
+ const tet = hull_4d[tet_idx]
666
+ const [p0, p1, p2, p3] = tet.vertices
667
+
668
+ // Convert barycentric coordinates to tetrahedral 3D coordinates
669
+ const tet0 = barycentric_to_tetrahedral([
670
+ p0.x,
671
+ p0.y,
672
+ p0.z,
673
+ 1 - p0.x - p0.y - p0.z,
674
+ ])
675
+ const tet1 = barycentric_to_tetrahedral([
676
+ p1.x,
677
+ p1.y,
678
+ p1.z,
679
+ 1 - p1.x - p1.y - p1.z,
680
+ ])
681
+ const tet2 = barycentric_to_tetrahedral([
682
+ p2.x,
683
+ p2.y,
684
+ p2.z,
685
+ 1 - p2.x - p2.y - p2.z,
686
+ ])
687
+ const tet3 = barycentric_to_tetrahedral([
688
+ p3.x,
689
+ p3.y,
690
+ p3.z,
691
+ 1 - p3.x - p3.y - p3.z,
692
+ ])
693
+
694
+ // Project to 2D screen space
695
+ const proj0 = project_3d_point(tet0.x, tet0.y, tet0.z)
696
+ const proj1 = project_3d_point(tet1.x, tet1.y, tet1.z)
697
+ const proj2 = project_3d_point(tet2.x, tet2.y, tet2.z)
698
+ const proj3 = project_3d_point(tet3.x, tet3.y, tet3.z)
699
+
700
+ // Compute tetrahedron centroid in barycentric coords (for dominant_element mode)
701
+ // All 4 faces share the same tetrahedron, so they get the same color for facet_index
702
+ const tet_centroid_bary = [
703
+ (p0.x + p1.x + p2.x + p3.x) / 4,
704
+ (p0.y + p1.y + p2.y + p3.y) / 4,
705
+ (p0.z + p1.z + p2.z + p3.z) / 4,
706
+ ((1 - p0.x - p0.y - p0.z) + (1 - p1.x - p1.y - p1.z) +
707
+ (1 - p2.x - p2.y - p2.z) + (1 - p3.x - p3.y - p3.z)) / 4,
708
+ ]
709
+
710
+ // Each tetrahedron has 4 triangular faces
711
+ const faces: [typeof proj0, typeof proj1, typeof proj2, number][] = [
712
+ [proj0, proj1, proj2, (p0.w + p1.w + p2.w) / 3],
713
+ [proj0, proj1, proj3, (p0.w + p1.w + p3.w) / 3],
714
+ [proj0, proj2, proj3, (p0.w + p2.w + p3.w) / 3],
715
+ [proj1, proj2, proj3, (p1.w + p2.w + p3.w) / 3],
716
+ ]
717
+
718
+ for (const [v0, v1, v2, avg_w] of faces) {
719
+ triangles.push({
720
+ vertices: [v0, v1, v2],
721
+ avg_depth: (v0.depth + v1.depth + v2.depth) / 3,
722
+ avg_w,
723
+ tet_idx,
724
+ centroid_bary: tet_centroid_bary,
725
+ })
726
+ }
494
727
  }
728
+
495
729
  // Sort by depth (back to front)
496
- triangles.sort((a, b) => a.avg_depth - b.avg_depth);
730
+ triangles.sort((a, b) => a.avg_depth - b.avg_depth)
731
+
497
732
  // Lazy computation for uniform mode: normalize alpha by formation energy
498
- let norm_alpha = null;
733
+ let norm_alpha: ((w: number) => number) | null = null
499
734
  if (hull_face_color_mode === `uniform`) {
500
- const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0);
501
- const min_fe = Math.min(0, ...formation_energies);
502
- norm_alpha = (w) => {
503
- const t = Math.max(0, Math.min(1, (0 - w) / Math.max(1e-6, 0 - min_fe)));
504
- return t * hull_face_opacity;
505
- };
735
+ const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
736
+ const min_fe = Math.min(0, ...formation_energies)
737
+ norm_alpha = (energy: number) => {
738
+ const t = Math.max(0, Math.min(1, (0 - energy) / Math.max(1e-6, 0 - min_fe)))
739
+ return t * hull_face_opacity
740
+ }
506
741
  }
742
+
507
743
  // Lazy computation for formation_energy mode
508
- let energy_face_scale = null;
509
- let min_w = 0;
744
+ let energy_face_scale: ((val: number) => string) | null = null
745
+ let min_w = 0
510
746
  if (hull_face_color_mode === `formation_energy`) {
511
- const all_avg_w = triangles.map((tri) => tri.avg_w);
512
- min_w = Math.min(...all_avg_w);
513
- energy_face_scale = helpers.get_energy_color_scale(`energy`, color_scale, all_avg_w.map((w) => ({ e_above_hull: w - min_w })));
747
+ const all_avg_w = triangles.map((tri) => tri.avg_w)
748
+ min_w = Math.min(...all_avg_w)
749
+ energy_face_scale = helpers.get_energy_color_scale(
750
+ `energy`,
751
+ color_scale,
752
+ all_avg_w.map((energy) => ({ e_above_hull: energy - min_w })), // Normalize to 0-based
753
+ )
514
754
  }
755
+
515
756
  // Helper to get face color based on mode
516
- const get_face_color = (tri) => {
517
- if (hull_face_color_mode === `uniform`) {
518
- return hull_face_color;
519
- }
520
- if (hull_face_color_mode === `formation_energy`) {
521
- return energy_face_scale(tri.avg_w - min_w);
522
- }
523
- if (hull_face_color_mode === `dominant_element`) {
524
- // Find element with highest fraction
525
- const max_idx = tri.centroid_bary.indexOf(Math.max(...tri.centroid_bary));
526
- const el = elements[max_idx];
527
- return element_colors[el] ?? `#888888`;
528
- }
529
- if (hull_face_color_mode === `facet_index`) {
530
- return PLOT_COLORS[tri.tet_idx % PLOT_COLORS.length];
531
- }
532
- return hull_face_color;
533
- };
757
+ const get_face_color = (tri: TriangleFace): string => {
758
+ if (hull_face_color_mode === `uniform`) {
759
+ return hull_face_color
760
+ }
761
+ if (hull_face_color_mode === `formation_energy`) {
762
+ return energy_face_scale?.(tri.avg_w - min_w) ?? hull_face_color
763
+ }
764
+ if (hull_face_color_mode === `dominant_element`) {
765
+ // Find element with highest fraction
766
+ const max_idx = tri.centroid_bary.indexOf(Math.max(...tri.centroid_bary))
767
+ const el = elements[max_idx]
768
+ return element_colors[el] ?? `#888888`
769
+ }
770
+ if (hull_face_color_mode === `facet_index`) {
771
+ return PLOT_COLORS[tri.tet_idx % PLOT_COLORS.length]
772
+ }
773
+ return hull_face_color
774
+ }
775
+
534
776
  // Draw each triangle
535
777
  for (const tri of triangles) {
536
- const [v0, v1, v2] = tri.vertices;
537
- // Uniform mode uses variable opacity; other modes use fixed opacity
538
- const alpha = hull_face_color_mode === `uniform`
539
- ? norm_alpha(tri.avg_w)
540
- : hull_face_opacity;
541
- const face_color = get_face_color(tri);
542
- ctx.save();
543
- ctx.beginPath();
544
- ctx.moveTo(v0.x, v0.y);
545
- ctx.lineTo(v1.x, v1.y);
546
- ctx.lineTo(v2.x, v2.y);
547
- ctx.closePath();
548
- ctx.fillStyle = add_alpha(face_color, alpha);
549
- ctx.fill();
550
- // Edge lines more pronounced with higher opacity
551
- ctx.strokeStyle = add_alpha(face_color, Math.min(0.4, alpha * 4));
552
- ctx.lineWidth = 1;
553
- ctx.stroke();
554
- ctx.restore();
778
+ const [v0, v1, v2] = tri.vertices
779
+ // Uniform mode uses variable opacity; other modes use fixed opacity
780
+ const alpha = hull_face_color_mode === `uniform`
781
+ ? (norm_alpha?.(tri.avg_w) ?? hull_face_opacity)
782
+ : hull_face_opacity
783
+ const face_color = get_face_color(tri)
784
+
785
+ ctx.save()
786
+ ctx.beginPath()
787
+ ctx.moveTo(v0.x, v0.y)
788
+ ctx.lineTo(v1.x, v1.y)
789
+ ctx.lineTo(v2.x, v2.y)
790
+ ctx.closePath()
791
+
792
+ ctx.fillStyle = add_alpha(face_color, alpha)
793
+ ctx.fill()
794
+
795
+ // Edge lines more pronounced with higher opacity
796
+ ctx.strokeStyle = add_alpha(face_color, Math.min(0.4, alpha * 4))
797
+ ctx.lineWidth = 1
798
+ ctx.stroke()
799
+ ctx.restore()
555
800
  }
556
- }
557
- function draw_data_points() {
558
- if (!ctx || sorted_points_cache.length === 0)
559
- return;
801
+ }
802
+
803
+ function draw_data_points(): void {
804
+ if (!ctx || sorted_points_cache.length === 0) return
805
+
560
806
  for (const { entry, projected } of sorted_points_cache) {
561
- const is_stable = entry.is_stable || entry.e_above_hull === 0;
562
- const is_entry_highlighted = is_highlighted(entry);
563
- const color = get_point_color(entry);
564
- const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale;
565
- const marker = entry.marker || `circle`;
566
- // Shadow
567
- const shadow_offset = Math.abs(entry.z) * 2 * canvas_dims.scale;
568
- ctx.fillStyle = `rgba(0, 0, 0, 0.2)`;
569
- const shadow_path = helpers.create_marker_path(size * 0.8, marker);
570
- ctx.save();
571
- ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset);
572
- ctx.fill(shadow_path);
573
- ctx.restore();
574
- // Highlights
575
- if (selected_entry && entry.entry_id === selected_entry.entry_id) {
576
- helpers.draw_selection_highlight(ctx, projected, size, canvas_dims.scale, pulse_time, pulse_opacity);
577
- }
578
- if (is_entry_highlighted) {
579
- helpers.draw_highlight_effect(ctx, projected, size, canvas_dims.scale, pulse_time, merged_highlight_style);
580
- }
581
- // Main point with marker symbol
582
- ctx.fillStyle =
583
- is_entry_highlighted && merged_highlight_style.effect === `color`
584
- ? merged_highlight_style.color
585
- : color;
586
- ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`;
587
- ctx.lineWidth = 0.5 * canvas_dims.scale;
588
- const marker_path = helpers.create_marker_path(size, marker);
589
- ctx.save();
590
- ctx.translate(projected.x, projected.y);
591
- ctx.fill(marker_path);
592
- ctx.stroke(marker_path);
593
- ctx.restore();
594
- // Labels
595
- const should_label = merged_config.show_labels && ((is_stable && show_stable_labels) ||
807
+ const is_stable = entry.is_stable || entry.e_above_hull === 0
808
+ const is_entry_highlighted = is_highlighted(entry)
809
+ const color = get_point_color(entry)
810
+ const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
811
+ const marker = entry.marker || `circle`
812
+
813
+ // Shadow
814
+ const shadow_offset = Math.abs(entry.z) * 2 * canvas_dims.scale
815
+ ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
816
+ const shadow_path = helpers.create_marker_path(size * 0.8, marker)
817
+ ctx.save()
818
+ ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
819
+ ctx.fill(shadow_path)
820
+ ctx.restore()
821
+
822
+ // Highlights
823
+ if (selected_entry && entry.entry_id === selected_entry.entry_id) {
824
+ helpers.draw_selection_highlight(
825
+ ctx,
826
+ projected,
827
+ size,
828
+ canvas_dims.scale,
829
+ pulse_time,
830
+ pulse_opacity,
831
+ )
832
+ }
833
+ if (is_entry_highlighted) {
834
+ helpers.draw_highlight_effect(
835
+ ctx,
836
+ projected,
837
+ size,
838
+ canvas_dims.scale,
839
+ pulse_time,
840
+ merged_highlight_style,
841
+ )
842
+ }
843
+
844
+ // Main point with marker symbol
845
+ ctx.fillStyle =
846
+ is_entry_highlighted && merged_highlight_style.effect === `color`
847
+ ? merged_highlight_style.color
848
+ : color
849
+ ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
850
+ ctx.lineWidth = 0.5 * canvas_dims.scale
851
+ const marker_path = helpers.create_marker_path(size, marker)
852
+ ctx.save()
853
+ ctx.translate(projected.x, projected.y)
854
+ ctx.fill(marker_path)
855
+ ctx.stroke(marker_path)
856
+ ctx.restore()
857
+ }
858
+
859
+ if (!merged_config.show_labels) return
860
+
861
+ const label_entries = helpers.get_composition_label_entries(
862
+ sorted_points_cache
863
+ .map(({ entry }) => entry)
864
+ .filter((entry) => {
865
+ if (entry.is_element) return false
866
+ const is_stable = entry.is_stable || entry.e_above_hull === 0
867
+ return (is_stable && show_stable_labels) ||
596
868
  (!is_stable && show_unstable_labels &&
597
- (entry.e_above_hull ?? 0) <= max_hull_dist_show_labels));
598
- if (should_label) {
599
- ctx.fillStyle = text_color;
600
- const label = helpers.get_entry_label(entry, elements);
601
- const font_size = Math.round(12 * canvas_dims.scale);
602
- ctx.font = `${font_size}px Arial`;
603
- ctx.textAlign = `center`;
604
- ctx.textBaseline = `middle`;
605
- ctx.fillText(label, projected.x, projected.y + size + 6 * canvas_dims.scale);
606
- }
869
+ (entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
870
+ }),
871
+ )
872
+
873
+ ctx.fillStyle = text_color
874
+ ctx.font = `${Math.round(12 * canvas_dims.scale)}px Arial`
875
+ ctx.textAlign = `center`
876
+ ctx.textBaseline = `middle`
877
+
878
+ for (const entry of label_entries) {
879
+ const is_stable = entry.is_stable || entry.e_above_hull === 0
880
+ const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
881
+ const projected = project_3d_point(entry.x, entry.y, entry.z)
882
+ const label = helpers.get_entry_label(entry, elements)
883
+ ctx.fillText(label, projected.x, projected.y + size + 6 * canvas_dims.scale)
607
884
  }
608
- }
609
- function render_frame() {
610
- if (!ctx || !canvas)
611
- return;
885
+ }
886
+
887
+ function render_frame(): void {
888
+ if (!ctx || !canvas) return
889
+
612
890
  // Use CSS dimensions for rendering (already scaled by DPR in context)
613
- const display_width = canvas.clientWidth || 600;
614
- const display_height = canvas.clientHeight || 600;
615
- ctx.clearRect(0, 0, display_width, display_height); // Clear canvas
616
- ctx.fillStyle = `transparent`; // Set background - use transparent to inherit from container
617
- ctx.fillRect(0, 0, display_width, display_height);
891
+ const display_width = canvas.clientWidth || 600
892
+ const display_height = canvas.clientHeight || 600
893
+
894
+ ctx.clearRect(0, 0, display_width, display_height) // Clear canvas
895
+
896
+ ctx.fillStyle = `transparent` // Set background - use transparent to inherit from container
897
+ ctx.fillRect(0, 0, display_width, display_height)
898
+
618
899
  if (elements.length !== 4) {
619
- if (elements.length > 0) {
620
- ctx.fillStyle = text_color;
621
- ctx.font = `16px Arial`;
622
- ctx.textAlign = `center`;
623
- ctx.textBaseline = `middle`;
624
- ctx.fillText(`Quaternary convex hull requires exactly 4 elements (got ${elements.length})`, display_width / 2, display_height / 2);
625
- }
626
- return;
900
+ if (elements.length > 0) {
901
+ ctx.fillStyle = text_color
902
+ ctx.font = `16px Arial`
903
+ ctx.textAlign = `center`
904
+ ctx.textBaseline = `middle`
905
+ ctx.fillText(
906
+ `Quaternary convex hull requires exactly 4 elements (got ${elements.length})`,
907
+ display_width / 2,
908
+ display_height / 2,
909
+ )
910
+ }
911
+ return
627
912
  }
628
- draw_structure_outline(); // Draw tetrahedron outline
629
- draw_convex_hull_faces(); // Draw convex hull faces (before points so they appear behind)
630
- draw_data_points(); // Draw data points (on top)
631
- }
632
- function handle_mouse_down(event) {
633
- is_dragging = true;
634
- drag_started = false;
635
- hover_data = null;
636
- on_point_hover?.(null);
637
- last_mouse = { x: event.clientX, y: event.clientY };
638
- }
639
- const handle_mouse_move = (event) => {
640
- if (!is_dragging)
641
- return;
642
- const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y];
913
+
914
+ draw_structure_outline() // Draw tetrahedron outline
915
+
916
+ draw_convex_hull_faces() // Draw convex hull faces (before points so they appear behind)
917
+
918
+ draw_data_points() // Draw data points (on top)
919
+ }
920
+
921
+ function handle_mouse_down(event: MouseEvent) {
922
+ is_dragging = true
923
+ drag_started = false
924
+ hover_data = null
925
+ on_point_hover?.(null)
926
+ last_mouse = { x: event.clientX, y: event.clientY }
927
+ }
928
+
929
+ const handle_mouse_move = (event: MouseEvent) => {
930
+ if (!is_dragging) return
931
+ const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
932
+
643
933
  // Mark as drag if any movement occurred
644
- if (dx !== 0 || dy !== 0)
645
- drag_started = true;
934
+ if (dx !== 0 || dy !== 0) drag_started = true
935
+
646
936
  // With Cmd/Ctrl held: pan the view instead of rotating
647
937
  if (event.metaKey || event.ctrlKey) {
648
- camera.center_x += dx;
649
- camera.center_y += dy;
938
+ camera.center_x += dx
939
+ camera.center_y += dy
940
+ } else {
941
+ camera.rotation_y += dx * 0.005
942
+ camera.rotation_x = Math.max(
943
+ -Math.PI / 3,
944
+ Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
945
+ )
650
946
  }
651
- else {
652
- camera.rotation_y += dx * 0.005;
653
- camera.rotation_x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005));
654
- }
655
- last_mouse = { x: event.clientX, y: event.clientY };
656
- };
657
- const handle_wheel = (event) => {
658
- event.preventDefault();
659
- camera.zoom = Math.max(1.0, Math.min(15, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)));
660
- };
661
- const handle_hover = (event) => {
662
- if (is_dragging)
663
- return;
664
- const entry = find_entry_at_mouse(event);
947
+ last_mouse = { x: event.clientX, y: event.clientY }
948
+ }
949
+
950
+ const handle_wheel = (event: WheelEvent) => {
951
+ event.preventDefault()
952
+ camera.zoom = Math.max(
953
+ 1.0,
954
+ Math.min(15, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
955
+ )
956
+ }
957
+
958
+ const handle_hover = (event: MouseEvent) => {
959
+ if (is_dragging) return
960
+ const entry = find_entry_at_mouse(event)
665
961
  hover_data = entry
666
- ? { entry, position: { x: event.clientX, y: event.clientY } }
667
- : null;
668
- on_point_hover?.(hover_data);
669
- };
670
- const find_entry_at_mouse = (event) => helpers.find_hull_entry_at_mouse(canvas, event, plot_entries, (x, y, z) => {
671
- const p = project_3d_point(x, y, z);
672
- return { x: p.x, y: p.y };
673
- });
674
- const handle_click = (event) => {
675
- event.stopPropagation();
962
+ ? { entry, position: { x: event.clientX, y: event.clientY } }
963
+ : null
964
+ on_point_hover?.(hover_data)
965
+ }
966
+
967
+ const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
968
+ helpers.find_hull_entry_at_mouse(
969
+ canvas,
970
+ event,
971
+ plot_entries,
972
+ (x: number, y: number, z: number) => {
973
+ const projected = project_3d_point(x, y, z)
974
+ return { x: projected.x, y: projected.y }
975
+ },
976
+ )
977
+
978
+ const handle_click = (event: MouseEvent) => {
979
+ event.stopPropagation()
980
+
676
981
  // Check if this was a drag operation (any mouse movement during drag)
677
- const was_drag = drag_started;
678
- drag_started = false; // Reset for next interaction
679
- if (was_drag)
680
- return; // Don't trigger click if this was a drag
681
- const entry = find_entry_at_mouse(event);
982
+ const was_drag = drag_started
983
+ drag_started = false // Reset for next interaction
984
+ if (was_drag) return // Don't trigger click if this was a drag
985
+
986
+ const entry = find_entry_at_mouse(event)
682
987
  if (entry) {
683
- on_point_click?.(entry);
684
- if (enable_click_selection) {
685
- selected_entry = entry;
686
- if (enable_structure_preview) {
687
- const structure = extract_structure_from_entry(entry);
688
- if (structure) {
689
- selected_structure = structure;
690
- modal_place_right = helpers.calculate_modal_side(wrapper);
691
- modal_open = true;
692
- }
693
- }
988
+ on_point_click?.(entry)
989
+ if (enable_click_selection) {
990
+ selected_entry = entry
991
+ if (enable_structure_preview) {
992
+ const structure = extract_structure_from_entry(entry)
993
+ if (structure) {
994
+ selected_structure = structure
995
+ modal_place_right = helpers.calculate_modal_side(wrapper)
996
+ modal_open = true
997
+ }
694
998
  }
695
- }
696
- else if (modal_open)
697
- close_structure_popup();
698
- };
699
- function close_structure_popup() {
700
- modal_open = false;
701
- selected_structure = null;
702
- selected_entry = null;
703
- }
704
- const handle_double_click = (event) => {
705
- const entry = find_entry_at_mouse(event);
999
+ }
1000
+ } else if (modal_open) close_structure_popup()
1001
+ }
1002
+
1003
+ function close_structure_popup() {
1004
+ modal_open = false
1005
+ selected_structure = null
1006
+ selected_entry = null
1007
+ }
1008
+
1009
+ const handle_double_click = (event: MouseEvent) => {
1010
+ const entry = find_entry_at_mouse(event)
706
1011
  if (entry) {
707
- copy_entry_data(entry, {
708
- x: event.clientX,
709
- y: event.clientY,
710
- });
1012
+ copy_entry_data(entry, {
1013
+ x: event.clientX,
1014
+ y: event.clientY,
1015
+ })
711
1016
  }
712
- };
713
- const render_once = () => {
1017
+ }
1018
+
1019
+ const render_once = () => {
714
1020
  if (!frame_id) {
715
- frame_id = requestAnimationFrame(() => {
716
- render_frame();
717
- frame_id = 0;
718
- });
1021
+ frame_id = requestAnimationFrame(() => {
1022
+ render_frame()
1023
+ frame_id = 0
1024
+ })
719
1025
  }
720
- };
721
- function update_canvas_size() {
722
- if (!canvas)
723
- return;
724
- const dpr = globalThis.devicePixelRatio || 1;
725
- const container = canvas.parentElement;
726
- const rect = container?.getBoundingClientRect();
727
- const [w, h] = rect ? [rect.width, rect.height] : [400, 400];
728
- canvas.width = Math.max(0, Math.round(w * dpr));
729
- canvas.height = Math.max(0, Math.round(h * dpr));
730
- canvas_dims = { width: w, height: h, scale: Math.min(w, h) / 600 };
731
- ctx = canvas.getContext(`2d`);
1026
+ }
1027
+
1028
+ function update_canvas_size() {
1029
+ if (!canvas) return
1030
+ const dpr = globalThis.devicePixelRatio || 1
1031
+ const container = canvas.parentElement
1032
+ const rect = container?.getBoundingClientRect()
1033
+ const [width, height] = rect ? [rect.width, rect.height] : [400, 400]
1034
+
1035
+ canvas.width = Math.max(0, Math.round(width * dpr))
1036
+ canvas.height = Math.max(0, Math.round(height * dpr))
1037
+ canvas_dims = { width, height, scale: Math.min(width, height) / 600 }
1038
+
1039
+ ctx = canvas.getContext(`2d`)
732
1040
  if (ctx) {
733
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
734
- ctx.imageSmoothingEnabled = true;
735
- ctx.imageSmoothingQuality = `high`;
1041
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1042
+ ctx.imageSmoothingEnabled = true
1043
+ ctx.imageSmoothingQuality = `high`
736
1044
  }
737
- render_once();
738
- }
739
- $effect(() => {
740
- if (!canvas)
741
- return;
1045
+ render_once()
1046
+ }
1047
+
1048
+ $effect(() => {
1049
+ if (!canvas) return
1050
+
742
1051
  // Initial setup
743
- update_canvas_size();
1052
+ update_canvas_size()
1053
+
744
1054
  // Watch for resize events - only update canvas, don't reset camera
745
- const resize_observer = new ResizeObserver(update_canvas_size);
746
- const container = canvas.parentElement;
747
- if (container)
748
- resize_observer.observe(container);
749
- return () => {
750
- if (frame_id)
751
- cancelAnimationFrame(frame_id);
752
- resize_observer.disconnect();
753
- };
754
- });
755
- // Fullscreen handling with camera reset
756
- let was_fullscreen = $state(fullscreen);
757
- $effect(() => {
1055
+ const resize_observer = new ResizeObserver(update_canvas_size)
1056
+
1057
+ const container = canvas.parentElement
1058
+ if (container) resize_observer.observe(container)
1059
+
1060
+ return () => { // Cleanup on unmount
1061
+ if (frame_id) cancelAnimationFrame(frame_id)
1062
+ resize_observer.disconnect()
1063
+ }
1064
+ })
1065
+
1066
+ // Fullscreen handling with camera reset
1067
+ let was_fullscreen = $state(fullscreen)
1068
+ $effect(() => {
758
1069
  setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
759
- if (entering_fullscreen !== was_fullscreen) {
760
- camera.center_x = 0;
761
- camera.center_y = 20;
762
- was_fullscreen = entering_fullscreen;
763
- }
764
- });
765
- set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`);
766
- });
767
- // Performance: Cache canvas dimensions and pre-compute sorted point projections
768
- let canvas_dims = $state({ width: 600, height: 600, scale: 1 });
769
- const sorted_points_cache = $derived.by(() => {
770
- if (!canvas || plot_entries.length === 0)
771
- return [];
1070
+ if (entering_fullscreen !== was_fullscreen) {
1071
+ camera.center_x = 0
1072
+ camera.center_y = 20
1073
+ was_fullscreen = entering_fullscreen
1074
+ }
1075
+ })
1076
+ set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`)
1077
+ })
1078
+
1079
+ // Performance: Cache canvas dimensions and pre-compute sorted point projections
1080
+ let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1081
+ const sorted_points_cache = $derived.by(() => {
1082
+ if (!canvas || plot_entries.length === 0) return []
772
1083
  return plot_entries
773
- .filter((entry) => entry.visible)
774
- .map((entry) => ({
1084
+ .filter((entry) => entry.visible)
1085
+ .map((entry) => ({
775
1086
  entry,
776
1087
  projected: project_3d_point(entry.x, entry.y, entry.z),
777
- }))
778
- .sort((a, b) => a.projected.depth - b.projected.depth);
779
- });
780
- let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
1088
+ }))
1089
+ .sort((a, b) => a.projected.depth - b.projected.depth)
1090
+ })
1091
+
1092
+ let style = $derived(
1093
+ `--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
781
1094
  --hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
782
1095
  --hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
783
- --hull-text-color:${merged_config.colors?.annotation || `var(--text-color, #212121)`}`);
1096
+ --hull-text-color:${
1097
+ merged_config.colors?.annotation || `var(--text-color, #212121)`
1098
+ }`,
1099
+ )
784
1100
  </script>
785
1101
 
786
1102
  <svelte:document
@@ -823,7 +1139,7 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
823
1139
  selected_entry,
824
1140
  })}
825
1141
  <h3 style="position: absolute; left: 1em; top: 1ex; margin: 0">
826
- {@html merged_controls.title || phase_stats?.chemical_system || ``}
1142
+ {@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
827
1143
  </h3>
828
1144
 
829
1145
  <canvas
@@ -848,8 +1164,8 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
848
1164
  <!-- Energy above hull Color Bar -->
849
1165
  {#if color_mode === `energy` && plot_entries.length > 0}
850
1166
  {@const hull_distances = plot_entries
851
- .map((e) => e.e_above_hull)
852
- .filter((v): v is number => typeof v === `number`)}
1167
+ .map((entry) => entry.e_above_hull)
1168
+ .filter((val): val is number => typeof val === `number`)}
853
1169
  {@const min_energy = hull_distances.length > 0 ? Math.min(...hull_distances) : 0}
854
1170
  {@const max_energy = hull_distances.length > 0 ? Math.max(...hull_distances, 0.1) : 0.1}
855
1171
  <ColorBar