// Draws the paper's figures as SVG into figures/, from data/*.json and numerics/*.json.
// Colours and type come from classes defined in paper/page.css, so the drawings follow
// the page theme. Every mark carries a
with its value and source.
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.dirname(fileURLToPath(import.meta.url))
const json = p => JSON.parse(fs.readFileSync(path.join(root, p), 'utf8'))
const out = (name, svg) => fs.writeFileSync(path.join(root, 'figures', `${name}.svg`), svg)
const esc = s => String(s).replace(/&/g, '&').replace(/ Math.round(x * 10) / 10
const lin = (d0, d1, r0, r1_) => v => r0 + (v - d0) / (d1 - d0) * (r1_ - r0)
const log = (d0, d1, r0, r1_) => v => r0 + (Math.log(v) - Math.log(d0)) / (Math.log(d1) - Math.log(d0)) * (r1_ - r0)
const text = (x, y, s, o = {}) =>
`${esc(s)}`
const line = (x1, y1, x2, y2, cls) => ``
const dot = (x, y, cls, title, r = 4.5) =>
`${esc(title)}`
const hollow = (x, y, strokeCls, title, r = 4.5) =>
`${esc(title)}`
const frame = (w, h, label, body) =>
`\n`
// Record Tc against year.
{
const d = json('data/records.json')
const W = 720, H = 380, L = 44, R = 150, T = 34, B = 34
const x = lin(1905, d.end_year + 1, L, W - R), y = lin(0, 320, H - B, T)
let s = ''
for (const t of [0, 50, 100, 150, 200, 250, 300]) {
s += line(L, y(t), W - R, y(t), t === 0 ? 'axis' : 'grid') + text(L - 8, y(t) + 4, t, { anchor: 'end' })
}
for (const yr of [1920, 1940, 1960, 1980, 2000, 2020]) s += text(x(yr), H - B + 18, yr, { anchor: 'middle' })
s += text(2, T - 14, 'Tc (K)')
const step = (pts, end, startAt) => {
let p = startAt ? `M${r1(x(startAt.year))},${r1(y(startAt.tc))}` : `M${r1(x(pts[0].year))},${r1(y(0))}`
let last = startAt ? startAt.tc : 0
for (const q of pts) { p += `L${r1(x(q.year))},${r1(y(last))}L${r1(x(q.year))},${r1(y(q.tc))}`; last = q.tc }
return p + `L${r1(x(end))},${r1(y(last))}`
}
// the any-pressure series starts from the ambient record standing when it first rose above it
const ambTop = d.ambient.filter(q => q.year <= d.any[0].year).pop()
s += ``
s += ``
for (const q of d.ambient.filter(q => q.year >= 1986)) s += dot(x(q.year), y(q.tc), 'f1', `${q.material}: ${q.tc} K, ${q.year}`, 3.5)
for (const q of d.any) s += dot(x(q.year), y(q.tc), 'f2', `${q.material}: ${q.tc} K, ${q.year}`, 3.5)
for (const q of d.unreproduced) s += hollow(x(q.year), y(q.tc), q.series === 'ambient' ? 's1' : 's2', `${q.material}: ${q.tc} K, ${q.year}, not independently reproduced`)
const xr = x(d.end_year) + 10
s += text(xr, y(250) + 4, 'Any pressure: 250 K', { cls: 't-ink' })
s += text(xr, y(250) + 19, 'LaH₁₀ at 170 GPa')
s += text(xr, y(138) + 4, 'Ambient pressure: 138 K', { cls: 't-ink' })
s += text(xr, y(138) + 19, 'Hg cuprates, since 1993')
s += text(xr, y(298) + 4, 'LaSc₂H₂₄, not reproduced')
s += text(xr, y(151) - 15, 'quenched Hg-1223,')
s += text(xr, y(151) - 1, 'one group')
s += text(x(1987) - 8, y(93) + 4, 'YBa₂Cu₃O₇', { anchor: 'end' })
s += text(x(2015) - 8, y(203) - 6, 'H₃S', { anchor: 'end' })
s += text(x(1994) - 8, y(164) - 6, 'Hg-1223 at 31 GPa', { anchor: 'end' })
s += text(x(1973), y(22.3) - 10, 'Nb₃Ge', { anchor: 'middle' })
// legend
const ly = 14, LX = 90
s += `` + text(LX + 28, ly + 4, 'Ambient pressure')
s += `` + text(LX + 178, ly + 4, 'Any pressure')
s += hollow(LX + 290, ly, 'muted-line', 'single group, not reproduced', 4) + text(LX + 300, ly + 4, 'Single group, not reproduced')
out('records', frame(W, H, 'Record superconducting transition temperature by year at ambient pressure and at any pressure', s))
}
// Coupling strength against phonon frequency, with Eliashberg isotherms.
{
const d = json('data/plane.json'), req = json('numerics/requirements.json')
const W = 720, H = 430, L = 56, R = 64, T = 34, B = 44
const XMAX = 4.1
const x = lin(0.5, XMAX, L, W - R), y = log(40, 4000, H - B, T)
let s = ``
for (const t of [50, 100, 200, 500, 1000, 2000]) s += line(L, y(t), W - R, y(t), 'grid') + text(L - 8, y(t) + 4, t, { anchor: 'end' })
s += line(L, H - B, W - R, H - B, 'axis')
for (const t of [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]) s += line(x(t), H - B, x(t), H - B + 4, 'axis') + text(x(t), H - B + 18, t.toFixed(1), { anchor: 'middle' })
s += text((L + W - R) / 2, H - 6, 'Electron-phonon coupling λ', { anchor: 'middle' })
s += text(2, T - 14, 'ω_log (K)')
const iso = k => req.isotherms[k].filter(p => p.lambda >= 0.5 && p.lambda <= XMAX + 0.01)
const pathOf = pts => pts.map((p, i) => `${i ? 'L' : 'M'}${r1(x(p.lambda))},${r1(y(p.omega_K))}`).join('')
const top = iso('300')
s += ``
for (const k of ['39', '100', '200', '300']) {
const pts = iso(k), end = pts[pts.length - 1]
s += ``
s += text(W - R + 6, y(end.omega_K) + 4, `${k} K`, { cls: k === '300' ? 't-ink' : '' })
}
s += text(x(3.3), y(2900), 'Tc above 300 K', { anchor: 'middle' })
const cls = { pressure: 'f2', predicted: 'f1', ambient: 'f3' }
for (const p of d.points) s += dot(x(p.lambda), y(p.omega_log), cls[p.group], `${p.label}: λ = ${p.lambda}, ω_log = ${p.omega_log} K. ${p.detail}`)
const at = (lam, om, dx, dy, str, anchor) => text(x(lam) + dx, y(om) + dy, str, { anchor })
s += at(2.67, 1119, 9, 4, 'LaH₁₀') + at(1.84, 1078, -9, 4, 'H₃S', 'end') + at(2.0, 1253, 0, -10, 'CaH₆, YH₆', 'middle')
s += ``
s += at(2.56, 391, 4, 18, 'Mg₂IrH₆, two calculations', 'middle') + at(1.96, 491, -9, 12, 'Li₂CuH₆', 'end')
s += at(2.21, 609, 9, -2, 'PdH₄')
s += at(1.35, 766, 0, -11, 'Mg₂RhH₆, Mg₂PtH₆', 'middle')
s += at(3.82, 328, 0, 22, 'Li₂AgH₆, Li₂AuH₆', 'middle') + at(3.28, 294, -9, 14, 'B₂C₈Cl', 'end')
s += at(0.87, 725, -9, 4, 'MgB₂', 'end') + at(1.24, 154, 9, 4, 'Nb') + at(1.45, 58, 9, 4, 'Pb')
const ly = 14, LX = 110
s += dot(LX + 5, ly, 'f2', 'measured; exists only under pressure') + text(LX + 15, ly + 4, 'Measured, 150 GPa or more')
s += dot(LX + 205, ly, 'f1', 'predicted at 1 atm') + text(LX + 215, ly + 4, 'Predicted at 1 atm')
s += dot(LX + 355, ly, 'f3', 'measured at 1 atm') + text(LX + 365, ly + 4, 'Measured at 1 atm')
out('plane', frame(W, H, 'Published coupling strength and logarithmic phonon frequency of superconductors against isotherms of transition temperature from the Eliashberg equations', s))
}
// Predicted Tc against energy above the hull.
{
const d = json('data/frontier.json')
const W = 720, H = 420, L = 44, R = 24, T = 34, B = 44
const x = lin(-62, 340, L, W - R), y = lin(0, 190, H - B, T)
let s = ''
for (const t of [0, 50, 100, 150]) s += line(L, y(t), W - R, y(t), t === 0 ? 'axis' : 'grid') + text(L - 8, y(t) + 4, t, { anchor: 'end' })
for (const t of [0, 100, 200, 300]) s += line(x(t), H - B, x(t), H - B + 4, 'axis') + text(x(t), H - B + 18, t, { anchor: 'middle' })
s += text((x(0) + W - R) / 2, H - 6, 'Energy above the convex hull (meV per atom)', { anchor: 'middle' })
s += text(2, T - 14, 'Tc (K)')
s += line(x(d.metastability_median), T + 6, x(d.metastability_median), H - B, 'muted-line')
s += line(x(d.metastability_p90), T + 6, x(d.metastability_p90), H - B, 'muted-line')
s += text(x(d.metastability_median) + 5, y(4), 'median')
s += text(x(d.metastability_p90) + 5, y(4), '90th percentile of known metastable phases')
// Each compound is drawn at its current hull distance. A thin whisker runs back to the
// value first published. Points that share a hull distance are nudged apart.
const nudge = { 'MgB₂': -5, 'LiMoN₂': 5, 'Mg₂PtH₆': -6, 'Mg₂PdH₆': 6, 'Li₂AgH₆': -6, 'PdH₄': 6 }
const whiskerAt = { 'Mg₂PdH₆': 0, 'KInH₃': 0 } // 0 = lower end of the Tc range, default upper end
for (const p of d.points) {
const cx = x(p.hull[1]) + (nudge[p.label] || 0)
const c = p.kind === 'measured' ? 3 : 1
const tcText = p.tc[0] === p.tc[1] ? p.tc[0] : `${p.tc[0]} to ${p.tc[1]}`
const hullText = p.hull[0] === p.hull[1] ? p.hull[1] : `${p.hull[1]} (first published as ${p.hull[0]})`
const title = `${p.label}: Tc ${tcText} K, ${hullText} meV per atom above the hull. ${p.status}`
if (p.hull[0] !== p.hull[1]) {
const yw = y(p.tc[p.label in whiskerAt ? whiskerAt[p.label] : 1])
s += ``
s += ``
}
if (p.tc[0] !== p.tc[1]) s += `${esc(title)}`
s += dot(cx, y(p.tc[1]), `f${c}`, title, 4)
if (p.tc[0] !== p.tc[1]) s += dot(cx, y(p.tc[0]), `f${c}`, title, 4)
}
const at = (h, t, dx, dy, str, anchor, cls) => text(x(h) + dx, y(t) + dy, str, { anchor, cls })
s += at(0, 39, -13, 6, 'MgB₂, measured', 'end', 't-ink') + at(6, 46, -6, 0, 'LiMoN₂', 'end') + at(0, 22.9, -10, 4, 'Mo₂Pr₃N₆', 'end')
s += at(67, 45, 6, 12, 'Mg₂RhH₆', null, 't-ink') + at(67, 45, 6, 26, 'lost on pressure release')
s += at(85.5, 175, 0, -22, 'Mg₂IrH₆', 'end', 't-ink') + at(85.5, 175, 0, -8, 'not formed', 'end')
s += at(114.6, 73, 9, -8, 'KInH₃')
s += at(195.7, 80, 16, 0, 'Mg₂PtH₆', null, 't-ink') + at(195.7, 80, 16, 14, 'Mg₄Pt₃H₆ formed')
s += at(195.7, 51, 16, 8, 'Mg₂PdH₆')
s += at(171.5, 140, -9, 0, 'Li₂AuH₆', 'end', 't-ink') + at(171.5, 140, -9, 14, 'unstable in dynamics', 'end')
s += at(319.1, 108, -16, 0, 'Li₂AgH₆', 'end', 't-ink') + at(319.1, 108, -16, 14, 'collapses in dynamics', 'end')
s += at(318.9, 146, 10, -22, 'PdH₄', 'end', 't-ink') + at(318.9, 146, 10, -8, 'harmonic only', 'end')
const ly = 14, LX = 90
s += dot(LX + 5, ly, 'f1', 'calculated') + text(LX + 15, ly + 4, 'Calculated, published range')
s += dot(LX + 205, ly, 'f3', 'measured') + text(LX + 215, ly + 4, 'Measured')
s += `` + text(LX + 318, ly + 4, 'Hull distance when first published')
out('frontier', frame(W, H, 'Calculated transition temperature against energy above the convex hull for ambient-pressure candidates', s))
}
// Survival function of calculated Tc by distance from the hull.
{
const d = json('numerics/tails.json')
const W = 720, H = 400, L = 56, R = 24, T = 34, B = 44
const x = lin(0, 118, L, W - R), y = log(1e-4, 1, H - B, T)
let s = ``
for (const [v, lab] of [[1, '1'], [0.1, '0.1'], [0.01, '0.01'], [0.001, '0.001'], [0.0001, '0.0001']]) s += line(L, y(v), W - R, y(v), v === 1e-4 ? 'axis' : 'grid') + text(L - 8, y(v) + 4, lab, { anchor: 'end' })
for (const t of [0, 20, 40, 60, 80, 100]) s += line(x(t), H - B, x(t), H - B + 4, 'axis') + text(x(t), H - B + 18, t, { anchor: 'middle' })
s += text((L + W - R) / 2, H - 6, 'Calculated Tc (K)', { anchor: 'middle' })
s += text(2, T - 14, 'Fraction at or above')
const grid = d.survival.grid
const merge = (a, na, b, nb) => a.map((v, i) => (v * na + b[i] * nb) / (na + nb))
const st = Object.fromEntries(d.strata.map(q => [`${q.lo}-${q.hi}`, q]))
const near = merge(d.survival['0-25'], st['0-25'].n, d.survival['25-50'], st['25-50'].n)
const series = [
{ key: 'ord1', vals: near, label: '0 to 50' },
{ key: 'ord2', vals: d.survival['50-100'], label: '50 to 100' },
{ key: 'ord3', vals: d.survival['100-200'], label: '100 to 200' },
{ key: 'ord4', vals: d.survival['200-376'], label: '200 to 376' },
]
// Exponential fit to the first curve: solid over the range it describes (2 to 30 K),
// dashed beyond, where the two highest compounds lie above it.
const f = d.near_hull_fit
const fit = t => f.p_u * Math.exp(-(t - f.u) / f.tau)
s += ``
s += ``
for (const q of series.slice().reverse()) {
let p = ''
grid.forEach((g, i) => { if (q.vals[i] > 0) p += `${p ? 'L' : 'M'}${r1(x(g))},${r1(y(Math.max(q.vals[i], 1e-4)))}` })
s += `${esc(`${q.label} meV per atom above the hull`)}`
}
// The two compounds above the fit.
const top = d.pareto_front.filter(q => q.e_hull <= 50).sort((a, b) => b.tc - a.tc)
s += text(x(top[1].tc) + 5, y(2 / f.n) - 5, `LiMoN₂, ${top[1].tc.toFixed(1)} K`)
s += text(x(top[0].tc) + 6, y(1 / f.n) + 1, `Mg₂RhH₆, ${top[0].tc.toFixed(1)} K`)
// Key, in the empty corner above the curves.
const kx = x(66), ky = T + 12
s += text(kx, ky + 4, 'Energy above the convex hull (meV per atom)', { cls: 't-ink' })
series.forEach((q, i) => { s += `` + text(kx + 28, ky + 18 * (i + 1) + 4, q.label) })
s += `` + text(kx + 28, ky + 94, `Exponential fit to 0 to 50, scale ${f.tau.toFixed(1)} K;`)
s += text(kx + 28, ky + 108, 'dashed beyond 30 K')
out('tail', frame(W, H, 'Fraction of compounds with calculated transition temperature at or above a given value, in four ranges of energy above the convex hull', s))
}
// Hopfield parameter of hydrogen against hydrogen number density: the release and the megabar hydrides.
{
const pts = json('numerics/h_alexandria_points.json')
const stat = json('numerics/h_alexandria.json')
const mb = json('data/h_census.json').rows.filter(r => r.include_in_stats && r.group === 'megabar')
const W = 720, H = 460, L = 52, R = 96, T = 34, B = 44
const x = log(0.002, 0.6, L, W - R), y = log(0.01, 20, H - B, T)
const K = 136.5
let s = ``
for (const t of [0.01, 0.1, 1, 10]) s += line(L, y(t), W - R, y(t), t === 0.01 ? 'axis' : 'grid') + text(L - 8, y(t) + 4, t, { anchor: 'end' })
for (const t of [0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5]) s += line(x(t), H - B, x(t), H - B + 4, 'axis') + text(x(t), H - B + 18, t, { anchor: 'middle' })
s += text((L + W - R) / 2, H - 6, 'Hydrogen number density (atoms per ų)', { anchor: 'middle' })
s += text(2, T - 14, 'Hopfield parameter of hydrogen (eV/Ų)')
s += ``
s += text(L + 8, y(10.2) + 4, 'needed for 300 K, single mode')
for (const t of [50, 100, 200, 300, 400, 500]) { const sv = (t / K) ** 2; s += line(W - R, y(sv), W - R + 4, y(sv), 'axis') + text(W - R + 8, y(sv) + 4, `${t} K`) }
s += text(W - R + 8, T - 14, 'asymptote')
const hl = (h, cls, w) => ``
const gm = stat.h.geometric_mean
s += hl(36.6, 'ink-line', 1.25) + hl(gm, 'muted-line', 1)
const kx = x(0.105), ky = y(0.075)
s += `` + text(kx + 28, ky + 4, '36.6 eV Å per proton:', { cls: 't-ink' }) + text(kx + 28, ky + 18, 'one proton in an electron gas', { cls: 't-ink' })
s += `` + text(kx + 28, ky + 40, `${gm.toFixed(1)} eV Å per proton:`) + text(kx + 28, ky + 54, 'geometric mean at 1 atm')
s += line(x(0.090), y(7.5), x(0.090), H - B, 'muted-line')
s += text(x(0.090) + 6, H - B - 22, 'densest hydrogen stable')
s += text(x(0.090) + 6, H - B - 8, 'at 1 atm (TiH₂, Mg₂FeH₆)')
// the release: one small mark per compound, no tooltip (4,619 of them)
let dots = ''
for (let i = 0; i < pts.rho.length; i++) dots += ``
s += `${dots}`
const val = r => r.eta_H_eV_A2 ?? r.S_eV_A2
for (const name of [...new Set(mb.map(r => r.compound))]) {
const q = mb.filter(r => r.compound === name).sort((a, b) => a.pressure_GPa - b.pressure_GPa)
if (q.length > 1) s += ``
}
for (const r of mb) s += dot(x(r.rho_H), y(val(r)), 'f2', `${r.compound} at ${r.pressure_GPa} GPa (calculated): ${r.rho_H.toFixed(3)} H per ų, ${val(r).toFixed(1)} eV/Ų, h = ${r.h_eV_A.toFixed(0)} eV Å`, 4)
// the five largest values at one atmosphere, drawn over the cloud and named
const top = stat.largest_eta_H.slice(0, 5)
for (const r of top) s += dot(x(r.rho_H), y(r.eta_H), 'f1', `${r.formula}, 1 atm (calculated): ${r.rho_H} H per ų, ${r.eta_H} eV/Ų, h = ${r.h} eV Å`, 3.5)
s += text(x(0.064) - 10, y(5.5) + 4, 'KPtH₆, RbPtH₆, CsPtH₆,', { anchor: 'end' }) + text(x(0.064) - 10, y(5.5) + 18, 'RbNiH₆, CsNiH₆', { anchor: 'end' })
const find = (name, p) => mb.find(r => r.compound === name && (p === undefined || r.pressure_GPa === p))
const lab = (r, dx, dy, str, anchor) => r ? text(x(r.rho_H) + dx, y(val(r)) + dy, str, { anchor }) : ''
s += lab(find('SH3', 220), -8, -8, 'H₃S', 'end') + lab(find('CaH6', 150), -8, 14, 'CaH₆', 'end') + lab(find('LaH10', 300), 7, -7, 'LaH₁₀')
s += lab(find('MgH6', 300), -8, -5, 'MgH₆', 'end') + lab(find('YH10', 400), -4, -10, 'YH₁₀', 'middle')
const ly = 14, LX = 250
s += `` + text(LX + 15, ly + 4, `Calculated at 1 atm (${pts.rho.length.toLocaleString('en-US')})`)
s += dot(LX + 215, ly, 'f2', 'calculated at megabar pressure', 4) + text(LX + 225, ly + 4, 'Calculated, megabar')
out('packing', frame(W, H, `Hopfield parameter of hydrogen against hydrogen number density for ${pts.rho.length} hydrides calculated at one atmosphere and five superhydrides at megabar pressure`, s))
}
// Distribution of the scattering strength per proton at one atmosphere.
{
const pts = json('numerics/h_alexandria_points.json')
const mb = json('data/h_census.json').rows.filter(r => r.include_in_stats && r.group === 'megabar')
const W = 720, H = 360, L = 52, R = 24, T = 44, B = 44
const per = 8, lo = 0.4, n = Math.ceil(Math.log10(120 / lo) * per)
const bin = h => Math.floor(Math.log10(h / lo) * per)
const all = new Array(n).fill(0), hot = new Array(n).fill(0)
for (let i = 0; i < pts.rho.length; i++) {
const b = bin(pts.eta[i] / pts.rho[i])
if (b >= 0 && b < n) { all[b]++; if (pts.tc20[i]) hot[b]++ }
}
const ymax = Math.ceil(Math.max(...all) / 100) * 100
const x = log(lo, lo * 10 ** (n / per), L, W - R), y = lin(0, ymax, H - B, T)
let s = ''
for (let t = 0; t <= ymax; t += 200) s += line(L, y(t), W - R, y(t), t === 0 ? 'axis' : 'grid') + text(L - 8, y(t) + 4, t, { anchor: 'end' })
for (const t of [0.5, 1, 2, 5, 10, 20, 50, 100]) s += line(x(t), H - B, x(t), H - B + 4, 'axis') + text(x(t), H - B + 18, t, { anchor: 'middle' })
s += text((L + W - R) / 2, H - 6, 'Scattering strength per proton, h (eV Å)', { anchor: 'middle' })
s += text(2, T - 24, 'Compounds')
const edge = b => lo * 10 ** (b / per)
for (let b = 0; b < n; b++) {
const x0 = x(edge(b)) + 1, w = x(edge(b + 1)) - x(edge(b)) - 2
if (all[b]) s += `${edge(b).toFixed(1)} to ${edge(b + 1).toFixed(1)} eV Å: ${all[b]} compounds, ${hot[b]} of them calculated above 20 K`
if (hot[b]) s += ``
}
s += line(x(36.6), T - 6, x(36.6), H - B, 'ink-line')
s += text(x(36.6) + 6, T + 34, 'one proton in an', { cls: 't-ink' }) + text(x(36.6) + 6, T + 48, 'electron gas, 36.6', { cls: 't-ink' })
// megabar compounds, one tick per pressure point
for (const r of mb) s += `${esc(r.compound)} at ${r.pressure_GPa} GPa: h = ${r.h_eV_A.toFixed(1)} eV Å`
s += text(x(23) - 6, T + 12, 'megabar hydrides', { anchor: 'end' })
const ly = T - 24, LX = 150
s += `` + text(LX + 18, ly + 4, `All ${pts.rho.length.toLocaleString('en-US')} labelled hydrides at 1 atm`)
s += `` + text(LX + 288, ly + 4, `Calculated Tc above 20 K (${pts.tc20.reduce((a, b) => a + b, 0)})`)
out('hdist', frame(W, H, 'Distribution of the scattering strength per proton among hydrides calculated at one atmosphere, with the subset calculated above 20 K', s))
}
console.log('figures written')