-
Notifications
You must be signed in to change notification settings - Fork 11
/
PeriodicTable.svelte
181 lines (165 loc) · 6.28 KB
/
PeriodicTable.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<script lang="ts">
import { goto } from '$app/navigation'
import * as d3sc from 'd3-scale-chromatic'
import type { Category, ChemicalElement, PeriodicTableEvents } from '.'
import { ElementPhoto, ElementTile, type ElementSymbol } from '.'
import element_data from './element-data'
import { elem_symbols } from './labels'
export let tile_props: {
show_name?: boolean
show_number?: boolean
show_symbol?: boolean
text_color_threshold?: number
} = {}
export let show_photo = true
export let style = ``
export let disabled = false // disable hover and click events from updating active_element
// either array of length 118 (one heat value for each element) or object with
// element symbol as key and heat value as value
export let heatmap_values: Record<ElementSymbol, number> | number[] = []
// links is either string with element property (name, symbol, number, ...) to use as link,
// or object with mapping element symbols to link
export let links: keyof ChemicalElement | Record<string, string> | null = null
export let log = false
export let color_scale: string | ((num: number) => string) = `Viridis`
export let active_element: ChemicalElement | null = null
export let active_category: Category | null = null
export let gap = `0.3cqw` // gap between element tiles, default is 0.3% of container width
export let inner_transition_metal_offset = 0.5
const default_lanth_act_tiles = [
{
name: `Lanthanides`,
symbol: `La-Lu`,
number: `57-71`,
category: `lanthanide`,
},
{
name: `Actinides`,
symbol: `Ac-Lr`,
number: `89-103`,
category: `actinide`,
},
]
// show lanthanides and actinides as tiles
export let lanth_act_tiles =
tile_props?.show_symbol == false ? [] : default_lanth_act_tiles
export let lanth_act_style: string = ``
type $$Events = PeriodicTableEvents // for type-safe event listening on this component
let heat_values: number[] = []
$: if (Array.isArray(heatmap_values)) {
if (heatmap_values.length > 118) {
console.error(
`heatmap_values is an array of numbers, length should be 118 or less, one for ` +
`each element possibly omitting elements at the end, got ${heatmap_values.length}`
)
} else heat_values = heatmap_values
} else if (typeof heatmap_values == `object`) {
const bad_keys = Object.keys(heatmap_values).filter(
(key) => !elem_symbols.includes(key)
)
if (bad_keys.length > 0) {
console.error(
`heatmap_values is an object, keys should be element symbols, got ${bad_keys}`
)
} else heat_values = elem_symbols.map((symbol) => heatmap_values[symbol])
}
$: cmap_max = Math.max(...heat_values)
$: set_active_element = (element: ChemicalElement | null) => () => {
if (disabled) return
active_element = element
}
let window_width: number
function handle_key(event: KeyboardEvent) {
if (disabled || !active_element) return
if (event.key == `Enter`) return goto(active_element.name.toLowerCase())
if (![`ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`].includes(event.key)) return
event.preventDefault() // prevent scrolling the page
event.stopPropagation()
// change the active element in the periodic table with arrow keys
// TODO doesn't allow navigating to lanthanides and actinides yet
const { column, row } = active_element
active_element =
element_data.find((elem) => {
return {
ArrowUp: elem.column == column && elem.row == row - 1,
ArrowDown: elem.column == column && elem.row == row + 1,
ArrowLeft: elem.column == column - 1 && elem.row == row,
ArrowRight: elem.column == column + 1 && elem.row == row,
}[event.key]
}) ?? active_element
}
$: c_scale =
typeof color_scale == `string` ? d3sc[`interpolate${color_scale}`] : color_scale
$: bg_color = (value: number | false): string | null => {
if (!heat_values?.length || !c_scale) return null
if (!value) return `transparent`
return c_scale(log ? Math.log(value) / Math.log(cmap_max) : value / cmap_max)
}
</script>
<svelte:window bind:innerWidth={window_width} on:keydown={handle_key} />
<div class="periodic-table-container" {style}>
<div class="periodic-table" style:gap>
<slot name="inset" {active_element} />
{#each element_data as element, idx}
{@const { column, row, category, name, symbol } = element}
{@const value = heat_values[idx]}
{@const active =
active_category === category.replaceAll(` `, `-`) ||
active_element?.name === name}
<ElementTile
{element}
href={links
? typeof links == `string`
? `${element[links]}`.toLowerCase()
: links[symbol]
: null}
style="grid-column: {column}; grid-row: {row};"
{value}
bg_color={bg_color(value)}
{active}
{...tile_props}
on:mouseenter={set_active_element(element)}
on:mouseleave={set_active_element(null)}
on:focus={set_active_element(element)}
on:blur={set_active_element(null)}
on:click
on:mouseenter
on:mouseleave
/>
{/each}
<!-- show tile for lanthanides and actinides with text La-Lu and Ac-Lr respectively -->
{#each lanth_act_tiles || [] as element, idx}
<ElementTile
{element}
style="opacity: 0.8; grid-column: 3; grid-row: {6 + idx}; {lanth_act_style};"
on:mouseenter={() => (active_category = element.category)}
on:mouseleave={() => (active_category = null)}
symbol_style="font-size: 30cqw;"
/>
{/each}
{#if inner_transition_metal_offset}
<!-- provide vertical offset for lanthanides + actinides -->
<div class="spacer" style:aspect-ratio={1 / inner_transition_metal_offset} />
{/if}
<slot name="bottom-left-inset" {active_element}>
{#if show_photo}
<ElementPhoto element={active_element} style="grid-area: 9/1/span 2/span 2;" />
{/if}
</slot>
</div>
</div>
<style>
.periodic-table-container {
/* needed for gap: 0.3cqw; to work */
container-type: inline-size;
}
div.periodic-table {
display: grid;
grid-template-columns: repeat(18, 1fr);
position: relative;
container-type: inline-size;
}
div.spacer {
grid-row: 8;
}
</style>