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.
- package/dist/BohrAtom.svelte +105 -0
- package/dist/BohrAtom.svelte.d.ts +21 -0
- package/dist/ControlPanel.svelte +158 -0
- package/dist/ControlPanel.svelte.d.ts +18 -0
- package/dist/Icon.svelte +23 -0
- package/dist/Icon.svelte.d.ts +8 -0
- package/dist/InfoCard.svelte +79 -0
- package/dist/InfoCard.svelte.d.ts +23 -0
- package/dist/Nucleus.svelte +64 -0
- package/dist/Nucleus.svelte.d.ts +16 -0
- package/dist/Spinner.svelte +44 -0
- package/dist/Spinner.svelte.d.ts +7 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +30 -0
- package/dist/colors/alloy-colors.json +111 -0
- package/dist/colors/dark-mode-colors.json +111 -0
- package/dist/colors/index.d.ts +26 -0
- package/dist/colors/index.js +72 -0
- package/dist/colors/jmol-colors.json +111 -0
- package/dist/colors/muted-colors.json +111 -0
- package/dist/colors/pastel-colors.json +111 -0
- package/dist/colors/vesta-colors.json +111 -0
- package/dist/composition/BarChart.svelte +260 -0
- package/dist/composition/BarChart.svelte.d.ts +33 -0
- package/dist/composition/BubbleChart.svelte +166 -0
- package/dist/composition/BubbleChart.svelte.d.ts +30 -0
- package/dist/composition/Composition.svelte +73 -0
- package/dist/composition/Composition.svelte.d.ts +27 -0
- package/dist/composition/PieChart.svelte +236 -0
- package/dist/composition/PieChart.svelte.d.ts +36 -0
- package/dist/composition/index.d.ts +5 -0
- package/dist/composition/index.js +5 -0
- package/dist/composition/parse.d.ts +14 -0
- package/dist/composition/parse.js +307 -0
- package/dist/element/ElementHeading.svelte +21 -0
- package/dist/element/ElementHeading.svelte.d.ts +8 -0
- package/dist/element/ElementPhoto.svelte +56 -0
- package/dist/element/ElementPhoto.svelte.d.ts +9 -0
- package/dist/element/ElementStats.svelte +73 -0
- package/dist/element/ElementStats.svelte.d.ts +8 -0
- package/dist/element/ElementTile.svelte +449 -0
- package/dist/element/ElementTile.svelte.d.ts +25 -0
- package/dist/element/data.d.ts +4958 -0
- package/dist/element/data.js +5628 -0
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +4 -0
- package/dist/icons.d.ts +435 -0
- package/dist/icons.js +435 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +43 -0
- package/dist/io/decompress.d.ts +16 -0
- package/dist/io/decompress.js +78 -0
- package/dist/io/export.d.ts +9 -0
- package/dist/io/export.js +205 -0
- package/dist/io/parse.d.ts +53 -0
- package/dist/io/parse.js +747 -0
- package/dist/labels.d.ts +31 -0
- package/dist/labels.js +209 -0
- package/dist/material/MaterialCard.svelte +135 -0
- package/dist/material/MaterialCard.svelte.d.ts +10 -0
- package/dist/material/SymmetryCard.svelte +23 -0
- package/dist/material/SymmetryCard.svelte.d.ts +9 -0
- package/dist/material/index.d.ts +2 -0
- package/dist/material/index.js +2 -0
- package/dist/math.d.ts +24 -0
- package/dist/math.js +216 -0
- package/dist/periodic-table/PeriodicTable.svelte +284 -0
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +50 -0
- package/dist/periodic-table/PropertySelect.svelte +20 -0
- package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
- package/dist/periodic-table/TableInset.svelte +18 -0
- package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
- package/dist/periodic-table/index.d.ts +9 -0
- package/dist/periodic-table/index.js +3 -0
- package/dist/plot/ColorBar.svelte +414 -0
- package/dist/plot/ColorBar.svelte.d.ts +22 -0
- package/dist/plot/ColorScaleSelect.svelte +31 -0
- package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
- package/dist/plot/ElementScatter.svelte +38 -0
- package/dist/plot/ElementScatter.svelte.d.ts +14 -0
- package/dist/plot/Line.svelte +42 -0
- package/dist/plot/Line.svelte.d.ts +15 -0
- package/dist/plot/PlotLegend.svelte +206 -0
- package/dist/plot/PlotLegend.svelte.d.ts +18 -0
- package/dist/plot/ScatterPlot.svelte +1753 -0
- package/dist/plot/ScatterPlot.svelte.d.ts +114 -0
- package/dist/plot/ScatterPlotControls.svelte +505 -0
- package/dist/plot/ScatterPlotControls.svelte.d.ts +33 -0
- package/dist/plot/ScatterPoint.svelte +72 -0
- package/dist/plot/ScatterPoint.svelte.d.ts +17 -0
- package/dist/plot/index.d.ts +168 -0
- package/dist/plot/index.js +46 -0
- package/dist/state.svelte.d.ts +12 -0
- package/dist/state.svelte.js +11 -0
- package/dist/structure/Bond.svelte +68 -0
- package/dist/structure/Bond.svelte.d.ts +13 -0
- package/dist/structure/Lattice.svelte +115 -0
- package/dist/structure/Lattice.svelte.d.ts +15 -0
- package/dist/structure/Structure.svelte +298 -0
- package/dist/structure/Structure.svelte.d.ts +28 -0
- package/dist/structure/StructureCard.svelte +26 -0
- package/dist/structure/StructureCard.svelte.d.ts +9 -0
- package/dist/structure/StructureControls.svelte +383 -0
- package/dist/structure/StructureControls.svelte.d.ts +23 -0
- package/dist/structure/StructureLegend.svelte +130 -0
- package/dist/structure/StructureLegend.svelte.d.ts +17 -0
- package/dist/structure/StructureScene.svelte +331 -0
- package/dist/structure/StructureScene.svelte.d.ts +47 -0
- package/dist/structure/bonding.d.ts +16 -0
- package/dist/structure/bonding.js +150 -0
- package/dist/structure/index.d.ts +98 -0
- package/dist/structure/index.js +114 -0
- package/dist/structure/pbc.d.ts +6 -0
- package/dist/structure/pbc.js +72 -0
- package/dist/trajectory/Sidebar.svelte +412 -0
- package/dist/trajectory/Sidebar.svelte.d.ts +14 -0
- package/dist/trajectory/Trajectory.svelte +1084 -0
- package/dist/trajectory/Trajectory.svelte.d.ts +49 -0
- package/dist/trajectory/TrajectoryError.svelte +120 -0
- package/dist/trajectory/TrajectoryError.svelte.d.ts +12 -0
- package/dist/trajectory/extract.d.ts +5 -0
- package/dist/trajectory/extract.js +157 -0
- package/dist/trajectory/index.d.ts +16 -0
- package/dist/trajectory/index.js +49 -0
- package/dist/trajectory/parse.d.ts +13 -0
- package/dist/trajectory/parse.js +1093 -0
- package/dist/trajectory/plotting.d.ts +12 -0
- package/dist/trajectory/plotting.js +148 -0
- package/license +21 -0
- package/package.json +131 -0
- 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;
|