react-cheminfo 0.6.0 → 0.8.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/README.md +16 -0
- package/lib/citation/core/index.d.ts +2 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +2 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/citation/core/platformPaper.d.ts +14 -0
- package/lib/citation/core/platformPaper.d.ts.map +1 -0
- package/lib/citation/core/platformPaper.js +28 -0
- package/lib/citation/core/platformPaper.js.map +1 -0
- package/lib/citation/core/teachingPaper.d.ts +15 -0
- package/lib/citation/core/teachingPaper.d.ts.map +1 -0
- package/lib/citation/core/teachingPaper.js +32 -0
- package/lib/citation/core/teachingPaper.js.map +1 -0
- package/lib/color/core/index.d.ts +1 -1
- package/lib/color/core/index.d.ts.map +1 -1
- package/lib/color/core/index.js +1 -1
- package/lib/color/core/index.js.map +1 -1
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/ecosystem/core/sites.js +5 -5
- package/lib/orbital/core/index.d.ts +1 -1
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -1
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts +15 -2
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js +15 -4
- package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
- package/lib/pedagogy/ui/index.d.ts +1 -1
- package/lib/pedagogy/ui/index.d.ts.map +1 -1
- package/lib/pedagogy/ui/index.js.map +1 -1
- package/lib/periodic/core/categories.d.ts +25 -0
- package/lib/periodic/core/categories.d.ts.map +1 -0
- package/lib/periodic/core/categories.js +70 -0
- package/lib/periodic/core/categories.js.map +1 -0
- package/lib/periodic/core/elements.d.ts +49 -0
- package/lib/periodic/core/elements.d.ts.map +1 -0
- package/lib/periodic/core/elements.js +175 -0
- package/lib/periodic/core/elements.js.map +1 -0
- package/lib/periodic/core/index.d.ts +6 -0
- package/lib/periodic/core/index.d.ts.map +1 -0
- package/lib/periodic/core/index.js +4 -0
- package/lib/periodic/core/index.js.map +1 -0
- package/lib/periodic/core/layout.d.ts +73 -0
- package/lib/periodic/core/layout.d.ts.map +1 -0
- package/lib/periodic/core/layout.js +132 -0
- package/lib/periodic/core/layout.js.map +1 -0
- package/lib/periodic/ui/CategoryLegend.d.ts +26 -0
- package/lib/periodic/ui/CategoryLegend.d.ts.map +1 -0
- package/lib/periodic/ui/CategoryLegend.js +53 -0
- package/lib/periodic/ui/CategoryLegend.js.map +1 -0
- package/lib/periodic/ui/ElementCell.d.ts +53 -0
- package/lib/periodic/ui/ElementCell.d.ts.map +1 -0
- package/lib/periodic/ui/ElementCell.js +56 -0
- package/lib/periodic/ui/ElementCell.js.map +1 -0
- package/lib/periodic/ui/PeriodicTable.d.ts +90 -0
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -0
- package/lib/periodic/ui/PeriodicTable.js +77 -0
- package/lib/periodic/ui/PeriodicTable.js.map +1 -0
- package/lib/periodic/ui/PeriodicTableChrome.d.ts +36 -0
- package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -0
- package/lib/periodic/ui/PeriodicTableChrome.js +72 -0
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -0
- package/lib/periodic/ui/index.d.ts +7 -0
- package/lib/periodic/ui/index.d.ts.map +1 -0
- package/lib/periodic/ui/index.js +4 -0
- package/lib/periodic/ui/index.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +2 -1
- package/src/citation/core/index.ts +2 -0
- package/src/citation/core/platformPaper.ts +32 -0
- package/src/citation/core/teachingPaper.ts +36 -0
- package/src/color/core/index.ts +6 -1
- package/src/core.ts +1 -0
- package/src/ecosystem/core/sites.ts +5 -5
- package/src/orbital/core/index.ts +1 -1
- package/src/pedagogy/ui/TutorialStepStrip.tsx +35 -9
- package/src/pedagogy/ui/index.ts +4 -1
- package/src/periodic/core/categories.ts +83 -0
- package/src/periodic/core/elements.ts +217 -0
- package/src/periodic/core/index.ts +27 -0
- package/src/periodic/core/layout.ts +183 -0
- package/src/periodic/ui/CategoryLegend.tsx +105 -0
- package/src/periodic/ui/ElementCell.tsx +137 -0
- package/src/periodic/ui/PeriodicTable.tsx +226 -0
- package/src/periodic/ui/PeriodicTableChrome.tsx +170 -0
- package/src/periodic/ui/index.ts +6 -0
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each family is called, and the colour it takes.
|
|
3
|
+
*
|
|
4
|
+
* Ten hues that stay distinguishable under deuteranopia and protanopia, and
|
|
5
|
+
* that keep the metals warm and the non-metals cool the way a wall chart does.
|
|
6
|
+
* Each is paired at load with the ink that stays readable on it, so a cell
|
|
7
|
+
* never has to compute a contrast while it renders.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readableInk } from '../../color/core/contrast.ts';
|
|
11
|
+
import type { Swatch } from '../../color/core/scale.ts';
|
|
12
|
+
|
|
13
|
+
import type { ElementCategory } from './elements.ts';
|
|
14
|
+
|
|
15
|
+
/** Families in the order a legend lists them, which is the order of the table. */
|
|
16
|
+
export const CATEGORY_ORDER: readonly ElementCategory[] = [
|
|
17
|
+
'alkali-metal',
|
|
18
|
+
'alkaline-earth-metal',
|
|
19
|
+
'transition-metal',
|
|
20
|
+
'post-transition-metal',
|
|
21
|
+
'metalloid',
|
|
22
|
+
'nonmetal',
|
|
23
|
+
'halogen',
|
|
24
|
+
'noble-gas',
|
|
25
|
+
'lanthanoid',
|
|
26
|
+
'actinoid',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** What each family is called in a legend. */
|
|
30
|
+
export const CATEGORY_LABELS: Record<ElementCategory, string> = {
|
|
31
|
+
'alkali-metal': 'Alkali metal',
|
|
32
|
+
'alkaline-earth-metal': 'Alkaline earth',
|
|
33
|
+
'transition-metal': 'Transition metal',
|
|
34
|
+
'post-transition-metal': 'Post-transition',
|
|
35
|
+
metalloid: 'Metalloid',
|
|
36
|
+
nonmetal: 'Non-metal',
|
|
37
|
+
halogen: 'Halogen',
|
|
38
|
+
'noble-gas': 'Noble gas',
|
|
39
|
+
lanthanoid: 'Lanthanoid',
|
|
40
|
+
actinoid: 'Actinoid',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const CATEGORY_BACKGROUNDS: Record<ElementCategory, string> = {
|
|
44
|
+
'alkali-metal': '#f4a58a',
|
|
45
|
+
'alkaline-earth-metal': '#f6c98a',
|
|
46
|
+
'transition-metal': '#e8b7d4',
|
|
47
|
+
'post-transition-metal': '#c8c3ea',
|
|
48
|
+
metalloid: '#c9d67c',
|
|
49
|
+
nonmetal: '#7fd4a8',
|
|
50
|
+
halogen: '#8fd9d2',
|
|
51
|
+
'noble-gas': '#9ec9f0',
|
|
52
|
+
lanthanoid: '#f2d06b',
|
|
53
|
+
actinoid: '#e5a3a3',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The colour of each family, with the ink that stays readable on it. */
|
|
57
|
+
export const CATEGORY_SWATCHES: Record<ElementCategory, Swatch> =
|
|
58
|
+
Object.fromEntries(
|
|
59
|
+
CATEGORY_ORDER.map((category) => [
|
|
60
|
+
category,
|
|
61
|
+
{
|
|
62
|
+
background: CATEGORY_BACKGROUNDS[category],
|
|
63
|
+
foreground: readableInk(CATEGORY_BACKGROUNDS[category]),
|
|
64
|
+
},
|
|
65
|
+
]),
|
|
66
|
+
) as Record<ElementCategory, Swatch>;
|
|
67
|
+
|
|
68
|
+
const UNKNOWN_BACKGROUND = '#e4e8ee';
|
|
69
|
+
|
|
70
|
+
/** Shown for a cell whose family is unknown. */
|
|
71
|
+
export const UNKNOWN_SWATCH: Swatch = {
|
|
72
|
+
background: UNKNOWN_BACKGROUND,
|
|
73
|
+
foreground: readableInk(UNKNOWN_BACKGROUND),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The swatch of one family.
|
|
78
|
+
* @param category - Family to look up.
|
|
79
|
+
* @returns Its colour and the ink to write on it.
|
|
80
|
+
*/
|
|
81
|
+
export function categorySwatch(category: ElementCategory): Swatch {
|
|
82
|
+
return CATEGORY_SWATCHES[category] ?? UNKNOWN_SWATCH;
|
|
83
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where every element sits in the table, and which family it belongs to.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the *position* and nothing else: the masses, the isotopes and
|
|
5
|
+
* the exact monoisotopic masses are `chemical-elements`' subject, and a site
|
|
6
|
+
* that needs them reads them from there. Importing that package pulls its whole
|
|
7
|
+
* isotope table, which a site wanting only an element picker must not be made
|
|
8
|
+
* to carry — so the symbols and names are repeated here, and a test asserts
|
|
9
|
+
* they still agree with it.
|
|
10
|
+
*
|
|
11
|
+
* `block` follows the element's *position* in the table, which is what a
|
|
12
|
+
* student reads off it. The configuration is the authority on which subshell is
|
|
13
|
+
* actually filling, and the two disagree for a handful of elements — lanthanum
|
|
14
|
+
* fills 5d, not 4f. See `ELEMENT_ANOMALIES` in the orbital module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Region of the table an element sits in. */
|
|
18
|
+
export type ElementBlock = 's' | 'p' | 'd' | 'f';
|
|
19
|
+
|
|
20
|
+
/** Coarse family, used to colour the table. */
|
|
21
|
+
export type ElementCategory =
|
|
22
|
+
| 'alkali-metal'
|
|
23
|
+
| 'alkaline-earth-metal'
|
|
24
|
+
| 'transition-metal'
|
|
25
|
+
| 'post-transition-metal'
|
|
26
|
+
| 'metalloid'
|
|
27
|
+
| 'nonmetal'
|
|
28
|
+
| 'halogen'
|
|
29
|
+
| 'noble-gas'
|
|
30
|
+
| 'lanthanoid'
|
|
31
|
+
| 'actinoid';
|
|
32
|
+
|
|
33
|
+
/** One element, as the table places it. */
|
|
34
|
+
export interface PeriodicElement {
|
|
35
|
+
/** Proton count, 1 to 118. */
|
|
36
|
+
atomicNumber: number;
|
|
37
|
+
/** Chemical symbol, e.g. `Fe`. */
|
|
38
|
+
symbol: string;
|
|
39
|
+
/** English name, spelled as `chemical-elements` spells it. */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Row of the table, 1 to 7. */
|
|
42
|
+
period: number;
|
|
43
|
+
/** IUPAC column 1 to 18; `null` for the lanthanoids and the actinoids. */
|
|
44
|
+
group: number | null;
|
|
45
|
+
block: ElementBlock;
|
|
46
|
+
category: ElementCategory;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `[symbol, name, period, group, category]`; atomic number is the position. */
|
|
50
|
+
type ElementRow = [string, string, number, number | null, ElementCategory];
|
|
51
|
+
|
|
52
|
+
const ROWS: ElementRow[] = [
|
|
53
|
+
['H', 'Hydrogen', 1, 1, 'nonmetal'],
|
|
54
|
+
['He', 'Helium', 1, 18, 'noble-gas'],
|
|
55
|
+
['Li', 'Lithium', 2, 1, 'alkali-metal'],
|
|
56
|
+
['Be', 'Beryllium', 2, 2, 'alkaline-earth-metal'],
|
|
57
|
+
['B', 'Boron', 2, 13, 'metalloid'],
|
|
58
|
+
['C', 'Carbon', 2, 14, 'nonmetal'],
|
|
59
|
+
['N', 'Nitrogen', 2, 15, 'nonmetal'],
|
|
60
|
+
['O', 'Oxygen', 2, 16, 'nonmetal'],
|
|
61
|
+
['F', 'Fluorine', 2, 17, 'halogen'],
|
|
62
|
+
['Ne', 'Neon', 2, 18, 'noble-gas'],
|
|
63
|
+
['Na', 'Sodium', 3, 1, 'alkali-metal'],
|
|
64
|
+
['Mg', 'Magnesium', 3, 2, 'alkaline-earth-metal'],
|
|
65
|
+
['Al', 'Aluminium', 3, 13, 'post-transition-metal'],
|
|
66
|
+
['Si', 'Silicon', 3, 14, 'metalloid'],
|
|
67
|
+
['P', 'Phosphorus', 3, 15, 'nonmetal'],
|
|
68
|
+
['S', 'Sulfur', 3, 16, 'nonmetal'],
|
|
69
|
+
['Cl', 'Chlorine', 3, 17, 'halogen'],
|
|
70
|
+
['Ar', 'Argon', 3, 18, 'noble-gas'],
|
|
71
|
+
['K', 'Potassium', 4, 1, 'alkali-metal'],
|
|
72
|
+
['Ca', 'Calcium', 4, 2, 'alkaline-earth-metal'],
|
|
73
|
+
['Sc', 'Scandium', 4, 3, 'transition-metal'],
|
|
74
|
+
['Ti', 'Titanium', 4, 4, 'transition-metal'],
|
|
75
|
+
['V', 'Vanadium', 4, 5, 'transition-metal'],
|
|
76
|
+
['Cr', 'Chromium', 4, 6, 'transition-metal'],
|
|
77
|
+
['Mn', 'Manganese', 4, 7, 'transition-metal'],
|
|
78
|
+
['Fe', 'Iron', 4, 8, 'transition-metal'],
|
|
79
|
+
['Co', 'Cobalt', 4, 9, 'transition-metal'],
|
|
80
|
+
['Ni', 'Nickel', 4, 10, 'transition-metal'],
|
|
81
|
+
['Cu', 'Copper', 4, 11, 'transition-metal'],
|
|
82
|
+
['Zn', 'Zinc', 4, 12, 'transition-metal'],
|
|
83
|
+
['Ga', 'Gallium', 4, 13, 'post-transition-metal'],
|
|
84
|
+
['Ge', 'Germanium', 4, 14, 'metalloid'],
|
|
85
|
+
['As', 'Arsenic', 4, 15, 'metalloid'],
|
|
86
|
+
['Se', 'Selenium', 4, 16, 'nonmetal'],
|
|
87
|
+
['Br', 'Bromine', 4, 17, 'halogen'],
|
|
88
|
+
['Kr', 'Krypton', 4, 18, 'noble-gas'],
|
|
89
|
+
['Rb', 'Rubidium', 5, 1, 'alkali-metal'],
|
|
90
|
+
['Sr', 'Strontium', 5, 2, 'alkaline-earth-metal'],
|
|
91
|
+
['Y', 'Yttrium', 5, 3, 'transition-metal'],
|
|
92
|
+
['Zr', 'Zirconium', 5, 4, 'transition-metal'],
|
|
93
|
+
['Nb', 'Niobium', 5, 5, 'transition-metal'],
|
|
94
|
+
['Mo', 'Molybdenum', 5, 6, 'transition-metal'],
|
|
95
|
+
['Tc', 'Technetium', 5, 7, 'transition-metal'],
|
|
96
|
+
['Ru', 'Ruthenium', 5, 8, 'transition-metal'],
|
|
97
|
+
['Rh', 'Rhodium', 5, 9, 'transition-metal'],
|
|
98
|
+
['Pd', 'Palladium', 5, 10, 'transition-metal'],
|
|
99
|
+
['Ag', 'Silver', 5, 11, 'transition-metal'],
|
|
100
|
+
['Cd', 'Cadmium', 5, 12, 'transition-metal'],
|
|
101
|
+
['In', 'Indium', 5, 13, 'post-transition-metal'],
|
|
102
|
+
['Sn', 'Tin', 5, 14, 'post-transition-metal'],
|
|
103
|
+
['Sb', 'Antimony', 5, 15, 'metalloid'],
|
|
104
|
+
['Te', 'Tellurium', 5, 16, 'metalloid'],
|
|
105
|
+
['I', 'Iodine', 5, 17, 'halogen'],
|
|
106
|
+
['Xe', 'Xenon', 5, 18, 'noble-gas'],
|
|
107
|
+
['Cs', 'Caesium', 6, 1, 'alkali-metal'],
|
|
108
|
+
['Ba', 'Barium', 6, 2, 'alkaline-earth-metal'],
|
|
109
|
+
['La', 'Lanthanum', 6, null, 'lanthanoid'],
|
|
110
|
+
['Ce', 'Cerium', 6, null, 'lanthanoid'],
|
|
111
|
+
['Pr', 'Praseodymium', 6, null, 'lanthanoid'],
|
|
112
|
+
['Nd', 'Neodymium', 6, null, 'lanthanoid'],
|
|
113
|
+
['Pm', 'Promethium', 6, null, 'lanthanoid'],
|
|
114
|
+
['Sm', 'Samarium', 6, null, 'lanthanoid'],
|
|
115
|
+
['Eu', 'Europium', 6, null, 'lanthanoid'],
|
|
116
|
+
['Gd', 'Gadolinium', 6, null, 'lanthanoid'],
|
|
117
|
+
['Tb', 'Terbium', 6, null, 'lanthanoid'],
|
|
118
|
+
['Dy', 'Dysprosium', 6, null, 'lanthanoid'],
|
|
119
|
+
['Ho', 'Holmium', 6, null, 'lanthanoid'],
|
|
120
|
+
['Er', 'Erbium', 6, null, 'lanthanoid'],
|
|
121
|
+
['Tm', 'Thulium', 6, null, 'lanthanoid'],
|
|
122
|
+
['Yb', 'Ytterbium', 6, null, 'lanthanoid'],
|
|
123
|
+
['Lu', 'Lutetium', 6, null, 'lanthanoid'],
|
|
124
|
+
['Hf', 'Hafnium', 6, 4, 'transition-metal'],
|
|
125
|
+
['Ta', 'Tantalum', 6, 5, 'transition-metal'],
|
|
126
|
+
['W', 'Tungsten', 6, 6, 'transition-metal'],
|
|
127
|
+
['Re', 'Rhenium', 6, 7, 'transition-metal'],
|
|
128
|
+
['Os', 'Osmium', 6, 8, 'transition-metal'],
|
|
129
|
+
['Ir', 'Iridium', 6, 9, 'transition-metal'],
|
|
130
|
+
['Pt', 'Platinum', 6, 10, 'transition-metal'],
|
|
131
|
+
['Au', 'Gold', 6, 11, 'transition-metal'],
|
|
132
|
+
['Hg', 'Mercury', 6, 12, 'transition-metal'],
|
|
133
|
+
['Tl', 'Thallium', 6, 13, 'post-transition-metal'],
|
|
134
|
+
['Pb', 'Lead', 6, 14, 'post-transition-metal'],
|
|
135
|
+
['Bi', 'Bismuth', 6, 15, 'post-transition-metal'],
|
|
136
|
+
['Po', 'Polonium', 6, 16, 'post-transition-metal'],
|
|
137
|
+
['At', 'Astatine', 6, 17, 'halogen'],
|
|
138
|
+
['Rn', 'Radon', 6, 18, 'noble-gas'],
|
|
139
|
+
['Fr', 'Francium', 7, 1, 'alkali-metal'],
|
|
140
|
+
['Ra', 'Radium', 7, 2, 'alkaline-earth-metal'],
|
|
141
|
+
['Ac', 'Actinium', 7, null, 'actinoid'],
|
|
142
|
+
['Th', 'Thorium', 7, null, 'actinoid'],
|
|
143
|
+
['Pa', 'Protactinium', 7, null, 'actinoid'],
|
|
144
|
+
['U', 'Uranium', 7, null, 'actinoid'],
|
|
145
|
+
['Np', 'Neptunium', 7, null, 'actinoid'],
|
|
146
|
+
['Pu', 'Plutonium', 7, null, 'actinoid'],
|
|
147
|
+
['Am', 'Americium', 7, null, 'actinoid'],
|
|
148
|
+
['Cm', 'Curium', 7, null, 'actinoid'],
|
|
149
|
+
['Bk', 'Berkelium', 7, null, 'actinoid'],
|
|
150
|
+
['Cf', 'Californium', 7, null, 'actinoid'],
|
|
151
|
+
['Es', 'Einsteinium', 7, null, 'actinoid'],
|
|
152
|
+
['Fm', 'Fermium', 7, null, 'actinoid'],
|
|
153
|
+
['Md', 'Mendelevium', 7, null, 'actinoid'],
|
|
154
|
+
['No', 'Nobelium', 7, null, 'actinoid'],
|
|
155
|
+
['Lr', 'Lawrencium', 7, null, 'actinoid'],
|
|
156
|
+
['Rf', 'Rutherfordium', 7, 4, 'transition-metal'],
|
|
157
|
+
['Db', 'Dubnium', 7, 5, 'transition-metal'],
|
|
158
|
+
['Sg', 'Seaborgium', 7, 6, 'transition-metal'],
|
|
159
|
+
['Bh', 'Bohrium', 7, 7, 'transition-metal'],
|
|
160
|
+
['Hs', 'Hassium', 7, 8, 'transition-metal'],
|
|
161
|
+
['Mt', 'Meitnerium', 7, 9, 'transition-metal'],
|
|
162
|
+
['Ds', 'Darmstadtium', 7, 10, 'transition-metal'],
|
|
163
|
+
['Rg', 'Roentgenium', 7, 11, 'transition-metal'],
|
|
164
|
+
['Cn', 'Copernicium', 7, 12, 'transition-metal'],
|
|
165
|
+
['Nh', 'Nihonium', 7, 13, 'post-transition-metal'],
|
|
166
|
+
['Fl', 'Flerovium', 7, 14, 'post-transition-metal'],
|
|
167
|
+
['Mc', 'Moscovium', 7, 15, 'post-transition-metal'],
|
|
168
|
+
['Lv', 'Livermorium', 7, 16, 'post-transition-metal'],
|
|
169
|
+
['Ts', 'Tennessine', 7, 17, 'halogen'],
|
|
170
|
+
['Og', 'Oganesson', 7, 18, 'noble-gas'],
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** Every element, ordered by atomic number. */
|
|
174
|
+
export const PERIODIC_ELEMENTS: readonly PeriodicElement[] = ROWS.map(
|
|
175
|
+
(row, index) => ({
|
|
176
|
+
atomicNumber: index + 1,
|
|
177
|
+
symbol: row[0],
|
|
178
|
+
name: row[1],
|
|
179
|
+
period: row[2],
|
|
180
|
+
group: row[3],
|
|
181
|
+
block: blockOf(index + 1, row[3]),
|
|
182
|
+
category: row[4],
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const BY_SYMBOL = new Map(
|
|
187
|
+
PERIODIC_ELEMENTS.map((element) => [element.symbol, element]),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Look up an element by its symbol.
|
|
192
|
+
* @param symbol - Chemical symbol, e.g. `Cl`. Case sensitive, as symbols are.
|
|
193
|
+
* @returns The element, or `undefined` when no element carries that symbol.
|
|
194
|
+
*/
|
|
195
|
+
export function elementBySymbol(symbol: string): PeriodicElement | undefined {
|
|
196
|
+
return BY_SYMBOL.get(symbol);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Look up an element by its atomic number.
|
|
201
|
+
* @param atomicNumber - Proton count, 1 to 118.
|
|
202
|
+
* @returns The element, or `undefined` when it is outside the table.
|
|
203
|
+
*/
|
|
204
|
+
export function elementByAtomicNumber(
|
|
205
|
+
atomicNumber: number,
|
|
206
|
+
): PeriodicElement | undefined {
|
|
207
|
+
return PERIODIC_ELEMENTS[atomicNumber - 1];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function blockOf(atomicNumber: number, group: number | null): ElementBlock {
|
|
211
|
+
if (group === null) return 'f';
|
|
212
|
+
// Helium sits above the p block but its only electrons are 1s.
|
|
213
|
+
if (atomicNumber === 2) return 's';
|
|
214
|
+
if (group <= 2) return 's';
|
|
215
|
+
if (group <= 12) return 'd';
|
|
216
|
+
return 'p';
|
|
217
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ElementBlock,
|
|
3
|
+
ElementCategory,
|
|
4
|
+
PeriodicElement,
|
|
5
|
+
} from './elements.ts';
|
|
6
|
+
export {
|
|
7
|
+
PERIODIC_ELEMENTS,
|
|
8
|
+
elementByAtomicNumber,
|
|
9
|
+
elementBySymbol,
|
|
10
|
+
} from './elements.ts';
|
|
11
|
+
export {
|
|
12
|
+
CATEGORY_LABELS,
|
|
13
|
+
CATEGORY_ORDER,
|
|
14
|
+
CATEGORY_SWATCHES,
|
|
15
|
+
UNKNOWN_SWATCH,
|
|
16
|
+
categorySwatch,
|
|
17
|
+
} from './categories.ts';
|
|
18
|
+
export type { Cell, ElementRange } from './layout.ts';
|
|
19
|
+
export {
|
|
20
|
+
COLUMN_COUNT,
|
|
21
|
+
INNER_TRANSITION_MARKERS,
|
|
22
|
+
INNER_TRANSITION_ROWS,
|
|
23
|
+
ROW_COUNT,
|
|
24
|
+
cellOf,
|
|
25
|
+
elementByArrowKey,
|
|
26
|
+
placedElements,
|
|
27
|
+
} from './layout.ts';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where every element is drawn.
|
|
3
|
+
*
|
|
4
|
+
* The group and the period place the main block, but not the lanthanoids and
|
|
5
|
+
* the actinoids: those have no group, and are drawn on the two rows underneath.
|
|
6
|
+
* The grid is therefore 18 columns by 10 rows — seven periods, a blank spacer,
|
|
7
|
+
* and the two inner-transition rows.
|
|
8
|
+
*
|
|
9
|
+
* The cells here are the *logical* ones, so column 1 is group 1 and row 1 is
|
|
10
|
+
* period 1. A table drawn with the group and period header strips offsets them
|
|
11
|
+
* by one; nothing in this module needs to know that.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ElementCategory, PeriodicElement } from './elements.ts';
|
|
15
|
+
import { PERIODIC_ELEMENTS, elementBySymbol } from './elements.ts';
|
|
16
|
+
|
|
17
|
+
/** A whole run of the table: one group, or one period. */
|
|
18
|
+
export interface ElementRange {
|
|
19
|
+
kind: 'group' | 'period';
|
|
20
|
+
value: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A cell of the grid, one-based as CSS grid lines are. */
|
|
24
|
+
export interface Cell {
|
|
25
|
+
column: number;
|
|
26
|
+
row: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Columns of the table. */
|
|
30
|
+
export const COLUMN_COUNT = 18;
|
|
31
|
+
|
|
32
|
+
/** Rows of the table, the blank spacer between the blocks included. */
|
|
33
|
+
export const ROW_COUNT = 10;
|
|
34
|
+
|
|
35
|
+
/** Row the lanthanoids are drawn on, and the actinoids on the one below. */
|
|
36
|
+
const INNER_TRANSITION_ROW = 9;
|
|
37
|
+
|
|
38
|
+
/** Atomic number the lanthanoids and the actinoids start at. */
|
|
39
|
+
const INNER_TRANSITION_STARTS = [57, 89] as const;
|
|
40
|
+
|
|
41
|
+
/** How many elements each inner-transition series holds. */
|
|
42
|
+
const INNER_TRANSITION_LENGTH = 15;
|
|
43
|
+
|
|
44
|
+
/** Column the inner-transition rows start at, under the d block. */
|
|
45
|
+
const INNER_TRANSITION_COLUMN = 3;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The cell an element occupies.
|
|
49
|
+
* @param element - Element to place.
|
|
50
|
+
* @returns Its column and row, both one-based.
|
|
51
|
+
*/
|
|
52
|
+
export function cellOf(element: PeriodicElement): Cell {
|
|
53
|
+
const inner = innerTransitionCell(element);
|
|
54
|
+
if (inner !== null) return inner;
|
|
55
|
+
return { column: element.group ?? 1, row: element.period };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every element with the cell it occupies, in reading order.
|
|
60
|
+
* @returns One entry per element, ordered by row then column, so a keyboard
|
|
61
|
+
* walk over the list moves the way the eye does.
|
|
62
|
+
*/
|
|
63
|
+
export function placedElements(): ReadonlyArray<{
|
|
64
|
+
element: PeriodicElement;
|
|
65
|
+
cell: Cell;
|
|
66
|
+
}> {
|
|
67
|
+
PLACED ??= PERIODIC_ELEMENTS.map((element) => ({
|
|
68
|
+
element,
|
|
69
|
+
cell: cellOf(element),
|
|
70
|
+
})).toSorted((first, second) =>
|
|
71
|
+
first.cell.row === second.cell.row
|
|
72
|
+
? first.cell.column - second.cell.column
|
|
73
|
+
: first.cell.row - second.cell.row,
|
|
74
|
+
);
|
|
75
|
+
return PLACED;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let PLACED: ReadonlyArray<{ element: PeriodicElement; cell: Cell }> | null =
|
|
79
|
+
null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The two cells the lanthanoids and the actinoids were lifted out of.
|
|
83
|
+
*
|
|
84
|
+
* Without them the main block has a hole in it and the two rows underneath
|
|
85
|
+
* belong nowhere; with them the reader can see where each series was taken
|
|
86
|
+
* from.
|
|
87
|
+
*/
|
|
88
|
+
export const INNER_TRANSITION_MARKERS: ReadonlyArray<{
|
|
89
|
+
cell: Cell;
|
|
90
|
+
label: string;
|
|
91
|
+
category: ElementCategory;
|
|
92
|
+
}> = [
|
|
93
|
+
{ cell: { column: 3, row: 6 }, label: '57–71', category: 'lanthanoid' },
|
|
94
|
+
{ cell: { column: 3, row: 7 }, label: '89–103', category: 'actinoid' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/** The period each inner-transition row belongs to, for its row label. */
|
|
98
|
+
export const INNER_TRANSITION_ROWS: ReadonlyArray<{
|
|
99
|
+
row: number;
|
|
100
|
+
period: number;
|
|
101
|
+
}> = [
|
|
102
|
+
{ row: INNER_TRANSITION_ROW, period: 6 },
|
|
103
|
+
{ row: INNER_TRANSITION_ROW + 1, period: 7 },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The element an arrow key walks to.
|
|
108
|
+
*
|
|
109
|
+
* The walk follows the grid, not the atomic number: down from carbon is
|
|
110
|
+
* silicon, the cell underneath it, and a step of 18 protons would instead be
|
|
111
|
+
* chromium because period 2 holds only eight elements. Left and right follow
|
|
112
|
+
* reading order, so the end of a period continues at the start of the next one
|
|
113
|
+
* and the two inner-transition rows come after the main block.
|
|
114
|
+
* @param key - The pressed key, as `KeyboardEvent.key` reports it.
|
|
115
|
+
* @param from - Symbol the walk starts at; any arrow lands on hydrogen when it names no element.
|
|
116
|
+
* @returns The element to move to, or `null` when the key is not an arrow or the table has no cell that way.
|
|
117
|
+
*/
|
|
118
|
+
export function elementByArrowKey(
|
|
119
|
+
key: string,
|
|
120
|
+
from: string | undefined,
|
|
121
|
+
): PeriodicElement | null {
|
|
122
|
+
const direction = ARROW_DIRECTIONS[key];
|
|
123
|
+
if (direction === undefined) return null;
|
|
124
|
+
|
|
125
|
+
const placed = placedElements();
|
|
126
|
+
const start = from === undefined ? undefined : elementBySymbol(from);
|
|
127
|
+
if (start === undefined) return placed[0]?.element ?? null;
|
|
128
|
+
|
|
129
|
+
if (direction.column !== 0) {
|
|
130
|
+
const index = readingOrderIndex().get(start.symbol);
|
|
131
|
+
if (index === undefined) return null;
|
|
132
|
+
return placed[index + direction.column]?.element ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const cell = cellOf(start);
|
|
136
|
+
return byCell().get(cellKey(cell.column, cell.row + direction.row)) ?? null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Which way each arrow key moves, in grid cells. */
|
|
140
|
+
const ARROW_DIRECTIONS: Record<string, { column: number; row: number }> = {
|
|
141
|
+
ArrowRight: { column: 1, row: 0 },
|
|
142
|
+
ArrowLeft: { column: -1, row: 0 },
|
|
143
|
+
ArrowDown: { column: 0, row: 1 },
|
|
144
|
+
ArrowUp: { column: 0, row: -1 },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function readingOrderIndex(): Map<string, number> {
|
|
148
|
+
READING_ORDER ??= new Map(
|
|
149
|
+
placedElements().map(({ element }, index) => [element.symbol, index]),
|
|
150
|
+
);
|
|
151
|
+
return READING_ORDER;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let READING_ORDER: Map<string, number> | null = null;
|
|
155
|
+
|
|
156
|
+
function byCell(): Map<string, PeriodicElement> {
|
|
157
|
+
BY_CELL ??= new Map(
|
|
158
|
+
placedElements().map(({ element, cell }) => [
|
|
159
|
+
cellKey(cell.column, cell.row),
|
|
160
|
+
element,
|
|
161
|
+
]),
|
|
162
|
+
);
|
|
163
|
+
return BY_CELL;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let BY_CELL: Map<string, PeriodicElement> | null = null;
|
|
167
|
+
|
|
168
|
+
function cellKey(column: number, row: number): string {
|
|
169
|
+
return `${String(column)}:${String(row)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function innerTransitionCell(element: PeriodicElement): Cell | null {
|
|
173
|
+
for (const [index, start] of INNER_TRANSITION_STARTS.entries()) {
|
|
174
|
+
const offset = element.atomicNumber - start;
|
|
175
|
+
if (offset >= 0 && offset < INNER_TRANSITION_LENGTH) {
|
|
176
|
+
return {
|
|
177
|
+
column: INNER_TRANSITION_COLUMN + offset,
|
|
178
|
+
row: INNER_TRANSITION_ROW + index,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key to the family colours, under the table.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CATEGORY_LABELS,
|
|
9
|
+
CATEGORY_ORDER,
|
|
10
|
+
categorySwatch,
|
|
11
|
+
} from '../core/categories.ts';
|
|
12
|
+
import type { ElementCategory } from '../core/elements.ts';
|
|
13
|
+
|
|
14
|
+
/** What {@link CategoryLegend} needs. */
|
|
15
|
+
export interface CategoryLegendProps {
|
|
16
|
+
/**
|
|
17
|
+
* Called with the family whose swatch was clicked. Without it the legend is
|
|
18
|
+
* a key rather than a control, and nothing in it is focusable.
|
|
19
|
+
* @default undefined
|
|
20
|
+
*/
|
|
21
|
+
onSelect?: (category: ElementCategory) => void;
|
|
22
|
+
/**
|
|
23
|
+
* The family drawn as the active one.
|
|
24
|
+
* @default undefined — none is
|
|
25
|
+
*/
|
|
26
|
+
selected?: ElementCategory;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The ten families, each with its colour.
|
|
31
|
+
* @param props - See {@link CategoryLegendProps}.
|
|
32
|
+
* @returns The legend.
|
|
33
|
+
*/
|
|
34
|
+
export function CategoryLegend(props: CategoryLegendProps): ReactElement {
|
|
35
|
+
const { onSelect, selected } = props;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={legendStyle}>
|
|
39
|
+
{CATEGORY_ORDER.map((category) => {
|
|
40
|
+
const swatch = categorySwatch(category);
|
|
41
|
+
const label = CATEGORY_LABELS[category];
|
|
42
|
+
const mark = (
|
|
43
|
+
<span style={{ ...swatchStyle, background: swatch.background }} />
|
|
44
|
+
);
|
|
45
|
+
if (onSelect === undefined) {
|
|
46
|
+
return (
|
|
47
|
+
<span key={category} style={itemStyle}>
|
|
48
|
+
{mark}
|
|
49
|
+
{label}
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<button
|
|
55
|
+
key={category}
|
|
56
|
+
type="button"
|
|
57
|
+
aria-pressed={category === selected}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
onSelect(category);
|
|
60
|
+
}}
|
|
61
|
+
style={{
|
|
62
|
+
...itemStyle,
|
|
63
|
+
...buttonStyle,
|
|
64
|
+
fontWeight: category === selected ? 700 : 400,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{mark}
|
|
68
|
+
{label}
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const legendStyle = {
|
|
77
|
+
color: 'rgb(95 107 124)',
|
|
78
|
+
display: 'flex',
|
|
79
|
+
flexWrap: 'wrap',
|
|
80
|
+
fontSize: 11,
|
|
81
|
+
gap: '2px 10px',
|
|
82
|
+
} as const satisfies CSSProperties;
|
|
83
|
+
|
|
84
|
+
const itemStyle = {
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
display: 'inline-flex',
|
|
87
|
+
gap: 4,
|
|
88
|
+
} as const satisfies CSSProperties;
|
|
89
|
+
|
|
90
|
+
const buttonStyle = {
|
|
91
|
+
background: 'none',
|
|
92
|
+
border: 'none',
|
|
93
|
+
color: 'inherit',
|
|
94
|
+
cursor: 'pointer',
|
|
95
|
+
font: 'inherit',
|
|
96
|
+
padding: 0,
|
|
97
|
+
} as const satisfies CSSProperties;
|
|
98
|
+
|
|
99
|
+
const swatchStyle = {
|
|
100
|
+
border: '1px solid rgb(255 255 255 / 0.55)',
|
|
101
|
+
borderRadius: 2,
|
|
102
|
+
display: 'inline-block',
|
|
103
|
+
height: 10,
|
|
104
|
+
width: 10,
|
|
105
|
+
} as const satisfies CSSProperties;
|