matterviz 0.3.1 → 0.3.2

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 (257) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/app.css +29 -0
  4. package/dist/brillouin/BrillouinZone.svelte +19 -61
  5. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  6. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  7. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  9. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  10. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  11. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  12. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  13. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  16. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  17. package/dist/chempot-diagram/color.d.ts +10 -0
  18. package/dist/chempot-diagram/color.js +33 -0
  19. package/dist/chempot-diagram/compute.d.ts +38 -0
  20. package/dist/chempot-diagram/compute.js +650 -0
  21. package/dist/chempot-diagram/index.d.ts +5 -0
  22. package/dist/chempot-diagram/index.js +5 -0
  23. package/dist/chempot-diagram/pointer.d.ts +16 -0
  24. package/dist/chempot-diagram/pointer.js +40 -0
  25. package/dist/chempot-diagram/temperature.d.ts +15 -0
  26. package/dist/chempot-diagram/temperature.js +37 -0
  27. package/dist/chempot-diagram/types.d.ts +83 -0
  28. package/dist/chempot-diagram/types.js +27 -0
  29. package/dist/colors/index.d.ts +3 -1
  30. package/dist/colors/index.js +4 -0
  31. package/dist/composition/BarChart.svelte +13 -22
  32. package/dist/composition/BubbleChart.svelte +5 -3
  33. package/dist/composition/FormulaFilter.svelte +586 -94
  34. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  35. package/dist/composition/PieChart.svelte +43 -18
  36. package/dist/composition/PieChart.svelte.d.ts +1 -1
  37. package/dist/convex-hull/ConvexHull.svelte +4 -2
  38. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  39. package/dist/convex-hull/ConvexHull2D.svelte +13 -44
  40. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  41. package/dist/convex-hull/ConvexHull3D.svelte +16 -7
  42. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull4D.svelte +17 -7
  44. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHullStats.svelte +701 -226
  47. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  48. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  49. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  50. package/dist/convex-hull/demo-temperature.js +36 -0
  51. package/dist/convex-hull/helpers.d.ts +1 -1
  52. package/dist/convex-hull/helpers.js +2 -4
  53. package/dist/convex-hull/index.d.ts +1 -0
  54. package/dist/convex-hull/index.js +1 -0
  55. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  56. package/dist/convex-hull/thermodynamics.js +106 -17
  57. package/dist/convex-hull/types.d.ts +5 -0
  58. package/dist/convex-hull/types.js +5 -0
  59. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  60. package/dist/element/BohrAtom.svelte +1 -1
  61. package/dist/element/data.js +2 -14
  62. package/dist/element/data.json.gz +0 -0
  63. package/dist/element/types.d.ts +1 -0
  64. package/dist/fermi-surface/FermiSurface.svelte +20 -64
  65. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  66. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  67. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  68. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  69. package/dist/fermi-surface/parse.js +16 -22
  70. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  71. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  72. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  73. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  74. package/dist/heatmap-matrix/index.d.ts +53 -0
  75. package/dist/heatmap-matrix/index.js +100 -0
  76. package/dist/heatmap-matrix/shared.d.ts +2 -0
  77. package/dist/heatmap-matrix/shared.js +4 -0
  78. package/dist/icons.d.ts +111 -0
  79. package/dist/icons.js +111 -0
  80. package/dist/index.d.ts +3 -1
  81. package/dist/index.js +3 -1
  82. package/dist/io/export.js +15 -3
  83. package/dist/io/file-drop.d.ts +7 -0
  84. package/dist/io/file-drop.js +43 -0
  85. package/dist/io/index.d.ts +2 -2
  86. package/dist/io/index.js +2 -112
  87. package/dist/io/types.d.ts +1 -0
  88. package/dist/io/url-drop.d.ts +2 -0
  89. package/dist/io/url-drop.js +118 -0
  90. package/dist/isosurface/Isosurface.svelte +101 -45
  91. package/dist/isosurface/IsosurfaceControls.svelte +19 -0
  92. package/dist/isosurface/parse.js +73 -30
  93. package/dist/isosurface/slice.d.ts +2 -1
  94. package/dist/isosurface/slice.js +3 -3
  95. package/dist/isosurface/types.d.ts +13 -1
  96. package/dist/isosurface/types.js +98 -0
  97. package/dist/labels.d.ts +2 -1
  98. package/dist/labels.js +1 -0
  99. package/dist/layout/InfoTag.svelte +62 -62
  100. package/dist/layout/SubpageGrid.svelte +74 -0
  101. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  102. package/dist/layout/index.d.ts +1 -0
  103. package/dist/layout/index.js +1 -0
  104. package/dist/layout/json-tree/JsonNode.svelte +83 -85
  105. package/dist/layout/json-tree/JsonTree.svelte +20 -19
  106. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  107. package/dist/layout/json-tree/JsonValue.svelte +196 -116
  108. package/dist/layout/json-tree/types.d.ts +10 -2
  109. package/dist/layout/json-tree/utils.d.ts +2 -0
  110. package/dist/layout/json-tree/utils.js +33 -0
  111. package/dist/math.d.ts +7 -0
  112. package/dist/math.js +358 -7
  113. package/dist/overlays/ContextMenu.svelte +3 -2
  114. package/dist/overlays/DraggablePane.svelte +163 -58
  115. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  116. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  117. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  118. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  119. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  120. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  121. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  122. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  123. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  124. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  125. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  126. package/dist/phase-diagram/index.d.ts +2 -0
  127. package/dist/phase-diagram/index.js +2 -0
  128. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  129. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  130. package/dist/phase-diagram/types.d.ts +10 -0
  131. package/dist/phase-diagram/utils.d.ts +7 -4
  132. package/dist/phase-diagram/utils.js +149 -59
  133. package/dist/plot/AxisLabel.svelte +26 -0
  134. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  135. package/dist/plot/BarPlot.svelte +473 -228
  136. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  137. package/dist/plot/BarPlotControls.svelte +3 -2
  138. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  139. package/dist/plot/ColorBar.svelte +54 -54
  140. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  141. package/dist/plot/ColorScaleSelect.svelte +1 -1
  142. package/dist/plot/ElementScatter.svelte +3 -2
  143. package/dist/plot/FillArea.svelte +4 -1
  144. package/dist/plot/Histogram.svelte +320 -230
  145. package/dist/plot/Histogram.svelte.d.ts +2 -2
  146. package/dist/plot/HistogramControls.svelte +29 -10
  147. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  148. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  149. package/dist/plot/PlotControls.svelte +109 -27
  150. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  151. package/dist/plot/PlotLegend.svelte +1 -1
  152. package/dist/plot/PortalSelect.svelte +2 -1
  153. package/dist/plot/ReferenceLine.svelte +2 -1
  154. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  155. package/dist/plot/ReferencePlane.svelte +1 -3
  156. package/dist/plot/ScatterPlot.svelte +343 -209
  157. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  158. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  159. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  160. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  161. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  162. package/dist/plot/ScatterPlotControls.svelte +95 -55
  163. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  164. package/dist/plot/ZeroLines.svelte +44 -0
  165. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  166. package/dist/plot/ZoomRect.svelte +21 -0
  167. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  168. package/dist/plot/axis-utils.d.ts +1 -1
  169. package/dist/plot/index.d.ts +6 -2
  170. package/dist/plot/index.js +6 -2
  171. package/dist/plot/interactions.d.ts +8 -10
  172. package/dist/plot/interactions.js +2 -3
  173. package/dist/plot/layout.d.ts +7 -1
  174. package/dist/plot/layout.js +12 -4
  175. package/dist/plot/reference-line.d.ts +4 -21
  176. package/dist/plot/reference-line.js +7 -81
  177. package/dist/plot/types.d.ts +42 -17
  178. package/dist/plot/types.js +10 -0
  179. package/dist/plot/utils/label-placement.js +13 -10
  180. package/dist/plot/utils.d.ts +1 -0
  181. package/dist/plot/utils.js +14 -0
  182. package/dist/rdf/RdfPlot.svelte +55 -66
  183. package/dist/settings.d.ts +3 -0
  184. package/dist/settings.js +17 -3
  185. package/dist/spectral/Bands.svelte +515 -143
  186. package/dist/spectral/Bands.svelte.d.ts +22 -2
  187. package/dist/spectral/helpers.d.ts +23 -1
  188. package/dist/spectral/helpers.js +65 -9
  189. package/dist/spectral/types.d.ts +2 -0
  190. package/dist/structure/AtomLegend.svelte +29 -8
  191. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  192. package/dist/structure/CellSelect.svelte +92 -22
  193. package/dist/structure/Structure.svelte +108 -118
  194. package/dist/structure/Structure.svelte.d.ts +1 -1
  195. package/dist/structure/StructureControls.svelte +25 -22
  196. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  197. package/dist/structure/StructureInfoPane.svelte +7 -1
  198. package/dist/structure/StructureScene.svelte +104 -66
  199. package/dist/structure/StructureScene.svelte.d.ts +2 -1
  200. package/dist/structure/atom-properties.d.ts +6 -2
  201. package/dist/structure/atom-properties.js +38 -25
  202. package/dist/structure/export.js +10 -7
  203. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  204. package/dist/structure/ferrox-wasm-types.js +0 -3
  205. package/dist/structure/ferrox-wasm.d.ts +3 -2
  206. package/dist/structure/ferrox-wasm.js +1 -2
  207. package/dist/structure/index.d.ts +6 -0
  208. package/dist/structure/index.js +22 -0
  209. package/dist/structure/parse.js +19 -16
  210. package/dist/structure/partial-occupancy.d.ts +25 -0
  211. package/dist/structure/partial-occupancy.js +102 -0
  212. package/dist/structure/validation.js +6 -3
  213. package/dist/symmetry/SymmetryStats.svelte +18 -4
  214. package/dist/symmetry/WyckoffTable.svelte +18 -10
  215. package/dist/symmetry/index.d.ts +7 -4
  216. package/dist/symmetry/index.js +83 -18
  217. package/dist/table/HeatmapTable.svelte +425 -65
  218. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  219. package/dist/table/ToggleMenu.svelte +2 -0
  220. package/dist/table/index.d.ts +2 -0
  221. package/dist/trajectory/Trajectory.svelte +147 -145
  222. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  223. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  224. package/dist/trajectory/constants.d.ts +6 -0
  225. package/dist/trajectory/constants.js +7 -0
  226. package/dist/trajectory/extract.js +3 -5
  227. package/dist/trajectory/format-detect.d.ts +9 -0
  228. package/dist/trajectory/format-detect.js +76 -0
  229. package/dist/trajectory/frame-reader.d.ts +17 -0
  230. package/dist/trajectory/frame-reader.js +339 -0
  231. package/dist/trajectory/helpers.d.ts +15 -0
  232. package/dist/trajectory/helpers.js +187 -0
  233. package/dist/trajectory/index.d.ts +1 -0
  234. package/dist/trajectory/index.js +11 -4
  235. package/dist/trajectory/parse/ase.d.ts +2 -0
  236. package/dist/trajectory/parse/ase.js +76 -0
  237. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  238. package/dist/trajectory/parse/hdf5.js +121 -0
  239. package/dist/trajectory/parse/index.d.ts +12 -0
  240. package/dist/trajectory/parse/index.js +304 -0
  241. package/dist/trajectory/parse/lammps.d.ts +5 -0
  242. package/dist/trajectory/parse/lammps.js +169 -0
  243. package/dist/trajectory/parse/vasp.d.ts +2 -0
  244. package/dist/trajectory/parse/vasp.js +65 -0
  245. package/dist/trajectory/parse/xyz.d.ts +2 -0
  246. package/dist/trajectory/parse/xyz.js +109 -0
  247. package/dist/trajectory/types.d.ts +11 -0
  248. package/dist/trajectory/types.js +1 -0
  249. package/dist/utils.d.ts +2 -0
  250. package/dist/utils.js +4 -0
  251. package/dist/xrd/XrdPlot.svelte +6 -4
  252. package/dist/xrd/calc-xrd.js +0 -1
  253. package/package.json +30 -24
  254. package/readme.md +4 -4
  255. package/dist/trajectory/parse.d.ts +0 -42
  256. package/dist/trajectory/parse.js +0 -1267
  257. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,10 +1,16 @@
