matterviz 0.3.3 → 0.3.5

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 (126) hide show
  1. package/dist/FilePicker.svelte +1 -1
  2. package/dist/app.css +7 -0
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/compute.js +8 -4
  5. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
  6. package/dist/chempot-diagram/async-compute.svelte.js +6 -5
  7. package/dist/chempot-diagram/chempot-worker.js +2 -2
  8. package/dist/chempot-diagram/compute.js +16 -16
  9. package/dist/composition/FormulaFilter.svelte +3 -3
  10. package/dist/constants.js +2 -8
  11. package/dist/convex-hull/ConvexHull.svelte +2 -2
  12. package/dist/convex-hull/ConvexHull2D.svelte +11 -10
  13. package/dist/convex-hull/ConvexHull3D.svelte +16 -14
  14. package/dist/convex-hull/ConvexHull4D.svelte +26 -14
  15. package/dist/convex-hull/ConvexHullControls.svelte +1 -1
  16. package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
  17. package/dist/convex-hull/ConvexHullStats.svelte +23 -6
  18. package/dist/convex-hull/GasPressureControls.svelte +3 -3
  19. package/dist/convex-hull/TemperatureSlider.svelte +1 -1
  20. package/dist/convex-hull/barycentric-coords.js +2 -2
  21. package/dist/convex-hull/helpers.js +45 -27
  22. package/dist/convex-hull/thermodynamics.js +2 -2
  23. package/dist/element/BohrAtom.svelte +25 -27
  24. package/dist/element/BohrAtom.svelte.d.ts +2 -2
  25. package/dist/element/data.d.ts +2 -3
  26. package/dist/element/data.js +1 -1
  27. package/dist/fermi-surface/FermiSurface.svelte +5 -2
  28. package/dist/fermi-surface/compute.js +3 -3
  29. package/dist/fermi-surface/parse.js +2 -2
  30. package/dist/fermi-surface/symmetry.js +1 -1
  31. package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
  32. package/dist/icons.d.ts +6 -6
  33. package/dist/icons.js +6 -6
  34. package/dist/io/decompress.js +12 -7
  35. package/dist/io/export.js +20 -16
  36. package/dist/io/is-binary.js +19 -4
  37. package/dist/isosurface/parse.js +8 -8
  38. package/dist/isosurface/types.js +9 -9
  39. package/dist/layout/InfoTag.svelte +1 -1
  40. package/dist/layout/json-tree/JsonNode.svelte +1 -0
  41. package/dist/layout/json-tree/utils.js +2 -1
  42. package/dist/marching-cubes.js +1 -1
  43. package/dist/math.js +1 -1
  44. package/dist/overlays/CopyButton.svelte +45 -0
  45. package/dist/overlays/CopyButton.svelte.d.ts +8 -0
  46. package/dist/overlays/InfoPaneCards.svelte +149 -0
  47. package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
  48. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
  49. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
  50. package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
  51. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
  52. package/dist/phase-diagram/parse.js +3 -3
  53. package/dist/phase-diagram/svg-to-diagram.js +10 -12
  54. package/dist/plot/BarPlot.svelte +24 -15
  55. package/dist/plot/BarPlot.svelte.d.ts +3 -2
  56. package/dist/plot/FillArea.svelte +2 -3
  57. package/dist/plot/FillArea.svelte.d.ts +3 -2
  58. package/dist/plot/Histogram.svelte +37 -19
  59. package/dist/plot/Line.svelte +2 -3
  60. package/dist/plot/Line.svelte.d.ts +2 -2
  61. package/dist/plot/PlotLegend.svelte +79 -8
  62. package/dist/plot/PlotLegend.svelte.d.ts +4 -0
  63. package/dist/plot/PortalSelect.svelte +5 -5
  64. package/dist/plot/ScatterPlot.svelte +47 -33
  65. package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
  66. package/dist/plot/ScatterPlot3D.svelte +6 -3
  67. package/dist/plot/ScatterPoint.svelte +10 -4
  68. package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
  69. package/dist/plot/SpacegroupBarPlot.svelte +5 -4
  70. package/dist/plot/data-cleaning.js +9 -9
  71. package/dist/plot/index.d.ts +0 -6
  72. package/dist/plot/scales.d.ts +3 -3
  73. package/dist/plot/scales.js +29 -29
  74. package/dist/plot/types.d.ts +5 -9
  75. package/dist/rdf/calc-rdf.js +1 -1
  76. package/dist/sanitize.js +22 -15
  77. package/dist/settings.d.ts +2 -0
  78. package/dist/settings.js +12 -3
  79. package/dist/spectral/Bands.svelte +6 -6
  80. package/dist/spectral/BandsAndDos.svelte +4 -4
  81. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  82. package/dist/spectral/Dos.svelte +2 -2
  83. package/dist/spectral/helpers.js +1 -1
  84. package/dist/structure/AtomLegend.svelte +4 -4
  85. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  86. package/dist/structure/Cylinder.svelte +7 -7
  87. package/dist/structure/Structure.svelte +169 -27
  88. package/dist/structure/Structure.svelte.d.ts +6 -2
  89. package/dist/structure/StructureControls.svelte +130 -16
  90. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  91. package/dist/structure/StructureInfoPane.svelte +519 -218
  92. package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
  93. package/dist/structure/StructureScene.svelte +399 -68
  94. package/dist/structure/StructureScene.svelte.d.ts +8 -4
  95. package/dist/structure/atom-properties.js +3 -1
  96. package/dist/structure/bond-order-perception.d.ts +13 -0
  97. package/dist/structure/bond-order-perception.js +367 -0
  98. package/dist/structure/bonding.d.ts +10 -1
  99. package/dist/structure/bonding.js +232 -11
  100. package/dist/structure/export.js +6 -4
  101. package/dist/structure/index.d.ts +19 -4
  102. package/dist/structure/index.js +3 -0
  103. package/dist/structure/label-placement.d.ts +14 -0
  104. package/dist/structure/label-placement.js +72 -0
  105. package/dist/structure/parse.d.ts +2 -1
  106. package/dist/structure/parse.js +25 -36
  107. package/dist/structure/supercell.js +35 -2
  108. package/dist/symmetry/SymmetryStats.svelte +1 -1
  109. package/dist/symmetry/cell-transform.js +15 -1
  110. package/dist/symmetry/index.js +3 -3
  111. package/dist/table/HeatmapTable.svelte +3 -3
  112. package/dist/table/ToggleMenu.svelte +1 -1
  113. package/dist/trajectory/Trajectory.svelte +2 -2
  114. package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
  115. package/dist/trajectory/extract.js +4 -4
  116. package/dist/trajectory/frame-reader.js +2 -2
  117. package/dist/trajectory/parse/ase.js +2 -6
  118. package/dist/trajectory/parse/hdf5.js +1 -3
  119. package/dist/trajectory/plotting.js +1 -1
  120. package/dist/utils.js +1 -1
  121. package/dist/xrd/calc-xrd.js +1 -1
  122. package/package.json +23 -38
  123. package/dist/structure/ferrox-wasm-types.d.ts +0 -46
  124. package/dist/structure/ferrox-wasm-types.js +0 -18
  125. package/dist/structure/ferrox-wasm.d.ts +0 -94
  126. package/dist/structure/ferrox-wasm.js +0 -249
