matterviz 0.1.0

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 (131) hide show
  1. package/dist/BohrAtom.svelte +105 -0
  2. package/dist/BohrAtom.svelte.d.ts +21 -0
  3. package/dist/ControlPanel.svelte +158 -0
  4. package/dist/ControlPanel.svelte.d.ts +18 -0
  5. package/dist/Icon.svelte +23 -0
  6. package/dist/Icon.svelte.d.ts +8 -0
  7. package/dist/InfoCard.svelte +79 -0
  8. package/dist/InfoCard.svelte.d.ts +23 -0
  9. package/dist/Nucleus.svelte +64 -0
  10. package/dist/Nucleus.svelte.d.ts +16 -0
  11. package/dist/Spinner.svelte +44 -0
  12. package/dist/Spinner.svelte.d.ts +7 -0
  13. package/dist/api.d.ts +6 -0
  14. package/dist/api.js +30 -0
  15. package/dist/colors/alloy-colors.json +111 -0
  16. package/dist/colors/dark-mode-colors.json +111 -0
  17. package/dist/colors/index.d.ts +26 -0
  18. package/dist/colors/index.js +72 -0
  19. package/dist/colors/jmol-colors.json +111 -0
  20. package/dist/colors/muted-colors.json +111 -0
  21. package/dist/colors/pastel-colors.json +111 -0
  22. package/dist/colors/vesta-colors.json +111 -0
  23. package/dist/composition/BarChart.svelte +260 -0
  24. package/dist/composition/BarChart.svelte.d.ts +33 -0
  25. package/dist/composition/BubbleChart.svelte +166 -0
  26. package/dist/composition/BubbleChart.svelte.d.ts +30 -0
  27. package/dist/composition/Composition.svelte +73 -0
  28. package/dist/composition/Composition.svelte.d.ts +27 -0
  29. package/dist/composition/PieChart.svelte +236 -0
  30. package/dist/composition/PieChart.svelte.d.ts +36 -0
  31. package/dist/composition/index.d.ts +5 -0
  32. package/dist/composition/index.js +5 -0
  33. package/dist/composition/parse.d.ts +14 -0
  34. package/dist/composition/parse.js +307 -0
  35. package/dist/element/ElementHeading.svelte +21 -0
  36. package/dist/element/ElementHeading.svelte.d.ts +8 -0
  37. package/dist/element/ElementPhoto.svelte +56 -0
  38. package/dist/element/ElementPhoto.svelte.d.ts +9 -0
  39. package/dist/element/ElementStats.svelte +73 -0
  40. package/dist/element/ElementStats.svelte.d.ts +8 -0
  41. package/dist/element/ElementTile.svelte +449 -0
  42. package/dist/element/ElementTile.svelte.d.ts +25 -0
  43. package/dist/element/data.d.ts +4958 -0
  44. package/dist/element/data.js +5628 -0
  45. package/dist/element/index.d.ts +4 -0
  46. package/dist/element/index.js +4 -0
  47. package/dist/icons.d.ts +435 -0
  48. package/dist/icons.js +435 -0
  49. package/dist/index.d.ts +82 -0
  50. package/dist/index.js +43 -0
  51. package/dist/io/decompress.d.ts +16 -0
  52. package/dist/io/decompress.js +78 -0
  53. package/dist/io/export.d.ts +9 -0
  54. package/dist/io/export.js +205 -0
  55. package/dist/io/parse.d.ts +53 -0
  56. package/dist/io/parse.js +747 -0
  57. package/dist/labels.d.ts +31 -0
  58. package/dist/labels.js +209 -0
  59. package/dist/material/MaterialCard.svelte +135 -0
  60. package/dist/material/MaterialCard.svelte.d.ts +10 -0
  61. package/dist/material/SymmetryCard.svelte +23 -0
  62. package/dist/material/SymmetryCard.svelte.d.ts +9 -0
  63. package/dist/material/index.d.ts +2 -0
  64. package/dist/material/index.js +2 -0
  65. package/dist/math.d.ts +24 -0
  66. package/dist/math.js +216 -0
  67. package/dist/periodic-table/PeriodicTable.svelte +284 -0
  68. package/dist/periodic-table/PeriodicTable.svelte.d.ts +50 -0
  69. package/dist/periodic-table/PropertySelect.svelte +20 -0
  70. package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
  71. package/dist/periodic-table/TableInset.svelte +18 -0
  72. package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
  73. package/dist/periodic-table/index.d.ts +9 -0
  74. package/dist/periodic-table/index.js +3 -0
  75. package/dist/plot/ColorBar.svelte +414 -0
  76. package/dist/plot/ColorBar.svelte.d.ts +22 -0
  77. package/dist/plot/ColorScaleSelect.svelte +31 -0
  78. package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
  79. package/dist/plot/ElementScatter.svelte +38 -0
  80. package/dist/plot/ElementScatter.svelte.d.ts +14 -0
  81. package/dist/plot/Line.svelte +42 -0
  82. package/dist/plot/Line.svelte.d.ts +15 -0
  83. package/dist/plot/PlotLegend.svelte +206 -0
  84. package/dist/plot/PlotLegend.svelte.d.ts +18 -0
  85. package/dist/plot/ScatterPlot.svelte +1753 -0
  86. package/dist/plot/ScatterPlot.svelte.d.ts +114 -0
  87. package/dist/plot/ScatterPlotControls.svelte +505 -0
  88. package/dist/plot/ScatterPlotControls.svelte.d.ts +33 -0
  89. package/dist/plot/ScatterPoint.svelte +72 -0
  90. package/dist/plot/ScatterPoint.svelte.d.ts +17 -0
  91. package/dist/plot/index.d.ts +168 -0
  92. package/dist/plot/index.js +46 -0
  93. package/dist/state.svelte.d.ts +12 -0
  94. package/dist/state.svelte.js +11 -0
  95. package/dist/structure/Bond.svelte +68 -0
  96. package/dist/structure/Bond.svelte.d.ts +13 -0
  97. package/dist/structure/Lattice.svelte +115 -0
  98. package/dist/structure/Lattice.svelte.d.ts +15 -0
  99. package/dist/structure/Structure.svelte +298 -0
  100. package/dist/structure/Structure.svelte.d.ts +28 -0
  101. package/dist/structure/StructureCard.svelte +26 -0
  102. package/dist/structure/StructureCard.svelte.d.ts +9 -0
  103. package/dist/structure/StructureControls.svelte +383 -0
  104. package/dist/structure/StructureControls.svelte.d.ts +23 -0
  105. package/dist/structure/StructureLegend.svelte +130 -0
  106. package/dist/structure/StructureLegend.svelte.d.ts +17 -0
  107. package/dist/structure/StructureScene.svelte +331 -0
  108. package/dist/structure/StructureScene.svelte.d.ts +47 -0
  109. package/dist/structure/bonding.d.ts +16 -0
  110. package/dist/structure/bonding.js +150 -0
  111. package/dist/structure/index.d.ts +98 -0
  112. package/dist/structure/index.js +114 -0
  113. package/dist/structure/pbc.d.ts +6 -0
  114. package/dist/structure/pbc.js +72 -0
  115. package/dist/trajectory/Sidebar.svelte +412 -0
  116. package/dist/trajectory/Sidebar.svelte.d.ts +14 -0
  117. package/dist/trajectory/Trajectory.svelte +1084 -0
  118. package/dist/trajectory/Trajectory.svelte.d.ts +49 -0
  119. package/dist/trajectory/TrajectoryError.svelte +120 -0
  120. package/dist/trajectory/TrajectoryError.svelte.d.ts +12 -0
  121. package/dist/trajectory/extract.d.ts +5 -0
  122. package/dist/trajectory/extract.js +157 -0
  123. package/dist/trajectory/index.d.ts +16 -0
  124. package/dist/trajectory/index.js +49 -0
  125. package/dist/trajectory/parse.d.ts +13 -0
  126. package/dist/trajectory/parse.js +1093 -0
  127. package/dist/trajectory/plotting.d.ts +12 -0
  128. package/dist/trajectory/plotting.js +148 -0
  129. package/license +21 -0
  130. package/package.json +131 -0
  131. package/readme.md +95 -0
