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,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;