@@ -1,13 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { type D3SymbolName, symbol_map } from '../labels'
3
3
  import type { HoverStyle, LabelStyle, Point } from './'
4
- import type { PointStyle, TweenedOptions, XyObj } from './types'
4
+ import type { PointStyle, XyObj } from './types'
5
5
  import { DEFAULTS } from '../settings'
6
6
  import * as d3_symbols from 'd3-shape'
7
7
  import { symbol } from 'd3-shape'
8
8
  import { cubicOut } from 'svelte/easing'
9
9
  import type { SVGAttributes } from 'svelte/elements'
10
- import { Tween } from 'svelte/motion'
10
+ import { Tween, type TweenOptions } from 'svelte/motion'
11
11
 
12
12
  let {
13
13
  x,
@@ -20,6 +20,7 @@
20
20
  origin = $bindable({ x: 0, y: 0 }),
21
21
  is_hovered = false,
22
22
  is_selected = false,
23
+ is_dimmed = false,
23
24
  leader_line_threshold = 15,
24
25
  ...rest
25
26
  }: Omit<SVGAttributes<SVGGElement>, `style` | `offset` | `origin` | `transform`> & {
@@ -29,10 +30,11 @@
29
30
  hover?: HoverStyle
30
31
  label?: LabelStyle
31
32
  offset?: Point[`offset`]
32
- point_tween?: TweenedOptions<XyObj>
33
+ point_tween?: TweenOptions<XyObj>
33
34
  origin?: XyObj
34
35
  is_hovered?: boolean
35
36
  is_selected?: boolean
37
+ is_dimmed?: boolean
36
38
  leader_line_threshold?: number
37
39
  } = $props()
38
40
 
@@ -46,7 +48,7 @@
46
48
 
47
49
  let marker_path = $derived.by(get_symbol_path)
48
50
 