1
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';
2
5
  import ScatterPlot from '../plot/ScatterPlot.svelte';
3
6
  import * as helpers from './helpers';
4
7
  import { SvelteMap } from 'svelte/reactivity';
5
- 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, ...rest } = $props();
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` ||
10
+ typeof attr_value === `number` ||
11
+ typeof attr_value === `boolean`;
6
12
  // Helper function to get line styling for a band
7
- function get_line_style(color, is_acoustic, mode_type, frequencies, band_idx) {
13
+ function get_line_style(color, is_acoustic, frequencies, band_idx) {
8
14
  const defaults = { stroke: color, stroke_width: is_acoustic ? 1.5 : 1 };
9
15
  if (typeof line_kwargs === `function`) {
10
16
  const custom = line_kwargs(frequencies, band_idx);
@@ -14,7 +20,8 @@ function get_line_style(color, is_acoustic, mode_type, frequencies, band_idx) {
14
20
  };
15
21
  }
16
22
  if (typeof line_kwargs === `object` && line_kwargs !== null) {
17
- const mode_kwargs = line_kwargs[mode_type];
23
+ const mode_key = is_acoustic ? `acoustic` : `optical`;
24
+ const mode_kwargs = line_kwargs[mode_key];
18
25
  const source = (mode_kwargs ?? line_kwargs);
19
26
  return {
20
27
  stroke: source.stroke ?? defaults.stroke,
@@ -103,10 +110,23 @@ let effective_fermi_level = $derived.by(() => {
103
110
  const efermi = source?.efermi;
104
111
  return typeof efermi === `number` ? efermi : undefined;
105
112
  });
106
- // Determine which segments to plot based on path_mode
107
- let segments_to_plot = $derived.by(() => {
113
+ let effective_spin_mode = $derived.by(() => {
114
+ if (detected_band_type !== `electronic`)
115
+ return null;
116
+ 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(() => {
108
129
  const all_segments = {};
109
- // Collect all segments from all structures
110
130
  for (const [label, bs] of Object.entries(band_structs_dict)) {
111
131
  for (const branch of bs.branches) {
112
132
  const start_label = bs.qpoints[branch.start_index]?.label ?? undefined;
@@ -116,27 +136,49 @@ let segments_to_plot = $derived.by(() => {
116
136
  all_segments[segment_key].push([label, bs]);
117
137
  }
118
138
  }
119
- const num_structs = Object.keys(band_structs_dict).length;
120
- const is_intersection = path_mode === `strict` || path_mode === `intersection`;
121
- if (is_intersection) {
122
- // Only segments present in all structures
123
- const common_segments = Object.keys(all_segments).filter((seg) => all_segments[seg].length === num_structs);
124
- // Warn in strict mode if not all segments are common
125
- if (path_mode === `strict` &&
126
- common_segments.length !== Object.keys(all_segments).length) {
127
- console.warn(`Band structures have different q-point paths. Use path_mode="union" or "intersection".`);
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 = {};
146
+ 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;
128
150
  }
129
- return new Set(common_segments);
130
151
  }
131
- // union - all segments
132
- return new Set(Object.keys(all_segments));
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;
158
+ 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);
133
167
  });
134
168
  // Map segments to x-axis positions
135
169
  $effect(() => {
170
+ if (Object.keys(band_structs_dict).length === 0 || segments_to_plot.size === 0) {
171
+ x_positions = {};
172
+ return;
173
+ }
136
174
  const positions = {};
137
175
  let current_x = 0;
138
176
  // Preserve physical path order using the first available structure
139
177
  const canonical = Object.values(band_structs_dict)[0];
178
+ if (!canonical) {
179
+ x_positions = {};
180
+ return;
181
+ }
140
182
  const ordered_segments = helpers.get_ordered_segments(canonical, segments_to_plot);
141
183
  for (let seg_idx = 0; seg_idx < ordered_segments.length; seg_idx++) {
142
184
  const segment_key = ordered_segments[seg_idx];
@@ -169,15 +211,19 @@ $effect(() => {
169
211
  }
170
212
  x_positions = positions;
171
213
  });
172
- // Convert band structures to scatter plot series
173
- let series_data = $derived.by(() => {
214
+ // Convert band structures to scatter plot series + track max slope in one pass
215
+ let { series_data, max_abs_slope } = $derived.by(() => {
174
216
  if (Object.keys(band_structs_dict).length === 0 || segments_to_plot.size === 0) {
175
- return [];
217
+ return { series_data: [], max_abs_slope: 1 };
176
218
  }
177
219
  const all_series = [];
220
+ let max_slope = 0;
178
221
  for (const [bs_idx, [label, bs]] of Object.entries(band_structs_dict).entries()) {
179
222
  const color = PLOT_COLORS[bs_idx % PLOT_COLORS.length];
180
223
  const structure_label = label || `Structure ${bs_idx + 1}`;
224
+ const gamma_indices = detected_band_type === `phonon`
225
+ ? helpers.find_gamma_indices(bs)
226
+ : [];
181
227
  for (const branch of bs.branches) {
182
228
  const start_idx = branch.start_index;
183
229
  const end_idx = branch.end_index + 1;
@@ -194,25 +240,73 @@ let series_data = $derived.by(() => {
194
240
  // Scale distances for this segment
195
241
  const segment_distances = bs.distance.slice(start_idx, end_idx);
196
242
  const scaled_distances = helpers.scale_segment_distances(segment_distances, x_start, x_end);
197
- // Create series for each band
243
+ // Create series for each band (and spin channel for electronic structures)
198
244
  for (let band_idx = 0; band_idx < bs.nb_bands; band_idx++) {
199
- const frequencies = bs.bands[band_idx].slice(start_idx, end_idx);
200
- const is_acoustic = detected_band_type === `phonon` &&
201
- band_idx < helpers.N_ACOUSTIC_MODES;
202
- const mode_type = is_acoustic ? `acoustic` : `optical`;
203
- const line_style = get_line_style(color, is_acoustic, mode_type, frequencies, band_idx);
204
- all_series.push({
205
- x: scaled_distances,
206
- y: frequencies,
207
- markers: `line`,
208
- label: structure_label,
209
- line_style,
210
- metadata: { band_idx },
211
- });
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
+ }
212
306
  }
213
307
  }
214
308
  }
215
- return all_series;
309
+ return { series_data: all_series, max_abs_slope: max_slope || 1 };
216
310
  });
217
311
  // Compute ribbon data for bands with width information
218
312
  let ribbon_data = $derived.by(() => {
@@ -252,7 +346,7 @@ let ribbon_data = $derived.by(() => {
252
346
  // Skip if all widths are zero or missing
253
347
  if (width_values.every((wv) => !wv || wv <= 0))
254
348
  continue;
255
- const y_values = bs.bands[band_idx].slice(start_idx, end_idx);
349
+ const y_values = convert_band_values(bs.bands[band_idx].slice(start_idx, end_idx));
256
350
  all_ribbons.push({
257
351
  x_values: scaled_distances,
258
352
  y_values,
@@ -323,8 +417,17 @@ let x_range = $derived.by(() => {
323
417
  });
324
418
  // Calculate y-range, enforcing 0 minimum for phonon bands without imaginary modes
325
419
  let y_range = $derived.by(() => {
326
- const all_freqs = Object.values(band_structs_dict).flatMap((bs) => bs.bands.flat());
327
- const finite = all_freqs.filter(Number.isFinite);
420
+ const all_freqs = Object.values(band_structs_dict).flatMap((bs) => [
421
+ ...bs.bands.flat(),
422
+ ...(bs.spin_down_bands?.flat() ?? []),
423
+ ]);
424
+ // Keep electronic y-range independent of phonon unit conversion options.
425
+ 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);
328
431
  if (!finite.length)
329
432
  return undefined;
330
433
  let min_val = Math.min(...finite), max_val = Math.max(...finite);
@@ -339,7 +442,7 @@ let y_range = $derived.by(() => {
339
442
  });
340
443
  // Internal y_axis that ScatterPlot binds to - syncs zoom changes back to parent
341
444
  let internal_y_axis = $derived({
342
- label: detected_band_type === `phonon` ? `Frequency (THz)` : `Energy (eV)`,
445
+ label: detected_band_type === `phonon` ? `Frequency (${units})` : `Energy (eV)`,
343
446
  format: `.2f`,
344
447
  label_shift: { y: 15 },
345
448
  range: y_range,
@@ -361,49 +464,226 @@ $effect(() => {
361
464
  y_axis = rest;
362
465
  }
363
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 [];
475
+ 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([
498
+ ...imaginary_mode_region,
499
+ ...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(() => {
519
+ if (is_strict_path_error) {
520
+ return strict_path_error ?? `Path mismatch in strict mode.`;
521
+ }
522
+ if (!band_structs || Object.keys(band_structs_dict).length === 0) {
523
+ return `No valid band structure data to display.`;
524
+ }
525
+ if (!has_series) {
526
+ return `No plottable band segments were found in the provided data.`;
527
+ }
528
+ return `No valid band structure data to display.`;
529
+ });
364
530
  let display = $state({ x_grid: false, y_grid: true, y_zero_line: true });
365
531
  </script>
366
-
367
- <ScatterPlot
368
- series={series_data}
369
- x_axis={{
370
- label: `Wave Vector`,
371
- ticks: Object.keys(x_axis_ticks).length > 0 ? x_axis_ticks : undefined,
372
- format: ``,
373
- range: x_range,
374
- ...x_axis,
375
- }}
376
- bind:y_axis={internal_y_axis}
377
- bind:display
378
- legend={show_legend && Object.keys(band_structs_dict).length > 1 ? {} : null}
379
- hover_config={{ threshold_px: 50 }}
380
- {...rest}
381
- >
382
- {#snippet tooltip({ x, y_formatted, label, metadata })}
383
- {@const y_label_full = internal_y_axis.label ?? ``}
384
- {@const [, y_label, y_unit] = y_label_full.match(/^(.+?)\s*\(([^)]+)\)$/) ??
532
+ {#if has_series && !is_strict_path_error}
533
+ <ScatterPlot
534
+ {id}
535
+ class={class_name}
536
+ {style}
537
+ data-testid={data_testid}
538
+ series={series_data}
539
+ {fill_regions}
540
+ x_axis={{
541
+ label: `Wave Vector`,
542
+ ticks: Object.keys(x_axis_ticks).length > 0 ? x_axis_ticks : undefined,
543
+ format: ``,
544
+ range: x_range,
545
+ ...x_axis,
546
+ }}
547
+ bind:y_axis={internal_y_axis}
548
+ bind:display
549
+ legend={show_legend && Object.keys(band_structs_dict).length > 1 ? {} : null}
550
+ hover_config={{ threshold_px: 50 }}
551
+ controls={{ show: show_controls }}
552
+ {...rest}
553
+ >
554
+ {#snippet tooltip({ x, y, y_formatted, label, metadata })}
555
+ {@const y_label_full = internal_y_axis.label ?? ``}
556
+ {@const [, y_label, y_unit] = y_label_full.match(/^(.+?)\s*\(([^)]+)\)$/) ??
385
557
  [, y_label_full, ``]}
386
- {@const segment = Object.entries(x_positions ?? {}).find(([, [start, end]]) =>
558
+ {@const segment = Object.entries(x_positions ?? {}).find(([, [start, end]]) =>
387
559
  x >= start && x <= end
388
560
  )}
389
- {@const path = segment?.[0].split(`_`).map((lbl) =>
561
+ {@const path = segment?.[0].split(`_`).map((lbl) =>
390
562
  lbl !== `null` ? helpers.pretty_sym_point(lbl) : ``
391
563
  ).filter(Boolean).join(` → `) || null}
392
- {@const band_idx = metadata?.band_idx}
393
- {@const num_structs = Object.keys(band_structs_dict).length}
394
- {#if num_structs > 1 && label}<strong>{label}</strong><br />{/if}
395
- {y_label || `Value`}: {y_formatted}{y_unit ? ` ${y_unit}` : ``}<br />
396
- {#if path}Path: {path}<br />{/if}
397
- {#if typeof band_idx === `number`}Band: {band_idx + 1}{/if}
398
- {/snippet}
564
+ {@const {
565
+ band_idx,
566
+ spin,
567
+ is_acoustic,
568
+ nb_bands,
569
+ frac_coords,
570
+ qpoint_label,
571
+ band_width,
572
+ slope,
573
+ } = (metadata ?? {}) as Partial<helpers.BandPointMeta>}
574
+ {@const num_structs = Object.keys(band_structs_dict).length}
575
+ {#if num_structs > 1 && label}<strong>{label}</strong><br />{/if}
576
+ {@html y_label || `Value`}: {y_formatted}{y_unit ? ` ${y_unit}` : ``}<br />
577
+ {#if path}Path: {path}<br />{/if}
578
+ {#if typeof band_idx === `number`}
579
+ Band: {band_idx + 1}{#if typeof nb_bands === `number`}&thinsp;/&thinsp;{
580
+ nb_bands
581
+ }{/if}
582
+ {#if typeof is_acoustic === `boolean`}
583
+ ({is_acoustic ? `acoustic` : `optical`})
584
+ {:else if detected_band_type === `electronic` && effective_fermi_level !== undefined}
585
+ ({y <= effective_fermi_level ? `valence` : `conduction`})
586
+ {/if}
587
+ {#if spin === `up` || spin === `down`}
588
+ {spin === `up` ? `↑` : `↓`}
589
+ {/if}
590
+ {/if}
591
+ {#if typeof qpoint_label === `string` && qpoint_label}
592
+ <br />At: {helpers.pretty_sym_point(qpoint_label)}
593
+ {/if}
594
+ {#if Array.isArray(frac_coords)}
595
+ <br />{detected_band_type === `electronic` ? `k` : `q`}: [{
596
+ frac_coords.map((coord: number) => format_num(coord, `.3f`)).join(`, `)
597
+ }]
598
+ {/if}
599
+ {#if typeof band_width === `number` && band_width > 0}
600
+ <br />Projection: {format_num(band_width, `.3~g`)}
601
+ {/if}
602
+ {#if typeof slope === `number` && Number.isFinite(slope)}
603
+ {@const rel = Math.abs(slope) / max_abs_slope}
604
+ <br />Dispersion: {rel < 0.15 ? `flat` : rel < 0.5 ? `moderate` : `steep`}
605
+ {/if}
606
+ {/snippet}
399
607
 
400
- {#snippet user_content({ height, x_scale_fn, y_scale_fn, pad })}
401
- <!-- Fat band ribbons (rendered behind band lines) -->
402
- {#each ribbon_data as
403
- ribbon
404
- (`${ribbon.structure_label}-${ribbon.segment_key}-${ribbon.band_idx}`)
405
- }
406
- {@const path_d = helpers.generate_ribbon_path(
608
+ {#snippet controls_extra()}
609
+ {#if show_path_mode_control}
610
+ <SettingsSection
611
+ title="Path Mode"
612
+ current_values={{ path_mode }}
613
+ on_reset={() => (path_mode = `strict`)}
614
+ >
615
+ <div class="pane-row">
616
+ <label for="bands-path-mode">Mode:</label>
617
+ <select id="bands-path-mode" bind:value={path_mode}>
618
+ <option value="strict">strict</option>
619
+ <option value="intersection">intersection</option>
620
+ <option value="union">union</option>
621
+ </select>
622
+ </div>
623
+ </SettingsSection>
624
+ {/if}
625
+
626
+ {#if show_units_control && detected_band_type === `phonon`}
627
+ <SettingsSection
628
+ title="Units"
629
+ current_values={{ units }}
630
+ on_reset={() => (units = `THz`)}
631
+ >
632
+ <div class="pane-row">
633
+ <label for="bands-units">Frequency:</label>
634
+ <select id="bands-units" bind:value={units}>
635
+ <option value="THz">THz</option>
636
+ <option value="eV">eV</option>
637
+ <option value="meV">meV</option>
638
+ <option value="cm-1">cm-1</option>
639
+ <option value="Ha">Ha</option>
640
+ </select>
641
+ </div>
642
+ </SettingsSection>
643
+ {/if}
644
+
645
+ {#if show_spin_control && detected_band_type === `electronic`}
646
+ <SettingsSection
647
+ title="Spin Display"
648
+ current_values={{ band_spin_mode }}
649
+ on_reset={() => (band_spin_mode = `overlay`)}
650
+ >
651
+ <div class="pane-row">
652
+ <label for="bands-spin-mode">Mode:</label>
653
+ <select id="bands-spin-mode" bind:value={band_spin_mode}>
654
+ <option value="overlay">overlay</option>
655
+ <option value="up_only">up only</option>
656
+ <option value="down_only">down only</option>
657
+ </select>
658
+ </div>
659
+ </SettingsSection>
660
+ {/if}
661
+
662
+ {#if show_annotation_controls && detected_band_type === `electronic`}
663
+ <SettingsSection
664
+ title="Annotations"
665
+ current_values={{ show_gap_annotation }}
666
+ on_reset={() => (show_gap_annotation = true)}
667
+ >
668
+ <div class="pane-row pane-checkbox">
669
+ <input
670
+ id="bands-gap-annotation"
671
+ type="checkbox"
672
+ bind:checked={show_gap_annotation}
673
+ />
674
+ <label for="bands-gap-annotation">Show band gap annotation</label>
675
+ </div>
676
+ </SettingsSection>
677
+ {/if}
678
+ {/snippet}
679
+
680
+ {#snippet user_content({ height, x_scale_fn, y_scale_fn, pad })}
681
+ <!-- Fat band ribbons (rendered behind band lines) -->
682
+ {#each ribbon_data as
683
+ ribbon
684
+ (`${ribbon.structure_label}-${ribbon.segment_key}-${ribbon.band_idx}`)
685
+ }
686
+ {@const path_d = helpers.generate_ribbon_path(
407
687
  ribbon.x_values,
408
688
  ribbon.y_values,
409
689
  ribbon.width_values,
@@ -412,78 +692,170 @@ let display = $state({ x_grid: false, y_grid: true, y_zero_line: true });
412
692
  ribbon.max_width,
413
693
  ribbon.scale,
414
694
  )}
415
- {#if path_d}
416
- <path
417
- d={path_d}
418
- fill={ribbon.color}
419
- opacity={ribbon.opacity}
420
- stroke="none"
421
- class="fat-band-ribbon"
422
- />
423
- {/if}
424
- {/each}
695
+ {#if path_d}
696
+ <path
697
+ d={path_d}
698
+ fill={ribbon.color}
699
+ opacity={ribbon.opacity}
700
+ stroke="none"
701
+ class="fat-band-ribbon"
702
+ />
703
+ {/if}
704
+ {/each}
425
705
 
426
- <!-- Symmetry point vertical lines (filter NaN from scale) -->
427
- {#each Object.keys(x_axis_ticks).map(Number).map((x) => x_scale_fn(x)).filter(
706
+ <!-- Symmetry point vertical lines (filter NaN from scale) -->
707
+ {#each Object.keys(x_axis_ticks).map(Number).map((x) => x_scale_fn(x)).filter(
428
708
  Number.isFinite,
429
709
  ) as
430
- scaled_x
431
- (scaled_x)
432
- }
433
- <line
434
- x1={scaled_x}
435
- x2={scaled_x}
436
- y1={pad.t}
437
- y2={height - pad.b}
438
- stroke="var(--bands-symmetry-line-color, light-dark(black, white))"
439
- stroke-width="var(--bands-symmetry-line-width, 1)"
440
- opacity="var(--bands-symmetry-line-opacity, 0.5)"
441
- />
442
- {/each}
710
+ scaled_x
711
+ (scaled_x)
712
+ }
713
+ <line
714
+ x1={scaled_x}
715
+ x2={scaled_x}
716
+ y1={pad.t}
717
+ y2={height - pad.b}
718
+ stroke="var(--bands-symmetry-line-color, light-dark(black, white))"
719
+ stroke-width="var(--bands-symmetry-line-width, 1)"
720
+ opacity="var(--bands-symmetry-line-opacity, 0.5)"
721
+ />
722
+ {/each}
443
723
 
444
- <!-- Fermi level line for electronic bands -->
445
- {@const fermi_y = effective_fermi_level !== undefined
724
+ <!-- Shared geometry for Fermi level and gap annotations -->
725
+ {@const fermi_y = effective_fermi_level !== undefined
446
726
  ? y_scale_fn(effective_fermi_level)
447
727
  : NaN}
448
- {@const bands_x_end = x_scale_fn(Object.values(x_positions ?? {}).flat().at(-1) ?? 1)}
449
- {#if Number.isFinite(fermi_y) && Number.isFinite(bands_x_end)}
450
- <line
451
- class="fermi-level-line"
452
- x1={pad.l}
453
- x2={bands_x_end}
454
- y1={fermi_y}
455
- y2={fermi_y}
456
- stroke="var(--bands-fermi-line-color, light-dark(#e74c3c, #ff6b6b))"
457
- stroke-width="var(--bands-fermi-line-width, 1.5)"
458
- stroke-dasharray="var(--bands-fermi-line-dash, 6,3)"
459
- opacity="var(--bands-fermi-line-opacity, 0.8)"
460
- />
461
- <text
462
- class="fermi-level-label"
463
- x={bands_x_end + 4}
464
- y={fermi_y}
465
- dy="0.35em"
466
- font-size="10"
467
- fill="var(--bands-fermi-line-color, light-dark(#e74c3c, #ff6b6b))"
468
- opacity="0.9"
469
- >
470
- E<tspan dy="2" font-size="8">F</tspan>
471
- </text>
472
- {/if}
728
+ {@const bands_x_end = x_scale_fn(Object.values(x_positions ?? {}).flat().at(-1) ?? 1)}
729
+ {@const gap_data = electronic_gap_annotation}
730
+ {@const vbm_y = gap_data ? y_scale_fn(gap_data.vbm) : NaN}
731
+ {@const cbm_y = gap_data ? y_scale_fn(gap_data.cbm) : NaN}
732
+ {@const gap_mid_y = (vbm_y + cbm_y) / 2}
733
+ {@const ef_needs_offset = Number.isFinite(gap_mid_y) &&
734
+ Math.abs(fermi_y - gap_mid_y) < 16}
735
+ {@const ef_label_y = ef_needs_offset
736
+ ? gap_mid_y + (fermi_y >= gap_mid_y ? 16 : -16)
737
+ : fermi_y}
738
+
739
+ <!-- Fermi level line for electronic bands -->
740
+ {#if Number.isFinite(fermi_y) && Number.isFinite(bands_x_end)}
741
+ <line
742
+ class="fermi-level-line"
743
+ x1={pad.l}
744
+ x2={bands_x_end}
745
+ y1={fermi_y}
746
+ y2={fermi_y}
747
+ stroke="var(--bands-fermi-line-color, light-dark(#e74c3c, #ff6b6b))"
748
+ stroke-width="var(--bands-fermi-line-width, 1.5)"
749
+ stroke-dasharray="var(--bands-fermi-line-dash, 6,3)"
750
+ opacity="var(--bands-fermi-line-opacity, 0.8)"
751
+ />
752
+ {#if ef_needs_offset}
753
+ <line
754
+ x1={bands_x_end}
755
+ y1={fermi_y}
756
+ x2={bands_x_end + 3}
757
+ y2={ef_label_y}
758
+ stroke="var(--bands-fermi-line-color, light-dark(#e74c3c, #ff6b6b))"
759
+ stroke-width="0.7"
760
+ opacity="0.5"
761
+ />
762
+ {/if}
763
+ <text
764
+ class="fermi-level-label"
765
+ x={bands_x_end + 4}
766
+ y={ef_label_y}
767
+ dy="0.35em"
768
+ font-size="10"
769
+ fill="var(--bands-fermi-line-color, light-dark(#e74c3c, #ff6b6b))"
770
+ opacity="0.9"
771
+ >
772
+ E<tspan dy="2" font-size="8">F</tspan>
773
+ </text>
774
+ {/if}
775
+
776
+ <!-- Reference frequency horizontal line -->
777
+ {@const ref_freq = reference_frequency !== null && reference_frequency !== undefined
778
+ ? convert_band_values([reference_frequency])[0]
779
+ : NaN}
780
+ {@const ref_y = Number.isFinite(ref_freq) ? y_scale_fn(ref_freq) : NaN}
781
+ {#if Number.isFinite(ref_y) && Number.isFinite(bands_x_end)}
782
+ <line
783
+ x1={pad.l}
784
+ x2={bands_x_end}
785
+ y1={ref_y}
786
+ y2={ref_y}
787
+ stroke="var(--bands-reference-line-color, light-dark(#d48860, #c47850))"
788
+ stroke-width="var(--bands-reference-line-width, 1)"
789
+ stroke-dasharray="var(--bands-reference-line-dash, 4,3)"
790
+ opacity="var(--bands-reference-line-opacity, 0.5)"
791
+ />
792
+ {/if}
793
+
794
+ <!-- Electronic band edge and gap annotation -->
795
+ {#if gap_data && Number.isFinite(vbm_y) && Number.isFinite(cbm_y) &&
796
+ Number.isFinite(bands_x_end)}
797
+ {#each [
798
+ [vbm_y, `var(--bands-gap-vbm-color, light-dark(#1f77b4, #7db7ff))`],
799
+ [cbm_y, `var(--bands-gap-cbm-color, light-dark(#2ca02c, #7ddc7d))`],
800
+ ] as [number, string][] as
801
+ [edge_y, color]
802
+ (edge_y)
803
+ }
804
+ <line
805
+ x1={pad.l}
806
+ x2={bands_x_end + 3}
807
+ y1={edge_y}
808
+ y2={edge_y}
809
+ stroke={color}
810
+ stroke-width="var(--bands-gap-line-width, 1)"
811
+ stroke-dasharray="var(--bands-gap-line-dash, 2,2)"
812
+ opacity="0.7"
813
+ />
814
+ {/each}
815
+ <text
816
+ x={bands_x_end + 4}
817
+ y={gap_mid_y}
818
+ dy="0.35em"
819
+ font-size="10"
820
+ fill="var(--text-color)"
821
+ >
822
+ E<tspan dy="2" font-size="8">g:</tspan>
823
+ <tspan dy="-2">{Number(gap_data.gap.toPrecision(4))} eV</tspan>
824
+ </text>
825
+ {/if}
826
+ {/snippet}
827
+ </ScatterPlot>
828
+ {:else}
829
+ <EmptyState
830
+ {id}
831
+ class={class_name}
832
+ {style}
833
+ data-testid={data_testid}
834
+ {...empty_state_attrs}
835
+ message={empty_state_message}
836
+ />
837
+ {/if}
473
838
 
