matterviz 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) 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/feedback/ClickFeedback.svelte +16 -5
  76. package/dist/feedback/DragOverlay.svelte +10 -2
  77. package/dist/feedback/Spinner.svelte +4 -2
  78. package/dist/feedback/StatusMessage.svelte +8 -2
  79. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  80. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  81. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  82. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  84. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  86. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  87. package/dist/fermi-surface/compute.js +16 -20
  88. package/dist/fermi-surface/parse.js +24 -14
  89. package/dist/fermi-surface/symmetry.js +2 -7
  90. package/dist/fermi-surface/types.d.ts +3 -5
  91. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  93. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  95. package/dist/icons.js +47 -0
  96. package/dist/index.d.ts +2 -1
  97. package/dist/index.js +2 -1
  98. package/dist/io/decompress.js +1 -1
  99. package/dist/io/export.d.ts +3 -0
  100. package/dist/io/export.js +129 -143
  101. package/dist/io/is-binary.js +2 -3
  102. package/dist/io/url-drop.js +1 -2
  103. package/dist/isosurface/Isosurface.svelte +202 -148
  104. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  105. package/dist/isosurface/parse.js +34 -29
  106. package/dist/isosurface/slice.js +5 -10
  107. package/dist/isosurface/types.d.ts +2 -1
  108. package/dist/isosurface/types.js +61 -12
  109. package/dist/labels.js +11 -8
  110. package/dist/layout/FullscreenToggle.svelte +11 -2
  111. package/dist/layout/InfoCard.svelte +38 -6
  112. package/dist/layout/InfoTag.svelte +63 -32
  113. package/dist/layout/PropertyFilter.svelte +82 -37
  114. package/dist/layout/SettingsSection.svelte +85 -55
  115. package/dist/layout/SubpageGrid.svelte +10 -2
  116. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  117. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  118. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  119. package/dist/layout/json-tree/utils.js +4 -2
  120. package/dist/marching-cubes.js +25 -2
  121. package/dist/math.d.ts +13 -17
  122. package/dist/math.js +133 -67
  123. package/dist/overlays/ContextMenu.svelte +65 -40
  124. package/dist/overlays/DraggablePane.svelte +211 -139
  125. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  126. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  127. package/dist/periodic-table/PropertySelect.svelte +25 -7
  128. package/dist/periodic-table/TableInset.svelte +8 -3
  129. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  134. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  136. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  137. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  138. package/dist/phase-diagram/build-diagram.js +9 -9
  139. package/dist/phase-diagram/colors.js +1 -3
  140. package/dist/phase-diagram/parse.js +10 -9
  141. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  142. package/dist/phase-diagram/utils.d.ts +1 -0
  143. package/dist/phase-diagram/utils.js +80 -25
  144. package/dist/plot/AxisLabel.svelte +28 -3
  145. package/dist/plot/BarPlot.svelte +1182 -734
  146. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  147. package/dist/plot/BarPlotControls.svelte +31 -5
  148. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  149. package/dist/plot/ColorBar.svelte +479 -329
  150. package/dist/plot/ColorScaleSelect.svelte +27 -6
  151. package/dist/plot/ElementScatter.svelte +36 -15
  152. package/dist/plot/FillArea.svelte +152 -95
  153. package/dist/plot/Histogram.svelte +934 -571
  154. package/dist/plot/Histogram.svelte.d.ts +1 -1
  155. package/dist/plot/HistogramControls.svelte +53 -9
  156. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  157. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  158. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  159. package/dist/plot/Line.svelte +63 -28
  160. package/dist/plot/PlotControls.svelte +157 -114
  161. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  162. package/dist/plot/PlotLegend.svelte +174 -91
  163. package/dist/plot/PlotTooltip.svelte +45 -6
  164. package/dist/plot/PortalSelect.svelte +175 -147
  165. package/dist/plot/ReferenceLine.svelte +76 -22
  166. package/dist/plot/ReferenceLine3D.svelte +132 -107
  167. package/dist/plot/ReferencePlane.svelte +146 -121
  168. package/dist/plot/ScatterPlot.svelte +1681 -1091
  169. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  170. package/dist/plot/ScatterPlot3D.svelte +256 -131
  171. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  172. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  173. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  174. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  175. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  176. package/dist/plot/ScatterPlotControls.svelte +65 -25
  177. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  178. package/dist/plot/ScatterPoint.svelte +98 -26
  179. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  180. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  181. package/dist/plot/Surface3D.svelte +159 -108
  182. package/dist/plot/ZeroLines.svelte +55 -3
  183. package/dist/plot/ZoomRect.svelte +4 -2
  184. package/dist/plot/axis-utils.js +1 -3
  185. package/dist/plot/data-cleaning.js +12 -28
  186. package/dist/plot/data-transform.js +2 -1
  187. package/dist/plot/fill-utils.js +2 -0
  188. package/dist/plot/layout.d.ts +4 -1
  189. package/dist/plot/layout.js +33 -14
  190. package/dist/plot/reference-line.d.ts +2 -2
  191. package/dist/plot/reference-line.js +7 -5
  192. package/dist/plot/scales.js +24 -36
  193. package/dist/plot/types.d.ts +11 -23
  194. package/dist/plot/types.js +6 -11
  195. package/dist/plot/utils/label-placement.d.ts +32 -15
  196. package/dist/plot/utils/label-placement.js +227 -66
  197. package/dist/plot/utils/series-visibility.js +2 -3
  198. package/dist/rdf/RdfPlot.svelte +143 -91
  199. package/dist/rdf/calc-rdf.js +4 -5
  200. package/dist/sanitize.d.ts +4 -0
  201. package/dist/sanitize.js +107 -0
  202. package/dist/settings.d.ts +18 -6
  203. package/dist/settings.js +46 -16
  204. package/dist/spectral/Bands.svelte +632 -453
  205. package/dist/spectral/BandsAndDos.svelte +90 -49
  206. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  207. package/dist/spectral/Dos.svelte +389 -258
  208. package/dist/spectral/helpers.js +55 -43
  209. package/dist/state.svelte.d.ts +1 -1
  210. package/dist/state.svelte.js +3 -2
  211. package/dist/structure/Arrow.svelte +59 -20
  212. package/dist/structure/AtomLegend.svelte +215 -134
  213. package/dist/structure/Bond.svelte +73 -47
  214. package/dist/structure/CanvasTooltip.svelte +10 -2
  215. package/dist/structure/CellSelect.svelte +72 -45
  216. package/dist/structure/Cylinder.svelte +33 -17
  217. package/dist/structure/Lattice.svelte +88 -33
  218. package/dist/structure/Structure.svelte +1063 -797
  219. package/dist/structure/Structure.svelte.d.ts +1 -1
  220. package/dist/structure/StructureControls.svelte +349 -118
  221. package/dist/structure/StructureExportPane.svelte +124 -89
  222. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  223. package/dist/structure/StructureInfoPane.svelte +304 -237
  224. package/dist/structure/StructureScene.svelte +879 -443
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  226. package/dist/structure/atom-properties.js +8 -8
  227. package/dist/structure/bonding.js +6 -7
  228. package/dist/structure/export.js +14 -29
  229. package/dist/structure/ferrox-wasm.js +1 -1
  230. package/dist/structure/index.d.ts +13 -3
  231. package/dist/structure/index.js +83 -23
  232. package/dist/structure/measure.d.ts +2 -2
  233. package/dist/structure/measure.js +4 -44
  234. package/dist/structure/parse.js +113 -141
  235. package/dist/structure/partial-occupancy.js +7 -10
  236. package/dist/structure/pbc.d.ts +1 -0
  237. package/dist/structure/pbc.js +16 -6
  238. package/dist/structure/supercell.d.ts +2 -2
  239. package/dist/structure/supercell.js +12 -22
  240. package/dist/structure/validation.js +1 -2
  241. package/dist/symmetry/SymmetryStats.svelte +84 -41
  242. package/dist/symmetry/WyckoffTable.svelte +26 -6
  243. package/dist/symmetry/cell-transform.js +5 -3
  244. package/dist/symmetry/index.js +8 -7
  245. package/dist/symmetry/spacegroups.js +148 -148
  246. package/dist/table/HeatmapTable.svelte +790 -554
  247. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  248. package/dist/table/ToggleMenu.svelte +125 -92
  249. package/dist/table/index.js +2 -4
  250. package/dist/theme/ThemeControl.svelte +21 -12
  251. package/dist/time.js +4 -1
  252. package/dist/tooltip/TooltipContent.svelte +33 -8
  253. package/dist/trajectory/Trajectory.svelte +758 -558
  254. package/dist/trajectory/TrajectoryError.svelte +14 -3
  255. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  256. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  257. package/dist/trajectory/extract.js +10 -26
  258. package/dist/trajectory/format-detect.js +5 -5
  259. package/dist/trajectory/frame-reader.d.ts +1 -1
  260. package/dist/trajectory/frame-reader.js +5 -12
  261. package/dist/trajectory/helpers.d.ts +0 -1
  262. package/dist/trajectory/helpers.js +2 -17
  263. package/dist/trajectory/index.js +14 -12
  264. package/dist/trajectory/parse/ase.js +5 -4
  265. package/dist/trajectory/parse/hdf5.js +26 -18
  266. package/dist/trajectory/parse/index.js +13 -18
  267. package/dist/trajectory/parse/lammps.js +17 -7
  268. package/dist/trajectory/parse/vasp.js +5 -2
  269. package/dist/trajectory/parse/xyz.js +8 -7
  270. package/dist/trajectory/plotting.js +13 -8
  271. package/dist/utils.d.ts +1 -0
  272. package/dist/utils.js +13 -0
  273. package/dist/xrd/XrdPlot.svelte +337 -247
  274. package/dist/xrd/broadening.js +14 -9
  275. package/dist/xrd/calc-xrd.js +12 -18
  276. package/dist/xrd/parse.d.ts +1 -1
  277. package/dist/xrd/parse.js +17 -17
  278. package/package.json +99 -103
  279. package/readme.md +1 -1
  280. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,533 +1,712 @@