49
- const default_tween_props: TweenedOptions<XyObj> = {
51
+ const default_tween_props: TweenOptions<XyObj> = {
50
52
  duration: 600,
51
53
  easing: cubicOut,
52
54
  }
@@ -92,6 +94,7 @@
92
94
  fill="var(--point-fill-color, {style.fill ?? `black`})"
93
95
  class="marker"
94
96
  class:is-hovered={is_hovered && (hover.enabled ?? true)}
97
+ class:is-dimmed={is_dimmed}
95
98
  style:cursor={style.cursor}
96
99
  />
97
100
  {#if label.text}
@@ -154,6 +157,9 @@
154
157
  stroke-width: var(--hover-stroke-width);
155
158
  filter: brightness(var(--hover-brightness));
156
159
  }
160
+ .marker.is-dimmed {
161
+ opacity: var(--scatter-point-dimmed-opacity, 0.25);
162
+ }
157
163
  .effect-ring {
158
164
  pointer-events: none;
159
165
  animation: ring-pulse var(--effect-ring-duration, 1s) ease-in-out
@@ -1,6 +1,7 @@
1
1
  import type { HoverStyle, LabelStyle, Point } from './';
2
- import type { PointStyle, TweenedOptions, XyObj } from './types';
2
+ import type { PointStyle, XyObj } from './types';
3
3
  import type { SVGAttributes } from 'svelte/elements';
4
+ import { type TweenOptions } from 'svelte/motion';
4
5
  type $$ComponentProps = Omit<SVGAttributes<SVGGElement>, `style` | `offset` | `origin` | `transform`> & {
5
6
  x: number;
6
7
  y: number;
@@ -8,10 +9,11 @@ type $$ComponentProps = Omit<SVGAttributes<SVGGElement>, `style` | `offset` | `o
8
9
  hover?: HoverStyle;
9
10
  label?: LabelStyle;
10
11
  offset?: Point[`offset`];
11
- point_tween?: TweenedOptions<XyObj>;
12
+ point_tween?: TweenOptions<XyObj>;
12
13
  origin?: XyObj;
13
14
  is_hovered?: boolean;
14
15
  is_selected?: boolean;
16
+ is_dimmed?: boolean;
15
17
  leader_line_threshold?: number;
16
18
  };
17
19
  declare const ScatterPoint: import("svelte").Component<$$ComponentProps, {}, "origin" | "point_tween">;
@@ -21,6 +21,7 @@
21
21
  let {
22
22
  data,
23
23
  show_counts = true,
24
+ show_legend = false,
24
25
  orientation = `vertical`,
25
26
  x_axis = {},
26
27
  y_axis = {},
@@ -111,9 +112,9 @@
111
112
  // Convert to BarSeries array, maintaining order of crystal systems
112
113
  const result: BarSeries[] = []
113
114
  for (const system of symmetry.CRYSTAL_SYSTEMS) {
114
- const data = series_by_system.get(system)
115
- if (data) {
116
- const { x, y } = data
115
+ const system_data = series_by_system.get(system)
116
+ if (system_data) {
117
+ const { x, y } = system_data
117
118
  const color = symmetry.CRYSTAL_SYSTEM_COLORS[system]
118
119
  result.push({ x, y, color, label: system, bar_width: 0.9, visible: true })
119
120
  }
@@ -285,7 +286,7 @@
285
286
  mode="overlay"
286
287
  x_axis={x_axis_config}
287
288
  y_axis={y_axis_config}
288
- show_legend={false}
289
+ {show_legend}
289
290
  show_controls={false}
290
291
  {tooltip}
291
292
  {user_content}
@@ -15,7 +15,7 @@ export function compute_local_variance(values, window_size) {
15
15
  if (len === 1)
16
16
  return [0];
17
17
  const half_window = Math.floor(window_size / 2);
18
- const result = new Array(len);
18
+ const result = Array(len);
19
19
  // Single pass for each index, no slice allocation (avoids O(n × window) allocations)
20
20
  for (let idx = 0; idx < len; idx++) {
21
21
  const start = Math.max(0, idx - half_window);
@@ -40,7 +40,7 @@ export function compute_local_variance(values, window_size) {
40
40
  function compute_derivatives(values) {
41
41
  if (values.length < 2)
42
42
  return [];
43
- const derivs = new Array(values.length - 1);
43
+ const derivs = Array(values.length - 1);
44
44
  for (let idx = 0; idx < values.length - 1; idx++) {
45
45
  derivs[idx] = values[idx + 1] - values[idx];
46
46
  }
@@ -216,7 +216,7 @@ export function detect_instability(x_values, y_values, config = {}) {
216
216
  export function smooth_moving_average(values, window) {
217
217
  if (values.length === 0 || window <= 1)
218
218
  return [...values];
219
- const result = new Array(values.length);
219
+ const result = Array(values.length);
220
220
  const half_window = Math.floor(window / 2);
221
221
  for (let idx = 0; idx < values.length; idx++) {
222
222
  const start = Math.max(0, idx - half_window);
@@ -253,7 +253,7 @@ function compute_savgol_coefficients(window, order) {
253
253
  const vtv_inv = invert_matrix(vtv);
254
254
  if (!vtv_inv) {
255
255
  // Fallback to uniform weights
256
- return new Array(size).fill(1 / size);
256
+ return Array(size).fill(1 / size);
257
257
  }
258
258
  const vt = transpose(vandermonde);
259
259
  const coeffs_matrix = multiply_matrices(vtv_inv, vt);
@@ -272,7 +272,7 @@ export function smooth_savitzky_golay(values, window, polynomial_order = DEFAULT
272
272
  return [...values];
273
273
  const coeffs = compute_savgol_coefficients(actual_window, polynomial_order);
274
274
  const half = Math.floor(actual_window / 2);
275
- const result = new Array(values.length);
275
+ const result = Array(values.length);
276
276
  // Cache coefficient sum to avoid O(n × window) redundant reductions in loop
277
277
  const coeffs_sum = coeffs.reduce((a, b) => a + b, 0);
278
278
  for (let idx = 0; idx < values.length; idx++) {
@@ -355,7 +355,7 @@ export function remove_local_outliers(y_values, config = {}) {
355
355
  iterations_used: 0,
356
356
  };
357
357
  }
358
- let kept_mask = new Array(len).fill(true);
358
+ let kept_mask = Array(len).fill(true);
359
359
  let iterations_used = 0;
360
360
  for (let iter = 0; iter < max_iterations; iter++) {
361
361
  let removed_any = false;
@@ -592,7 +592,7 @@ export function clean_series(series, config = {}) {
592
592
  }
593
593
  }
594
594
  // Build result series
595
- const result_series = (in_place ? series : { ...series });
595
+ const result_series = in_place ? series : { ...series };
596
596
  result_series.x = x_arr;
597
597
  result_series.y = y_arr;
598
598
  if (metadata !== undefined)
@@ -791,7 +791,7 @@ function transpose(matrix) {
791
791
  return [];
792
792
  const rows = matrix.length;
793
793
  const cols = matrix[0].length;
794
- const result = Array.from({ length: cols }, () => new Array(rows));
794
+ const result = Array.from({ length: cols }, () => Array(rows).fill(0));
795
795
  for (let row = 0; row < rows; row++) {
796
796
  for (let col = 0; col < cols; col++) {
797
797
  result[col][row] = matrix[row][col];
@@ -803,7 +803,7 @@ function multiply_matrices(a, b) {
803
803
  const rows_a = a.length;
804
804
  const cols_a = a[0]?.length ?? 0;
805
805
  const cols_b = b[0]?.length ?? 0;
806
- const result = Array.from({ length: rows_a }, () => new Array(cols_b).fill(0));
806
+ const result = Array.from({ length: rows_a }, () => Array(cols_b).fill(0));
807
807
  for (let row = 0; row < rows_a; row++) {
808
808
  for (let col = 0; col < cols_b; col++) {
809
809
  for (let k = 0; k < cols_a; k++) {
@@ -1,9 +1,3 @@
1
- export interface TweenedOptions<T> {
2
- delay?: number;
3
- duration?: number | ((from: T, to: T) => number);
4
- easing?: (t: number) => number;
5
- interpolate?: (a: T, b: T) => (t: number) => T;
6
- }
7
1
  export { default as AxisLabel } from './AxisLabel.svelte';
8
2
  export { default as BarPlot } from './BarPlot.svelte';
9
3
  export { default as BarPlotControls } from './BarPlotControls.svelte';
@@ -16,8 +16,8 @@ export interface ArcsinhScale {
16
16
  export type PlotScaleFn = ScaleContinuousNumeric<number, number> | ScaleTime<number, number> | ArcsinhScale;
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
- export declare function create_scale(scale_type: ScaleType, domain: [number, number], range: [number, number]): ScaleContinuousNumeric<number, number> | ArcsinhScale;
20
- export declare function create_time_scale(domain: [number, number], range: [number, number]): ScaleTime<number, number, never>;
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>;
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;
@@ -25,7 +25,7 @@ options?: {
25
25
  interval_padding?: number;
26
26
  }): number[];
27
27
  export declare function calculate_domain(values: number[], scale_type?: ScaleType): [number, number];
28
- export declare function get_nice_data_range(points: Point[], get_value: (p: Point) => number, range: [number | null, number | null], scale_type: ScaleType, padding_factor: number, is_time?: boolean): [number, number];
28
+ export declare function get_nice_data_range(points: Point[], get_value: (p: Point) => number, limits: [number | null, number | null], scale_type: ScaleType, padding_factor: number, is_time?: boolean): [number, number];
29
29
  export declare function generate_log_ticks(min: number, max: number, ticks_option?: TicksOption): number[];
30
30
  export declare function get_tick_label(tick_value: number, ticks_option: TicksOption | undefined): string | null;
31
31
  export declare function create_color_scale(color_scale_config: {
@@ -10,8 +10,8 @@ export function scale_arcsinh(threshold = 1) {
10
10
  if (!Number.isFinite(threshold) || threshold <= 0) {
11
11
  throw new Error(`arcsinh threshold must be a positive finite number, got ${threshold}`);
12
12
  }
13
- let _domain = [0, 1];
14
- let _range = [0, 1];
13
+ let current_domain = [0, 1];
14
+ let current_range = [0, 1];
15
15
  // Forward transform: data value → arcsinh-space
16
16
  const arcsinh_transform = (x) => Math.asinh(x / threshold);
17
17
  // Inverse transform: arcsinh-space → data value
@@ -20,8 +20,8 @@ export function scale_arcsinh(threshold = 1) {
20
20
  // Accepts Date for compatibility with D3 time scales
21
21
  const scale = ((value) => {
22
22
  const num_value = value instanceof Date ? value.getTime() : value;
23
- const [d_min, d_max] = _domain;
24
- const [r_min, r_max] = _range;
23
+ const [d_min, d_max] = current_domain;
24
+ const [r_min, r_max] = current_range;
25
25
  // Handle identical domain endpoints (degenerate case)
26
26
  if (d_max === d_min)
27
27
  return (r_min + r_max) / 2;
@@ -39,21 +39,21 @@ export function scale_arcsinh(threshold = 1) {
39
39
  // Domain getter/setter
40
40
  scale.domain = function (domain) {
41
41
  if (domain === undefined)
42
- return _domain;
43
- _domain = domain;
42
+ return current_domain;
43
+ current_domain = domain;
44
44
  return scale;
45
45
  };
46
46
  // Range getter/setter
47
- scale.range = function (range) {
48
- if (range === undefined)
49
- return _range;
50
- _range = range;
47
+ scale.range = function (output_range) {
48
+ if (output_range === undefined)
49
+ return current_range;
50
+ current_range = output_range;
51
51
  return scale;
52
52
  };
53
53
  // Invert: screen position → data value
54
54
  scale.invert = (value) => {
55
- const [d_min, d_max] = _domain;
56
- const [r_min, r_max] = _range;
55
+ const [d_min, d_max] = current_domain;
56
+ const [r_min, r_max] = current_range;
57
57
  // Handle identical domain endpoints (degenerate case)
58
58
  if (d_max === d_min)
59
59
  return (d_min + d_max) / 2;
@@ -71,13 +71,13 @@ export function scale_arcsinh(threshold = 1) {
71
71
  // Copy the scale
72
72
  scale.copy = () => {
73
73
  const copy = scale_arcsinh(threshold);
74
- copy.domain(_domain);
75
- copy.range(_range);
74
+ copy.domain(current_domain);
75
+ copy.range(current_range);
76
76
  return copy;
77
77
  };
78
78
  // Generate nice ticks for arcsinh scale
79
79
  scale.ticks = (count = 10) => {
80
- return generate_arcsinh_ticks(_domain[0], _domain[1], threshold, count);
80
+ return generate_arcsinh_ticks(current_domain[0], current_domain[1], threshold, count);
81
81
  };
82
82
  scale.threshold = threshold;
83
83
  return scale;
@@ -181,26 +181,26 @@ function generate_positive_arcsinh_ticks(min, max, threshold, count) {
181
181
  // Note: Time scales are handled separately via create_time_scale() since ScaleTime
182
182
  // has incompatible types (invert returns Date, not number). Use is_time_scale()
183
183
  // to detect time mode and call create_time_scale() directly when needed.
184
- export function create_scale(scale_type, domain, range) {
184
+ export function create_scale(scale_type, domain, output_range) {
185
185
  const [min_val, max_val] = domain;
186
186
  const type_name = get_scale_type_name(scale_type);
187
187
  if (type_name === `log`) {
188
188
  return scaleLog()
189
189
  .domain([Math.max(min_val, math.LOG_EPS), max_val])
190
- .range(range);
190
+ .range(output_range);
191
191
  }
192
192
  if (type_name === `arcsinh`) {
193
193
  const threshold = get_arcsinh_threshold(scale_type);
194
- return scale_arcsinh(threshold).domain(domain).range(range);
194
+ return scale_arcsinh(threshold).domain(domain).range(output_range);
195
195
  }
196
196
  // For 'time' or 'linear', return linear scale (time scales need create_time_scale())
197
- return scaleLinear().domain(domain).range(range);
197
+ return scaleLinear().domain(domain).range(output_range);
198
198
  }
199
199
  // Create a time scale for time-based data
200
- export function create_time_scale(domain, range) {
200
+ export function create_time_scale(domain, output_range) {
201
201
  return scaleTime()
202
202
  .domain([new Date(domain[0]), new Date(domain[1])])
203
- .range(range);
203
+ .range(output_range);
204
204
  }
205
205
  // Unified tick generation function
206
206
  export function generate_ticks(domain, scale_type, ticks_option, scale_fn, // D3 scale function with .ticks() method
@@ -276,8 +276,8 @@ export function calculate_domain(values, scale_type = `linear`) {
276
276
  return type_name === `log` ? [Math.max(min_val, math.LOG_EPS), max_val] : [min_val, max_val];
277
277
  }
278
278
  // Advanced domain calculation with padding and nice boundaries (from ScatterPlot)
279
- export function get_nice_data_range(points, get_value, range, scale_type, padding_factor, is_time = false) {
280
- const [min, max] = range;
279
+ export function get_nice_data_range(points, get_value, limits, scale_type, padding_factor, is_time = false) {
280
+ const [min, max] = limits;
281
281
  const [min_ext, max_ext] = extent(points, get_value);
282
282
  let data_min = min ?? min_ext ?? 0;
283
283
  let data_max = max ?? max_ext ?? 1;
@@ -422,14 +422,14 @@ export function create_color_scale(color_scale_config, auto_color_range) {
422
422
  }
423
423
  // Create an arcsinh-based color scale (custom sequential scale)
424
424
  // Returns a D3-compatible scale with both getter and setter for domain
425
- // Scale function reads from _domain closure on each call for stable identity
425
+ // Scale function reads from closure state on each call for stable identity
426
426
  function create_arcsinh_color_scale(interpolator, initial_domain, threshold) {
427
427
  // Guard against extremely small thresholds that could cause precision issues
428
428
  const safe_threshold = Math.max(threshold, Number.EPSILON);
429
- let _domain = initial_domain;
430
- // Single scale function that reads current _domain on each call
429
+ let current_domain = initial_domain;
430
+ // Single scale function that reads current domain on each call
431
431
  const scale = ((value) => {
432
- const [d_min, d_max] = _domain;
432
+ const [d_min, d_max] = current_domain;
433
433
  // Handle identical domain endpoints - return middle of color range
434
434
  if (d_max === d_min)
435
435
  return interpolator(0.5);
@@ -443,8 +443,8 @@ function create_arcsinh_color_scale(interpolator, initial_domain, threshold) {
443
443
  // Domain getter/setter for D3 compatibility - returns same scale instance
444
444
  scale.domain = function (new_domain) {
445
445
  if (new_domain === undefined)
446
- return _domain;
447
- _domain = new_domain;
446
+ return current_domain;
447
+ current_domain = new_domain;
448
448
  return scale;
449
449
  };
450
450
  return scale;
@@ -3,14 +3,10 @@ import type { 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
+ import type { TweenOptions } from 'svelte/motion';
7
+ export type { TweenOptions } from 'svelte/motion';
6
8
  import type PlotLegend from './PlotLegend.svelte';
7
9
  import type { TicksOption } from './scales';
8
- export interface TweenedOptions<T> {
9
- delay?: number;
10
- duration?: number | ((from: T, to: T) => number);
11
- easing?: (t: number) => number;
12
- interpolate?: (a: T, b: T) => (t: number) => T;
13
- }
14
10
  export type XyObj = {
15
11
  x: number;
16
12
  y: number;
@@ -89,7 +85,7 @@ export interface PlotPoint<Metadata = Record<string, unknown>> extends Point<Met
89
85
  point_hover?: HoverStyle;
90
86
  point_label?: LabelStyle;
91
87
  point_offset?: XyObj;
92
- point_tween?: TweenedOptions<XyObj>;
88
+ point_tween?: TweenOptions<XyObj>;
93
89
  }
94
90
  export type Markers = `line` | `points` | `line+points` | `none`;
95
91
  export interface DataSeries<Metadata = Record<string, unknown>> {
@@ -106,7 +102,7 @@ export interface DataSeries<Metadata = Record<string, unknown>> {
106
102
  point_hover?: HoverStyle[] | HoverStyle;
107
103
  point_label?: LabelStyle[] | LabelStyle;
108
104
  point_offset?: XyObj[] | XyObj;
109
- point_tween?: TweenedOptions<XyObj>;
105
+ point_tween?: TweenOptions<XyObj>;
110
106
  visible?: boolean;
111
107
  label?: string;
112
108
  legend_group?: string;
@@ -217,7 +213,7 @@ export type HoverConfig = {
217
213
  };
218
214
  export type LegendConfig = Omit<ComponentProps<typeof PlotLegend>, `series_data` | `on_drag_start` | `on_drag` | `on_drag_end`> & {
219
215
  margin?: number | Sides;
220
- tween?: TweenedOptions<XyObj>;
216
+ tween?: TweenOptions<XyObj>;
221
217
  responsive?: boolean;
222
218
  draggable?: boolean;
223
219
  axis_clearance?: number;
@@ -29,7 +29,7 @@ export function calculate_rdf(structure, options = {}) {
29
29
  }
30
30
  const bin_size = cutoff / n_bins;
31
31
  const r = Array.from({ length: n_bins }, (_, idx) => (idx + 0.5) * bin_size);
32
- const g_r = new Array(n_bins).fill(0);
32
+ const g_r = Array(n_bins).fill(0);
33
33
  if (sites.length === 0)
34
34
  return { r, g_r };
35
35
  // Get occupancy weight for a site-species pair (supports mixed occupancy)
package/dist/sanitize.js CHANGED
@@ -1,16 +1,5 @@
1
1
  import DOMPurify from 'dompurify';
2
2
  import { format_formula_html } from './phase-diagram/utils';
3
- // SSR: provide a DOM for DOMPurify when no browser window exists (e.g. during vite build)
4
- let ssr_window;
5
- if (typeof globalThis.window === `undefined`) {
6
- try {
7
- const { Window } = await import(`happy-dom`);
8
- ssr_window = new Window();
9
- }
10
- catch {
11
- // happy-dom not available at runtime — get_purify() will fall back to pass-through
12
- }
13
- }
14
3
  const SAFE_TAGS = [`a`, `b`, `i`, `em`, `strong`, `sub`, `sup`, `br`, `span`, `code`, `small`];
15
4
  const SAFE_ATTRS = [`style`, `class`, `title`, `href`, `target`, `rel`];
16
5
  // only allow safe CSS properties for text formatting
@@ -26,9 +15,9 @@ let purify;
26
15
  function get_purify() {
27
16
  if (purify !== undefined)
28
17
  return purify;
29
- const instance = ssr_window
30
- ? DOMPurify(ssr_window)
31
- : DOMPurify();
18
+ if (typeof globalThis.window === `undefined`)
19
+ return (purify = null);
20
+ const instance = DOMPurify();
32
21
  if (typeof instance.sanitize !== `function`) {
33
22
  purify = null;
34
23
  return null;
@@ -68,12 +57,30 @@ function sanitize_svg_content(html, allowed_tags, allowed_attrs) {
68
57
  return wrapped;
69
58
  return wrapped.slice(open_end + 1, close_start);
70
59
  }
60
+ const stringify_html_input = (html) => {
61
+ if (html == null)
62
+ return ``;
63
+ if (typeof html === `string`)
64
+ return html;
65
+ if (typeof html === `number`)
66
+ return Number.isNaN(html) ? `NaN` : `${html}`;
67
+ if (typeof html === `boolean` || typeof html === `bigint`)
68
+ return `${html}`;
69
+ if (typeof html !== `object`)
70
+ return ``;
71
+ try {
72
+ return JSON.stringify(html) ?? ``;
73
+ }
74
+ catch {
75
+ return ``;
76
+ }
77
+ };
71
78
  // Sanitize HTML string, allowing only safe formatting tags and links.
72
79
  // Two-pass: happy-dom promotes dangerous children when a non-allowed parent is
73
80
  // stripped (e.g. <div><script>…</script></div> → <script>…</script>). The first
74
81
  // pass explicitly removes dangerous tags so they can't survive promotion.
75
82
  export function sanitize_html(html) {
76
- const str = html == null ? `` : String(html);
83
+ const str = stringify_html_input(html);
77
84
  const dp = get_purify();
78
85
  if (!dp)
79
86
  return str;
@@ -100,6 +100,8 @@ export interface SettingsConfig {
100
100
  show_image_atoms: SettingType<boolean>;
101
101
  sphere_segments: SettingType<number>;
102
102
  bond_thickness: SettingType<number>;
103
+ auto_bond_order: SettingType<boolean>;
104
+ aromatic_display: SettingType<`aromatic` | `kekule`>;
103
105
  show_bonds: SettingType<ShowBonds>;
104
106
  bond_color: SettingType<string>;
105
107
  bonding_strategy: SettingType<BondingStrategy>;
package/dist/settings.js CHANGED
@@ -110,6 +110,15 @@ export const SETTINGS_CONFIG = {
110
110
  minimum: 0.01,
111
111
  maximum: 1.0,
112
112
  },
113
+ auto_bond_order: {
114
+ value: false,
115
+ description: `Automatically perceive double/triple/aromatic bonds from geometry (main-group/organic; metals fall back to single)`,
116
+ },
117
+ aromatic_display: {
118
+ value: `aromatic`,
119
+ description: `How to render perceived aromatic rings`,
120
+ enum: { aromatic: `Aromatic (1.5)`, kekule: `Kekulé (alternating)` },
121
+ },
113
122
  show_bonds: {
114
123
  value: `always`,
115
124
  description: `When to display bonds between atoms`,
@@ -243,11 +252,11 @@ export const SETTINGS_CONFIG = {
243
252
  maximum: 5,
244
253
  },
245
254
  site_label_color: {
246
- value: `#ffffff`,
255
+ value: `#111111`,
247
256
  description: `Text color for atom labels`,
248
257
  },
249
258
  site_label_bg_color: {
250
- value: `#000000`,
259
+ value: `color-mix(in srgb, #000000 0%, transparent)`,
251
260
  description: `Background color for atom labels`,
252
261
  },
253
262
  site_label_padding: {
@@ -1111,7 +1120,7 @@ export const DEFAULTS = extract_values(SETTINGS_CONFIG);
1111
1120
  // Helper to merge with defaults - handles nested structure
1112
1121
  export const merge = (user) => ({
1113
1122
  ...DEFAULTS,
1114
- ...(user || {}),
1123
+ ...user,
1115
1124
  structure: merge_nested(DEFAULTS.structure, user?.structure),
1116
1125
  trajectory: merge_nested(DEFAULTS.trajectory, user?.trajectory),
1117
1126
  composition: merge_nested(DEFAULTS.composition, user?.composition),
@@ -234,17 +234,17 @@
234
234
 
235
235
  // Collect all path segments across structures once (shared by strict checks and plotting)
236
236
  let all_segments = $derived.by(() => {
237
- const all_segments: Record<string, [string, BaseBandStructure][]> = {}
237
+ const collected_segments: Record<string, [string, BaseBandStructure][]> = {}
238
238
  for (const [label, bs] of Object.entries(band_structs_dict)) {
239
239
  for (const branch of bs.branches) {
240
240
  const start_label = bs.qpoints[branch.start_index]?.label ?? undefined
241
241
  const end_label = bs.qpoints[branch.end_index]?.label ?? undefined
242
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])
243
+ collected_segments[segment_key] ??= []
244
+ collected_segments[segment_key].push([label, bs])
245
245
  }
246
246
  }
247
- return all_segments
247
+ return collected_segments
248
248
  })
249
249
 
250
250
  let num_structures = $derived(Object.keys(band_structs_dict).length)
@@ -626,8 +626,8 @@
626
626
  }
627
627
  // Range became invalid - clear parent's range to propagate reset
628
628
  if (`range` in y_axis) {
629
- const { range: _omit, ...rest } = y_axis
630
- y_axis = rest
629
+ const { range: _omit, ...axis_without_range } = y_axis
630
+ y_axis = axis_without_range
631
631
  }
632
632
  })
