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,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Ac": [112, 171, 250],
|
|
3
|
+
"Ag": [192, 192, 192],
|
|
4
|
+
"Al": [129, 178, 214],
|
|
5
|
+
"Am": [84, 92, 242],
|
|
6
|
+
"Ar": [207, 254, 196],
|
|
7
|
+
"As": [116, 208, 87],
|
|
8
|
+
"At": [117, 79, 69],
|
|
9
|
+
"Au": [255, 209, 35],
|
|
10
|
+
"B": [31, 162, 15],
|
|
11
|
+
"Ba": [0, 201, 0],
|
|
12
|
+
"Be": [94, 215, 123],
|
|
13
|
+
"Bh": [224, 0, 56],
|
|
14
|
+
"Bi": [158, 79, 181],
|
|
15
|
+
"Bk": [138, 79, 227],
|
|
16
|
+
"Br": [126, 49, 2],
|
|
17
|
+
"C": [76, 76, 76],
|
|
18
|
+
"Ca": [90, 150, 189],
|
|
19
|
+
"Cd": [255, 217, 143],
|
|
20
|
+
"Ce": [255, 255, 199],
|
|
21
|
+
"Cf": [161, 54, 212],
|
|
22
|
+
"Cl": [49, 252, 2],
|
|
23
|
+
"Cm": [120, 92, 227],
|
|
24
|
+
"Co": [0, 0, 175],
|
|
25
|
+
"Cr": [0, 0, 158],
|
|
26
|
+
"Cs": [87, 23, 143],
|
|
27
|
+
"Cu": [34, 71, 220],
|
|
28
|
+
"Db": [209, 0, 79],
|
|
29
|
+
"Dy": [31, 255, 199],
|
|
30
|
+
"Er": [0, 230, 117],
|
|
31
|
+
"Es": [179, 31, 212],
|
|
32
|
+
"Eu": [97, 255, 199],
|
|
33
|
+
"F": [176, 185, 230],
|
|
34
|
+
"Fe": [181, 113, 0],
|
|
35
|
+
"Fm": [179, 31, 186],
|
|
36
|
+
"Fr": [66, 0, 102],
|
|
37
|
+
"Ga": [158, 227, 115],
|
|
38
|
+
"Gd": [69, 255, 199],
|
|
39
|
+
"Ge": [126, 110, 166],
|
|
40
|
+
"H": [255, 204, 204],
|
|
41
|
+
"He": [252, 232, 206],
|
|
42
|
+
"Hf": [77, 194, 255],
|
|
43
|
+
"Hg": [184, 184, 208],
|
|
44
|
+
"Ho": [0, 255, 156],
|
|
45
|
+
"Hs": [230, 0, 46],
|
|
46
|
+
"I": [148, 0, 148],
|
|
47
|
+
"In": [166, 117, 115],
|
|
48
|
+
"Ir": [23, 84, 135],
|
|
49
|
+
"K": [161, 33, 246],
|
|
50
|
+
"Kr": [250, 193, 243],
|
|
51
|
+
"La": [90, 196, 73],
|
|
52
|
+
"Li": [134, 223, 115],
|
|
53
|
+
"Lr": [199, 0, 102],
|
|
54
|
+
"Lu": [0, 171, 36],
|
|
55
|
+
"Md": [179, 13, 166],
|
|
56
|
+
"Mg": [251, 123, 21],
|
|
57
|
+
"Mn": [167, 8, 157],
|
|
58
|
+
"Mo": [84, 181, 181],
|
|
59
|
+
"Mt": [235, 0, 38],
|
|
60
|
+
"N": [176, 185, 230],
|
|
61
|
+
"Na": [249, 220, 60],
|
|
62
|
+
"Nb": [115, 194, 201],
|
|
63
|
+
"Nd": [199, 255, 199],
|
|
64
|
+
"Ne": [254, 55, 181],
|
|
65
|
+
"Ni": [183, 187, 189],
|
|
66
|
+
"No": [189, 13, 135],
|
|
67
|
+
"Np": [0, 128, 255],
|
|
68
|
+
"O": [254, 3, 0],
|
|
69
|
+
"Os": [38, 102, 150],
|
|
70
|
+
"P": [192, 156, 194],
|
|
71
|
+
"Pa": [0, 161, 255],
|
|
72
|
+
"Pb": [87, 89, 97],
|
|
73
|
+
"Pd": [0, 105, 133],
|
|
74
|
+
"Pm": [163, 255, 199],
|
|
75
|
+
"Po": [171, 92, 0],
|
|
76
|
+
"Pr": [217, 255, 199],
|
|
77
|
+
"Pt": [208, 208, 224],
|
|
78
|
+
"Pu": [0, 107, 255],
|
|
79
|
+
"Ra": [0, 125, 0],
|
|
80
|
+
"Rb": [112, 46, 176],
|
|
81
|
+
"Re": [38, 125, 171],
|
|
82
|
+
"Rf": [204, 0, 89],
|
|
83
|
+
"Rh": [10, 125, 140],
|
|
84
|
+
"Rn": [66, 130, 150],
|
|
85
|
+
"Ru": [36, 143, 143],
|
|
86
|
+
"S": [255, 250, 0],
|
|
87
|
+
"Sb": [158, 99, 181],
|
|
88
|
+
"Sc": [181, 99, 171],
|
|
89
|
+
"Se": [154, 239, 15],
|
|
90
|
+
"Sg": [217, 0, 69],
|
|
91
|
+
"Si": [27, 59, 250],
|
|
92
|
+
"Sm": [143, 255, 199],
|
|
93
|
+
"Sn": [154, 142, 185],
|
|
94
|
+
"Sr": [0, 255, 0],
|
|
95
|
+
"Ta": [77, 166, 255],
|
|
96
|
+
"Tb": [48, 255, 199],
|
|
97
|
+
"Tc": [59, 158, 158],
|
|
98
|
+
"Te": [212, 122, 0],
|
|
99
|
+
"Th": [0, 186, 255],
|
|
100
|
+
"Ti": [120, 202, 255],
|
|
101
|
+
"Tl": [166, 84, 77],
|
|
102
|
+
"Tm": [0, 212, 82],
|
|
103
|
+
"U": [0, 143, 255],
|
|
104
|
+
"V": [229, 25, 0],
|
|
105
|
+
"W": [33, 148, 214],
|
|
106
|
+
"Xe": [66, 158, 176],
|
|
107
|
+
"Y": [148, 255, 255],
|
|
108
|
+
"Yb": [0, 191, 56],
|
|
109
|
+
"Zn": [143, 143, 129],
|
|
110
|
+
"Zr": [0, 255, 0]
|
|
111
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
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 } from './parse';
|
|
5
|
+
// Constants for bar chart calculations
|
|
6
|
+
const MIN_FONT_SCALE = 0.6;
|
|
7
|
+
const MAX_FONT_SCALE = 1.2;
|
|
8
|
+
const MIN_SEGMENT_SIZE_FOR_LABEL = 15; // pixels
|
|
9
|
+
let { composition, width = 200, height = 60, segment_gap = 0, border_radius = 8, outer_corners_only = true, show_labels = true, show_percentages = false, show_amounts = true, color_scheme = `Vesta`, segment_content, style = ``, class: class_name = ``, interactive = true, ...rest } = $props();
|
|
10
|
+
let element_colors = $derived(element_color_schemes[color_scheme] || element_color_schemes.Vesta);
|
|
11
|
+
let percentages = $derived(composition_to_percentages(composition));
|
|
12
|
+
// Calculate bar segments for horizontal layout
|
|
13
|
+
let segments = $derived.by(() => {
|
|
14
|
+
const element_entries = Object.entries(composition).filter(([_, amount]) => amount && amount > 0);
|
|
15
|
+
if (element_entries.length === 0)
|
|
16
|
+
return [];
|
|
17
|
+
const THIN_SEGMENT_THRESHOLD = 20; // Percentage below which segment is considered thin
|
|
18
|
+
const EXTERNAL_LABEL_SIZE_THRESHOLD = 5; // Lower threshold for external labels
|
|
19
|
+
let above_labels = 0;
|
|
20
|
+
let below_labels = 0;
|
|
21
|
+
return element_entries.map(([element, amount]) => {
|
|
22
|
+
const percentage = percentages[element] || 0;
|
|
23
|
+
const color = element_colors[element] || `#cccccc`;
|
|
24
|
+
// Calculate font scale based on percentage (approximate segment width)
|
|
25
|
+
const approx_segment_width = (percentage / 100) * width;
|
|
26
|
+
const segment_size = Math.min(approx_segment_width, height);
|
|
27
|
+
const font_scale = Math.min(MAX_FONT_SCALE, Math.max(MIN_FONT_SCALE, segment_size / 40));
|
|
28
|
+
// Determine label display requirements
|
|
29
|
+
const can_show_label = segment_size >= MIN_SEGMENT_SIZE_FOR_LABEL;
|
|
30
|
+
const is_thin = percentage < THIN_SEGMENT_THRESHOLD;
|
|
31
|
+
const can_show_external_label = segment_size >= EXTERNAL_LABEL_SIZE_THRESHOLD;
|
|
32
|
+
const needs_external_label = is_thin && can_show_external_label;
|
|
33
|
+
// Balance labels above and below for better visual distribution
|
|
34
|
+
let external_label_position = null;
|
|
35
|
+
if (needs_external_label) {
|
|
36
|
+
external_label_position = above_labels <= below_labels
|
|
37
|
+
? `above`
|
|
38
|
+
: `below`;
|
|
39
|
+
if (external_label_position === `above`)
|
|
40
|
+
above_labels++;
|
|
41
|
+
else
|
|
42
|
+
below_labels++;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
element: element,
|
|
46
|
+
amount: amount,
|
|
47
|
+
percentage,
|
|
48
|
+
color,
|
|
49
|
+
width_percent: percentage,
|
|
50
|
+
font_scale,
|
|
51
|
+
text_color: choose_bw_for_contrast(null, color),
|
|
52
|
+
can_show_label,
|
|
53
|
+
is_thin,
|
|
54
|
+
needs_external_label,
|
|
55
|
+
external_label_position,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
let hovered_element = $state(null);
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
{#snippet label_content(segment: SegmentData)}
|
|
63
|
+
<span class="element-symbol" style:font-size="{10 * segment.font_scale}px">{
|
|
64
|
+
segment.element
|
|
65
|
+
}</span>{#if show_amounts}<sub
|
|
66
|
+
class="amount"
|
|
67
|
+
style:font-size="{8 * segment.font_scale}px"
|
|
68
|
+
>{segment.amount}</sub>{/if}
|
|
69
|
+
{#if show_percentages}
|
|
70
|
+
<sub class="percentage" style:font-size="{8 * segment.font_scale}px">
|
|
71
|
+
{format_num(segment.percentage, 1)}%
|
|
72
|
+
</sub>
|
|
73
|
+
{/if}
|
|
74
|
+
{/snippet}
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
class="stacked-bar-chart-container {class_name}"
|
|
78
|
+
style:--bar-max-width="{width}px"
|
|
79
|
+
style:--bar-height="{height}px"
|
|
80
|
+
style:--segment-gap="{segment_gap}px"
|
|
81
|
+
style:--border-radius="{border_radius}px"
|
|
82
|
+
{style}
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
<!-- External labels above the bar -->
|
|
86
|
+
<div class="external-labels-above">
|
|
87
|
+
{#each segments as segment (segment.element)}
|
|
88
|
+
{#if show_labels && segment.needs_external_label &&
|
|
89
|
+
segment.external_label_position === `above`}
|
|
90
|
+
<div
|
|
91
|
+
class="external-label"
|
|
92
|
+
class:hovered={hovered_element === segment.element}
|
|
93
|
+
style:color={segment.color}
|
|
94
|
+
style:font-size="{7 * segment.font_scale}px"
|
|
95
|
+
style:flex={segment.width_percent}
|
|
96
|
+
>
|
|
97
|
+
{@render label_content(segment)}
|
|
98
|
+
</div>
|
|
99
|
+
{:else}
|
|
100
|
+
<div style:flex={segment.width_percent}></div>
|
|
101
|
+
{/if}
|
|
102
|
+
{/each}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="bar-segments" class:outer-corners-only={outer_corners_only}>
|
|
106
|
+
{#each segments as segment (segment.element)}
|
|
107
|
+
<div
|
|
108
|
+
class="bar-segment"
|
|
109
|
+
class:interactive
|
|
110
|
+
class:hovered={hovered_element === segment.element}
|
|
111
|
+
style:background-color={segment.color}
|
|
112
|
+
style:flex={segment.width_percent}
|
|
113
|
+
onmouseenter={() => interactive && (hovered_element = segment.element)}
|
|
114
|
+
onmouseleave={() => interactive && (hovered_element = null)}
|
|
115
|
+
{...interactive && {
|
|
116
|
+
role: `button`,
|
|
117
|
+
tabindex: 0,
|
|
118
|
+
'aria-label': `${segment.element}: ${segment.amount} ${
|
|
119
|
+
segment.amount === 1 ? `atom` : `atoms`
|
|
120
|
+
} (${segment.percentage.toFixed(1)}%)`,
|
|
121
|
+
}}
|
|
122
|
+
title="{segment.element}: {segment.amount} {segment.amount === 1
|
|
123
|
+
? `atom`
|
|
124
|
+
: `atoms`} ({segment.percentage.toFixed(1)}%)"
|
|
125
|
+
>
|
|
126
|
+
{#if show_labels && segment.can_show_label && !segment.needs_external_label}
|
|
127
|
+
<div
|
|
128
|
+
class="bar-label"
|
|
129
|
+
style:color={segment.text_color}
|
|
130
|
+
style:font-size="{7 * segment.font_scale}px"
|
|
131
|
+
>
|
|
132
|
+
{@render label_content(segment)}
|
|
133
|
+
</div>
|
|
134
|
+
{/if}
|
|
135
|
+
|
|
136
|
+
{#if segment_content}
|
|
137
|
+
{@render segment_content(segment)}
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
{/each}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- External labels below the bar -->
|
|
144
|
+
<div class="external-labels-below">
|
|
145
|
+
{#each segments as segment (segment.element)}
|
|
146
|
+
{#if show_labels && segment.needs_external_label &&
|
|
147
|
+
segment.external_label_position === `below`}
|
|
148
|
+
<div
|
|
149
|
+
class="external-label"
|
|
150
|
+
class:hovered={hovered_element === segment.element}
|
|
151
|
+
style:color={segment.color}
|
|
152
|
+
style:font-size="{7 * segment.font_scale}px"
|
|
153
|
+
style:flex={segment.width_percent}
|
|
154
|
+
>
|
|
155
|
+
{@render label_content(segment)}
|
|
156
|
+
</div>
|
|
157
|
+
{:else}
|
|
158
|
+
<div style:flex={segment.width_percent}></div>
|
|
159
|
+
{/if}
|
|
160
|
+
{/each}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
.stacked-bar-chart-container {
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
width: 100%;
|
|
169
|
+
max-width: var(--bar-max-width);
|
|
170
|
+
min-width: var(--bar-min-width, 100px);
|
|
171
|
+
gap: 2px;
|
|
172
|
+
}
|
|
173
|
+
.external-labels-above,
|
|
174
|
+
.external-labels-below {
|
|
175
|
+
display: flex;
|
|
176
|
+
width: 100%;
|
|
177
|
+
gap: var(--segment-gap);
|
|
178
|
+
min-height: 20px;
|
|
179
|
+
}
|
|
180
|
+
.external-label {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
text-align: center;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
white-space: nowrap;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
transition: all 0.2s ease;
|
|
189
|
+
}
|
|
190
|
+
.external-label.hovered {
|
|
191
|
+
font-weight: 700;
|
|
192
|
+
}
|
|
193
|
+
.bar-segments {
|
|
194
|
+
display: flex;
|
|
195
|
+
width: 100%;
|
|
196
|
+
height: var(--bar-height, 50px);
|
|
197
|
+
max-height: var(--bar-max-height, 25px);
|
|
198
|
+
gap: var(--segment-gap);
|
|
199
|
+
border-radius: var(--border-radius);
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
.bar-segments:not(.outer-corners-only) .bar-segment {
|
|
203
|
+
border-radius: var(--border-radius);
|
|
204
|
+
}
|
|
205
|
+
.bar-segments.outer-corners-only .bar-segment:first-child {
|
|
206
|
+
border-top-left-radius: var(--border-radius);
|
|
207
|
+
border-bottom-left-radius: var(--border-radius);
|
|
208
|
+
}
|
|
209
|
+
.bar-segments.outer-corners-only .bar-segment:last-child {
|
|
210
|
+
border-top-right-radius: var(--border-radius);
|
|
211
|
+
border-bottom-right-radius: var(--border-radius);
|
|
212
|
+
}
|
|
213
|
+
.bar-segment {
|
|
214
|
+
position: relative;
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
transition: all 0.2s ease;
|
|
219
|
+
min-width: 0; /* Allow flex items to shrink */
|
|
220
|
+
}
|
|
221
|
+
.bar-segment.interactive {
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
.bar-segment.interactive:hover,
|
|
225
|
+
.bar-segment.hovered {
|
|
226
|
+
filter: brightness(1.1);
|
|
227
|
+
transform: scaleY(1.05);
|
|
228
|
+
}
|
|
229
|
+
.bar-segment.interactive:focus {
|
|
230
|
+
outline: 2px solid var(--focus-color, #0066cc);
|
|
231
|
+
outline-offset: 2px;
|
|
232
|
+
}
|
|
233
|
+
.bar-label {
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: center;
|
|
237
|
+
text-align: center;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
transition: all 0.2s ease;
|
|
242
|
+
}
|
|
243
|
+
.bar-segment.hovered .bar-label {
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
}
|
|
246
|
+
.element-symbol {
|
|
247
|
+
font-weight: 700;
|
|
248
|
+
}
|
|
249
|
+
.amount,
|
|
250
|
+
.percentage {
|
|
251
|
+
margin-left: 1px;
|
|
252
|
+
transform: translateY(5px);
|
|
253
|
+
}
|
|
254
|
+
.amount {
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
}
|
|
257
|
+
.percentage {
|
|
258
|
+
font-weight: 400;
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
segment_gap?: number;
|
|
9
|
+
border_radius?: number;
|
|
10
|
+
outer_corners_only?: boolean;
|
|
11
|
+
show_labels?: boolean;
|
|
12
|
+
show_percentages?: boolean;
|
|
13
|
+
show_amounts?: boolean;
|
|
14
|
+
color_scheme?: keyof typeof element_color_schemes;
|
|
15
|
+
segment_content?: Snippet<[
|
|
16
|
+
{
|
|
17
|
+
element: ElementSymbol;
|
|
18
|
+
amount: number;
|
|
19
|
+
percentage: number;
|
|
20
|
+
color: string;
|
|
21
|
+
width_percent: number;
|
|
22
|
+
font_scale: number;
|
|
23
|
+
text_color: string;
|
|
24
|
+
}
|
|
25
|
+
]>;
|
|
26
|
+
style?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
interactive?: boolean;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
declare const BarChart: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type BarChart = ReturnType<typeof BarChart>;
|
|
33
|
+
export default BarChart;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">import { element_color_schemes } from '../colors';
|
|
2
|
+
import { choose_bw_for_contrast } from '../labels';
|
|
3
|
+
import { hierarchy, pack } from 'd3-hierarchy';
|
|
4
|
+
// Constants for bubble positioning and sizing
|
|
5
|
+
const MIN_FONT_SCALE = 0.4;
|
|
6
|
+
const MAX_FONT_SCALE = 1.0;
|
|
7
|
+
let { composition, size = 200, padding = 0, show_labels = true, show_amounts = true, color_scheme = `Vesta`, bubble_content, style = ``, class: class_name = ``, interactive = true, ...rest } = $props();
|
|
8
|
+
let element_colors = $derived(element_color_schemes[color_scheme] || element_color_schemes.Vesta);
|
|
9
|
+
// Function to determine text color based on background
|
|
10
|
+
function get_text_color(background_color) {
|
|
11
|
+
return choose_bw_for_contrast(null, background_color);
|
|
12
|
+
}
|
|
13
|
+
// Calculate bubble data with proper circle packing
|
|
14
|
+
let bubbles = $derived.by(() => {
|
|
15
|
+
const element_entries = Object.entries(composition).filter(([_, amount]) => amount && amount > 0);
|
|
16
|
+
if (element_entries.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
// Create hierarchy data structure for D3 pack
|
|
19
|
+
const hierarchy_data = {
|
|
20
|
+
children: element_entries.map(([element, amount]) => ({
|
|
21
|
+
element: element,
|
|
22
|
+
amount: amount,
|
|
23
|
+
color: element_colors[element] || `#cccccc`,
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
// Use D3's pack layout for proper circle packing
|
|
27
|
+
const pack_layout = pack()
|
|
28
|
+
.size([size - 2 * padding, size - 2 * padding])
|
|
29
|
+
.padding(padding * 0.1); // Small padding between circles
|
|
30
|
+
const root = pack_layout(hierarchy(hierarchy_data).sum((d) => d && `amount` in d ? d.amount : 0));
|
|
31
|
+
// Get max radius for font scaling
|
|
32
|
+
const max_radius = Math.max(...root.leaves().map((d) => d.r || 0));
|
|
33
|
+
return root.leaves().map((node) => {
|
|
34
|
+
const radius = node.r || 0;
|
|
35
|
+
const data = node.data;
|
|
36
|
+
// Calculate font scale based on bubble size
|
|
37
|
+
// Scale from MIN_FONT_SCALE (for very small bubbles) to MAX_FONT_SCALE (for large bubbles)
|
|
38
|
+
const font_scale = Math.min(MAX_FONT_SCALE, MIN_FONT_SCALE + (radius / max_radius) * (MAX_FONT_SCALE - MIN_FONT_SCALE));
|
|
39
|
+
return {
|
|
40
|
+
element: data.element,
|
|
41
|
+
amount: data.amount,
|
|
42
|
+
radius,
|
|
43
|
+
x: (node.x || 0) + padding, // Offset by padding
|
|
44
|
+
y: (node.y || 0) + padding,
|
|
45
|
+
color: data.color,
|
|
46
|
+
font_scale,
|
|
47
|
+
text_color: get_text_color(data.color),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
let hovered_element = $state(null);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="bubble-chart-container {class_name}" {style} {...rest}>
|
|
55
|
+
<svg viewBox="0 0 {size} {size}" class="bubble-chart">
|
|
56
|
+
{#each bubbles as bubble (bubble.element)}
|
|
57
|
+
<circle
|
|
58
|
+
cx={bubble.x}
|
|
59
|
+
cy={bubble.y}
|
|
60
|
+
r={bubble.radius}
|
|
61
|
+
fill={bubble.color}
|
|
62
|
+
stroke="white"
|
|
63
|
+
stroke-width={hovered_element === bubble.element ? 1.5 : 1}
|
|
64
|
+
class="bubble"
|
|
65
|
+
class:interactive
|
|
66
|
+
class:hovered={hovered_element === bubble.element}
|
|
67
|
+
onmouseenter={() => interactive && (hovered_element = bubble.element)}
|
|
68
|
+
onmouseleave={() => interactive && (hovered_element = null)}
|
|
69
|
+
{...interactive && {
|
|
70
|
+
role: `button`,
|
|
71
|
+
tabindex: 0,
|
|
72
|
+
'aria-label': `${bubble.element}: ${bubble.amount} ${
|
|
73
|
+
bubble.amount === 1 ? `atom` : `atoms`
|
|
74
|
+
}`,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<title>
|
|
78
|
+
{bubble.element}: {bubble.amount} {bubble.amount === 1 ? `atom` : `atoms`}
|
|
79
|
+
</title>
|
|
80
|
+
</circle>
|
|
81
|
+
|
|
82
|
+
{#if bubble_content}
|
|
83
|
+
{@render bubble_content(bubble)}
|
|
84
|
+
{/if}
|
|
85
|
+
{/each}
|
|
86
|
+
|
|
87
|
+
{#if show_labels}
|
|
88
|
+
{#each bubbles as bubble (bubble.element)}
|
|
89
|
+
<foreignObject
|
|
90
|
+
x={bubble.x - (size * 0.15 * bubble.font_scale) / 2}
|
|
91
|
+
y={bubble.y - (size * 0.075 * bubble.font_scale) / 2}
|
|
92
|
+
width={size * 0.15 * bubble.font_scale}
|
|
93
|
+
height={size * 0.075 * bubble.font_scale}
|
|
94
|
+
class="bubble-label-container"
|
|
95
|
+
class:hovered={hovered_element === bubble.element}
|
|
96
|
+
>
|
|
97
|
+
<div class="bubble-label" style:color={bubble.text_color}>
|
|
98
|
+
<span class="element-symbol" style:font-size="{14 * bubble.font_scale}px">{
|
|
99
|
+
bubble.element
|
|
100
|
+
}</span>
|
|
101
|
+
{#if show_amounts}
|
|
102
|
+
<sub class="amount" style:font-size="{10 * bubble.font_scale}px">
|
|
103
|
+
{bubble.amount}
|
|
104
|
+
</sub>{/if}
|
|
105
|
+
</div>
|
|
106
|
+
</foreignObject>
|
|
107
|
+
{/each}
|
|
108
|
+
{/if}
|
|
109
|
+
</svg>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.bubble-chart-container {
|
|
114
|
+
display: inline-block;
|
|
115
|
+
}
|
|
116
|
+
.bubble-chart {
|
|
117
|
+
overflow: visible;
|
|
118
|
+
width: 100%;
|
|
119
|
+
height: auto;
|
|
120
|
+
}
|
|
121
|
+
.bubble {
|
|
122
|
+
transition: all 0.2s ease;
|
|
123
|
+
}
|
|
124
|
+
.bubble.interactive {
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
}
|
|
127
|
+
.bubble.interactive:hover,
|
|
128
|
+
.bubble.hovered {
|
|
129
|
+
filter: brightness(1.1);
|
|
130
|
+
}
|
|
131
|
+
.bubble.interactive:focus {
|
|
132
|
+
outline: none;
|
|
133
|
+
}
|
|
134
|
+
.bubble-label-container {
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
transition: all 0.2s ease;
|
|
137
|
+
}
|
|
138
|
+
.bubble-label-container.hovered {
|
|
139
|
+
font-weight: 700;
|
|
140
|
+
}
|
|
141
|
+
.bubble-label {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
text-align: center;
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 100%;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
transition: all 0.2s ease;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
foreignobject {
|
|
153
|
+
overflow: visible;
|
|
154
|
+
}
|
|
155
|
+
.bubble-label.hovered {
|
|
156
|
+
font-weight: 700;
|
|
157
|
+
}
|
|
158
|
+
.element-symbol {
|
|
159
|
+
font-weight: 700;
|
|
160
|
+
}
|
|
161
|
+
.amount {
|
|
162
|
+
font-weight: 500;
|
|
163
|
+
margin-left: 1px;
|
|
164
|
+
transform: translateY(5px);
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CompositionType, 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
|
+
padding?: number;
|
|
8
|
+
show_labels?: boolean;
|
|
9
|
+
show_amounts?: boolean;
|
|
10
|
+
color_scheme?: keyof typeof element_color_schemes;
|
|
11
|
+
bubble_content?: Snippet<[
|
|
12
|
+
{
|
|
13
|
+
element: ElementSymbol;
|
|
14
|
+
amount: number;
|
|
15
|
+
radius: number;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
color: string;
|
|
19
|
+
font_scale: number;
|
|
20
|
+
text_color: string;
|
|
21
|
+
}
|
|
22
|
+
]>;
|
|
23
|
+
style?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
interactive?: boolean;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
declare const BubbleChart: import("svelte").Component<Props, {}, "">;
|
|
29
|
+
type BubbleChart = ReturnType<typeof BubbleChart>;
|
|
30
|
+
export default BubbleChart;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">import { element_color_schemes } from '../colors';
|
|
2
|
+
import BarChart from './BarChart.svelte';
|
|
3
|
+
import BubbleChart from './BubbleChart.svelte';
|
|
4
|
+
import { parse_composition_input } from './parse';
|
|
5
|
+
import PieChart from './PieChart.svelte';
|
|
6
|
+
let { input, mode = `pie`, size = 100, width = 300, height = 60, inner_radius = 0, show_labels = true, show_amounts = true, show_percentages = false, color_scheme = `Vesta`, interactive = true, on_composition_change, center_content, style = ``, class: class_name = ``, ...rest } = $props();
|
|
7
|
+
let composition = $derived.by(() => {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = parse_composition_input(input);
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
console.error(`Failed to parse composition:`, error);
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
// Call the composition change callback in an effect, not in the derived
|
|
18
|
+
$effect(() => {
|
|
19
|
+
on_composition_change?.(composition);
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="composition-container {class_name}" {style} {...rest}>
|
|
24
|
+
<div class="visualization">
|
|
25
|
+
{#if mode === `pie`}
|
|
26
|
+
<PieChart
|
|
27
|
+
{composition}
|
|
28
|
+
{size}
|
|
29
|
+
{inner_radius}
|
|
30
|
+
{show_labels}
|
|
31
|
+
{show_amounts}
|
|
32
|
+
{show_percentages}
|
|
33
|
+
{color_scheme}
|
|
34
|
+
{interactive}
|
|
35
|
+
{center_content}
|
|
36
|
+
/>
|
|
37
|
+
{:else if mode === `bubble`}
|
|
38
|
+
<BubbleChart
|
|
39
|
+
{composition}
|
|
40
|
+
{size}
|
|
41
|
+
{show_labels}
|
|
42
|
+
{show_amounts}
|
|
43
|
+
{color_scheme}
|
|
44
|
+
{interactive}
|
|
45
|
+
/>
|
|
46
|
+
{:else if mode === `bar`}
|
|
47
|
+
<BarChart
|
|
48
|
+
{composition}
|
|
49
|
+
{width}
|
|
50
|
+
{height}
|
|
51
|
+
{show_labels}
|
|
52
|
+
{show_amounts}
|
|
53
|
+
{show_percentages}
|
|
54
|
+
{color_scheme}
|
|
55
|
+
{interactive}
|
|
56
|
+
/>
|
|
57
|
+
{/if}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.composition-container {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 1rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.visualization {
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CompositionType } from '..';
|
|
2
|
+
import { element_color_schemes } from '../colors';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
input: string | CompositionType;
|
|
6
|
+
mode?: `pie` | `bubble` | `bar`;
|
|
7
|
+
size?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
inner_radius?: number;
|
|
11
|
+
show_labels?: boolean;
|
|
12
|
+
show_amounts?: boolean;
|
|
13
|
+
show_percentages?: boolean;
|
|
14
|
+
color_scheme?: keyof typeof element_color_schemes;
|
|
15
|
+
interactive?: boolean;
|
|
16
|
+
on_composition_change?: (composition: CompositionType) => void;
|
|
17
|
+
center_content?: Snippet<[{
|
|
18
|
+
composition: CompositionType;
|
|
19
|
+
total_atoms: number;
|
|
20
|
+
}]>;
|
|
21
|
+
style?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
declare const Composition: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type Composition = ReturnType<typeof Composition>;
|
|
27
|
+
export default Composition;
|