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,409 +1,598 @@
1
- <script lang="ts">import { normalize_show_controls } from '../controls';
2
- import { ClickFeedback, DragOverlay } from '../feedback';
3
- import Icon from '../Icon.svelte';
4
- import { symbol_map } from '../labels';
5
- import { set_fullscreen_bg, setup_fullscreen_effect } from '../layout';
6
- import { ScatterPlot } from '../plot';
7
- import { DEFAULTS } from '../settings';
8
- import { SvelteMap } from 'svelte/reactivity';
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
- import { HULL_STABILITY_TOL, is_unary_entry } from './types';
19
- // Binary convex hull rendered as energy vs composition (x in [0, 1])
20
- let { entries = [], controls = {}, config = {}, on_point_click, on_point_hover, fullscreen = $bindable(DEFAULTS.convex_hull.binary.fullscreen), enable_info_pane = true, wrapper = $bindable(), label_threshold = 50, show_stable = $bindable(DEFAULTS.convex_hull.binary.show_stable), show_unstable = $bindable(DEFAULTS.convex_hull.binary.show_unstable), color_mode = $bindable(DEFAULTS.convex_hull.binary.color_mode), color_scale = $bindable(DEFAULTS.convex_hull.binary.color_scale), info_pane_open = $bindable(DEFAULTS.convex_hull.binary.info_pane_open), legend_pane_open = $bindable(DEFAULTS.convex_hull.binary.legend_pane_open), max_hull_dist_show_phases = $bindable(DEFAULTS.convex_hull.binary.max_hull_dist_show_phases), max_hull_dist_show_labels = $bindable(DEFAULTS.convex_hull.binary.max_hull_dist_show_labels), show_stable_labels = $bindable(DEFAULTS.convex_hull.binary.show_stable_labels), show_unstable_labels = $bindable(DEFAULTS.convex_hull.binary.show_unstable_labels), on_file_drop, enable_click_selection = true, enable_structure_preview = true, energy_source_mode = $bindable(`precomputed`), phase_stats = $bindable(null), display = $bindable({ x_grid: false, y_grid: false }), stable_entries = $bindable([]), unstable_entries = $bindable([]), highlighted_entries = $bindable([]), highlight_style = {}, x_axis = {}, y_axis = {}, selected_entry = $bindable(null), temperature = $bindable(), interpolate_temperature = true, max_interpolation_gap = 500, gas_config, gas_pressures = $bindable({}), children, tooltip: custom_tooltip, ...rest } = $props();
21
- const merged_controls = $derived({ ...default_controls, ...controls });
22
- const controls_config = $derived(normalize_show_controls(merged_controls.show));
23
- const merged_config = $derived({
1
+ <script lang="ts">
2
+ import type { D3InterpolateName } from '../colors'
3
+ import type { CompositionType } from '../composition'
4
+ import { normalize_show_controls } from '../controls'
5
+ import { sanitize_html } from '../sanitize'
6
+ import type { ElementSymbol } from '../element'
7
+ import { ClickFeedback, DragOverlay } from '../feedback'
8
+ import Icon from '../Icon.svelte'
9
+ import type { D3SymbolName } from '../labels'
10
+ import { symbol_map } from '../labels'
11
+ import { set_fullscreen_bg, setup_fullscreen_effect } from '../layout'
12
+ import type {
13
+ AxisConfig,
14
+ ScatterHandlerEvent,
15
+ ScatterHandlerProps,
16
+ UserContentProps,
17
+ } from '../plot'
18
+ import { ScatterPlot } from '../plot'
19
+ import { DEFAULTS } from '../settings'
20
+ import type { AnyStructure } from '../structure'
21
+ import { SvelteMap } from 'svelte/reactivity'
22
+ import ConvexHullControls from './ConvexHullControls.svelte'
23
+ import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
24
+ import ConvexHullTooltip from './ConvexHullTooltip.svelte'
25
+ import GasPressureControls from './GasPressureControls.svelte'
26
+ import * as helpers from './helpers'
27
+ import type { BaseConvexHullProps } from './index'
28
+ import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
29
+ import StructurePopup from './StructurePopup.svelte'
30
+ import TemperatureSlider from './TemperatureSlider.svelte'
31
+ import * as thermo from './thermodynamics'
32
+ import type {
33
+ ConvexHullEntry,
34
+ HighlightStyle,
35
+ HoverData3D,
36
+ PhaseData,
37
+ } from './types'
38
+ import { compute_hull_stability, is_unary_entry } from './helpers'
39
+
40
+ // Binary convex hull rendered as energy vs composition (x in [0, 1])
41
+ let {
42
+ entries = [],
43
+ controls = {},
44
+ config = {},
45
+ on_point_click,
46
+ on_point_hover,
47
+ fullscreen = $bindable(DEFAULTS.convex_hull.binary.fullscreen),
48
+ enable_info_pane = true,
49
+ wrapper = $bindable(),
50
+ label_threshold = 50,
51
+ show_stable = $bindable(DEFAULTS.convex_hull.binary.show_stable),
52
+ show_unstable = $bindable(DEFAULTS.convex_hull.binary.show_unstable),
53
+ color_mode = $bindable(DEFAULTS.convex_hull.binary.color_mode),
54
+ color_scale = $bindable(
55
+ DEFAULTS.convex_hull.binary.color_scale as D3InterpolateName,
56
+ ),
57
+ info_pane_open = $bindable(DEFAULTS.convex_hull.binary.info_pane_open),
58
+ legend_pane_open = $bindable(DEFAULTS.convex_hull.binary.legend_pane_open),
59
+ max_hull_dist_show_phases = $bindable(
60
+ DEFAULTS.convex_hull.binary.max_hull_dist_show_phases,
61
+ ),
62
+ max_hull_dist_show_labels = $bindable(
63
+ DEFAULTS.convex_hull.binary.max_hull_dist_show_labels,
64
+ ),
65
+ show_stable_labels = $bindable(DEFAULTS.convex_hull.binary.show_stable_labels),
66
+ show_unstable_labels = $bindable(
67
+ DEFAULTS.convex_hull.binary.show_unstable_labels,
68
+ ),
69
+ on_file_drop,
70
+ enable_click_selection = true,
71
+ enable_structure_preview = true,
72
+ energy_source_mode = $bindable(`precomputed`),
73
+ phase_stats = $bindable(null),
74
+ display = $bindable({ x_grid: false, y_grid: false }),
75
+ stable_entries = $bindable([]),
76
+ unstable_entries = $bindable([]),
77
+ highlighted_entries = $bindable([]),
78
+ highlight_style = {},
79
+ x_axis = {},
80
+ y_axis = {},
81
+ selected_entry = $bindable(null),
82
+ temperature = $bindable(),
83
+ interpolate_temperature = true,
84
+ max_interpolation_gap = 500,
85
+ gas_config,
86
+ gas_pressures = $bindable({}),
87
+ children,
88
+ tooltip: custom_tooltip,
89
+ ...rest
90
+ }: BaseConvexHullProps<ConvexHullEntry> & {
91
+ highlight_style?: HighlightStyle
92
+ x_axis?: AxisConfig
93
+ y_axis?: AxisConfig
94
+ } = $props()
95
+
96
+ const merged_controls = $derived({ ...default_controls, ...controls })
97
+ const controls_config = $derived(normalize_show_controls(merged_controls.show))
98
+ const merged_config = $derived({
24
99
  ...default_hull_config,
25
100
  point_size: 6, // Binary diagrams use slightly smaller points
26
101
  ...config,
27
102
  colors: { ...default_hull_config.colors, ...(config.colors || {}) },
28
103
  margin: { t: 40, r: 40, b: 60, l: 60, ...(config.margin || {}) },
29
- });
30
- // Merge highlight style with defaults (consistent with 3D/4D)
31
- const merged_highlight_style = $derived(helpers.merge_highlight_style(highlight_style));
32
- // Helper to check if entry is highlighted
33
- const is_highlighted = (entry) => helpers.is_entry_highlighted(entry, highlighted_entries);
34
- // Temperature-dependent free energy support
35
- const { has_temp_data, available_temperatures } = $derived(helpers.analyze_temperature_data(entries));
36
- // Initialize or reset temperature when it's undefined or no longer valid
37
- $effect(() => {
38
- if (has_temp_data &&
39
- available_temperatures.length > 0 &&
40
- (temperature === undefined || !available_temperatures.includes(temperature)))
41
- temperature = available_temperatures[0];
42
- });
43
- // Filter entries by temperature when in temperature mode
44
- const temp_filtered_entries = $derived(has_temp_data && temperature !== undefined
45
- ? helpers.filter_entries_at_temperature(entries, temperature, {
104
+ })
105
+
106
+ // Merge highlight style with defaults (consistent with 3D/4D)
107
+ const merged_highlight_style = $derived(
108
+ helpers.merge_highlight_style(highlight_style),
109
+ )
110
+
111
+ // Helper to check if entry is highlighted
112
+ const is_highlighted = (entry: ConvexHullEntry): boolean =>
113
+ helpers.is_entry_highlighted(entry, highlighted_entries)
114
+
115
+ // Temperature-dependent free energy support
116
+ const { has_temp_data, available_temperatures } = $derived(
117
+ helpers.analyze_temperature_data(entries),
118
+ )
119
+
120
+ // Initialize or reset temperature when it's undefined or no longer valid
121
+ $effect(() => {
122
+ if (
123
+ has_temp_data &&
124
+ available_temperatures.length > 0 &&
125
+ (temperature === undefined || !available_temperatures.includes(temperature))
126
+ ) temperature = available_temperatures[0]
127
+ })
128
+
129
+ // Filter entries by temperature when in temperature mode
130
+ const temp_filtered_entries = $derived(
131
+ has_temp_data && temperature !== undefined
132
+ ? helpers.filter_entries_at_temperature(entries, temperature, {
46
133
  interpolate: interpolate_temperature,
47
134
  max_interpolation_gap,
48
- })
49
- : entries);
50
- // Gas-dependent chemical potential support (corrections based on T, P)
51
- 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));
52
- let { // Compute energy mode information
53
- 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));
54
- const effective_entries = $derived(helpers.get_effective_entries(gas_corrected_entries, energy_mode, unary_refs, thermo.compute_e_form_per_atom));
55
- // Process data and element set
56
- const pd_data = $derived(thermo.process_hull_entries(effective_entries));
57
- const polymorph_stats_map = $derived(helpers.compute_all_polymorph_stats(effective_entries)); // Pre-compute polymorph stats once for O(1) tooltip lookups
58
- const elements = $derived.by(() => {
135
+ })
136
+ : entries,
137
+ )
138
+
139
+ // Gas-dependent chemical potential support (corrections based on T, P)
140
+ const {
141
+ entries: gas_corrected_entries,
142
+ analysis: gas_analysis,
143
+ merged_config: merged_gas_config,
144
+ } = $derived(
145
+ helpers.get_gas_corrected_entries(
146
+ temp_filtered_entries,
147
+ gas_config,
148
+ gas_pressures,
149
+ temperature ?? helpers.DEFAULT_GAS_TEMP,
150
+ ),
151
+ )
152
+
153
+ let { // Compute energy mode information
154
+ has_precomputed_e_form,
155
+ has_precomputed_hull,
156
+ can_compute_e_form,
157
+ can_compute_hull,
158
+ energy_mode,
159
+ unary_refs,
160
+ } = $derived(
161
+ helpers.compute_energy_mode_info(
162
+ gas_corrected_entries,
163
+ thermo.find_lowest_energy_unary_refs,
164
+ energy_source_mode,
165
+ ),
166
+ )
167
+
168
+ const effective_entries = $derived(
169
+ helpers.get_effective_entries(
170
+ gas_corrected_entries,
171
+ energy_mode,
172
+ unary_refs,
173
+ thermo.compute_e_form_per_atom,
174
+ ),
175
+ )
176
+
177
+ // Process data and element set
178
+ const pd_data = $derived(thermo.process_hull_entries(effective_entries))
179
+
180
+ const polymorph_stats_map = $derived(
181
+ helpers.compute_all_polymorph_stats(effective_entries),
182
+ ) // Pre-compute polymorph stats once for O(1) tooltip lookups
183
+
184
+ const elements = $derived.by(() => {
59
185
  if (pd_data.elements.length > 2) {
60
- console.error(`ConvexHull2D: Dataset contains ${pd_data.elements.length} elements, but binary diagrams require exactly 2. Found: [${pd_data.elements.join(`, `)}]`);
61
- return [];
186
+ console.error(
187
+ `ConvexHull2D: Dataset contains ${pd_data.elements.length} elements, but binary diagrams require exactly 2. Found: [${
188
+ pd_data.elements.join(`, `)
189
+ }]`,
190
+ )
191
+ return []
62
192
  }
63
- return pd_data.elements;
64
- });
65
- // Coordinate computation ----------------------------------------------------
66
- function compute_binary_coordinates(raw_entries, elems) {
67
- if (elems.length !== 2)
68
- return [];
69
- const [el1, el2] = elems;
70
- const coords = [];
193
+ return pd_data.elements
194
+ })
195
+
196
+ // Coordinate computation ----------------------------------------------------
197
+ function compute_binary_coordinates(
198
+ raw_entries: PhaseData[],
199
+ elems: ElementSymbol[],
200
+ ): ConvexHullEntry[] {
201
+ if (elems.length !== 2) return []
202
+ const [el1, el2] = elems
203
+ const coords: ConvexHullEntry[] = []
71
204
  for (const entry of raw_entries) {
72
- // Require formation energy per atom to place along y
73
- const e_form = entry.e_form_per_atom;
74
- if (typeof e_form !== `number`)
75
- continue;
76
- const total = Object.values(entry.composition).reduce((s, v) => s + v, 0);
77
- if (total <= 0)
78
- continue;
79
- const frac_b = (entry.composition[el2] || 0) / total;
80
- const is_element = is_unary_entry(entry);
81
- coords.push({ ...entry, x: frac_b, y: e_form, z: 0, is_element, visible: true });
205
+ // Require formation energy per atom to place along y
206
+ const e_form = entry.e_form_per_atom
207
+ if (typeof e_form !== `number`) continue
208
+ const total = Object.values(entry.composition).reduce((s, v) => s + v, 0)
209
+ if (total <= 0) continue
210
+ const frac_b = (entry.composition[el2] || 0) / total
211
+ const is_element = is_unary_entry(entry)
212
+ coords.push({ ...entry, x: frac_b, y: e_form, z: 0, is_element, visible: true })
82
213
  }
83
214
  // Ensure elemental references at x=0 and x=1 with y=0 to close the hull
84
- const el_a = coords.find((e) => e.is_element && e.x === 0);
85
- const el_b = coords.find((e) => e.is_element && e.x === 1);
215
+ const el_a: ConvexHullEntry | undefined = coords.find((e) =>
216
+ e.is_element && e.x === 0
217
+ )
218
+ const el_b: ConvexHullEntry | undefined = coords.find((e) =>
219
+ e.is_element && e.x === 1
220
+ )
86
221
  if (!el_a) {
87
- coords.push({
88
- composition: { [el1]: 1 },
89
- energy: 0,
90
- x: 0,
91
- y: 0,
92
- z: 0,
93
- is_element: true,
94
- visible: true,
95
- });
222
+ coords.push({
223
+ composition: { [el1]: 1 } as CompositionType,
224
+ energy: 0,
225
+ x: 0,
226
+ y: 0,
227
+ z: 0,
228
+ is_element: true,
229
+ visible: true,
230
+ })
96
231
  }
97
232
  if (!el_b) {
98
- coords.push({
99
- composition: { [el2]: 1 },
100
- energy: 0,
101
- x: 1,
102
- y: 0,
103
- z: 0,
104
- is_element: true,
105
- visible: true,
106
- });
233
+ coords.push({
234
+ composition: { [el2]: 1 } as CompositionType,
235
+ energy: 0,
236
+ x: 1,
237
+ y: 0,
238
+ z: 0,
239
+ is_element: true,
240
+ visible: true,
241
+ })
107
242
  }
108
- return coords;
109
- }
110
- const coords_entries = $derived.by(() => {
111
- if (elements.length !== 2)
112
- return [];
243
+ return coords
244
+ }
245
+
246
+ const coords_entries = $derived.by(() => {
247
+ if (elements.length !== 2) return []
113
248
  try {
114
- return compute_binary_coordinates(pd_data.entries, elements);
249
+ return compute_binary_coordinates(pd_data.entries, elements)
250
+ } catch (error) {
251
+ console.error(`Error computing binary coordinates:`, error)
252
+ return []
115
253
  }
116
- catch (error) {
117
- console.error(`Error computing binary coordinates:`, error);
118
- return [];
119
- }
120
- });
121
- // Compute hull and enrich entries with e_above_hull (before filtering)
122
- const { all_enriched_entries, hull_points } = $derived.by(() => {
254
+ })
255
+
256
+ // Compute hull and enrich entries with e_above_hull (before filtering)
257
+ const { all_enriched_entries, hull_points } = $derived.by(() => {
123
258
  if (coords_entries.length === 0) {
124
- return { all_enriched_entries: [], hull_points: [] };
259
+ return { all_enriched_entries: [], hull_points: [] }
125
260
  }
261
+
126
262
  // Build lower hull input: one minimum-energy point per composition x.
127
- const min_y_by_x = new SvelteMap();
263
+ // Excluded entries don't participate in hull construction.
264
+ const min_y_by_x = new SvelteMap<number, number>()
128
265
  for (const entry of coords_entries) {
129
- const current_min_y = min_y_by_x.get(entry.x);
130
- if (current_min_y === undefined || entry.y < current_min_y) {
131
- min_y_by_x.set(entry.x, entry.y);
132
- }
266
+ if (entry.exclude_from_hull) continue
267
+ const current_min_y = min_y_by_x.get(entry.x)
268
+ if (current_min_y === undefined || entry.y < current_min_y) {
269
+ min_y_by_x.set(entry.x, entry.y)
270
+ }
133
271
  }
272
+
134
273
  const hull_input = [...min_y_by_x].map(([x_coord, min_y]) => ({
135
- x: x_coord,
136
- y: min_y,
137
- }));
138
- const hull_points = thermo.compute_lower_hull_2d(hull_input);
274
+ x: x_coord,
275
+ y: min_y,
276
+ }))
277
+ const hull_points = thermo.compute_lower_hull_2d(hull_input)
278
+
139
279
  const all_enriched_entries = coords_entries.map((entry) => {
140
- const y_hull = thermo.interpolate_hull_2d(hull_points, entry.x);
141
- const e_above_hull = y_hull == null ? 0 : Math.max(0, entry.y - y_hull);
142
- return {
143
- ...entry,
144
- e_above_hull,
145
- is_stable: e_above_hull <= HULL_STABILITY_TOL,
146
- visible: true,
147
- };
148
- });
149
- return { all_enriched_entries, hull_points };
150
- });
151
- // Auto threshold: show all for few entries, use default for many, interpolate between
152
- const max_hull_dist_in_data = $derived(helpers.calc_max_hull_dist_in_data(all_enriched_entries));
153
- const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(all_enriched_entries.length, max_hull_dist_in_data, DEFAULTS.convex_hull.binary.max_hull_dist_show_phases));
154
- // Initialize threshold to auto value on first load
155
- let initialized = $state(false);
156
- $effect(() => {
280
+ const y_hull = thermo.interpolate_hull_2d(hull_points, entry.x)
281
+ const raw_dist = y_hull == null ? 0 : entry.y - y_hull
282
+ return {
283
+ ...entry, ...compute_hull_stability(raw_dist, entry.exclude_from_hull), visible: true,
284
+ }
285
+ })
286
+ return { all_enriched_entries, hull_points }
287
+ })
288
+
289
+ // Auto threshold: show all for few entries, use default for many, interpolate between
290
+ const max_hull_dist_in_data = $derived(
291
+ helpers.calc_max_hull_dist_in_data(all_enriched_entries),
292
+ )
293
+ const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
294
+ all_enriched_entries.length,
295
+ max_hull_dist_in_data,
296
+ DEFAULTS.convex_hull.binary.max_hull_dist_show_phases,
297
+ ))
298
+
299
+ // Initialize threshold to auto value on first load
300
+ let initialized = $state(false)
301
+ $effect(() => {
157
302
  if (!initialized && all_enriched_entries.length > 0) {
158
- initialized = true;
159
- max_hull_dist_show_phases = auto_default_threshold;
303
+ initialized = true
304
+ max_hull_dist_show_phases = auto_default_threshold
160
305
  }
161
- });
162
- // Filter by threshold and compute visibility
163
- const plot_entries = $derived(all_enriched_entries
164
- .filter((e) => e.is_stable || (e.e_above_hull ?? 0) <= max_hull_dist_show_phases)
165
- .map((e) => ({
166
- ...e,
167
- visible: (e.is_stable && show_stable) || (!e.is_stable && show_unstable),
168
- })));
169
- // Update bindable entries arrays when plot_entries change (single pass)
170
- $effect(() => {
171
- const stable = [];
172
- const unstable = [];
306
+ })
307
+
308
+ // Filter by threshold and compute visibility
309
+ const plot_entries = $derived(
310
+ all_enriched_entries
311
+ .filter((e) =>
312
+ e.is_stable || (e.e_above_hull ?? 0) <= max_hull_dist_show_phases
313
+ )
314
+ .map((e) => ({
315
+ ...e,
316
+ visible: (e.is_stable && show_stable) || (!e.is_stable && show_unstable),
317
+ })),
318
+ )
319
+
320
+ // Update bindable entries arrays when plot_entries change (single pass)
321
+ $effect(() => {
322
+ const stable: ConvexHullEntry[] = []
323
+ const unstable: ConvexHullEntry[] = []
173
324
  for (const entry of plot_entries) {
174
- if (entry.is_stable)
175
- stable.push(entry);
176
- else
177
- unstable.push(entry);
325
+ if (entry.is_stable) stable.push(entry)
326
+ else unstable.push(entry)
178
327
  }
179
- stable_entries = stable;
180
- unstable_entries = unstable;
181
- });
182
- let reset_counter = $state(0);
183
- // Drag and drop state (to match 3D/4D components)
184
- let drag_over = $state(false);
185
- // Copy feedback state
186
- let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } });
187
- // Structure popup state
188
- let structure_popup = $state({
328
+ stable_entries = stable
329
+ unstable_entries = unstable
330
+ })
331
+
332
+ let reset_counter = $state(0)
333
+ // Drag and drop state (to match 3D/4D components)
334
+ let drag_over = $state(false)
335
+ // Copy feedback state
336
+ let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
337
+
338
+ // Structure popup state
339
+ let structure_popup = $state<{
340
+ open: boolean
341
+ structure: AnyStructure | null
342
+ entry: ConvexHullEntry | null
343
+ place_right: boolean
344
+ }>({
189
345
  open: false,
190
346
  structure: null,
191
347
  entry: null,
192
348
  place_right: true,
193
- });
194
- // Axis mapping helpers ------------------------------------------------------
195
- const x_domain = $derived([0, 1]);
196
- const y_domain = $derived.by(() => {
197
- const ys = plot_entries.map((entry) => entry.y);
198
- if (ys.length === 0)
199
- return [-1, 0];
200
- const min_y_data = Math.min(...ys);
201
- const max_y_data = Math.max(...ys);
202
- const span = Math.max(1e-9, max_y_data - min_y_data);
203
- const pad = 0.05 * span;
204
- return [min_y_data - pad, max_y_data + pad];
205
- });
206
- // Build ScatterPlot series --------------------------------------------------
207
- // Map MarkerSymbol to D3SymbolName (type-safe via symbol_map lookup)
208
- const marker_to_d3_symbol = (marker) => {
209
- if (!marker)
210
- return undefined;
211
- const name = marker.charAt(0).toUpperCase() + marker.slice(1);
212
- return name in symbol_map ? name : undefined;
213
- };
214
- // Pre-compute visible entries to avoid redundant filtering
215
- const visible_entries = $derived(plot_entries.filter((e) => e.visible));
216
- const scatter_points_series = $derived.by(() => {
217
- const is_energy_mode = color_mode === `energy`;
218
- const count = visible_entries.length;
349
+ })
350
+
351
+ // Axis mapping helpers ------------------------------------------------------
352
+ const x_domain = $derived<[number, number]>([0, 1])
353
+ const y_domain = $derived.by((): [number, number] => {
354
+ const ys = plot_entries.map((entry) => entry.y)
355
+ if (ys.length === 0) return [-1, 0]
356
+ const min_y_data = Math.min(...ys)
357
+ const max_y_data = Math.max(...ys)
358
+ const span = Math.max(1e-9, max_y_data - min_y_data)
359
+ const pad = 0.05 * span
360
+ return [min_y_data - pad, max_y_data + pad]
361
+ })
362
+
363
+ // Build ScatterPlot series --------------------------------------------------
364
+
365
+ // Map MarkerSymbol to D3SymbolName (type-safe via symbol_map lookup)
366
+ const marker_to_d3_symbol = (marker?: string): D3SymbolName | undefined => {
367
+ if (!marker) return undefined
368
+ const name = marker.charAt(0).toUpperCase() + marker.slice(1)
369
+ return name in symbol_map ? (name as D3SymbolName) : undefined
370
+ }
371
+
372
+ // Pre-compute visible entries to avoid redundant filtering
373
+ const visible_entries = $derived(plot_entries.filter((e) => e.visible))
374
+
375
+ const scatter_points_series = $derived.by(() => {
376
+ const is_energy_mode = color_mode === `energy`
377
+ const count = visible_entries.length
378
+
219
379
  // Single pass: extract x, y, color_values, and point_style simultaneously
220
- const x_vals = new Array(count);
221
- const y_vals = new Array(count);
222
- const color_values = is_energy_mode ? new Array(count) : [];
223
- const point_style = new Array(count);
380
+ const x_vals: number[] = new Array(count)
381
+ const y_vals: number[] = new Array(count)
382
+ const color_values: number[] = is_energy_mode ? new Array(count) : []
383
+ const point_style = new Array(count)
384
+
224
385
  for (let idx = 0; idx < count; idx++) {
225
- const entry = visible_entries[idx];
226
- x_vals[idx] = entry.x;
227
- y_vals[idx] = entry.y;
228
- if (is_energy_mode)
229
- color_values[idx] = entry.e_above_hull ?? 0;
230
- const is_stable = entry.is_stable || entry.e_above_hull === 0;
231
- const base_radius = entry.size || (is_stable ? 6 : 4);
232
- const hl = is_highlighted(entry) ? merged_highlight_style : null;
233
- point_style[idx] = {
234
- fill: hl && (hl.effect === `color` || hl.effect === `both`)
235
- ? hl.color
236
- : is_energy_mode
237
- ? undefined
238
- : merged_config.colors?.[is_stable ? `stable` : `unstable`],
239
- stroke: is_stable ? `#ffffff` : `#000000`,
240
- radius: hl && (hl.effect === `size` || hl.effect === `both`)
241
- ? base_radius * hl.size_multiplier
242
- : base_radius,
243
- symbol_type: marker_to_d3_symbol(entry.marker),
244
- is_highlighted: !!hl,
245
- highlight_effect: hl?.effect,
246
- highlight_color: hl?.color,
247
- };
386
+ const entry = visible_entries[idx]
387
+ x_vals[idx] = entry.x
388
+ y_vals[idx] = entry.y
389
+ if (is_energy_mode) color_values[idx] = entry.e_above_hull ?? 0
390
+
391
+ const is_stable = entry.is_stable || entry.e_above_hull === 0
392
+ const base_radius = entry.size || (is_stable ? 6 : 4)
393
+ const hl = is_highlighted(entry) ? merged_highlight_style : null
394
+
395
+ point_style[idx] = {
396
+ fill: hl && (hl.effect === `color` || hl.effect === `both`)
397
+ ? hl.color
398
+ : is_energy_mode
399
+ ? undefined
400
+ : merged_config.colors?.[is_stable ? `stable` : `unstable`],
401
+ stroke: is_stable ? `#ffffff` : `#000000`,
402
+ radius: hl && (hl.effect === `size` || hl.effect === `both`)
403
+ ? base_radius * hl.size_multiplier
404
+ : base_radius,
405
+ symbol_type: marker_to_d3_symbol(entry.marker),
406
+ is_highlighted: !!hl,
407
+ highlight_effect: hl?.effect,
408
+ highlight_color: hl?.color,
409
+ }
248
410
  }
411
+
249
412
  return {
250
- x: x_vals,
251
- y: y_vals,
252
- metadata: visible_entries,
253
- markers: `points`,
254
- point_style,
255
- ...(is_energy_mode ? { color_values } : {}),
256
- };
257
- });
258
- const hull_segments_series = $derived.by(() => {
259
- if (!merged_config.show_hull || hull_points.length < 2)
260
- return [];
261
- const segments = [];
413
+ x: x_vals,
414
+ y: y_vals,
415
+ metadata: visible_entries,
416
+ markers: `points` as const,
417
+ point_style,
418
+ ...(is_energy_mode ? { color_values } : {}),
419
+ }
420
+ })
421
+
422
+ const hull_segments_series = $derived.by(() => {
423
+ if (!merged_config.show_hull || hull_points.length < 2) return []
424
+ const segments = []
262
425
  for (let idx = 0; idx < hull_points.length - 1; idx++) {
263
- const p1 = hull_points[idx];
264
- const p2 = hull_points[idx + 1];
265
- segments.push({
266
- x: [p1.x, p2.x],
267
- y: [p1.y, p2.y],
268
- markers: `line`,
269
- line_style: {
270
- stroke: CONVEX_HULL_STYLE.structure_line.color,
271
- stroke_width: CONVEX_HULL_STYLE.structure_line.line_width,
272
- line_dash: `${CONVEX_HULL_STYLE.structure_line.dash[0]},${CONVEX_HULL_STYLE.structure_line.dash[1]}`,
273
- },
274
- });
426
+ const p1 = hull_points[idx]
427
+ const p2 = hull_points[idx + 1]
428
+ segments.push({
429
+ x: [p1.x, p2.x] as const,
430
+ y: [p1.y, p2.y] as const,
431
+ markers: `line` as const,
432
+ line_style: {
433
+ stroke: CONVEX_HULL_STYLE.structure_line.color,
434
+ stroke_width: CONVEX_HULL_STYLE.structure_line.line_width,
435
+ line_dash: `${CONVEX_HULL_STYLE.structure_line.dash[0]},${
436
+ CONVEX_HULL_STYLE.structure_line.dash[1]
437
+ }`,
438
+ },
439
+ })
275
440
  }
276
- return segments;
277
- });
278
- const scatter_series = $derived([scatter_points_series, ...hull_segments_series]);
279
- // Map selected_entry to ScatterPlot point index (series_idx: 0 = points series)
280
- // Use object identity comparison (e === entry) instead of entry_id comparison
281
- // because synthetic elemental entries lack entry_id, and undefined === undefined
282
- // would incorrectly match the first entry with undefined entry_id
283
- const selected_scatter_point = $derived.by(() => {
284
- const entry = selected_entry;
285
- if (!entry)
286
- return null;
287
- const idx = visible_entries.findIndex((vis_entry) => vis_entry === entry);
288
- return idx >= 0 ? { series_idx: 0, point_idx: idx } : null;
289
- });
290
- // Convex hull statistics - compute internally and expose via bindable prop
291
- $effect(() => {
292
- phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 3);
293
- });
294
- function extract_structure_from_entry(entry) {
295
- if (!entry.entry_id)
296
- return null;
297
- const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id);
298
- return orig_entry?.structure || null;
299
- }
300
- function reset_all() {
301
- fullscreen = DEFAULTS.convex_hull.binary.fullscreen;
302
- info_pane_open = DEFAULTS.convex_hull.binary.info_pane_open;
303
- legend_pane_open = DEFAULTS.convex_hull.binary.legend_pane_open;
304
- color_mode = DEFAULTS.convex_hull.binary.color_mode;
305
- color_scale = DEFAULTS.convex_hull.binary.color_scale;
306
- show_stable = DEFAULTS.convex_hull.binary.show_stable;
307
- show_unstable = DEFAULTS.convex_hull.binary.show_unstable;
308
- show_stable_labels = DEFAULTS.convex_hull.binary.show_stable_labels;
309
- show_unstable_labels = DEFAULTS.convex_hull.binary.show_unstable_labels;
441
+ return segments
442
+ })
443
+
444
+ const scatter_series = $derived([scatter_points_series, ...hull_segments_series])
445
+
446
+ // Map selected_entry to ScatterPlot point index (series_idx: 0 = points series)
447
+ // Use object identity comparison (e === entry) instead of entry_id comparison
448
+ // because synthetic elemental entries lack entry_id, and undefined === undefined
449
+ // would incorrectly match the first entry with undefined entry_id
450
+ const selected_scatter_point = $derived.by(() => {
451
+ const entry = selected_entry
452
+ if (!entry) return null
453
+ const idx = visible_entries.findIndex((vis_entry) => vis_entry === entry)
454
+ return idx >= 0 ? { series_idx: 0, point_idx: idx } : null
455
+ })
456
+
457
+ // Convex hull statistics - compute internally and expose via bindable prop
458
+ $effect(() => {
459
+ phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 3)
460
+ })
461
+
462
+ function extract_structure_from_entry(
463
+ entry: ConvexHullEntry,
464
+ ): AnyStructure | null {
465
+ if (!entry.entry_id) return null
466
+ const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
467
+ return orig_entry?.structure as AnyStructure || null
468
+ }
469
+
470
+ function reset_all() {
471
+ fullscreen = DEFAULTS.convex_hull.binary.fullscreen
472
+ info_pane_open = DEFAULTS.convex_hull.binary.info_pane_open
473
+ legend_pane_open = DEFAULTS.convex_hull.binary.legend_pane_open
474
+ color_mode = DEFAULTS.convex_hull.binary.color_mode
475
+ color_scale = DEFAULTS.convex_hull.binary.color_scale as D3InterpolateName
476
+ show_stable = DEFAULTS.convex_hull.binary.show_stable
477
+ show_unstable = DEFAULTS.convex_hull.binary.show_unstable
478
+ show_stable_labels = DEFAULTS.convex_hull.binary.show_stable_labels
479
+ show_unstable_labels = DEFAULTS.convex_hull.binary.show_unstable_labels
310
480
  // Use auto-computed threshold based on entry count instead of static default
311
- max_hull_dist_show_phases = auto_default_threshold;
312
- max_hull_dist_show_labels = DEFAULTS.convex_hull.binary.max_hull_dist_show_labels;
313
- reset_counter += 1;
314
- }
315
- // Track whether any settings differ from defaults (to show/hide reset button)
316
- // Must match all properties that reset_all() resets
317
- // Use auto_default_threshold for comparison since that's the effective default
318
- const has_changes_to_reset = $derived(fullscreen !== DEFAULTS.convex_hull.binary.fullscreen ||
319
- info_pane_open !== DEFAULTS.convex_hull.binary.info_pane_open ||
320
- legend_pane_open !== DEFAULTS.convex_hull.binary.legend_pane_open ||
321
- color_mode !== DEFAULTS.convex_hull.binary.color_mode ||
322
- color_scale !== DEFAULTS.convex_hull.binary.color_scale ||
323
- show_stable !== DEFAULTS.convex_hull.binary.show_stable ||
324
- show_unstable !== DEFAULTS.convex_hull.binary.show_unstable ||
325
- show_stable_labels !== DEFAULTS.convex_hull.binary.show_stable_labels ||
326
- show_unstable_labels !== DEFAULTS.convex_hull.binary.show_unstable_labels ||
327
- // Compare with auto-computed threshold, with small tolerance for floating point
328
- Math.abs(max_hull_dist_show_phases - auto_default_threshold) > 0.001 ||
329
- max_hull_dist_show_labels !==
330
- DEFAULTS.convex_hull.binary.max_hull_dist_show_labels);
331
- // Custom hover tooltip state used with ScatterPlot events
332
- let hover_data = $state(null);
333
- const handle_keydown = (event) => {
334
- if (event.target.tagName.match(/INPUT|TEXTAREA/))
335
- return;
336
- const actions = {
337
- b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
338
- s: () => show_stable = !show_stable,
339
- u: () => show_unstable = !show_unstable,
340
- l: () => show_stable_labels = !show_stable_labels,
341
- };
342
- actions[event.key.toLowerCase()]?.();
343
- };
344
- async function handle_file_drop(event) {
345
- drag_over = false;
346
- const data = await helpers.parse_hull_entries_from_drop(event);
347
- if (data)
348
- on_file_drop?.(data);
349
- }
350
- async function copy_entry_data(entry, position) {
481
+ max_hull_dist_show_phases = auto_default_threshold
482
+ max_hull_dist_show_labels = DEFAULTS.convex_hull.binary.max_hull_dist_show_labels
483
+ reset_counter += 1
484
+ }
485
+
486
+ // Track whether any settings differ from defaults (to show/hide reset button)
487
+ // Must match all properties that reset_all() resets
488
+ // Use auto_default_threshold for comparison since that's the effective default
489
+ const has_changes_to_reset = $derived(
490
+ fullscreen !== DEFAULTS.convex_hull.binary.fullscreen ||
491
+ info_pane_open !== DEFAULTS.convex_hull.binary.info_pane_open ||
492
+ legend_pane_open !== DEFAULTS.convex_hull.binary.legend_pane_open ||
493
+ color_mode !== DEFAULTS.convex_hull.binary.color_mode ||
494
+ color_scale !== DEFAULTS.convex_hull.binary.color_scale ||
495
+ show_stable !== DEFAULTS.convex_hull.binary.show_stable ||
496
+ show_unstable !== DEFAULTS.convex_hull.binary.show_unstable ||
497
+ show_stable_labels !== DEFAULTS.convex_hull.binary.show_stable_labels ||
498
+ show_unstable_labels !== DEFAULTS.convex_hull.binary.show_unstable_labels ||
499
+ // Compare with auto-computed threshold, with small tolerance for floating point
500
+ Math.abs(max_hull_dist_show_phases - auto_default_threshold) > 0.001 ||
501
+ max_hull_dist_show_labels !==
502
+ DEFAULTS.convex_hull.binary.max_hull_dist_show_labels,
503
+ )
504
+
505
+ // Custom hover tooltip state used with ScatterPlot events
506
+ let hover_data = $state<HoverData3D<ConvexHullEntry> | null>(null)
507
+
508
+ const handle_keydown = (event: KeyboardEvent) => {
509
+ if ((event.target as HTMLElement).tagName.match(/INPUT|TEXTAREA/)) return
510
+ const actions: Record<string, () => void> = {
511
+ b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
512
+ s: () => show_stable = !show_stable,
513
+ u: () => show_unstable = !show_unstable,
514
+ l: () => show_stable_labels = !show_stable_labels,
515
+ }
516
+ actions[event.key.toLowerCase()]?.()
517
+ }
518
+
519
+ async function handle_file_drop(event: DragEvent): Promise<void> {
520
+ drag_over = false
521
+ const data = await helpers.parse_hull_entries_from_drop(event)
522
+ if (data) on_file_drop?.(data)
523
+ }
524
+
525
+ async function copy_entry_data(
526
+ entry: ConvexHullEntry,
527
+ position: { x: number; y: number },
528
+ ) {
351
529
  await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
352
- copy_feedback.visible = visible;
353
- copy_feedback.position = pos;
354
- });
355
- }
356
- function close_structure_popup() {
357
- structure_popup = { open: false, structure: null, entry: null, place_right: true };
358
- selected_entry = null;
359
- }
360
- // Track last clicked entry for double-click detection
361
- let last_clicked_entry = null;
362
- let last_click_time = 0;
363
- function handle_point_click_internal(data) {
364
- const { metadata: entry, event } = data;
365
- if (!entry)
366
- return;
367
- const now = Date.now();
530
+ copy_feedback.visible = visible
531
+ copy_feedback.position = pos
532
+ })
533
+ }
534
+
535
+ function close_structure_popup() {
536
+ structure_popup = { open: false, structure: null, entry: null, place_right: true }
537
+ selected_entry = null
538
+ }
539
+
540
+ // Track last clicked entry for double-click detection
541
+ let last_clicked_entry: ConvexHullEntry | null = null
542
+ let last_click_time = 0
543
+
544
+ function handle_point_click_internal(data: ScatterHandlerEvent<ConvexHullEntry>) {
545
+ const { metadata: entry, event } = data
546
+ if (!entry) return
547
+
548
+ const now = Date.now()
368
549
  const is_double_click = last_clicked_entry === entry &&
369
- now - last_click_time < 300;
550
+ now - last_click_time < 300
551
+
370
552
  if (is_double_click) {
371
- // Double-click: copy to clipboard
372
- copy_entry_data(entry, { x: event.clientX, y: event.clientY });
373
- last_clicked_entry = null;
374
- last_click_time = 0;
375
- }
376
- else {
377
- // Single click: select entry and optionally show structure preview
378
- last_clicked_entry = entry;
379
- last_click_time = now;
380
- on_point_click?.(entry);
381
- if (enable_click_selection) {
382
- selected_entry = entry;
383
- if (enable_structure_preview) {
384
- const structure = extract_structure_from_entry(entry);
385
- if (structure) {
386
- const viewport_width = globalThis.innerWidth;
387
- const click_x = event.clientX;
388
- const space_on_right = viewport_width - click_x;
389
- const space_on_left = click_x;
390
- const place_right = space_on_right >= space_on_left;
391
- structure_popup = { open: true, structure, entry, place_right };
392
- event.stopPropagation();
393
- }
394
- }
553
+ // Double-click: copy to clipboard
554
+ copy_entry_data(entry, { x: event.clientX, y: event.clientY })
555
+ last_clicked_entry = null
556
+ last_click_time = 0
557
+ } else {
558
+ // Single click: select entry and optionally show structure preview
559
+ last_clicked_entry = entry
560
+ last_click_time = now
561
+
562
+ on_point_click?.(entry)
563
+ if (enable_click_selection) {
564
+ selected_entry = entry
565
+ if (enable_structure_preview) {
566
+ const structure = extract_structure_from_entry(entry)
567
+ if (structure) {
568
+ const viewport_width = globalThis.innerWidth
569
+ const click_x = event.clientX
570
+ const space_on_right = viewport_width - click_x
571
+ const space_on_left = click_x
572
+ const place_right = space_on_right >= space_on_left
573
+
574
+ structure_popup = { open: true, structure, entry, place_right }
575
+ event.stopPropagation()
576
+ }
395
577
  }
578
+ }
396
579
  }
397
- }
398
- // Fullscreen handling
399
- $effect(() => {
400
- setup_fullscreen_effect(fullscreen, wrapper);
401
- set_fullscreen_bg(wrapper, fullscreen, `--hull-2d-bg-fullscreen`);
402
- });
403
- let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
580
+ }
581
+
582
+ // Fullscreen handling
583
+ $effect(() => {
584
+ setup_fullscreen_effect(fullscreen, wrapper)
585
+ set_fullscreen_bg(wrapper, fullscreen, `--hull-2d-bg-fullscreen`)
586
+ })
587
+
588
+ let style = $derived(
589
+ `--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
404
590
  --hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
405
591
  --hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
406
- --hull-text-color:${merged_config.colors?.annotation || `var(--text-color, #212121)`};`);
592
+ --hull-text-color:${
593
+ merged_config.colors?.annotation || `var(--text-color, #212121)`
594
+ };`,
595
+ )
407
596
  </script>
408
597
 
409
598
  <svelte:document
@@ -561,7 +750,7 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
561
750
  selected_entry,
562
751
  })}
563
752
  <h3 style="position: absolute; left: 1em; top: 1ex; margin: 0">
564
- {@html merged_controls.title || phase_stats?.chemical_system || ``}
753
+ {@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
565
754
  </h3>
566
755
 
567
756
  <ClickFeedback