633
633
 
@@ -69,10 +69,10 @@
69
69
  // Only include range if it's valid (don't override child's auto-range with undefined)
70
70
  bands_y_axis = shared_y_axis
71
71
  ? {
72
- ...(bands_props.y_axis ?? {}),
72
+ ...bands_props.y_axis,
73
73
  ...(is_valid_range(base_range) && { range: base_range }),
74
74
  }
75
- : { ...(bands_props.y_axis ?? {}) }
75
+ : { ...bands_props.y_axis }
76
76
  })
77
77
 
78
78
  // Propagate synced range to DOS y-axis (untrack current to avoid overwriting child zoom)
@@ -88,10 +88,10 @@
88
88
  dos_y_axis = shared_y_axis
89
89
  ? {
90
90
  label: ``,
91
- ...(dos_props.y_axis ?? {}),
91
+ ...dos_props.y_axis,
92
92
  ...(is_valid_range(base_range) && { range: base_range }),
93
93
  }
94
- : { label: ``, ...(dos_props.y_axis ?? {}) }
94
+ : { label: ``, ...dos_props.y_axis }
95
95
  })
96
96
 
97
97
  let hovered_frequency = $state<number | null>(null)
@@ -131,7 +131,7 @@
131
131
  ) return
132
132
  // Only include range if it's valid (don't override child's auto-range with undefined)
