matterviz 0.3.5 → 0.3.7

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 (229) hide show
  1. package/dist/MillerIndexInput.svelte +5 -5
  2. package/dist/api/optimade.js +3 -3
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
  8. package/dist/brillouin/compute.js +21 -21
  9. package/dist/brillouin/index.d.ts +1 -1
  10. package/dist/brillouin/index.js +0 -1
  11. package/dist/brillouin/types.d.ts +8 -13
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
  15. package/dist/chempot-diagram/compute.js +1 -7
  16. package/dist/chempot-diagram/temperature.d.ts +1 -1
  17. package/dist/chempot-diagram/temperature.js +1 -3
  18. package/dist/chempot-diagram/types.d.ts +4 -9
  19. package/dist/colors/index.js +5 -5
  20. package/dist/composition/Composition.svelte +2 -1
  21. package/dist/composition/Formula.svelte +7 -4
  22. package/dist/composition/FormulaFilter.svelte +1 -3
  23. package/dist/composition/format.js +4 -4
  24. package/dist/composition/parse.d.ts +2 -1
  25. package/dist/composition/parse.js +61 -46
  26. package/dist/convex-hull/ConvexHull2D.svelte +62 -51
  27. package/dist/convex-hull/ConvexHull3D.svelte +101 -90
  28. package/dist/convex-hull/ConvexHull4D.svelte +70 -58
  29. package/dist/convex-hull/ConvexHullControls.svelte +24 -35
  30. package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
  31. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
  32. package/dist/convex-hull/ConvexHullStats.svelte +9 -2
  33. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
  34. package/dist/convex-hull/GasPressureControls.svelte +7 -7
  35. package/dist/convex-hull/StructurePopup.svelte +65 -30
  36. package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
  37. package/dist/convex-hull/TemperatureSlider.svelte +8 -5
  38. package/dist/convex-hull/barycentric-coords.d.ts +2 -2
  39. package/dist/convex-hull/barycentric-coords.js +2 -2
  40. package/dist/convex-hull/gas-thermodynamics.js +2 -4
  41. package/dist/convex-hull/helpers.d.ts +13 -2
  42. package/dist/convex-hull/helpers.js +37 -16
  43. package/dist/convex-hull/index.d.ts +1 -0
  44. package/dist/convex-hull/index.js +1 -0
  45. package/dist/convex-hull/thermodynamics.d.ts +2 -1
  46. package/dist/convex-hull/thermodynamics.js +7 -7
  47. package/dist/convex-hull/types.d.ts +15 -15
  48. package/dist/effects.svelte.d.ts +12 -0
  49. package/dist/effects.svelte.js +37 -0
  50. package/dist/element/BohrAtom.svelte +4 -4
  51. package/dist/element/data.json.gz.d.ts +3 -1
  52. package/dist/element/index.d.ts +1 -1
  53. package/dist/element/index.js +0 -1
  54. package/dist/fermi-surface/FermiSurface.svelte +4 -4
  55. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  56. package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
  57. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  58. package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
  59. package/dist/fermi-surface/compute.js +2 -2
  60. package/dist/fermi-surface/export.js +13 -26
  61. package/dist/fermi-surface/parse.js +8 -12
  62. package/dist/fermi-surface/types.d.ts +2 -5
  63. package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
  64. package/dist/heatmap-matrix/index.js +6 -6
  65. package/dist/io/decompress.d.ts +2 -1
  66. package/dist/io/decompress.js +1 -1
  67. package/dist/io/export.js +1 -1
  68. package/dist/io/index.d.ts +1 -1
  69. package/dist/io/index.js +0 -1
  70. package/dist/io/url-drop.js +7 -1
  71. package/dist/isosurface/IsosurfaceControls.svelte +11 -25
  72. package/dist/isosurface/slice.js +1 -1
  73. package/dist/isosurface/types.js +12 -12
  74. package/dist/labels.d.ts +1 -1
  75. package/dist/labels.js +14 -11
  76. package/dist/layout/InfoTag.svelte +6 -4
  77. package/dist/layout/PropertyFilter.svelte +4 -2
  78. package/dist/layout/json-tree/JsonTree.svelte +22 -14
  79. package/dist/layout/json-tree/JsonValue.svelte +2 -2
  80. package/dist/layout/json-tree/types.d.ts +3 -2
  81. package/dist/layout/json-tree/types.js +0 -1
  82. package/dist/layout/json-tree/utils.d.ts +4 -4
  83. package/dist/layout/json-tree/utils.js +12 -20
  84. package/dist/marching-cubes.js +13 -15
  85. package/dist/math.d.ts +11 -1
  86. package/dist/math.js +15 -6
  87. package/dist/overlays/DragControlTab.svelte +98 -0
  88. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  89. package/dist/overlays/DraggablePane.svelte +7 -84
  90. package/dist/overlays/index.d.ts +1 -0
  91. package/dist/overlays/index.js +1 -0
  92. package/dist/periodic-table/PeriodicTable.svelte +11 -11
  93. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
  94. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  95. package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
  96. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  97. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
  98. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
  99. package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
  100. package/dist/phase-diagram/build-diagram.js +11 -18
  101. package/dist/phase-diagram/diagram-input.d.ts +5 -9
  102. package/dist/phase-diagram/index.d.ts +2 -2
  103. package/dist/phase-diagram/index.js +0 -2
  104. package/dist/phase-diagram/parse.d.ts +2 -2
  105. package/dist/phase-diagram/parse.js +6 -10
  106. package/dist/phase-diagram/svg-to-diagram.js +15 -15
  107. package/dist/phase-diagram/types.d.ts +5 -11
  108. package/dist/phase-diagram/utils.d.ts +2 -2
  109. package/dist/phase-diagram/utils.js +9 -11
  110. package/dist/plot/BarPlot.svelte +162 -314
  111. package/dist/plot/BarPlot.svelte.d.ts +5 -4
  112. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  113. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  114. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  115. package/dist/plot/ColorBar.svelte +19 -17
  116. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  117. package/dist/plot/FillArea.svelte +2 -4
  118. package/dist/plot/FillArea.svelte.d.ts +1 -1
  119. package/dist/plot/Histogram.svelte +167 -281
  120. package/dist/plot/Histogram.svelte.d.ts +1 -1
  121. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  122. package/dist/plot/InteractiveAxisLabel.svelte +5 -3
  123. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  124. package/dist/plot/PlotAxis.svelte +169 -0
  125. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  126. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  127. package/dist/plot/ReferenceLine3D.svelte +53 -51
  128. package/dist/plot/ReferencePlane.svelte +39 -42
  129. package/dist/plot/ScatterPlot.svelte +300 -367
  130. package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
  131. package/dist/plot/ScatterPlot3D.svelte +33 -6
  132. package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
  133. package/dist/plot/ScatterPlot3DControls.svelte +9 -9
  134. package/dist/plot/ScatterPlotControls.svelte +3 -4
  135. package/dist/plot/ScatterPoint.svelte +18 -27
  136. package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
  137. package/dist/plot/Surface3D.svelte +4 -7
  138. package/dist/plot/ZeroLines.svelte +2 -1
  139. package/dist/plot/ZeroLines.svelte.d.ts +2 -1
  140. package/dist/plot/ZoomRect.svelte +2 -2
  141. package/dist/plot/ZoomRect.svelte.d.ts +3 -3
  142. package/dist/plot/adaptive-density.d.ts +69 -0
  143. package/dist/plot/adaptive-density.js +191 -0
  144. package/dist/plot/auto-place.d.ts +43 -0
  145. package/dist/plot/auto-place.js +122 -0
  146. package/dist/plot/axis-utils.js +3 -5
  147. package/dist/plot/binned-scatter-types.d.ts +59 -0
  148. package/dist/plot/binned-scatter-types.js +1 -0
  149. package/dist/plot/data-cleaning.js +1 -1
  150. package/dist/plot/data-transform.js +1 -1
  151. package/dist/plot/fill-utils.d.ts +4 -9
  152. package/dist/plot/fill-utils.js +29 -44
  153. package/dist/plot/index.d.ts +4 -0
  154. package/dist/plot/index.js +2 -0
  155. package/dist/plot/interactions.d.ts +4 -4
  156. package/dist/plot/interactions.js +4 -3
  157. package/dist/plot/layout.d.ts +20 -2
  158. package/dist/plot/layout.js +59 -16
  159. package/dist/plot/reference-line.d.ts +1 -1
  160. package/dist/plot/reference-line.js +9 -11
  161. package/dist/plot/scales.d.ts +1 -1
  162. package/dist/plot/scales.js +20 -23
  163. package/dist/plot/types.d.ts +30 -58
  164. package/dist/plot/types.js +2 -6
  165. package/dist/plot/utils/label-placement.d.ts +24 -3
  166. package/dist/plot/utils/label-placement.js +82 -12
  167. package/dist/plot/utils/series-visibility.d.ts +8 -2
  168. package/dist/plot/utils/series-visibility.js +23 -5
  169. package/dist/rdf/RdfPlot.svelte +5 -5
  170. package/dist/rdf/calc-rdf.js +3 -3
  171. package/dist/sanitize.d.ts +2 -0
  172. package/dist/sanitize.js +2 -0
  173. package/dist/spectral/Bands.svelte +1 -1
  174. package/dist/spectral/BandsAndDos.svelte +22 -16
  175. package/dist/spectral/BrillouinBandsDos.svelte +20 -16
  176. package/dist/spectral/Dos.svelte +1 -1
  177. package/dist/spectral/helpers.d.ts +4 -2
  178. package/dist/spectral/helpers.js +44 -35
  179. package/dist/spectral/index.d.ts +1 -1
  180. package/dist/spectral/index.js +0 -1
  181. package/dist/structure/AtomLegend.svelte +23 -6
  182. package/dist/structure/AtomLegend.svelte.d.ts +1 -0
  183. package/dist/structure/CanvasTooltip.svelte +9 -9
  184. package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
  185. package/dist/structure/CellSelect.svelte +14 -16
  186. package/dist/structure/Structure.svelte +317 -68
  187. package/dist/structure/Structure.svelte.d.ts +4 -2
  188. package/dist/structure/StructureControls.svelte +20 -45
  189. package/dist/structure/StructureExportPane.svelte +2 -1
  190. package/dist/structure/StructureInfoPane.svelte +10 -8
  191. package/dist/structure/StructureScene.svelte +527 -177
  192. package/dist/structure/StructureScene.svelte.d.ts +5 -2
  193. package/dist/structure/atom-properties.js +4 -4
  194. package/dist/structure/bond-order-perception.js +115 -98
  195. package/dist/structure/bonding.d.ts +27 -1
  196. package/dist/structure/bonding.js +187 -16
  197. package/dist/structure/export.js +1 -1
  198. package/dist/structure/index.d.ts +3 -2
  199. package/dist/structure/index.js +0 -2
  200. package/dist/structure/parse.js +88 -59
  201. package/dist/symmetry/WyckoffTable.svelte +7 -0
  202. package/dist/symmetry/index.js +13 -14
  203. package/dist/table/HeatmapTable.svelte +45 -66
  204. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  205. package/dist/table/ToggleMenu.svelte +19 -10
  206. package/dist/theme/themes.mjs +12 -0
  207. package/dist/tooltip/index.d.ts +1 -1
  208. package/dist/tooltip/index.js +0 -1
  209. package/dist/trajectory/Trajectory.svelte +43 -15
  210. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
  211. package/dist/trajectory/extract.js +1 -1
  212. package/dist/trajectory/frame-reader.js +4 -4
  213. package/dist/trajectory/helpers.d.ts +5 -4
  214. package/dist/trajectory/helpers.js +9 -17
  215. package/dist/trajectory/index.d.ts +2 -2
  216. package/dist/trajectory/index.js +2 -2
  217. package/dist/trajectory/parse/ase.js +4 -4
  218. package/dist/trajectory/parse/hdf5.js +1 -1
  219. package/dist/trajectory/parse/index.js +2 -3
  220. package/dist/trajectory/parse/lammps.js +1 -1
  221. package/dist/trajectory/parse/vasp.js +1 -1
  222. package/dist/trajectory/plotting.d.ts +1 -1
  223. package/dist/trajectory/plotting.js +38 -38
  224. package/dist/trajectory/types.d.ts +1 -1
  225. package/dist/utils.d.ts +1 -0
  226. package/dist/utils.js +9 -0
  227. package/dist/xrd/calc-xrd.js +3 -4
  228. package/dist/xrd/parse.js +1 -1
  229. package/package.json +42 -22