1
- <script lang="ts">import { PLOT_COLORS } from '../colors';
2
- import EmptyState from '../EmptyState.svelte';
3
- import { format_num } from '../labels';
4
- import { SettingsSection } from '../layout';
5
- import ScatterPlot from '../plot/ScatterPlot.svelte';
6
- import * as helpers from './helpers';
7
- import { SvelteMap } from 'svelte/reactivity';
8
- let { band_structs, line_kwargs = {}, path_mode = `strict`, band_type = undefined, show_legend = true, x_axis = {}, y_axis = $bindable({}), x_positions = $bindable(), reference_frequency = null, ribbon_config = {}, fermi_level = undefined, units = $bindable(`THz`), band_spin_mode = $bindable(`overlay`), highlight_regions = [], shade_imaginary_modes = true, show_gap_annotation = true, show_controls = true, show_path_mode_control = true, show_units_control = true, show_spin_control = true, show_annotation_controls = true, id = undefined, class: class_name = undefined, style = undefined, 'data-testid': data_testid = undefined, ...rest } = $props();
9
- const is_dom_attr_value = (attr_value) => typeof attr_value === `string` ||
1
+ <script lang="ts">
2
+ import { PLOT_COLORS } from '../colors'
3
+ import EmptyState from '../EmptyState.svelte'
4
+ import { format_num } from '../labels'
5
+ import { sanitize_html } from '../sanitize'
6
+ import { SettingsSection } from '../layout'
7
+ import type { Vec2 } from '../math'
8
+ import ScatterPlot from '../plot/ScatterPlot.svelte'
9
+ import type { AxisConfig, DataSeries, FillRegion } from '../plot/types'
10
+ import * as helpers from './helpers'
11
+ import type {
12
+ BandsSpinMode,
13
+ BandStructureType,
14
+ BaseBandStructure,
15
+ FrequencyUnit,
16
+ LineKwargs,
17
+ PathMode,
18
+ RibbonConfig,
19
+ } from './types'
20
+ import type { ComponentProps } from 'svelte'
21
+ import { SvelteMap } from 'svelte/reactivity'
22
+
23
+ type Dom_attr_value = string | number | boolean
24
+
25
+ let {
26
+ band_structs,
27
+ line_kwargs = {},
28
+ path_mode = `strict`,
29
+ band_type = undefined,
30
+ show_legend = true,
31
+ x_axis = {},
32
+ y_axis = $bindable({}),
33
+ x_positions = $bindable(),
34
+ reference_frequency = null,
35
+ ribbon_config = {},
36
+ fermi_level = undefined,
37
+ units = $bindable(`THz`),
38
+ band_spin_mode = $bindable(`overlay`),
39
+ highlight_regions = [],
40
+ shade_imaginary_modes = true,
41
+ show_gap_annotation = true,
42
+ show_controls = true,
43
+ show_path_mode_control = true,
44
+ show_units_control = true,
45
+ show_spin_control = true,
46
+ show_annotation_controls = true,
47
+ id = undefined,
48
+ class: class_name = undefined,
49
+ style = undefined,
50
+ 'data-testid': data_testid = undefined,
51
+ ...rest
52
+ }: ComponentProps<typeof ScatterPlot> & {
53
+ band_structs: BaseBandStructure | Record<string, BaseBandStructure>
54
+ x_axis?: AxisConfig
55
+ y_axis?: AxisConfig
56
+ line_kwargs?: LineKwargs
57
+ path_mode?: PathMode
58
+ band_type?: BandStructureType
59
+ show_legend?: boolean
60
+ x_positions?: Record<string, [number, number]>
61
+ reference_frequency?: number | null
62
+ ribbon_config?: RibbonConfig | Record<string, RibbonConfig>
63
+ fermi_level?: number // Fermi level for electronic bands (auto-detected if not provided)
64
+ units?: FrequencyUnit // Phonon frequency display units (electronic always eV)
65
+ band_spin_mode?: BandsSpinMode // Electronic spin display: overlay (default), up_only, down_only
66
+ highlight_regions?: {
67
+ y_min: number
68
+ y_max: number
69
+ color?: string
70
+ opacity?: number
71
+ label?: string
72
+ }[]
73
+ shade_imaginary_modes?: boolean // Shade y<0 region for phonon plots with imaginary modes
74
+ show_gap_annotation?: boolean // Annotate electronic VBM/CBM and gap when available
75
+ show_controls?: boolean
76
+ show_path_mode_control?: boolean
77
+ show_units_control?: boolean
78
+ show_spin_control?: boolean
79
+ show_annotation_controls?: boolean
80
+ id?: string
81
+ class?: string
82
+ style?: string
83
+ 'data-testid'?: string
84
+ } = $props()
85
+
86
+ const is_dom_attr_value = (attr_value: unknown): attr_value is Dom_attr_value =>
87
+ typeof attr_value === `string` ||
10
88
  typeof attr_value === `number` ||