@@ -0,0 +1,236 @@
1
+ <script lang="ts">import { format_num } from '..';
2
+ import { element_color_schemes } from '../colors';
3
+ import { choose_bw_for_contrast } from '../labels';
4
+ import { composition_to_percentages, get_total_atoms } from './parse';
5
+ // Constants for pie chart calculations
6
+ const VERY_THIN_SLICE_THRESHOLD = 20; // degrees
7
+ const MEDIUM_SLICE_THRESHOLD = 90; // degrees - increased to move more slices toward outer edge
8
+ const MIN_FONT_SCALE = 0.4;
9
+ const MAX_FONT_SCALE = 1.0;
10
+ const MAX_ANGLE_FOR_FULL_SCALE = 120; // degrees
11
+ let { composition, size = 200, stroke_width = 0.5, inner_radius = 0, show_labels = true, show_percentages = false, show_amounts = true, color_scheme = `Vesta`, center_content, segment_content, style = ``, class: class_name = ``, interactive = true, ...rest } = $props();
12
+ let element_colors = $derived(element_color_schemes[color_scheme] || element_color_schemes.Vesta);
13
+ let percentages = $derived(composition_to_percentages(composition));
14
+ let total_atoms = $derived(get_total_atoms(composition));
15
+ let outer_radius = $derived(size / 2 - stroke_width);
16
+ let inner_radius_adjusted = $derived(Math.min(inner_radius, outer_radius - 10));
17
+ let center = $derived(size / 2);
18
+ // Function to determine text color based on background
19
+ function get_text_color(background_color, is_inside_slice) {
20
+ if (is_inside_slice) {
21
+ // For text inside slices, use the background color of the slice
22
+ return choose_bw_for_contrast(null, background_color);
23
+ }
24
+ else {
25
+ // For text outside slices, use default text color (black/dark)
26
+ return `var(--text-color, #333)`;
27
+ }
28
+ }
29
+ // Calculate pie segments
30
+ let segments = $derived.by(() => {
31
+ let current_angle = -90; // Start from top
32
+ return Object.entries(composition)
33
+ .filter(([_, amount]) => amount && amount > 0)
34
+ .map(([element, amount]) => {
35
+ const percentage = percentages[element] || 0;
36
+ const angle_span = (percentage / 100) * 360;
37
+ const start_angle = current_angle;
38
+ const end_angle = current_angle + angle_span;
39
+ current_angle = end_angle;
40
+ // Convert to radians for calculations
41
+ const start_rad = (start_angle * Math.PI) / 180;
42
+ const end_rad = (end_angle * Math.PI) / 180;
43
+ const mid_rad = (((start_angle + end_angle) / 2) * Math.PI) / 180;
44
+ // Arc coordinates for outer radius
45
+ const x1_outer = center + outer_radius * Math.cos(start_rad);
46
+ const y1_outer = center + outer_radius * Math.sin(start_rad);
47
+ const x2_outer = center + outer_radius * Math.cos(end_rad);
48
+ const y2_outer = center + outer_radius * Math.sin(end_rad);
49
+ // Arc coordinates for inner radius
50
+ const x1_inner = center + inner_radius_adjusted * Math.cos(start_rad);
51
+ const y1_inner = center + inner_radius_adjusted * Math.sin(start_rad);
52
+ const x2_inner = center + inner_radius_adjusted * Math.cos(end_rad);
53
+ const y2_inner = center + inner_radius_adjusted * Math.sin(end_rad);
54
+ const large_arc = angle_span > 180 ? 1 : 0;
55
+ // Create donut path if inner radius > 0, otherwise regular pie slice
56
+ const path = inner_radius_adjusted > 0
57
+ ? `M ${x1_outer} ${y1_outer} A ${outer_radius} ${outer_radius} 0 ${large_arc} 1 ${x2_outer} ${y2_outer} L ${x2_inner} ${y2_inner} A ${inner_radius_adjusted} ${inner_radius_adjusted} 0 ${large_arc} 0 ${x1_inner} ${y1_inner} Z`
58
+ : `M ${center} ${center} L ${x1_outer} ${y1_outer} A ${outer_radius} ${outer_radius} 0 ${large_arc} 1 ${x2_outer} ${y2_outer} Z`;
59
+ // Position labels with three-tier strategy
60
+ const is_very_thin_slice = angle_span < VERY_THIN_SLICE_THRESHOLD; // Place outside
61
+ const is_medium_slice = angle_span >= VERY_THIN_SLICE_THRESHOLD &&
62
+ angle_span < MEDIUM_SLICE_THRESHOLD; // Near outer edge
63
+ let label_radius;
64
+ let is_outside_slice = false;
65
+ if (is_very_thin_slice) {
66
+ // Very thin slices: place outside with distance proportional to chart size
67
+ label_radius = outer_radius + outer_radius * 0.2;
68
+ is_outside_slice = true;
69
+ }
70
+ else if (is_medium_slice) {
71
+ // Medium slices: place closer to outer edge, proportional to chart size
72
+ label_radius = outer_radius - outer_radius * 0.3;
73
+ is_outside_slice = false;
74
+ }
75
+ else {
76
+ // Large slices: place in middle of ring
77
+ label_radius = (outer_radius + inner_radius_adjusted) / 2;
78
+ is_outside_slice = false;
79
+ }
80
+ // Calculate font scale based on slice size
81
+ // Scale from MIN_FONT_SCALE (for very thin slices) to MAX_FONT_SCALE (for large slices)
82
+ const font_scale = Math.min(MAX_FONT_SCALE, MIN_FONT_SCALE +
83
+ (angle_span / MAX_ANGLE_FOR_FULL_SCALE) *
84
+ (MAX_FONT_SCALE - MIN_FONT_SCALE));
85
+ const color = element_colors[element] || `#cccccc`;
86
+ return {
87
+ element: element,
88
+ amount: amount,
89
+ percentage,
90
+ color,
91
+ start_angle,
92
+ end_angle,
93
+ path,
94
+ label_x: center + label_radius * Math.cos(mid_rad),
95
+ label_y: center + label_radius * Math.sin(mid_rad),
96
+ is_outside_slice,
97
+ font_scale,
98
+ text_color: get_text_color(color, !is_outside_slice),
99
+ };
100
+ });
101
+ });
102
+ let hovered_element = $state(null);
103
+ </script>
104
+
105
+ <div class="pie-chart-container {class_name}" {style} {...rest}>
106
+ <svg viewBox="0 0 {size} {size}" class="pie-chart">
107
+ {#each segments as segment (segment.element)}
108
+ <path
109
+ d={segment.path}
110
+ fill={segment.color}
111
+ stroke="white"
112
+ stroke-width={hovered_element === segment.element ? stroke_width + 1 : stroke_width}
113
+ class="pie-segment"
114
+ class:interactive
115
+ class:hovered={hovered_element === segment.element}
116
+ onmouseenter={() => interactive && (hovered_element = segment.element)}
117
+ onmouseleave={() => interactive && (hovered_element = null)}
118
+ {...interactive && {
119
+ role: `button`,
120
+ tabindex: 0,
121
+ 'aria-label': `${segment.element}: ${segment.amount} ${
122
+ segment.amount === 1 ? `atom` : `atoms`
123
+ } (${segment.percentage.toFixed(1)}%)`,
124
+ }}
125
+ >
126
+ <title>
127
+ {segment.element}: {segment.amount}
128
+ {segment.amount === 1 ? `atom` : `atoms`} ({segment.percentage.toFixed(1)}%)
129
+ </title>
130
+ </path>
131
+
132
+ {#if segment_content}
133
+ {@render segment_content(segment)}
134
+ {/if}
135
+ {/each}
136
+
137
+ {#if show_labels}
138
+ {#each segments as segment (segment.element)}
139
+ <foreignObject
140
+ x={segment.label_x - (size * 0.15 * segment.font_scale) / 2}
141
+ y={segment.label_y - (size * 0.075 * segment.font_scale) / 2}
142
+ width={size * 0.15 * segment.font_scale}
143
+ height={size * 0.075 * segment.font_scale}
144
+ class="pie-label-container"
145
+ class:hovered={hovered_element === segment.element}
146
+ >
147
+ <div
148
+ class="pie-label"
149
+ class:inside-slice={!segment.is_outside_slice}
150
+ class:outside-slice={segment.is_outside_slice}
151
+ style:color={segment.text_color}
152
+ >
153
+ <span class="element-symbol" style:font-size="{14 * segment.font_scale}px">{
154
+ segment.element
155
+ }</span>
156
+ {#if show_amounts}
157
+ <sub class="amount" style:font-size="{10 * segment.font_scale}px">
158
+ {segment.amount}
159
+ </sub>{/if}
160
+ {#if show_percentages}
161
+ <sub class="percentage" style:font-size="{11 * segment.font_scale}px">
162
+ {format_num(segment.percentage, 1)}%
163
+ </sub>
164
+ {/if}
165
+ </div>
166
+ </foreignObject>
167
+ {/each}
168
+ {/if}
169
+
170
+ {#if center_content}
171
+ <g class="center-content">
172
+ {@render center_content({ composition, total_atoms })}
173
+ </g>
174
+ {/if}
175
+ </svg>
176
+ </div>
177
+
178
+ <style>
179
+ .pie-chart-container {
180
+ display: inline-block;
181
+ }
182
+ .pie-chart {
183
+ overflow: visible;
184
+ width: 100%;
185
+ height: auto;
186
+ }
187
+ .pie-segment {
188
+ transition: all 0.2s ease;
189
+ }
190
+ .pie-segment.interactive {
191
+ cursor: pointer;
192
+ }
193
+ .pie-segment.interactive:hover,
194
+ .pie-segment.hovered {
195
+ filter: brightness(1.1);
196
+ }
197
+ .pie-segment.interactive:focus {
198
+ outline: none;
199
+ }
200
+ .pie-label-container {
201
+ pointer-events: none;
202
+ transition: all 0.2s ease;
203
+ }
204
+ .pie-label-container.hovered {
205
+ font-weight: 700;
206
+ }
207
+ .pie-label {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ text-align: center;
212
+ width: 100%;
213
+ height: 100%;
214
+ font-weight: 600;
215
+ transition: all 0.2s ease;
216
+ white-space: nowrap;
217
+ }
218
+ foreignobject {
219
+ overflow: visible;
220
+ }
221
+ .pie-label.hovered {
222
+ font-weight: 700;
223
+ }
224
+ .element-symbol {
225
+ font-weight: 700;
226
+ }
227
+ .amount {
228
+ font-weight: 500;
229
+ margin-left: 1px;
230
+ transform: translateY(5px);
231
+ }
232
+ .percentage {
233
+ font-weight: 400;
234
+ transform: translateY(4px);
235
+ }
236
+ </style>
@@ -0,0 +1,36 @@
1
+ import { type CompositionType, type ElementSymbol } from '..';
2
+ import { element_color_schemes } from '../colors';
3
+ import type { Snippet } from 'svelte';
4
+ interface Props {
5
+ composition: CompositionType;
6
+ size?: number;
7
+ stroke_width?: number;
8
+ inner_radius?: number;
9
+ show_labels?: boolean;
10
+ show_percentages?: boolean;
11
+ show_amounts?: boolean;
12
+ color_scheme?: keyof typeof element_color_schemes;
13
+ center_content?: Snippet<[{
14
+ composition: CompositionType;
15
+ total_atoms: number;
16
+ }]>;
17
+ segment_content?: Snippet<[
18
+ {
19
+ element: ElementSymbol;
20
+ amount: number;
21
+ percentage: number;
22
+ color: string;
23
+ start_angle: number;
24
+ end_angle: number;
25
+ font_scale: number;
26
+ text_color: string;
27
+ }
28
+ ]>;
29
+ style?: string;
30
+ class?: string;
31
+ interactive?: boolean;
32
+ [key: string]: unknown;
33
+ }
34
+ declare const PieChart: import("svelte").Component<Props, {}, "">;
35
+ type PieChart = ReturnType<typeof PieChart>;
36
+ export default PieChart;
@@ -0,0 +1,5 @@
1
+ export { default as BarChart } from './BarChart.svelte';
2
+ export { default as BubbleChart } from './BubbleChart.svelte';
3
+ export { default as Composition } from './Composition.svelte';
4
+ export * from './parse';
5
+ export { default as PieChart } from './PieChart.svelte';
@@ -0,0 +1,5 @@
1
+ export { default as BarChart } from './BarChart.svelte';
2
+ export { default as BubbleChart } from './BubbleChart.svelte';
3
+ export { default as Composition } from './Composition.svelte';
4
+ export * from './parse';
5
+ export { default as PieChart } from './PieChart.svelte';
@@ -0,0 +1,14 @@
1
+ import type { CompositionType, ElementSymbol } from '..';
2
+ export declare function atomic_number_to_element_symbol(atomic_number: number): ElementSymbol | null;
3
+ export declare function element_symbol_to_atomic_number(symbol: ElementSymbol): number | null;
4
+ export declare function convert_atomic_numbers_to_symbols(atomic_composition: Record<number, number>): CompositionType;
5
+ export declare function convert_symbols_to_atomic_numbers(symbol_composition: CompositionType): Record<number, number>;
6
+ export declare function parse_formula(formula: string): CompositionType;
7
+ export declare function normalize_composition(composition: CompositionType | Record<number, number> | Record<string | number, number>): CompositionType;
8
+ export declare function composition_to_percentages(composition: CompositionType, by_weight?: boolean): CompositionType;
9
+ export declare function get_total_atoms(composition: CompositionType): number;
10
+ export declare function is_empty_composition(composition: CompositionType): boolean;
11
+ export declare function parse_composition_input(input: string | CompositionType | Record<number, number> | Record<string | number, number>): CompositionType;
12
+ export declare function format_composition_formula(composition: CompositionType, sort_fn: (symbols: ElementSymbol[]) => ElementSymbol[]): string;
13
+ export declare function get_alphabetical_formula(input: string | CompositionType | Record<string, unknown>): string;
14
+ export declare function get_electro_neg_formula(input: string | CompositionType | Record<string, unknown>): string;
@@ -0,0 +1,307 @@
1
+ import { elem_symbols } from '..';
2
+ import element_data from '../element/data';
3
+ // Create a mapping from atomic numbers to element symbols
4
+ const atomic_number_to_symbol = {};
5
+ const symbol_to_atomic_number = {};
6
+ // Create mass/electronegativity maps for O(1) lookups in loops below
7
+ const element_atomic_mass_map = new Map();
8
+ const element_electronegativity_map = new Map();
9
+ // Populate maps at module load time
10
+ for (const element of element_data) {
11
+ atomic_number_to_symbol[element.number] = element.symbol;
12
+ symbol_to_atomic_number[element.symbol] = element.number;
13
+ element_atomic_mass_map.set(element.symbol, element.atomic_mass);
14
+ element_electronegativity_map.set(element.symbol, element.electronegativity ?? 0);
15
+ }
16
+ // Convert atomic number to element symbol
17
+ // Example: 26 -> "Fe", 8 -> "O"
18
+ export function atomic_number_to_element_symbol(atomic_number) {
19
+ return atomic_number_to_symbol[atomic_number] || null;
20
+ }
21
+ // Convert element symbol to atomic number
22
+ // Example: "Fe" -> 26, "O" -> 8
23
+ export function element_symbol_to_atomic_number(symbol) {
24
+ return symbol_to_atomic_number[symbol] || null;
25
+ }
26
+ // Check if an object represents a composition with atomic numbers as keys
27
+ // Returns true if ALL keys are valid atomic numbers (1-118) and at least one key exists
28
+ function is_atomic_number_composition(obj) {
29
+ const keys = Object.keys(obj);
30
+ return (keys.length > 0 &&
31
+ keys.every((key) => {
32
+ const num = Number(key);
33
+ return (Number.isInteger(num) &&
34
+ num >= 1 &&
35
+ num <= 118 &&
36
+ atomic_number_to_symbol[num]);
37
+ }));
38
+ }
39
+ // Convert a composition with atomic numbers to element symbols
40
+ // Example: {26: 2, 8: 3} -> {Fe: 2, O: 3}
41
+ export function convert_atomic_numbers_to_symbols(atomic_composition) {
42
+ const composition = {};
43
+ for (const [atomic_number_str, amount] of Object.entries(atomic_composition)) {
44
+ const atomic_number = Number(atomic_number_str);
45
+ const symbol = atomic_number_to_element_symbol(atomic_number);
46
+ if (!symbol)
47
+ throw new Error(`Invalid atomic number: ${atomic_number}`);
48
+ if (typeof amount === `number` && amount > 0) {
49
+ composition[symbol] = (composition[symbol] || 0) + amount;
50
+ }
51
+ }
52
+ return composition;
53
+ }
54
+ // Convert a composition with element symbols to atomic numbers
55
+ // Example: {Fe: 2, O: 3} -> {26: 2, 8: 3}
56
+ export function convert_symbols_to_atomic_numbers(symbol_composition) {
57
+ const atomic_composition = {};
58
+ for (const [symbol, amount] of Object.entries(symbol_composition)) {
59
+ const atomic_number = element_symbol_to_atomic_number(symbol);
60
+ if (!atomic_number)
61
+ throw new Error(`Invalid element symbol: ${symbol}`);
62
+ if (typeof amount === `number` && amount > 0) {
63
+ atomic_composition[atomic_number] = (atomic_composition[atomic_number] || 0) +
64
+ amount;
65
+ }
66
+ }
67
+ return atomic_composition;
68
+ }
69
+ // Parse a chemical formula string into a composition object
70
+ // Examples: "Fe2O3" -> {Fe: 2, O: 3}, "CaCO3" -> {Ca: 1, C: 1, O: 3}
71
+ export function parse_formula(formula) {
72
+ const composition = {};
73
+ // Remove whitespace and handle parentheses by expanding them
74
+ const cleaned_formula = expand_parentheses(formula.replace(/\s/g, ``));
75
+ // Match element symbols followed by numbers
76
+ // This regex matches: Capital letter, optional lowercase letter, optional number
77
+ const pattern = /([A-Z][a-z]?)(\d*)/g;
78
+ let match;
79
+ while ((match = pattern.exec(cleaned_formula)) !== null) {
80
+ const element = match[1];
81
+ const count = match[2] ? parseInt(match[2], 10) : 1;
82
+ // Validate element symbol
83
+ if (!elem_symbols.includes(element)) {
84
+ throw new Error(`Invalid element symbol: ${element}`);
85
+ }
86
+ composition[element] = (composition[element] || 0) + count;
87
+ }
88
+ return composition;
89
+ }
90
+ // Expand parentheses in chemical formulas
91
+ // Example: "Ca(OH)2" -> "CaO2H2"
92
+ function expand_parentheses(formula) {
93
+ // Handle nested parentheses by expanding from innermost to outermost
94
+ while (formula.includes(`(`)) {
95
+ formula = formula.replace(/\(([^()]+)\)(\d*)/g, (_match, group, multiplier) => {
96
+ const mult = multiplier ? parseInt(multiplier, 10) : 1;
97
+ let expanded = ``;
98
+ // Parse elements within parentheses
99
+ const inner_pattern = /([A-Z][a-z]?)(\d*)/g;
100
+ let inner_match;
101
+ while ((inner_match = inner_pattern.exec(group)) !== null) {
102
+ const element = inner_match[1];
103
+ const count = inner_match[2] ? parseInt(inner_match[2], 10) : 1;
104
+ expanded += element + (count * mult > 1 ? count * mult : ``);
105
+ }
106
+ return expanded;
107
+ });
108
+ }
109
+ return formula;
110
+ }
111
+ // Normalize a composition by ensuring all values are positive numbers
112
+ // Handles both element symbol and atomic number compositions
113
+ export function normalize_composition(composition) {
114
+ // If it's an atomic number composition, convert to symbols first
115
+ if (is_atomic_number_composition(composition)) {
116
+ const atomic_comp = composition;
117
+ const symbol_comp = convert_atomic_numbers_to_symbols(atomic_comp);
118
+ return normalize_composition(symbol_comp);
119
+ }
120
+ const normalized = {};
121
+ for (const [element, amount] of Object.entries(composition)) {
122
+ if (typeof amount === `number` && amount > 0) {
123
+ normalized[element] = amount;
124
+ }
125
+ }
126
+ return normalized;
127
+ }
128
+ // Convert composition to percentages by weight or by count
129
+ export function composition_to_percentages(composition, by_weight = false) {
130
+ if (by_weight) {
131
+ // Calculate weight-based percentages using atomic masses
132
+ let total_weight = 0;
133
+ const element_weights = {};
134
+ // Calculate weight for each element
135
+ for (const [element, amount] of Object.entries(composition)) {
136
+ if (typeof amount === `number` && amount > 0) {
137
+ const atomic_mass = element_atomic_mass_map.get(element);
138
+ if (atomic_mass === undefined) {
139
+ throw new Error(`Unknown element: ${element}`);
140
+ }
141
+ const weight = amount * atomic_mass;
142
+ element_weights[element] = weight;
143
+ total_weight += weight;
144
+ }
145
+ }
146
+ if (total_weight === 0)
147
+ return {};
148
+ // Convert to percentages
149
+ const percentages = {};
150
+ for (const [element, weight] of Object.entries(element_weights)) {
151
+ percentages[element] = (weight / total_weight) * 100;
152
+ }
153
+ return percentages;
154
+ }
155
+ // Calculate count-based percentages (original implementation)
156
+ const total = Object.values(composition).reduce((sum, count) => sum + (count || 0), 0);
157
+ if (total === 0)
158
+ return {};
159
+ const percentages = {};
160
+ for (const [element, amount] of Object.entries(composition)) {
161
+ if (typeof amount === `number`) {
162
+ percentages[element] = (amount / total) * 100;
163
+ }
164
+ }
165
+ return percentages;
166
+ }
167
+ // Get the total number of atoms in a composition
168
+ export function get_total_atoms(composition) {
169
+ return Object.values(composition).reduce((sum, count) => sum + (count || 0), 0);
170
+ }
171
+ // Check if a composition is empty (no elements with positive amounts)
172
+ export function is_empty_composition(composition) {
173
+ return get_total_atoms(composition) === 0;
174
+ }
175
+ // Convert composition input (string, symbol object, or atomic number object) to normalized composition object
176
+ export function parse_composition_input(input) {
177
+ if (typeof input === `string`) {
178
+ // First try to parse as JSON (for composition objects like {"Fe": 70, "Cr": 18})
179
+ if (input.trim().startsWith(`{`) && input.trim().endsWith(`}`)) {
180
+ try {
181
+ const parsed_json = JSON.parse(input);
182
+ return normalize_composition(parsed_json);
183
+ }
184
+ catch {
185
+ // If JSON parsing fails, fall through to formula parsing
186
+ }
187
+ }
188
+ // If not JSON or JSON parsing failed, treat as chemical formula
189
+ return normalize_composition(parse_formula(input));
190
+ }
191
+ else
192
+ return normalize_composition(input);
193
+ }
194
+ // Format a composition object into a chemical formula string
195
+ // @param composition - Composition object like {Fe: 2, O: 3}
196
+ // @param sort_fn - Function to sort element symbols
197
+ // @returns Formatted chemical formula with subscripts
198
+ export function format_composition_formula(composition, sort_fn) {
199
+ const formula = [];
200
+ const symbols = Object.keys(composition);
201
+ for (const el of sort_fn(symbols)) {
202
+ const amount = composition[el];
203
+ if (amount && amount > 0) {
204
+ if (amount === 1)
205
+ formula.push(el);
206
+ else
207
+ formula.push(`${el}<sub>${amount}</sub>`);
208
+ }
209
+ }
210
+ return formula.join(` `);
211
+ }
212
+ // Versatile function to create an alphabetical formula from any input type
213
+ // @param input - String formula, composition object, or structure
214
+ // @returns Alphabetically sorted chemical formula
215
+ export function get_alphabetical_formula(input) {
216
+ if (typeof input === `string`) {
217
+ // If it's already a string, parse it and reformat alphabetically
218
+ try {
219
+ const composition = parse_composition_input(input);
220
+ return format_composition_formula(composition, (symbols) => symbols.sort());
221
+ }
222
+ catch {
223
+ // If parsing fails, return the original string
224
+ return input;
225
+ }
226
+ }
227
+ else if (`sites` in input || `lattice` in input) {
228
+ // It's a structure object - need to extract composition
229
+ try {
230
+ const composition = extract_composition_from_structure(input);
231
+ return format_composition_formula(composition, (symbols) => symbols.sort());
232
+ }
233
+ catch {
234
+ return `Unknown`;
235
+ }
236
+ }
237
+ else {
238
+ // It's a composition object
239
+ return format_composition_formula(input, (symbols) => symbols.sort());
240
+ }
241
+ }
242
+ // Versatile function to create an electronegativity-sorted formula from any input type
243
+ // @param input - String formula, composition object, or structure
244
+ // @returns Electronegativity-sorted chemical formula
245
+ export function get_electro_neg_formula(input) {
246
+ const sort_by_electronegativity = (symbols) => {
247
+ return symbols.sort((el1, el2) => {
248
+ const elec_neg1 = element_electronegativity_map.get(el1) ?? 0;
249
+ const elec_neg2 = element_electronegativity_map.get(el2) ?? 0;
250
+ // Sort by electronegativity (ascending), then alphabetically for ties
251
+ if (elec_neg1 !== elec_neg2)
252
+ return elec_neg1 - elec_neg2;
253
+ return el1.localeCompare(el2);
254
+ });
255
+ };
256
+ if (typeof input === `string`) {
257
+ // If it's already a string, parse it and reformat by electronegativity
258
+ try {
259
+ const composition = parse_composition_input(input);
260
+ return format_composition_formula(composition, sort_by_electronegativity);
261
+ }
262
+ catch {
263
+ // If parsing fails, return the original string
264
+ return input;
265
+ }
266
+ }
267
+ else if (`sites` in input || `lattice` in input) {
268
+ // It's a structure object - need to extract composition
269
+ try {
270
+ const composition = extract_composition_from_structure(input);
271
+ return format_composition_formula(composition, sort_by_electronegativity);
272
+ }
273
+ catch {
274
+ return `Unknown`;
275
+ }
276
+ }
277
+ else {
278
+ // It's a composition object
279
+ return format_composition_formula(input, sort_by_electronegativity);
280
+ }
281
+ }
282
+ // Extract composition from a structure object
283
+ // @param structure - Structure object with sites property
284
+ // @returns Composition object
285
+ function extract_composition_from_structure(structure) {
286
+ const composition = {};
287
+ if (!structure.sites || !Array.isArray(structure.sites)) {
288
+ throw new Error(`Invalid structure object`);
289
+ }
290
+ for (const site of structure.sites) {
291
+ if (site.species && Array.isArray(site.species)) {
292
+ for (const species of site.species) {
293
+ const element = species.element;
294
+ const occu = species.occu || 1;
295
+ if (composition[element] === undefined) {
296
+ composition[element] = occu;
297
+ }
298
+ else {
299
+ const current = composition[element];
300
+ if (current !== undefined)
301
+ composition[element] = current + occu;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ return composition;
307
+ }
@@ -0,0 +1,21 @@
1
+ <script lang="ts">let { element, style = `` } = $props();
2
+ export {};
3
+ </script>
4
+
5
+ <h2 {style}>
6
+ {element.number} - {element.name} <small>{element.category}</small>
7
+ </h2>
8
+
9
+ <style>
10
+ h2 {
11
+ font-size: min(7vw, 3em);
12
+ white-space: nowrap;
13
+ text-align: center;
14
+ margin: 0 0 1em;
15
+ }
16
+ h2 > small {
17
+ margin-left: min(1vw, 10pt);
18
+ font-weight: 100;
19
+ opacity: 0.7;
20
+ }
21
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ChemicalElement } from '..';
2
+ interface Props {
3
+ element: ChemicalElement;
4
+ style?: string;
5
+ }
6
+ declare const ElementHeading: import("svelte").Component<Props, {}, "">;
7
+ type ElementHeading = ReturnType<typeof ElementHeading>;
8
+ export default ElementHeading;