@@ -17,7 +17,7 @@ export type PlotScaleFn = ScaleContinuousNumeric<number, number> | ScaleTime<num
17
17
  export declare function scale_arcsinh(threshold?: number): ArcsinhScale;
18
18
  export declare function generate_arcsinh_ticks(min: number, max: number, threshold?: number, count?: number): number[];
19
19
  export declare function create_scale(scale_type: ScaleType, domain: [number, number], output_range: [number, number]): ScaleContinuousNumeric<number, number> | ArcsinhScale;
20
- export declare function create_time_scale(domain: [number, number], output_range: [number, number]): ScaleTime<number, number, never>;
20
+ export declare const create_time_scale: (domain: [number, number], output_range: [number, number]) => ScaleTime<number, number, never>;
21
21
  export declare function generate_ticks(domain: [number, number], scale_type: ScaleType, ticks_option: TicksOption | undefined, scale_fn: PlotScaleFn, // D3 scale function with .ticks() method
22
22
  options?: {
23
23
  format?: string;
@@ -107,7 +107,7 @@ export function generate_arcsinh_ticks(min, max, threshold = 1, count = 10) {
107
107
  const ticks = [0];
108
108
  // Add positive ticks
109
109
  const pos_ticks = generate_positive_arcsinh_ticks(0, hi, safe_threshold, half_count);
110
- ticks.push(...pos_ticks.filter((t) => t > 0));
110
+ ticks.push(...pos_ticks.filter((tick) => tick > 0));
111
111
  // Add negative ticks (mirror of positive)
112
112
  const neg_ticks = generate_positive_arcsinh_ticks(0, -lo, safe_threshold, half_count);
113
113
  ticks.push(...neg_ticks.filter((tick) => tick > 0).map((tick) => -tick));
@@ -116,13 +116,12 @@ export function generate_arcsinh_ticks(min, max, threshold = 1, count = 10) {
116
116
  // Add boundaries if not already present and we have room
117
117
  const sorted = dedupe_sort(ticks);
118
118
  if (sorted.length < count) {
119
- // Prefer the boundary with larger absolute value (more visually distinct)
120
- const abs_lo = Math.abs(lo);
121
- const abs_hi = Math.abs(hi);
122
- if (abs_hi >= abs_lo && !sorted.includes(hi))
123
- ticks.push(hi);
124
- else if (!sorted.includes(lo))
125
- ticks.push(lo);
119
+ // Snap the larger-magnitude boundary to a clean power of 10 (raw extremes would render
120
+ // as long unrounded labels); keeps some coverage for very small tick counts.
121
+ const boundary = Math.abs(hi) >= Math.abs(lo) ? hi : lo;
122
+ const nice = Math.sign(boundary) * 10 ** Math.floor(Math.log10(Math.abs(boundary)));
123
+ if (Number.isFinite(nice) && nice !== 0 && !sorted.includes(nice))
124
+ ticks.push(nice);
126
125
  }
127
126
  }
128
127
  return dedupe_sort(ticks);
@@ -146,12 +145,10 @@ function generate_positive_arcsinh_ticks(min, max, threshold, count) {
146
145
  }
147
146
  }
148
147
  else {
149
- // Large range: combine linear near zero with powers of 10
150
- // Add domain boundaries to ensure endpoints are represented
151
- // This prevents cases like min=50, max=500 only showing powers of 10 (100)
152
- if (min > 0)
153
- ticks.push(min);
154
- ticks.push(max);
148
+ // Large range: combine linear near zero with powers of 10.
149
+ // Domain endpoints are intentionally NOT added as ticks: raw extremes render as long
150
+ // unrounded labels (e.g. 1325.8239811994677). Powers of 10 plus 2x/5x multiples below
151
+ // already give clean round ticks; pass axis.ticks/axis.format for custom labels.
155
152
  // Add threshold as a tick if in range
156
153
  if (threshold >= min && threshold <= max)
157
154
  ticks.push(threshold);
@@ -197,11 +194,9 @@ export function create_scale(scale_type, domain, output_range) {
197
194
  return scaleLinear().domain(domain).range(output_range);
198
195
  }
199
196
  // Create a time scale for time-based data
200
- export function create_time_scale(domain, output_range) {
201
- return scaleTime()
202
- .domain([new Date(domain[0]), new Date(domain[1])])
203
- .range(output_range);
204
- }
197
+ export const create_time_scale = (domain, output_range) => scaleTime()
198
+ .domain([new Date(domain[0]), new Date(domain[1])])
199
+ .range(output_range);
205
200
  // Unified tick generation function
206
201
  export function generate_ticks(domain, scale_type, ticks_option, scale_fn, // D3 scale function with .ticks() method
207
202
  options = {}) {
@@ -359,7 +354,8 @@ export function get_nice_data_range(points, get_value, limits, scale_type, paddi
359
354
  ]) // Ensure log domain > 0
360
355
  : scaleLinear().domain([data_min, data_max]);
361
356
  scale.nice();
362
- return scale.domain();
357
+ const [nice_min = data_min, nice_max = data_max] = scale.domain();
358
+ return [nice_min, nice_max];
363
359
  }
364
360
  // Generate logarithmic ticks (from ScatterPlot)
365
361
  export function generate_log_ticks(min, max, ticks_option) {
@@ -400,9 +396,10 @@ export function get_tick_label(tick_value, ticks_option) {
400
396
  // Create a color scale function from configuration
401
397
  export function create_color_scale(color_scale_config, auto_color_range) {
402
398
  const scheme = typeof color_scale_config === `string` ? color_scale_config : color_scale_config.scheme;
403
- const interpolator = (typeof d3_sc[scheme] === `function`
404
- ? d3_sc[scheme]
405
- : d3_sc.interpolateViridis);
399
+ const candidate_interpolator = Object.entries(d3_sc).find(([key]) => key === scheme)?.[1];
400
+ const interpolator = typeof candidate_interpolator === `function`
401
+ ? candidate_interpolator
402
+ : d3_sc.interpolateViridis;
406
403
  const [min_val, max_val] = (typeof color_scale_config === `string` ? undefined : color_scale_config.value_range) ??
407
404
  auto_color_range;
408
405
  const scale_type = typeof color_scale_config === `string` ? undefined : color_scale_config.type;
@@ -1,26 +1,17 @@
1
1
  import type { D3SymbolName } from '../labels';
2
- import type { Vec2, Vec3 } from '../math';
2
+ import type { Point2D, Point3D, Vec2, Vec3 } from '../math';
3
3
  import type DraggablePane from '../overlays/DraggablePane.svelte';
4
4
  import type { ComponentProps, Snippet } from 'svelte';
5
5
  import type { HTMLAttributes } from 'svelte/elements';
6
6
  import type { TweenOptions } from 'svelte/motion';
7
7
  export type { TweenOptions } from 'svelte/motion';
8
+ import type { Sides } from './layout';
8
9
  import type PlotLegend from './PlotLegend.svelte';
9
10
  import type { TicksOption } from './scales';
10
- export type XyObj = {
11
- x: number;
12
- y: number;
13
- };
14
11
  export type XyShift = {
15
12
  x?: number;
16
13
  y?: number;
17
14
  };
18
- export type Sides = {
19
- t?: number;
20
- b?: number;
21
- l?: number;
22
- r?: number;
23
- };
24
15
  export type InitialRanges = {
25
16
  initial_x_range: Vec2;
26
17
  initial_x2_range: Vec2;
@@ -31,7 +22,7 @@ export type Point<Metadata = Record<string, unknown>> = {
31
22
  x: number;
32
23
  y: number;
33
24
  metadata?: Metadata;
34
- offset?: XyObj;
25
+ offset?: Point2D;
35
26
  };
36
27
  export interface PointStyle {
37
28
  fill?: string;
@@ -57,10 +48,14 @@ export interface HoverStyle {
57
48
  }
58
49
  export interface LabelStyle {
59
50
  text?: string;
60
- offset?: XyObj;
51
+ offset?: Point2D;
61
52
  font_size?: string;
62
53
  font_family?: string;
63
54
  auto_placement?: boolean;
55
+ size?: {
56
+ width: number;
57
+ height: number;
58
+ };
64
59
  }
65
60
  export interface BarStyle {
66
61
  color?: string;
@@ -84,8 +79,8 @@ export interface PlotPoint<Metadata = Record<string, unknown>> extends Point<Met
84
79
  point_style?: PointStyle;
85
80
  point_hover?: HoverStyle;
86
81
  point_label?: LabelStyle;
87
- point_offset?: XyObj;
88
- point_tween?: TweenOptions<XyObj>;
82
+ point_offset?: Point2D;
83
+ point_tween?: TweenOptions<Point2D>;
89
84
  }
90
85
  export type Markers = `line` | `points` | `line+points` | `none`;
91
86
  export interface DataSeries<Metadata = Record<string, unknown>> {
@@ -101,8 +96,8 @@ export interface DataSeries<Metadata = Record<string, unknown>> {
101
96
  point_style?: PointStyle[] | PointStyle;
102
97
  point_hover?: HoverStyle[] | HoverStyle;
103
98
  point_label?: LabelStyle[] | LabelStyle;
104
- point_offset?: XyObj[] | XyObj;
105
- point_tween?: TweenOptions<XyObj>;
99
+ point_offset?: Point2D[] | Point2D;
100
+ point_tween?: TweenOptions<Point2D>;
106
101
  visible?: boolean;
107
102
  label?: string;
108
103
  legend_group?: string;
@@ -184,7 +179,7 @@ export interface ArcsinhScaleConfig {
184
179
  threshold?: number;
185
180
  }
186
181
  export type ScaleType = `linear` | `log` | `arcsinh` | `time` | ArcsinhScaleConfig;
187
- export declare function is_scale_type_name(val: string): val is ScaleTypeName;
182
+ export declare const is_scale_type_name: (val: string) => val is ScaleTypeName;
188
183
  export declare function get_scale_type_name(scale_type: ScaleType | undefined): ScaleTypeName;
189
184
  export declare function get_arcsinh_threshold(scale_type: ScaleType | undefined): number;
190
185
  export declare function is_time_scale(scale_type: ScaleType | undefined, format: string | undefined): boolean;
@@ -207,13 +202,18 @@ export interface LabelPlacementConfig {
207
202
  weights?: LabelPlacementWeights;
208
203
  leader_line_threshold?: number;
209
204
  max_labels?: number;
205
+ candidate_gap?: number;
206
+ max_neighbors?: {
207
+ count: number;
208
+ radius: number;
209
+ };
210
210
  }
211
211
  export type HoverConfig = {
212
212
  threshold_px: number;
213
213
  };
214
214
  export type LegendConfig = Omit<ComponentProps<typeof PlotLegend>, `series_data` | `on_drag_start` | `on_drag` | `on_drag_end`> & {
215
215
  margin?: number | Sides;
216
- tween?: TweenOptions<XyObj>;
216
+ tween?: TweenOptions<Point2D>;
217
217
  responsive?: boolean;
218
218
  draggable?: boolean;
219
219
  axis_clearance?: number;
@@ -278,7 +278,7 @@ export interface BarSeries<Metadata = Record<string, unknown>> {
278
278
  point_style?: PointStyle[] | PointStyle;
279
279
  point_hover?: HoverStyle[] | HoverStyle;
280
280
  point_label?: LabelStyle[] | LabelStyle;
281
- point_offset?: XyObj[] | XyObj;
281
+ point_offset?: Point2D[] | Point2D;
282
282
  }
283
283
  export interface TickLabelConfig {
284
284
  inside?: boolean;
@@ -294,7 +294,7 @@ export interface AxisOption {
294
294
  unit?: string;
295
295
  }
296
296
  export type Y2SyncMode = `none` | `synced` | `align`;
297
- export declare function is_y2_sync_mode(val: string): val is Y2SyncMode;
297
+ export declare const is_y2_sync_mode: (val: string) => val is Y2SyncMode;
298
298
  export interface Y2SyncConfig {
299
299
  mode: Y2SyncMode;
300
300
  align_value?: number;
@@ -440,17 +440,10 @@ export declare const DEFAULT_GRID_STYLE: {
440
440
  export declare const DEFAULT_MARKERS: "line+points";
441
441
  export declare const DEFAULT_SERIES_COLORS: readonly ["#4e79a7", "#f28e2c", "#e15759", "#76b7b2", "#59a14f", "#edc949", "#af7aa1", "#ff9da7", "#9c755f", "#bab0ab"];
442
442
  export declare const DEFAULT_SERIES_SYMBOLS: readonly ["Circle", "Square", "Triangle", "Cross", "Diamond", "Star", "Wye"];
443
- export type XyzObj = {
444
- x: number;
445
- y: number;
446
- z: number;
447
- };
448
443
  export interface ScatterPoint3D<Metadata = Record<string, unknown>> extends Point<Metadata> {
449
444
  z: number;
450
445
  }
451
- export interface DataSeries3D<Metadata = Record<string, unknown>> extends Omit<DataSeries<Metadata>, `x` | `y` | `y_axis` | `filtered_data`> {
452
- x: readonly number[];
453
- y: readonly number[];
446
+ export interface DataSeries3D<Metadata = Record<string, unknown>> extends Omit<DataSeries<Metadata>, `y_axis` | `filtered_data`> {
454
447
  z: readonly number[];
455
448
  filtered_data?: InternalPoint3D<Metadata>[];
456
449
  }
@@ -471,8 +464,8 @@ export interface Surface3DConfig {
471
464
  z_fn?: (x: number, y: number) => number;
472
465
  u_range?: Vec2;
473
466
  v_range?: Vec2;
474
- parametric_fn?: (u: number, v: number) => XyzObj;
475
- points?: XyzObj[];
467
+ parametric_fn?: (u: number, v: number) => Point3D;
468
+ points?: Point3D[];
476
469
  triangles?: Vec3[];
477
470
  color?: string;
478
471
  color_fn?: (x: number, y: number, z: number) => string;
@@ -502,13 +495,8 @@ export interface DisplayConfig3D extends DisplayConfig {
502
495
  projection_opacity?: number;
503
496
  projection_scale?: number;
504
497
  }
505
- export interface Scatter3DHandlerProps<Metadata = Record<string, unknown>> {
506
- x: number;
507
- y: number;
498
+ export interface Scatter3DHandlerProps<Metadata = Record<string, unknown>> extends Omit<HandlerProps<Metadata>, `x_axis` | `x2_axis` | `y_axis` | `y2_axis`> {
508
499
  z: number;
509
- metadata?: Metadata | null;
510
- label?: string | null;
511
- series_idx: number;
512
500
  x_axis: AxisConfig3D;
513
501
  y_axis: AxisConfig3D;
514
502
  z_axis: AxisConfig3D;
@@ -516,7 +504,6 @@ export interface Scatter3DHandlerProps<Metadata = Record<string, unknown>> {
516
504
  y_formatted: string;
517
505
  z_formatted: string;
518
506
  color_value?: number | null;
519
- fullscreen?: boolean;
520
507
  }
521
508
  export type Scatter3DHandlerEvent<Metadata = Record<string, unknown>> = Scatter3DHandlerProps<Metadata> & {
522
509
  event?: MouseEvent;
@@ -707,16 +694,13 @@ export type RefLine = RefLineBase & ({
707
694
  p2: [RefLineValue, RefLineValue];
708
695
  });
709
696
  export declare const REF_LINE_STYLE_DEFAULTS: Required<RefLineStyle>;
710
- export interface RefLine3DBase {
711
- id?: string | number;
712
- x_span?: [number | null, number | null];
713
- y_span?: [number | null, number | null];
697
+ type Ref3DBase = Omit<RefLineBase, `coord_mode` | `x_axis` | `y_axis` | `style` | `hover_style` | `annotation` | `on_click` | `on_hover`> & {
714
698
  z_span?: [number | null, number | null];
699
+ };
700
+ export interface RefLine3DBase extends Ref3DBase {
715
701
  style?: RefLineStyle;
716
- visible?: boolean;
717
- label?: string;
718
- metadata?: Record<string, unknown>;
719
702
  hover_style?: RefLineStyle;
703
+ annotation?: RefLineAnnotation;
720
704
  on_click?: (event: {
721
705
  line_idx: number;
722
706
  line_id?: string | number;
@@ -725,10 +709,6 @@ export interface RefLine3DBase {
725
709
  line_idx: number;
726
710
  line_id?: string | number;
727
711
  } | null) => void;
728
- z_index?: LayerZIndex;
729
- show_in_legend?: boolean;
730
- legend_group?: string;
731
- annotation?: RefLineAnnotation;
732
712
  }
733
713
  export type RefLine3D = RefLine3DBase & ({
734
714
  type: `x-axis`;
@@ -758,16 +738,8 @@ export interface RefPlaneStyle {
758
738
  wireframe_color?: string;
759
739
  double_sided?: boolean;
760
740
  }
761
- export interface RefPlaneBase {
762
- id?: string | number;
763
- x_span?: [number | null, number | null];
764
- y_span?: [number | null, number | null];
765
- z_span?: [number | null, number | null];
741
+ export interface RefPlaneBase extends Ref3DBase {
766
742
  style?: RefPlaneStyle;
767
- visible?: boolean;
768
- label?: string;
769
- show_in_legend?: boolean;
770
- metadata?: Record<string, unknown>;
771
743
  }
772
744
  export type RefPlane = RefPlaneBase & ({
773
745
  type: `xy`;
@@ -1,8 +1,6 @@
1
1
  // Type guard for select value narrowing (avoids unsafe casts)
2
2
  const SCALE_TYPE_NAMES = new Set([`linear`, `log`, `arcsinh`, `time`]);
3
- export function is_scale_type_name(val) {
4
- return SCALE_TYPE_NAMES.has(val);
5
- }
3
+ export const is_scale_type_name = (val) => SCALE_TYPE_NAMES.has(val);
6
4
  // Helper to normalize ScaleType to base type name
7
5
  export function get_scale_type_name(scale_type) {
8
6
  if (!scale_type)
@@ -34,9 +32,7 @@ export function is_time_scale(scale_type, format) {
34
32
  }
35
33
  // Type guard for select value narrowing (avoids unsafe casts)
36
34
  const Y2_SYNC_MODES = new Set([`none`, `synced`, `align`]);
37
- export function is_y2_sync_mode(val) {
38
- return Y2_SYNC_MODES.has(val);
39
- }
35
+ export const is_y2_sync_mode = (val) => Y2_SYNC_MODES.has(val);
40
36
  export const LINE_TYPES = [`solid`, `dashed`, `dotted`];
41
37
  // Define grid cell identifiers
42
38
  export const CELLS_3X3 = [
@@ -1,4 +1,5 @@
1
- import type { AxisConfig, DataSeries, XyObj } from '..';
1
+ import type { Point2D } from '../../math';
2
+ import type { AxisConfig, DataSeries } from '..';
2
3
  import type { PlotScaleFn } from '../scales';
3
4
  import type { LabelPlacementConfig, LabelPlacementWeights } from '../types';
4
5
  export interface Rect {
@@ -21,13 +22,33 @@ interface AnchorInfo {
21
22
  interface LabelState extends Rect {
22
23
  anchor_idx: number;
23
24
  }
25
+ export interface LabelSize {
26
+ width: number;
27
+ height: number;
28
+ }
29
+ export interface LeaderLineSegment {
30
+ x1: number;
31
+ y1: number;
32
+ x2: number;
33
+ y2: number;
34
+ }
35
+ export interface LeaderLineOptions {
36
+ point: Point2D;
37
+ point_radius: number;
38
+ label_center: Point2D;
39
+ label_size: LabelSize;
40
+ min_length?: number;
41
+ label_padding?: number;
42
+ }
24
43
  export declare function parse_font_size(size_str?: string): number;
44
+ export declare function estimate_label_size(text: string, font_size_str?: string): LabelSize;
25
45
  export declare function rect_overlap_area(a: Rect, b: Rect): number;
26
46
  export declare function rect_circle_overlap(rect: Rect, cx: number, cy: number, radius: number): number;
27
47
  export declare function segments_intersect(ax1: number, ay1: number, ax2: number, ay2: number, bx1: number, by1: number, bx2: number, by2: number): boolean;
28
48
  export declare function segment_rect_intersects(sx1: number, sy1: number, sx2: number, sy2: number, rect: Rect): boolean;
29
49
  export declare function rect_out_of_bounds_area(rect: Rect, bounds: PlotBounds): number;
30
- export declare function generate_candidates(ax: number, ay: number, point_radius: number, label_w: number, label_h: number, gap: number): XyObj[];
50
+ export declare function label_leader_segment({ point, point_radius, label_center, label_size, min_length, label_padding, }: LeaderLineOptions): LeaderLineSegment | null;
51
+ export declare function generate_candidates(ax: number, ay: number, point_radius: number, label_w: number, label_h: number, gap: number): Point2D[];
31
52
  export declare function compute_delta_energy(labels: LabelState[], anchors: AnchorInfo[], changed_idx: number, old_state: LabelState, new_state: LabelState, weights: Required<LabelPlacementWeights>, bounds: PlotBounds): number;
32
53
  export declare function compute_label_positions(filtered_series: DataSeries[], config: LabelPlacementConfig, scales: {
33
54
  x_scale_fn: PlotScaleFn;
@@ -43,5 +64,5 @@ export declare function compute_label_positions(filtered_series: DataSeries[], c
43
64
  l: number;
44
65
  r: number;
45
66
  };
46
- }): Record<string, XyObj>;
67
+ }): Record<string, Point2D>;
47
68
  export {};
@@ -7,6 +7,14 @@ const DEFAULT_WEIGHTS = {
7
7
  distance: 0.5,
8
8
  bounds: 100,
9
9
  };
10
+ const NEIGHBOR_CELL_OFFSETS = [-1, 0, 1];
11
+ const copy_state = (target, source) => {
12
+ target.x = source.x;
13
+ target.y = source.y;
14
+ target.w = source.w;
15
+ target.h = source.h;
16
+ target.anchor_idx = source.anchor_idx;
17
+ };
10
18
  export function parse_font_size(size_str) {
11
19
  if (!size_str)
12
20
  return 12;
@@ -16,6 +24,15 @@ export function parse_font_size(size_str) {
16
24
  const value = parseFloat(match[1]);
17
25
  return match[2] === `em` || match[2] === `rem` ? value * 16 : value;
18
26
  }
27
+ export function estimate_label_size(text, font_size_str) {
28
+ const font_size = parse_font_size(font_size_str);
29
+ const label_lines = text.split(/\r?\n/);
30
+ const max_line_length = Math.max(...label_lines.map((line) => line.length));
31
+ return {
32
+ width: max_line_length * font_size * 0.6 + 10,
33
+ height: label_lines.length * font_size * 1.2,
34
+ };
35
+ }
19
36
  // === Geometry helpers ===
20
37
  export function rect_overlap_area(a, b) {
21
38
  const ox = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
@@ -64,6 +81,26 @@ export function rect_out_of_bounds_area(rect, bounds) {
64
81
  penalty += (rect.y + rect.h - bounds.max_y) * rect.w;
65
82
  return penalty;
66
83
  }
84
+ export function label_leader_segment({ point, point_radius, label_center, label_size, min_length = 6, label_padding = 1, }) {
85
+ const delta_x = label_center.x - point.x;
86
+ const delta_y = label_center.y - point.y;
87
+ const center_distance = Math.hypot(delta_x, delta_y);
88
+ if (center_distance <= 0)
89
+ return null;
90
+ const unit_x = delta_x / center_distance;
91
+ const unit_y = delta_y / center_distance;
92
+ const start_x = point.x + unit_x * point_radius;
93
+ const start_y = point.y + unit_y * point_radius;
94
+ const half_width = label_size.width / 2 + label_padding;
95
+ const half_height = label_size.height / 2 + label_padding;
96
+ const edge_distance = Math.min(Math.abs(unit_x) > 0.001 ? half_width / Math.abs(unit_x) : Infinity, Math.abs(unit_y) > 0.001 ? half_height / Math.abs(unit_y) : Infinity);
97
+ const visible_length = center_distance - point_radius - edge_distance;
98
+ if (visible_length <= min_length)
99
+ return null;
100
+ const end_x = label_center.x - unit_x * edge_distance;
101
+ const end_y = label_center.y - unit_y * edge_distance;
102
+ return { x1: start_x, y1: start_y, x2: end_x, y2: end_y };
103
+ }
67
104
  // 8 candidate positions around anchor: R, TR, T, TL, L, BL, B, BR
68
105
  // Positions are top-left corner of the label bounding box.
69
106
  // All positions keep a full `offset` gap from the marker edge.
@@ -131,6 +168,41 @@ export function compute_delta_energy(labels, anchors, changed_idx, old_state, ne
131
168
  }
132
169
  return delta;
133
170
  }
171
+ function cull_dense_labels(label_infos, max_neighbors, radius) {
172
+ if (radius <= 0 || label_infos.length <= max_neighbors + 1)
173
+ return label_infos;
174
+ const radius_squared = radius * radius;
175
+ const get_cell = (value) => Math.floor(value / radius);
176
+ const grid = {};
177
+ label_infos.forEach(({ anchor }, label_idx) => {
178
+ const key = `${get_cell(anchor.x)},${get_cell(anchor.y)}`;
179
+ const bucket = grid[key] ?? (grid[key] = []);
180
+ bucket.push(label_idx);
181
+ });
182
+ return label_infos.filter(({ anchor }, label_idx) => {
183
+ const cell_x = get_cell(anchor.x);
184
+ const cell_y = get_cell(anchor.y);
185
+ let neighbors = 0;
186
+ for (const cell_x_offset of NEIGHBOR_CELL_OFFSETS) {
187
+ for (const cell_y_offset of NEIGHBOR_CELL_OFFSETS) {
188
+ const bucket = grid[`${cell_x + cell_x_offset},${cell_y + cell_y_offset}`];
189
+ if (!bucket)
190
+ continue;
191
+ for (const neighbor_idx of bucket) {
192
+ if (neighbor_idx === label_idx)
193
+ continue;
194
+ const delta_x = anchor.x - label_infos[neighbor_idx].anchor.x;
195
+ const delta_y = anchor.y - label_infos[neighbor_idx].anchor.y;
196
+ if (delta_x * delta_x + delta_y * delta_y <= radius_squared)
197
+ neighbors += 1;
198
+ if (neighbors > max_neighbors)
199
+ return false;
200
+ }
201
+ }
202
+ }
203
+ return true;
204
+ });
205
+ }
134
206
  // === Main export ===
135
207
  export function compute_label_positions(filtered_series, config, scales, bounds) {
136
208
  const { x_scale_fn, y_scale_fn, y2_scale_fn, x_axis } = scales;
@@ -142,7 +214,8 @@ export function compute_label_positions(filtered_series, config, scales, bounds)
142
214
  max_y: height - pad.b,
143
215
  };
144
216
  // Collect all label data in a single pass
145
- const label_infos = [];
217
+ let label_infos = [];
218
+ const candidate_gap = config.candidate_gap ?? 4;
146
219
  for (const series of filtered_series) {
147
220
  for (const pt of series.filtered_data ?? []) {
148
221
  if (!pt.point_label?.auto_placement || !pt.point_label.text)
@@ -151,19 +224,23 @@ export function compute_label_positions(filtered_series, config, scales, bounds)
151
224
  ? x_scale_fn(new Date(pt.x))
152
225
  : x_scale_fn(pt.x);
153
226
  const ay = (series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(pt.y);
154
- const font_size = parse_font_size(pt.point_label.font_size);
155
- const label_w = pt.point_label.text.length * font_size * 0.6 + 10;
156
- const label_h = font_size * 1.2;
227
+ const label_size = pt.point_label.size ??
228
+ estimate_label_size(pt.point_label.text, pt.point_label.font_size);
229
+ const label_w = label_size.width;
230
+ const label_h = label_size.height;
157
231
  const radius = pt.point_style?.radius ?? 3;
158
232
  label_infos.push({
159
233
  id: `${pt.series_idx}-${pt.point_idx}`,
160
234
  anchor: { x: ax, y: ay, radius },
161
235
  width: label_w,
162
236
  height: label_h,
163
- candidates: generate_candidates(ax, ay, radius, label_w, label_h, 4),
237
+ candidates: generate_candidates(ax, ay, radius, label_w, label_h, candidate_gap),
164
238
  });
165
239
  }
166
240
  }
241
+ if (config.max_neighbors) {
242
+ label_infos = cull_dense_labels(label_infos, config.max_neighbors.count, config.max_neighbors.radius);
243
+ }
167
244
  const num_labels = label_infos.length;
168
245
  if (num_labels === 0)
169
246
  return {};
@@ -218,13 +295,6 @@ export function compute_label_positions(filtered_series, config, scales, bounds)
218
295
  // Reusable scratch objects to avoid allocations in the hot loop
219
296
  const old_scratch = { x: 0, y: 0, w: 0, h: 0, anchor_idx: 0 };
220
297
  const new_scratch = { x: 0, y: 0, w: 0, h: 0, anchor_idx: 0 };
221
- const copy_state = (dst, src) => {
222
- dst.x = src.x;
223
- dst.y = src.y;
224
- dst.w = src.w;
225
- dst.h = src.h;
226
- dst.anchor_idx = src.anchor_idx;
227
- };
228
298
  for (let step = 0; step < total_steps; step++) {
229
299
  const temperature = Math.max(0.001, 1.0 - step * cooling_rate);
230
300
  const label_idx = Math.floor(next_random() * num_labels);
@@ -1,9 +1,15 @@
1
1
  import type { DataSeries } from '../types';
2
2
  export type StrRecord = Record<string, unknown>;
3
+ type SeriesSource = [string, string, string, unknown, unknown];
4
+ export type SeriesVisibilitySnapshot = {
5
+ visibility: boolean[];
6
+ source: SeriesSource[];
7
+ };
3
8
  export declare function have_compatible_units<Metadata extends StrRecord = StrRecord>(series1: DataSeries<Metadata>, series2: DataSeries<Metadata>): boolean;
4
9
  export declare function toggle_series_visibility<Metadata extends StrRecord = StrRecord>(series: DataSeries<Metadata>[], series_idx: number): DataSeries<Metadata>[];
5
10
  export declare function toggle_group_visibility<Metadata extends StrRecord = StrRecord>(series: DataSeries<Metadata>[], series_indices: number[]): DataSeries<Metadata>[];
6
- export declare function handle_legend_double_click<Metadata extends StrRecord = StrRecord>(series: DataSeries<Metadata>[], idx: number, prev_visibility: boolean[] | null): {
11
+ export declare function handle_legend_double_click<Metadata extends StrRecord = StrRecord>(series: DataSeries<Metadata>[], idx: number, prev_snapshot: SeriesVisibilitySnapshot | null): {
7
12
  series: DataSeries<Metadata>[];
8
- previous_visibility: boolean[] | null;
13
+ prev_visibility: SeriesVisibilitySnapshot | null;
9
14
  };
15
+ export {};
@@ -1,3 +1,8 @@
1
+ const series_source = (series, length = series.length) => series
2
+ .slice(0, length)
3
+ .map((srs) => [srs.label ?? ``, srs.unit ?? ``, srs.y_axis ?? ``, srs.x, srs.y]);
4
+ const same_series_source = (series, snapshot_series) => series.length === snapshot_series.length &&
5
+ series.every((source, idx) => source.every((part, part_idx) => Object.is(part, snapshot_series[idx][part_idx])));
1
6
  export function have_compatible_units(series1, series2) {
2
7
  if (!series1.unit || !series2.unit)
3
8
  return true;
@@ -34,12 +39,18 @@ export function toggle_group_visibility(series, series_indices) {
34
39
  const new_visibility = !all_visible;
35
40
  return series.map((srs, idx) => idx_set.has(idx) ? { ...srs, visible: new_visibility } : srs);
36
41
  }
37
- export function handle_legend_double_click(series, idx, prev_visibility) {
42
+ export function handle_legend_double_click(series, idx, prev_snapshot) {
38
43
  if (idx < 0 || idx >= series.length) {
39
- return { series, previous_visibility: prev_visibility };
44
+ return { series, prev_visibility: prev_snapshot };
40
45
  }
41
46
  const { label } = series[idx];
42
47
  const current = series.map((srs) => srs.visible ?? true);
48
+ const current_source = prev_snapshot
49
+ ? series_source(series, prev_snapshot.visibility.length)
50
+ : [];
51
+ const prev_visibility = prev_snapshot && same_series_source(current_source, prev_snapshot.source)
52
+ ? prev_snapshot.visibility
53
+ : null;
43
54
  // Only check original series (ignore new ones added after isolation)
44
55
  const check_series = prev_visibility ? series.slice(0, prev_visibility.length) : series;
45
56
  const is_isolated = check_series.every((srs, srs_idx) => {
@@ -52,16 +63,23 @@ export function handle_legend_double_click(series, idx, prev_visibility) {
52
63
  series: series.map((srs, srs_idx) => srs_idx < prev_visibility.length && current[srs_idx] !== prev_visibility[srs_idx]
53
64
  ? { ...srs, visible: prev_visibility[srs_idx] }
54
65
  : srs),
55
- previous_visibility: null,
66
+ prev_visibility: null,
56
67
  };
57
68
  }
58
69
  // Isolate series
59
- const new_prev = prev_visibility ?? (current.filter(Boolean).length > 1 ? [...current] : null);
70
+ const new_prev = prev_visibility
71
+ ? prev_snapshot
72
+ : current.filter(Boolean).length > 1
73
+ ? {
74
+ visibility: [...current],
75
+ source: series_source(series),
76
+ }
77
+ : null;
60
78
  return {
61
79
  series: series.map((srs, srs_idx) => {
62
80
  const in_group = label ? srs.label === label : srs_idx === idx;
63
81
  return (srs.visible ?? true) !== in_group ? { ...srs, visible: in_group } : srs;
64
82
  }),
65
- previous_visibility: new_prev,
83
+ prev_visibility: new_prev,
66
84
  };
67
85
  }
@@ -130,10 +130,10 @@
130
130
  for (const { struct, label } of struct_list) {
131
131
  if (mode === `element_pairs`) {
132
132
  const pairs = calculate_all_pair_rdfs(struct, { cutoff, n_bins, pbc })
133
- result.push(...pairs.map((p) => ({
134
- label: p.element_pair ? `${p.element_pair[0]}-${p.element_pair[1]}` : label,
133
+ result.push(...pairs.map((pair) => ({
134
+ label: pair.element_pair ? `${pair.element_pair[0]}-${pair.element_pair[1]}` : label,
135
135
  legend_group: label, // Group by structure name for multi-structure plots
136
- pattern: p,
136
+ pattern: pair,
137
137
  })))
138
138
  } else {
139
139
  const pattern = calculate_rdf(struct, { cutoff, n_bins, pbc })
@@ -143,8 +143,8 @@
143
143
  return result
144
144
  })
145
145
 
146
- const max_r = $derived(Math.max(...entries.flatMap((e) => e.pattern.r), 0))
147
- const max_g = $derived(Math.max(1.2, ...entries.flatMap((e) => e.pattern.g_r)))
146
+ const max_r = $derived(Math.max(...entries.flatMap((entry) => entry.pattern.r), 0))
147
+ const max_g = $derived(Math.max(1.2, ...entries.flatMap((entry) => entry.pattern.g_r)))
148
148
 
149
149
  const series = $derived<DataSeries[]>(
150
150
  entries.map((ent, idx) => ({
@@ -1,5 +1,8 @@
1
1
  import { calc_lattice_params, create_lattice_converters, euclidean_dist, pbc_dist, } from '../math';
2
2
  import { make_supercell } from '../structure/supercell';
3
+ const get_occu = (site, elem) => elem ? (site.species.find((spec) => spec.element === elem)?.occu ?? 0) : 1;
4
+ const has_species = (site, elem) => !elem || site.species.some((spec) => spec.element === elem);
5
+ const sum_occu = (sites, elem) => sites.reduce((sum, site) => sum + get_occu(site, elem), 0);
3
6
  // Calculate radial distribution function
4
7
  export function calculate_rdf(structure, options = {}) {
5
8
  const { center_species, neighbor_species, cutoff = 15, n_bins = 75, auto_expand = true, expansion_factor = 2.0, } = options;
@@ -33,8 +36,6 @@ export function calculate_rdf(structure, options = {}) {
33
36
  if (sites.length === 0)
34
37
  return { r, g_r };
35
38
  // Get occupancy weight for a site-species pair (supports mixed occupancy)
36
- const get_occu = (site, elem) => elem ? (site.species.find((spec) => spec.element === elem)?.occu ?? 0) : 1;
37
- const has_species = (site, elem) => !elem || site.species.some((spec) => spec.element === elem);
38
39
  const centers = sites.filter((site) => has_species(site, center_species));
39
40
  const neighbors = sites.filter((site) => has_species(site, neighbor_species));
40
41
  if (centers.length === 0 || neighbors.length === 0) {
@@ -61,7 +62,6 @@ export function calculate_rdf(structure, options = {}) {
61
62
  }
62
63
  }
63
64
  // Normalize using occupancy-weighted pair count (excludes self-interactions for same species)
64
- const sum_occu = (arr, elem) => arr.reduce((sum, site) => sum + get_occu(site, elem), 0);
65
65
  const center_weight = sum_occu(centers, center_species);
66
66
  const neighbor_weight = sum_occu(neighbors, neighbor_species);
67
67
  const self_weight = center_species === neighbor_species