474
- <!-- Reference frequency horizontal line -->
475
- {@const ref_y = reference_frequency !== null ? y_scale_fn(reference_frequency) : NaN}
476
- {#if Number.isFinite(ref_y) && Number.isFinite(bands_x_end)}
477
- <line
478
- x1={pad.l}
479
- x2={bands_x_end}
480
- y1={ref_y}
481
- y2={ref_y}
482
- stroke="var(--bands-reference-line-color, light-dark(#d48860, #c47850))"
483
- stroke-width="var(--bands-reference-line-width, 1)"
484
- stroke-dasharray="var(--bands-reference-line-dash, 4,3)"
485
- opacity="var(--bands-reference-line-opacity, 0.5)"
486
- />
487
- {/if}
488
- {/snippet}
489
- </ScatterPlot>
839
+ <style>
840
+ .pane-row {
841
+ display: flex;
842
+ align-items: center;
843
+ gap: 0.5em;
844
+ margin: 0.3em 0;
845
+ font-size: 0.9em;
846
+ }
847
+ .pane-row label {
848
+ min-width: 4.5em;
849
+ flex-shrink: 0;
850
+ }
851
+ .pane-row select {
852
+ flex: 1;
853
+ min-width: 0;
854
+ }
855
+ .pane-checkbox {
856
+ gap: 0.4em;
857
+ }
858
+ .pane-checkbox label {
859
+ min-width: 0;
860
+ }
861
+ </style>