133
133
  bands_y_axis = {
134
- ...(bands_props.y_axis ?? {}),
134
+ ...bands_props.y_axis,
135
135
  ...(helpers.is_valid_range(base_range) && { range: base_range }),
136
136
  }
137
137
  })
@@ -150,10 +150,10 @@
150
150
  dos_y_axis = is_desktop
151
151
  ? {
152
152
  label: ``,
153
- ...(dos_props.y_axis ?? {}),
153
+ ...dos_props.y_axis,
154
154
  ...(helpers.is_valid_range(base_range) && { range: base_range }),
155
155
  }
156
- : { ...(dos_props.y_axis ?? {}) }
156
+ : { ...dos_props.y_axis }
157
157
  })
158
158
 
159
159
  let hovered_frequency = $state<number | null>(null)
@@ -370,8 +370,8 @@
370
370
  }
371
371
  // Range became invalid - clear parent's range to propagate reset
372
372
  if (`range` in y_axis) {
373
- const { range: _omit, ...rest } = y_axis
374
- y_axis = rest
373
+ const { range: _omit, ...axis_without_range } = y_axis
374
+ y_axis = axis_without_range
375
375
  }
376
376
  })
377
377
 
@@ -224,7 +224,7 @@ function apply_gaussian_smearing_core(freqs_or_energies, densities, sigma) {
224
224
  const orig_sum = densities.reduce((acc, d) => acc + d, 0);
225
225
  if (sigma <= 0 || orig_sum === 0)
226
226
  return densities;
227
- const smeared = new Array(densities.length).fill(0);
227
+ const smeared = Array(densities.length).fill(0);
228
228
  const truncation_width = 4; // Truncate Gaussian at ±4σ (contribution < 0.01%)
229
229
  for (let idx = 0; idx < freqs_or_energies.length; idx++) {
230
230
  const energy = freqs_or_energies[idx];
@@ -176,8 +176,8 @@
176
176
  function remap_element(from: ElementSymbol, to: ElementSymbol) {
177
177
  if (from === to && element_mapping?.[from]) {
178
178
  // Remove mapping if mapping back to original element
179
- const { [from]: _, ...rest } = element_mapping
180
- element_mapping = Object.keys(rest).length > 0 ? rest : undefined
179
+ const { [from]: _removed_mapping, ...mapping } = element_mapping
180
+ element_mapping = Object.keys(mapping).length > 0 ? mapping : undefined
181
181
  } else if (from !== to) {
182
182
  element_mapping = { ...element_mapping, [from]: to }
183
183
  }
@@ -202,8 +202,8 @@
202
202
  }
203
203
 
204
204
  function clear_element_radius(elem: ElementSymbol) {
205
- const { [elem]: _, ...rest } = element_radius_overrides ?? {}
206
- element_radius_overrides = rest
205
+ const { [elem]: _removed_radius, ...radii } = element_radius_overrides ?? {}
206
+ element_radius_overrides = radii
207
207
  }
208
208
 
209
209
  const get_element_radius = (elem: ElementSymbol): number =>