11
- typeof attr_value === `boolean`;
12
- // Helper function to get line styling for a band
13
- function get_line_style(color, is_acoustic, frequencies, band_idx) {
14
- const defaults = { stroke: color, stroke_width: is_acoustic ? 1.5 : 1 };
89
+ typeof attr_value === `boolean`
90
+
91
+ // Helper function to get line styling for a band
92
+ function get_line_style(
93
+ color: string,
94
+ is_acoustic: boolean,
95
+ frequencies: number[],
96
+ band_idx: number,
97
+ ): { stroke: string; stroke_width: number } {
98
+ const defaults = { stroke: color, stroke_width: is_acoustic ? 1.5 : 1 }
99
+
15
100
  if (typeof line_kwargs === `function`) {
16
- const custom = line_kwargs(frequencies, band_idx);
17
- return {
18
- stroke: custom.stroke ?? defaults.stroke,
19
- stroke_width: custom.stroke_width ?? defaults.stroke_width,
20
- };
101
+ const custom = line_kwargs(frequencies, band_idx)
102
+ return {
103
+ stroke: (custom.stroke as string) ?? defaults.stroke,
104
+ stroke_width: (custom.stroke_width as number) ?? defaults.stroke_width,
105
+ }
21
106
  }
107
+
22
108
  if (typeof line_kwargs === `object` && line_kwargs !== null) {
23
- const mode_key = is_acoustic ? `acoustic` : `optical`;
24
- const mode_kwargs = line_kwargs[mode_key];
25
- const source = (mode_kwargs ?? line_kwargs);
26
- return {
27
- stroke: source.stroke ?? defaults.stroke,
28
- stroke_width: source.stroke_width ?? defaults.stroke_width,
29
- };
109
+ const mode_key = is_acoustic ? `acoustic` : `optical`
110
+ const mode_kwargs = (line_kwargs as Record<string, unknown>)[mode_key] as
111
+ | Record<string, unknown>
112
+ | undefined
113
+ const source = (mode_kwargs ?? line_kwargs) as Record<string, unknown>
114
+ return {
115
+ stroke: (source.stroke as string) ?? defaults.stroke,
116
+ stroke_width: (source.stroke_width as number) ?? defaults.stroke_width,
117
+ }
30
118
  }
31
- return defaults;
32
- }
33
- // Normalize input to dict format
34
- // Supports multiple formats:
35
- // - matterviz format: qpoints + branches arrays
36
- // - pymatgen phonon: qpoints + bands (or frequencies_cm) arrays
37
- // - pymatgen electronic: kpoints + bands arrays
38
- let band_structs_dict = $derived.by(() => {
39
- if (!band_structs)
40
- return {};
119
+
120
+ return defaults
121
+ }
122
+
123
+ // Ribbon data structure for rendering
124
+ interface RibbonData {
125
+ x_values: number[]
126
+ y_values: number[]
127
+ width_values: number[]
128
+ color: string
129
+ opacity: number
130
+ max_width: number
131
+ scale: number
132
+ band_idx: number
133
+ structure_label: string
134
+ segment_key: string
135
+ }
136
+
137
+ // Normalize input to dict format
138
+ // Supports multiple formats:
139
+ // - matterviz format: qpoints + branches arrays
140
+ // - pymatgen phonon: qpoints + bands (or frequencies_cm) arrays
141
+ // - pymatgen electronic: kpoints + bands arrays
142
+ let band_structs_dict = $derived.by(() => {
143
+ if (!band_structs) return {}
144
+
41
145
  // Detect single band structure by checking for characteristic fields
42
146
  // - pymatgen format: has @class or @module markers (may also have branches)
43
147
  // - matterviz format: has qpoints + branches (no pymatgen markers)
44
148
  const has_qpoints = `qpoints` in band_structs &&
45
- Array.isArray(band_structs.qpoints) &&
46
- band_structs.qpoints.length > 0;
149
+ Array.isArray(band_structs.qpoints) &&
150
+ band_structs.qpoints.length > 0
47
151
  const has_kpoints = `kpoints` in band_structs &&
48
- Array.isArray(band_structs.kpoints) &&
49
- band_structs.kpoints.length > 0;
50
- const has_bands = `bands` in band_structs;
152
+ Array.isArray(band_structs.kpoints) &&
153
+ band_structs.kpoints.length > 0
154
+ const has_bands = `bands` in band_structs
51
155
  const has_frequencies_cm = `frequencies_cm` in band_structs &&
52
- Array.isArray(band_structs.frequencies_cm);
53
- const has_branches = `branches` in band_structs;
156
+ Array.isArray(band_structs.frequencies_cm)
157
+ const has_branches = `branches` in band_structs
54
158
  // Pymatgen structures have explicit class/module markers
55
- const is_pymatgen = `@class` in band_structs || `@module` in band_structs;
159
+ const is_pymatgen = `@class` in band_structs || `@module` in band_structs
160
+
56
161
  // Pymatgen single: has markers and point/band data (may have branches too)
57
162
  const is_pymatgen_single = is_pymatgen &&
58
- (has_qpoints || has_kpoints) &&
59
- (has_bands || has_frequencies_cm);
163
+ (has_qpoints || has_kpoints) &&
164
+ (has_bands || has_frequencies_cm)
60
165
  // Matterviz single: has qpoints + branches but NO pymatgen markers
61
- const is_matterviz_single = !is_pymatgen && has_qpoints && has_branches;
62
- const is_single = is_matterviz_single || is_pymatgen_single;
63
- const result = {};
166
+ const is_matterviz_single = !is_pymatgen && has_qpoints && has_branches
167
+ const is_single = is_matterviz_single || is_pymatgen_single
168
+
169
+ const result: Record<string, BaseBandStructure> = {}
170
+
64
171
  if (is_single) {
65
- const normalized = helpers.normalize_band_structure(band_structs);
66
- if (normalized)
67
- result.default = normalized;
68
- }
69
- else {
70
- for (const [key, bs] of Object.entries(band_structs)) {
71
- const normalized = helpers.normalize_band_structure(bs);
72
- if (normalized)
73
- result[key] = normalized;
74
- }
172
+ const normalized = helpers.normalize_band_structure(band_structs)
173
+ if (normalized) result.default = normalized
174
+ } else {
175
+ for (const [key, bs] of Object.entries(band_structs)) {
176
+ const normalized = helpers.normalize_band_structure(bs)
177
+ if (normalized) result[key] = normalized
178
+ }
75
179
  }
76
- return result;
77
- });
78
- // Auto-detect band type if not explicitly set
79
- let detected_band_type = $derived.by(() => {
80
- if (band_type)
81
- return band_type;
82
- if (!band_structs)
83
- return `phonon`;
180
+ return result
181
+ })
182
+
183
+ // Auto-detect band type if not explicitly set
184
+ let detected_band_type = $derived.by((): BandStructureType => {
185
+ if (band_type) return band_type
186
+ if (!band_structs) return `phonon`
187
+
84
188
  // Single structure has marker fields; dict of structures has label keys
85
189
  const is_single = `@class` in band_structs || `@module` in band_structs ||
86
- `kpoints` in band_structs || `qpoints` in band_structs;
87
- const source = (is_single ? band_structs : Object.values(band_structs)[0]);
88
- if (!source)
89
- return `phonon`;
190
+ `kpoints` in band_structs || `qpoints` in band_structs
191
+ const source = (is_single ? band_structs : Object.values(band_structs)[0]) as
192
+ | Record<string, unknown>
193
+ | undefined
194
+ if (!source) return `phonon`
195
+
90
196
  // Electronic: has kpoints, BandStructure* class (not Phonon*), or electronic_structure module
91
- const py_class_name = String(source[`@class`] ?? ``);
92
- if ((`kpoints` in source && Array.isArray(source.kpoints) &&
197
+ const py_class_name = String(source[`@class`] ?? ``)
198
+ if (
199
+ (`kpoints` in source && Array.isArray(source.kpoints) &&
93
200
  source.kpoints.length > 0) ||
94
- (py_class_name.startsWith(`BandStructure`) &&
95
- !py_class_name.startsWith(`Phonon`)) ||
96
- String(source[`@module`] ?? ``).includes(`electronic_structure`))
97
- return `electronic`;
98
- return `phonon`;
99
- });
100
- // Auto-detect Fermi level from electronic band structure data if not explicitly provided
101
- let effective_fermi_level = $derived.by(() => {
102
- if (fermi_level !== undefined)
103
- return fermi_level;
104
- if (detected_band_type !== `electronic`)
105
- return undefined;
201
+ (py_class_name.startsWith(`BandStructure`) &&
202
+ !py_class_name.startsWith(`Phonon`)) ||
203
+ String(source[`@module`] ?? ``).includes(`electronic_structure`)
204
+ ) return `electronic`
205
+
206
+ return `phonon`
207
+ })
208
+
209
+ // Auto-detect Fermi level from electronic band structure data if not explicitly provided
210
+ let effective_fermi_level = $derived.by((): number | undefined => {
211
+ if (fermi_level !== undefined) return fermi_level
212
+ if (detected_band_type !== `electronic`) return undefined
213
+
106
214
  // Check raw input for efermi field
107
- const source = `efermi` in band_structs
108
- ? band_structs
109
- : Object.values(band_structs)[0];
110
- const efermi = source?.efermi;
111
- return typeof efermi === `number` ? efermi : undefined;
112
- });
113
- let effective_spin_mode = $derived.by(() => {
114
- if (detected_band_type !== `electronic`)
115
- return null;
215
+ const source = `efermi` in (band_structs as object)
216
+ ? band_structs
217
+ : Object.values(band_structs)[0]
218
+ const efermi = (source as Record<string, unknown>)?.efermi
219
+ return typeof efermi === `number` ? efermi : undefined
220
+ })
221
+
222
+ let effective_spin_mode = $derived.by((): BandsSpinMode => {
223
+ if (detected_band_type !== `electronic`) return null
116
224
  return (band_spin_mode === `up_only` || band_spin_mode === `down_only`)
117
- ? band_spin_mode
118
- : `overlay`;
119
- });
120
- const convert_band_values = (values) => {
121
- if (detected_band_type !== `phonon`)
122
- return values;
123
- if (units === `THz`)
124
- return values;
125
- return helpers.convert_frequencies(values, units);
126
- };
127
- // Collect all path segments across structures once (shared by strict checks and plotting)
128
- let all_segments = $derived.by(() => {
129
- const all_segments = {};
225
+ ? band_spin_mode
226
+ : `overlay`
227
+ })
228
+
229
+ const convert_band_values = (values: number[]): number[] => {
230
+ if (detected_band_type !== `phonon`) return values
231
+ if (units === `THz`) return values
232
+ return helpers.convert_frequencies(values, units)
233
+ }
234
+
235
+ // Collect all path segments across structures once (shared by strict checks and plotting)
236
+ let all_segments = $derived.by(() => {
237
+ const all_segments: Record<string, [string, BaseBandStructure][]> = {}
130
238
  for (const [label, bs] of Object.entries(band_structs_dict)) {
131
- for (const branch of bs.branches) {
132
- const start_label = bs.qpoints[branch.start_index]?.label ?? undefined;
133
- const end_label = bs.qpoints[branch.end_index]?.label ?? undefined;
134
- const segment_key = helpers.get_segment_key(start_label, end_label);
135
- all_segments[segment_key] ??= [];
136
- all_segments[segment_key].push([label, bs]);
137
- }
239
+ for (const branch of bs.branches) {
240
+ const start_label = bs.qpoints[branch.start_index]?.label ?? undefined
241
+ const end_label = bs.qpoints[branch.end_index]?.label ?? undefined
242
+ const segment_key = helpers.get_segment_key(start_label, end_label)
243
+ all_segments[segment_key] ??= []
244
+ all_segments[segment_key].push([label, bs])
245
+ }
138
246
  }
139
- return all_segments;
140
- });
141
- let num_structures = $derived(Object.keys(band_structs_dict).length);
142
- let all_segment_keys = $derived(Object.keys(all_segments));
143
- let common_segment_keys = $derived.by(() => all_segment_keys.filter((segment_key) => all_segments[segment_key].length === num_structures));
144
- let empty_state_attrs = $derived.by(() => {
145
- const attrs = {};
247
+ return all_segments
248
+ })
249
+
250
+ let num_structures = $derived(Object.keys(band_structs_dict).length)
251
+ let all_segment_keys = $derived(Object.keys(all_segments))
252
+ let common_segment_keys = $derived.by(() =>
253
+ all_segment_keys.filter(
254
+ (segment_key) => all_segments[segment_key].length === num_structures,
255
+ )
256
+ )
257
+ let empty_state_attrs = $derived.by(() => {
258
+ const attrs: Record<string, Dom_attr_value> = {}
146
259
  for (const [attr_name, attr_value] of Object.entries(rest)) {
147
- if ((attr_name === `role` || attr_name.startsWith(`aria-`)) &&
148
- is_dom_attr_value(attr_value)) {
149
- attrs[attr_name] = attr_value;
150
- }
260
+ if (
261
+ (attr_name === `role` || attr_name.startsWith(`aria-`)) &&
262
+ is_dom_attr_value(attr_value)
263
+ ) {
264
+ attrs[attr_name] = attr_value
265
+ }
151
266
  }
152
- return attrs;
153
- });
154
- // Compute path mismatch details for strict mode handling
155
- let strict_path_error = $derived.by(() => {
156
- if (path_mode !== `strict`)
157
- return null;
267
+ return attrs
268
+ })
269
+
270
+ // Compute path mismatch details for strict mode handling
271
+ let strict_path_error = $derived.by((): string | null => {
272
+ if (path_mode !== `strict`) return null
158
273
  return common_segment_keys.length === all_segment_keys.length
159
- ? null
160
- : `Band structures have different q-point paths. Switch to path_mode="union" or "intersection" to compare non-identical paths.`;
161
- });
162
- // Determine which segments to plot based on path_mode
163
- let segments_to_plot = $derived.by(() => {
164
- if (path_mode === `union`)
165
- return new Set(all_segment_keys);
166
- return new Set(common_segment_keys);
167
- });
168
- // Map segments to x-axis positions
169
- $effect(() => {
274
+ ? null
275
+ : `Band structures have different q-point paths. Switch to path_mode="union" or "intersection" to compare non-identical paths.`
276
+ })
277
+
278
+ // Determine which segments to plot based on path_mode
279
+ let segments_to_plot = $derived.by(() => {
280
+ if (path_mode === `union`) return new Set(all_segment_keys)
281
+ return new Set(common_segment_keys)
282
+ })
283
+
284
+ // Map segments to x-axis positions
285
+ $effect(() => {
170
286
  if (Object.keys(band_structs_dict).length === 0 || segments_to_plot.size === 0) {
171
- x_positions = {};
172
- return;
287
+ x_positions = {}
288
+ return
173
289
  }
174
- const positions = {};
175
- let current_x = 0;
290
+ const positions: Record<string, [number, number]> = {}
291
+ let current_x = 0
292
+
176
293
  // Preserve physical path order using the first available structure
177
- const canonical = Object.values(band_structs_dict)[0];
294
+ const canonical = Object.values(band_structs_dict)[0]
178
295
  if (!canonical) {
179
- x_positions = {};
180
- return;
296
+ x_positions = {}
297
+ return
181
298
  }
182
- const ordered_segments = helpers.get_ordered_segments(canonical, segments_to_plot);
299
+ const ordered_segments = helpers.get_ordered_segments(canonical, segments_to_plot)
300
+
183
301
  for (let seg_idx = 0; seg_idx < ordered_segments.length; seg_idx++) {
184
- const segment_key = ordered_segments[seg_idx];
185
- if (positions[segment_key])
186
- continue;
187
- const [start_label, end_label] = segment_key.split(`_`);
188
- // Find the first band structure that has this segment
189
- for (const bs of Object.values(band_structs_dict)) {
190
- const matching_branch = bs.branches.find((branch) => {
191
- const branch_start = bs.qpoints[branch.start_index]?.label || `null`;
192
- const branch_end = bs.qpoints[branch.end_index]?.label || `null`;
193
- return branch_start === start_label && branch_end === end_label;
194
- });
195
- if (matching_branch) {
196
- // Check if this is a discontinuity: consecutive indices mean no path between points
197
- const is_discontinuity = matching_branch.end_index - matching_branch.start_index === 1;
198
- if (is_discontinuity) {
199
- // Place at same x position as current, no advancement
200
- positions[segment_key] = [current_x, current_x];
201
- }
202
- else {
203
- const segment_len = bs.distance[matching_branch.end_index] -
204
- bs.distance[matching_branch.start_index];
205
- positions[segment_key] = [current_x, current_x + segment_len];
206
- current_x += segment_len;
207
- }
208
- break;
209
- }
302
+ const segment_key = ordered_segments[seg_idx]
303
+ if (positions[segment_key]) continue
304
+
305
+ const [start_label, end_label] = segment_key.split(`_`)
306
+
307
+ // Find the first band structure that has this segment
308
+ for (const bs of Object.values(band_structs_dict)) {
309
+ const matching_branch = bs.branches.find((branch) => {
310
+ const branch_start = bs.qpoints[branch.start_index]?.label || `null`
311
+ const branch_end = bs.qpoints[branch.end_index]?.label || `null`
312
+ return branch_start === start_label && branch_end === end_label
313
+ })
314
+
315
+ if (matching_branch) {
316
+ // Check if this is a discontinuity: consecutive indices mean no path between points
317
+ const is_discontinuity =
318
+ matching_branch.end_index - matching_branch.start_index === 1
319
+
320
+ if (is_discontinuity) {
321
+ // Place at same x position as current, no advancement
322
+ positions[segment_key] = [current_x, current_x]
323
+ } else {
324
+ const segment_len = bs.distance[matching_branch.end_index] -
325
+ bs.distance[matching_branch.start_index]
326
+ positions[segment_key] = [current_x, current_x + segment_len]
327
+ current_x += segment_len
328
+ }
329
+ break
210
330
  }
331
+ }
211
332
  }
212
- x_positions = positions;
213
- });
214
- // Convert band structures to scatter plot series + track max slope in one pass
215
- let { series_data, max_abs_slope } = $derived.by(() => {
333
+
334
+ x_positions = positions
335
+ })
336
+
337
+ // Convert band structures to scatter plot series + track max slope in one pass
338
+ let { series_data, max_abs_slope } = $derived.by((): {
339
+ series_data: DataSeries[]
340
+ max_abs_slope: number
341
+ } => {
216
342
  if (Object.keys(band_structs_dict).length === 0 || segments_to_plot.size === 0) {
217
- return { series_data: [], max_abs_slope: 1 };
343
+ return { series_data: [], max_abs_slope: 1 }
218
344
  }
219
- const all_series = [];
220
- let max_slope = 0;
345
+
346
+ const all_series: DataSeries[] = []
347
+ let max_slope = 0
348
+
221
349
  for (const [bs_idx, [label, bs]] of Object.entries(band_structs_dict).entries()) {
222
- const color = PLOT_COLORS[bs_idx % PLOT_COLORS.length];
223
- const structure_label = label || `Structure ${bs_idx + 1}`;
224
- const gamma_indices = detected_band_type === `phonon`
225
- ? helpers.find_gamma_indices(bs)
226
- : [];
227
- for (const branch of bs.branches) {
228
- const start_idx = branch.start_index;
229
- const end_idx = branch.end_index + 1;
230
- const start_label = bs.qpoints[start_idx]?.label ?? undefined;
231
- const end_label = bs.qpoints[end_idx - 1]?.label ?? undefined;
232
- const segment_key = helpers.get_segment_key(start_label, end_label);
233
- if (!segments_to_plot.has(segment_key))
234
- continue;
235
- // Skip discontinuous segments (consecutive labeled points)
236
- const is_discontinuity = branch.end_index - branch.start_index === 1;
237
- if (is_discontinuity)
238
- continue;
239
- const [x_start, x_end] = x_positions?.[segment_key] || [0, 1];
240
- // Scale distances for this segment
241
- const segment_distances = bs.distance.slice(start_idx, end_idx);
242
- const scaled_distances = helpers.scale_segment_distances(segment_distances, x_start, x_end);
243
- // Create series for each band (and spin channel for electronic structures)
244
- for (let band_idx = 0; band_idx < bs.nb_bands; band_idx++) {
245
- const frequencies = convert_band_values(bs.bands[band_idx].slice(start_idx, end_idx));
246
- const is_acoustic = helpers.classify_acoustic(bs, band_idx, gamma_indices);
247
- const line_style_up = get_line_style(color, is_acoustic === true, frequencies, band_idx);
248
- const spin_down_band = bs.spin_down_bands?.[band_idx];
249
- const has_spin_down_channel = detected_band_type === `electronic` &&
250
- Array.isArray(spin_down_band) &&
251
- spin_down_band.length >= end_idx;
252
- const track_max_slope = (meta) => {
253
- for (const pt of meta) {
254
- if (typeof pt.slope === `number` && Number.isFinite(pt.slope)) {
255
- max_slope = Math.max(max_slope, Math.abs(pt.slope));
256
- }
257
- }
258
- };
259
- if (effective_spin_mode !== `down_only`) {
260
- const meta = helpers.build_point_metadata({
261
- x_vals: scaled_distances,
262
- y_vals: frequencies,
263
- band_idx,
264
- spin: `up`,
265
- is_acoustic,
266
- bs,
267
- start_idx,
268
- });
269
- track_max_slope(meta);
270
- all_series.push({
271
- x: scaled_distances,
272
- y: frequencies,
273
- markers: `line`,
274
- label: has_spin_down_channel
275
- ? `${structure_label} (↑)`
276
- : structure_label,
277
- line_style: line_style_up,
278
- metadata: meta,
279
- });
280
- }
281
- if (has_spin_down_channel && effective_spin_mode !== `up_only`) {
282
- const spin_down_frequencies = convert_band_values(spin_down_band.slice(start_idx, end_idx));
283
- const meta = helpers.build_point_metadata({
284
- x_vals: scaled_distances,
285
- y_vals: spin_down_frequencies,
286
- band_idx,
287
- spin: `down`,
288
- is_acoustic,
289
- bs,
290
- start_idx,
291
- });
292
- track_max_slope(meta);
293
- all_series.push({
294
- x: scaled_distances,
295
- y: spin_down_frequencies,
296
- markers: `line`,
297
- label: `${structure_label} (↓)`,
298
- line_style: {
299
- ...line_style_up,
300
- line_dash: `4,2`,
301
- stroke_width: Math.max(1, line_style_up.stroke_width - 0.1),
302
- },
303
- metadata: meta,
304
- });
305
- }
350
+ const color = PLOT_COLORS[bs_idx % PLOT_COLORS.length]
351
+ const structure_label = label || `Structure ${bs_idx + 1}`
352
+ const gamma_indices = detected_band_type === `phonon`
353
+ ? helpers.find_gamma_indices(bs)
354
+ : []
355
+
356
+ for (const branch of bs.branches) {
357
+ const start_idx = branch.start_index
358
+ const end_idx = branch.end_index + 1
359
+ const start_label = bs.qpoints[start_idx]?.label ?? undefined
360
+ const end_label = bs.qpoints[end_idx - 1]?.label ?? undefined
361
+ const segment_key = helpers.get_segment_key(start_label, end_label)
362
+
363
+ if (!segments_to_plot.has(segment_key)) continue
364
+
365
+ // Skip discontinuous segments (consecutive labeled points)
366
+ const is_discontinuity = branch.end_index - branch.start_index === 1
367
+ if (is_discontinuity) continue
368
+
369
+ const [x_start, x_end] = x_positions?.[segment_key] || [0, 1]
370
+
371
+ // Scale distances for this segment
372
+ const segment_distances = bs.distance.slice(start_idx, end_idx)
373
+ const scaled_distances = helpers.scale_segment_distances(
374
+ segment_distances,
375
+ x_start,
376
+ x_end,
377
+ )
378
+
379
+ // Create series for each band (and spin channel for electronic structures)
380
+ for (let band_idx = 0; band_idx < bs.nb_bands; band_idx++) {
381
+ const frequencies = convert_band_values(
382
+ bs.bands[band_idx].slice(start_idx, end_idx),
383
+ )
384
+ const is_acoustic = helpers.classify_acoustic(bs, band_idx, gamma_indices)
385
+
386
+ const line_style_up = get_line_style(
387
+ color,
388
+ is_acoustic === true,
389
+ frequencies,
390
+ band_idx,
391
+ )
392
+
393
+ const spin_down_band = bs.spin_down_bands?.[band_idx]
394
+ const has_spin_down_channel = detected_band_type === `electronic` &&
395
+ Array.isArray(spin_down_band) &&
396
+ spin_down_band.length >= end_idx
397
+
398
+ const track_max_slope = (meta: helpers.BandPointMeta[]) => {
399
+ for (const pt of meta) {
400
+ if (typeof pt.slope === `number` && Number.isFinite(pt.slope)) {
401
+ max_slope = Math.max(max_slope, Math.abs(pt.slope))
402
+ }
306
403
  }
404
+ }
405
+
406
+ if (effective_spin_mode !== `down_only`) {
407
+ const meta = helpers.build_point_metadata({
408
+ x_vals: scaled_distances,
409
+ y_vals: frequencies,
410
+ band_idx,
411
+ spin: `up`,
412
+ is_acoustic,
413
+ bs,
414
+ start_idx,
415
+ })
416
+ track_max_slope(meta)
417
+ all_series.push({
418
+ x: scaled_distances,
419
+ y: frequencies,
420
+ markers: `line`,
421
+ label: has_spin_down_channel
422
+ ? `${structure_label} (↑)`
423
+ : structure_label,
424
+ line_style: line_style_up,
425
+ metadata: meta,
426
+ })
427
+ }
428
+
429
+ if (has_spin_down_channel && effective_spin_mode !== `up_only`) {
430
+ const spin_down_frequencies = convert_band_values(
431
+ spin_down_band.slice(start_idx, end_idx),
432
+ )
433
+ const meta = helpers.build_point_metadata({
434
+ x_vals: scaled_distances,
435
+ y_vals: spin_down_frequencies,
436
+ band_idx,
437
+ spin: `down`,
438
+ is_acoustic,
439
+ bs,
440
+ start_idx,
441
+ })
442
+ track_max_slope(meta)
443
+ all_series.push({
444
+ x: scaled_distances,
445
+ y: spin_down_frequencies,
446
+ markers: `line`,
447
+ label: `${structure_label} (↓)`,
448
+ line_style: {
449
+ ...line_style_up,
450
+ line_dash: `4,2`,
451
+ stroke_width: Math.max(1, line_style_up.stroke_width - 0.1),
452
+ },
453
+ metadata: meta,
454
+ })
455
+ }
307
456
  }
457
+ }
308
458
  }
309
- return { series_data: all_series, max_abs_slope: max_slope || 1 };
310
- });
311
- // Compute ribbon data for bands with width information
312
- let ribbon_data = $derived.by(() => {
459
+
460
+ return { series_data: all_series, max_abs_slope: max_slope || 1 }
461
+ })
462
+
463
+ // Compute ribbon data for bands with width information
464
+ let ribbon_data = $derived.by((): RibbonData[] => {
313
465
  if (Object.keys(band_structs_dict).length === 0 || segments_to_plot.size === 0) {
314
- return [];
466
+ return []
315
467
  }
316
- const all_ribbons = [];
468
+
469
+ const all_ribbons: RibbonData[] = []
470
+
317
471
  for (const [bs_idx, [label, bs]] of Object.entries(band_structs_dict).entries()) {
318
- // Skip if this band structure has no width data
319
- if (!bs.band_widths || bs.band_widths.length === 0)
320
- continue;
321
- const color = PLOT_COLORS[bs_idx % PLOT_COLORS.length];
322
- const structure_label = label || `Structure ${bs_idx + 1}`;
323
- const config = helpers.get_ribbon_config(ribbon_config, label);
324
- for (const branch of bs.branches) {
325
- const start_idx = branch.start_index;
326
- const end_idx = branch.end_index + 1;
327
- const start_label = bs.qpoints[start_idx]?.label ?? undefined;
328
- const end_label = bs.qpoints[end_idx - 1]?.label ?? undefined;
329
- const segment_key = helpers.get_segment_key(start_label, end_label);
330
- if (!segments_to_plot.has(segment_key))
331
- continue;
332
- // Skip discontinuous segments
333
- const is_discontinuity = branch.end_index - branch.start_index === 1;
334
- if (is_discontinuity)
335
- continue;
336
- const [x_start, x_end] = x_positions?.[segment_key] || [0, 1];
337
- // Scale distances for this segment
338
- const segment_distances = bs.distance.slice(start_idx, end_idx);
339
- const scaled_distances = helpers.scale_segment_distances(segment_distances, x_start, x_end);
340
- // Create ribbon data for each band that has width data
341
- for (let band_idx = 0; band_idx < bs.nb_bands; band_idx++) {
342
- const band_widths = bs.band_widths[band_idx];
343
- if (!band_widths)
344
- continue;
345
- const width_values = band_widths.slice(start_idx, end_idx);
346
- // Skip if all widths are zero or missing
347
- if (width_values.every((wv) => !wv || wv <= 0))
348
- continue;
349
- const y_values = convert_band_values(bs.bands[band_idx].slice(start_idx, end_idx));
350
- all_ribbons.push({
351
- x_values: scaled_distances,
352
- y_values,
353
- width_values,
354
- color: config.color ?? color,
355
- opacity: config.opacity ?? 0.3,
356
- max_width: config.max_width ?? 6,
357
- scale: config.scale ?? 1,
358
- band_idx,
359
- structure_label,
360
- segment_key,
361
- });
362
- }
472
+ // Skip if this band structure has no width data
473
+ if (!bs.band_widths || bs.band_widths.length === 0) continue
474
+
475
+ const color = PLOT_COLORS[bs_idx % PLOT_COLORS.length]
476
+ const structure_label = label || `Structure ${bs_idx + 1}`
477
+ const config = helpers.get_ribbon_config(ribbon_config, label)
478
+
479
+ for (const branch of bs.branches) {
480
+ const start_idx = branch.start_index
481
+ const end_idx = branch.end_index + 1
482
+ const start_label = bs.qpoints[start_idx]?.label ?? undefined
483
+ const end_label = bs.qpoints[end_idx - 1]?.label ?? undefined
484
+ const segment_key = helpers.get_segment_key(start_label, end_label)
485
+
486
+ if (!segments_to_plot.has(segment_key)) continue
487
+
488
+ // Skip discontinuous segments
489
+ const is_discontinuity = branch.end_index - branch.start_index === 1
490
+ if (is_discontinuity) continue
491
+
492
+ const [x_start, x_end] = x_positions?.[segment_key] || [0, 1]
493
+
494
+ // Scale distances for this segment
495
+ const segment_distances = bs.distance.slice(start_idx, end_idx)
496
+ const scaled_distances = helpers.scale_segment_distances(
497
+ segment_distances,
498
+ x_start,
499
+ x_end,
500
+ )
501
+
502
+ // Create ribbon data for each band that has width data
503
+ for (let band_idx = 0; band_idx < bs.nb_bands; band_idx++) {
504
+ const band_widths = bs.band_widths[band_idx]
505
+ if (!band_widths) continue
506
+
507
+ const width_values = band_widths.slice(start_idx, end_idx)
508
+ // Skip if all widths are zero or missing
509
+ if (width_values.every((wv) => !wv || wv <= 0)) continue
510
+
511
+ const y_values = convert_band_values(
512
+ bs.bands[band_idx].slice(start_idx, end_idx),
513
+ )
514
+
515
+ all_ribbons.push({
516
+ x_values: scaled_distances,
517
+ y_values,
518
+ width_values,
519
+ color: config.color ?? color,
520
+ opacity: config.opacity ?? 0.3,
521
+ max_width: config.max_width ?? 6,
522
+ scale: config.scale ?? 1,
523
+ band_idx,
524
+ structure_label,
525
+ segment_key,
526
+ })
363
527
  }
528
+ }
364
529
  }
365
- return all_ribbons;
366
- });
367
- // Get x-axis tick positions with custom labels for symmetry points
368
- let x_axis_ticks = $derived.by(() => {
369
- const tick_map = new SvelteMap();
530
+
531
+ return all_ribbons
532
+ })
533
+
534
+ // Get x-axis tick positions with custom labels for symmetry points
535
+ let x_axis_ticks = $derived.by(() => {
536
+ const tick_map = new SvelteMap<number, string[]>()
537
+ const add_label = (pos: number, label: string) => {
538
+ let labels = tick_map.get(pos)
539
+ if (!labels) {
540
+ labels = []
541
+ tick_map.set(pos, labels)
542
+ }
543
+ if (!labels.includes(label)) labels.push(label)
544
+ }
545
+
370
546
  Object.entries(x_positions ?? {})
371
- .sort(([, [a]], [, [b]]) => a - b)
372
- .forEach(([segment_key, [x_start, x_end]]) => {
373
- const [start_lbl, end_lbl] = segment_key.split(`_`);
547
+ .sort(([, [a]], [, [b]]) => a - b)
548
+ .forEach(([segment_key, [x_start, x_end]]) => {
549
+ const [start_lbl, end_lbl] = segment_key.split(`_`)
374
550
  const pretty_start = start_lbl !== `null`
375
- ? helpers.pretty_sym_point(start_lbl)
376
- : ``;
377
- const pretty_end = end_lbl !== `null` ? helpers.pretty_sym_point(end_lbl) : ``;
551
+ ? helpers.pretty_sym_point(start_lbl)
552
+ : ``
553
+ const pretty_end = end_lbl !== `null` ? helpers.pretty_sym_point(end_lbl) : ``
554
+
378
555
  // Check if this is a discontinuity (zero-length segment)
379
- const is_discontinuity = Math.abs(x_end - x_start) < 1e-6;
556
+ const is_discontinuity = Math.abs(x_end - x_start) < 1e-6
557
+
380
558
  if (is_discontinuity && pretty_start && pretty_end) {
381
- // Combine labels at discontinuity points
382
- if (!tick_map.has(x_start))
383
- tick_map.set(x_start, []);
384
- const labels = tick_map.get(x_start);
385
- if (!labels.includes(pretty_start))
386
- labels.push(pretty_start);
387
- if (!labels.includes(pretty_end))
388
- labels.push(pretty_end);
559
+ // Combine labels at discontinuity points
560
+ add_label(x_start, pretty_start)
561
+ add_label(x_start, pretty_end)
562
+ } else {
563
+ // Normal segment with distinct start/end
564
+ if (pretty_start) add_label(x_start, pretty_start)
565
+ if (pretty_end) add_label(x_end, pretty_end)
389
566
  }
390
- else {
391
- // Normal segment with distinct start/end
392
- if (pretty_start) {
393
- if (!tick_map.has(x_start))
394
- tick_map.set(x_start, []);
395
- const labels = tick_map.get(x_start);
396
- if (!labels.includes(pretty_start))
397
- labels.push(pretty_start);
398
- }
399
- if (pretty_end) {
400
- if (!tick_map.has(x_end))
401
- tick_map.set(x_end, []);
402
- const labels = tick_map.get(x_end);
403
- if (!labels.includes(pretty_end))
404
- labels.push(pretty_end);
405
- }
406
- }
407
- });
567
+ })
568
+
408
569
  // Merge labels at same position with pipe separator
409
- return Object.fromEntries(Array.from(tick_map.entries()).map(([pos, labels]) => [
570
+ return Object.fromEntries(
571
+ Array.from(tick_map.entries()).map(([pos, labels]) => [
410
572
  pos,
411
573
  labels.join(` | `),
412
- ]));
413
- });
414
- let x_range = $derived.by(() => {
415
- const flat = Object.values(x_positions ?? {}).flat();
416
- return [flat[0] ?? 0, flat.at(-1) ?? 1];
417
- });
418
- // Calculate y-range, enforcing 0 minimum for phonon bands without imaginary modes
419
- let y_range = $derived.by(() => {
574
+ ]),
575
+ )
576
+ })
577
+
578
+ let x_range = $derived.by((): Vec2 => {
579
+ const flat = Object.values(x_positions ?? {}).flat()
580
+ return [flat[0] ?? 0, flat.at(-1) ?? 1]
581
+ })
582
+
583
+ // Calculate y-range, enforcing 0 minimum for phonon bands without imaginary modes
584
+ let y_range = $derived.by((): Vec2 | undefined => {
420
585
  const all_freqs = Object.values(band_structs_dict).flatMap((bs) => [
421
- ...bs.bands.flat(),
422
- ...(bs.spin_down_bands?.flat() ?? []),
423
- ]);
586
+ ...bs.bands.flat(),
587
+ ...(bs.spin_down_bands?.flat() ?? []),
588
+ ])
424
589
  // Keep electronic y-range independent of phonon unit conversion options.
425
590
  const display_values = detected_band_type === `phonon`
426
- ? convert_band_values(all_freqs)
427
- : all_freqs;
428
- if (!display_values.length)
429
- return undefined;
430
- const finite = display_values.filter(Number.isFinite);
431
- if (!finite.length)
432
- return undefined;
433
- let min_val = Math.min(...finite), max_val = Math.max(...finite);
591
+ ? convert_band_values(all_freqs)
592
+ : all_freqs
593
+ if (!display_values.length) return undefined
594
+ const finite = display_values.filter(Number.isFinite)
595
+ if (!finite.length) return undefined
596
+ let min_val = Math.min(...finite), max_val = Math.max(...finite)
434
597
  if (
435
- // clamp phonon min to 0 if negatives are noise
436
- detected_band_type === `phonon` && min_val < 0 &&
437
- helpers.negative_fraction(finite) < helpers.IMAGINARY_MODE_NOISE_THRESHOLD) {
438
- min_val = 0;
598
+ // clamp phonon min to 0 if negatives are noise
599
+ detected_band_type === `phonon` && min_val < 0 &&
600
+ helpers.negative_fraction(finite) < helpers.IMAGINARY_MODE_NOISE_THRESHOLD
601
+ ) {
602
+ min_val = 0
439
603
  }
440
- const padding = (max_val - min_val) * 0.02;
441
- return [min_val === 0 ? 0 : min_val - padding, max_val + padding];
442
- });
443
- // Internal y_axis that ScatterPlot binds to - syncs zoom changes back to parent
444
- let internal_y_axis = $derived({
604
+ const padding = (max_val - min_val) * 0.02
605
+ return [min_val === 0 ? 0 : min_val - padding, max_val + padding]
606
+ })
607
+
608
+ // Internal y_axis that ScatterPlot binds to - syncs zoom changes back to parent
609
+ let internal_y_axis = $derived({
445
610
  label: detected_band_type === `phonon` ? `Frequency (${units})` : `Energy (eV)`,
446
611
  format: `.2f`,
447
612
  label_shift: { y: 15 },
448
613
  range: y_range,
449
614
  ...y_axis,
450
- });
451
- // Sync zoom changes from ScatterPlot back to parent via bindable y_axis
452
- // Also clears parent range when internal range becomes invalid (auto-range reset)
453
- $effect(() => {
454
- const range = internal_y_axis.range;
615
+ })
616
+
617
+ // Sync zoom changes from ScatterPlot back to parent via bindable y_axis
618
+ // Also clears parent range when internal range becomes invalid (auto-range reset)
619
+ $effect(() => {
620
+ const range = internal_y_axis.range
455
621
  if (helpers.is_valid_range(range)) {
456
- if (y_axis.range?.[0] !== range[0] || y_axis.range?.[1] !== range[1]) {
457
- y_axis = { ...y_axis, range };
458
- }
459
- return;
622
+ if (y_axis.range?.[0] !== range[0] || y_axis.range?.[1] !== range[1]) {
623
+ y_axis = { ...y_axis, range }
624
+ }
625
+ return
460
626
  }
461
627
  // Range became invalid - clear parent's range to propagate reset
462
628
  if (`range` in y_axis) {
463
- const { range: _omit, ...rest } = y_axis;
464
- y_axis = rest;
629
+ const { range: _omit, ...rest } = y_axis
630
+ y_axis = rest
465
631
  }
466
- });
467
- let has_series = $derived(series_data.length > 0);
468
- let is_strict_path_error = $derived(path_mode === `strict` && !!strict_path_error);
469
- let imaginary_mode_region = $derived.by(() => {
470
- if (detected_band_type !== `phonon` ||
471
- !shade_imaginary_modes ||
472
- !y_range ||
473
- y_range[0] >= 0)
474
- return [];
632
+ })
633
+
634
+ let has_series = $derived(series_data.length > 0)
635
+ let is_strict_path_error = $derived(path_mode === `strict` && !!strict_path_error)
636
+
637
+ let imaginary_mode_region = $derived.by((): FillRegion[] => {
638
+ if (
639
+ detected_band_type !== `phonon` ||
640
+ !shade_imaginary_modes ||
641
+ !y_range ||
642
+ y_range[0] >= 0
643
+ ) return []
475
644
  return [{
476
- lower: y_range[0],
477
- upper: 0,
478
- fill: `var(--bands-imaginary-region-color, light-dark(#f8d7da, #5a1a1f))`,
479
- fill_opacity: 0.2,
480
- label: `Imaginary modes`,
481
- show_in_legend: false,
482
- z_index: `below-lines`,
483
- }];
484
- });
485
- let custom_highlight_regions = $derived.by(() => (highlight_regions ?? [])
486
- .filter((region) => Number.isFinite(region.y_min) && Number.isFinite(region.y_max))
487
- .map((region) => ({
488
- lower: Math.min(region.y_min, region.y_max),
489
- upper: Math.max(region.y_min, region.y_max),
490
- fill: region.color ??
491
- `var(--bands-highlight-region-color, light-dark(#f6e8c3, #4d3f20))`,
492
- fill_opacity: region.opacity ?? 0.2,
493
- label: region.label,
494
- show_in_legend: Boolean(region.label),
495
- z_index: `below-lines`,
496
- })));
497
- let fill_regions = $derived([
645
+ lower: y_range[0],
646
+ upper: 0,
647
+ fill: `var(--bands-imaginary-region-color, light-dark(#f8d7da, #5a1a1f))`,
648
+ fill_opacity: 0.2,
649
+ label: `Imaginary modes`,
650
+ show_in_legend: false,
651
+ z_index: `below-lines`,
652
+ }]
653
+ })
654
+
655
+ let custom_highlight_regions = $derived.by((): FillRegion[] =>
656
+ (highlight_regions ?? [])
657
+ .filter((region) =>
658
+ Number.isFinite(region.y_min) && Number.isFinite(region.y_max)
659
+ )
660
+ .map((region) => ({
661
+ lower: Math.min(region.y_min, region.y_max),
662
+ upper: Math.max(region.y_min, region.y_max),
663
+ fill: region.color ??
664
+ `var(--bands-highlight-region-color, light-dark(#f6e8c3, #4d3f20))`,
665
+ fill_opacity: region.opacity ?? 0.2,
666
+ label: region.label,
667
+ show_in_legend: Boolean(region.label),
668
+ z_index: `below-lines` as const,
669
+ }))
670
+ )
671
+
672
+ let fill_regions = $derived([
498
673
  ...imaginary_mode_region,
499
674
  ...custom_highlight_regions,
500
- ]);
501
- let electronic_gap_annotation = $derived.by(() => {
502
- if (!show_gap_annotation ||
503
- detected_band_type !== `electronic` ||
504
- effective_fermi_level === undefined)
505
- return null;
506
- const all_energies = series_data.flatMap((series_item) => series_item.y.filter(Number.isFinite));
507
- const occupied = all_energies.filter((energy) => energy <= effective_fermi_level);
508
- const unoccupied = all_energies.filter((energy) => energy > effective_fermi_level);
509
- if (!occupied.length || !unoccupied.length)
510
- return null;
511
- const vbm = Math.max(...occupied);
512
- const cbm = Math.min(...unoccupied);
513
- const gap = cbm - vbm;
514
- if (!(gap > 0))
515
- return null;
516
- return { vbm, cbm, gap };
517
- });
518
- let empty_state_message = $derived.by(() => {
675
+ ])
676
+
677
+ let electronic_gap_annotation = $derived.by(() => {
678
+ if (
679
+ !show_gap_annotation ||
680
+ detected_band_type !== `electronic` ||
681
+ effective_fermi_level === undefined
682
+ ) return null
683
+ const all_energies = series_data.flatMap((series_item) =>
684
+ series_item.y.filter(Number.isFinite)
685
+ )
686
+ const occupied = all_energies.filter((energy) => energy <= effective_fermi_level)
687
+ const unoccupied = all_energies.filter((energy) => energy > effective_fermi_level)
688
+ if (!occupied.length || !unoccupied.length) return null
689
+ const vbm = Math.max(...occupied)
690
+ const cbm = Math.min(...unoccupied)
691
+ const gap = cbm - vbm
692
+ if (!(gap > 0)) return null
693
+ return { vbm, cbm, gap }
694
+ })
695
+
696
+ let empty_state_message = $derived.by(() => {
519
697
  if (is_strict_path_error) {
520
- return strict_path_error ?? `Path mismatch in strict mode.`;
698
+ return strict_path_error ?? `Path mismatch in strict mode.`
521
699
  }
522
700
  if (!band_structs || Object.keys(band_structs_dict).length === 0) {
523
- return `No valid band structure data to display.`;
701
+ return `No valid band structure data to display.`
524
702
  }
525
703
  if (!has_series) {
526
- return `No plottable band segments were found in the provided data.`;
704
+ return `No plottable band segments were found in the provided data.`
527
705
  }
528
- return `No valid band structure data to display.`;
529
- });
530
- let display = $state({ x_grid: false, y_grid: true, y_zero_line: true });
706
+ return `No valid band structure data to display.`
707
+ })
708
+
709
+ let display = $state({ x_grid: false, y_grid: true, y_zero_line: true })
531
710
  </script>
532
711
  {#if has_series && !is_strict_path_error}
533
712
  <ScatterPlot
@@ -573,7 +752,7 @@ let display = $state({ x_grid: false, y_grid: true, y_zero_line: true });
573
752
  } = (metadata ?? {}) as Partial<helpers.BandPointMeta>}
574
753
  {@const num_structs = Object.keys(band_structs_dict).length}
575
754
  {#if num_structs > 1 && label}<strong>{label}</strong><br />{/if}
576
- {@html y_label || `Value`}: {y_formatted}{y_unit ? ` ${y_unit}` : ``}<br />
755
+ {@html sanitize_html(y_label || `Value`)}: {y_formatted}{y_unit ? ` ${y_unit}` : ``}<br />
577
756
  {#if path}Path: {path}<br />{/if}
578
757
  {#if typeof band_idx === `number`}
579
758
  Band: {band_idx + 1}{#if typeof nb_bands === `number`}&thinsp;/&thinsp;{
@@ -774,7 +953,7 @@ let display = $state({ x_grid: false, y_grid: true, y_zero_line: true });
774
953
  {/if}
775
954
 
776
955
  <!-- Reference frequency horizontal line -->
777
- {@const ref_freq = reference_frequency !== null && reference_frequency !== undefined
956
+ {@const ref_freq = reference_frequency != null
778
957
  ? convert_band_values([reference_frequency])[0]
779
958
  : NaN}
780
959
  {@const ref_y = Number.isFinite(ref_freq) ? y_scale_fn(ref_freq) : NaN}