ns = Inputs.text().classList[0]
// custom css to override some ojs defaults for inputs
html`<style>
.${ns} {
--label-width: 80px;
}
form.${ns} {
flex-wrap: wrap;
}
.plot-inputs form.${ns} {
flex-direction: column;
}
.${ns} div label {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
width: auto;
}
.${ns} div label:hover,
.${ns} div label:active,
.${ns} div label:focus {
background-color: #fbe4b4;
}
.${ns} input[type="checkbox"] {
accent-color: black;
margin-bottom: 0;
}
.${ns} div input[type="number"] {
background-color: #f4f4f4;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
flex-shrink: 3;
border: none;
}
.${ns} select {
background-color: #f4f4f4;
border: none;
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
//width: auto;
}
.${ns} .hist {
width: 100%;
display: flex;
flex-direction: column;
row-gap: 0em;
}
</style>`tol = ({
QualBright: ['#4477AA', '#EE6677', '#228833', '#CCBB44', '#66CCEE','#AA3377'],
QualHighContrast: ['#004488', '#DDAA33', '#BB5566'],
QualVibrant: ['#EE7733', '#0077BB', '#33BBEE', '#EE3377', '#CC3311', '#009988'],
QualMuted: ['#CC6677', '#332288', '#DDCC77', '#117733', '#88CCEE','#882255', '#44AA99', '#999933', '#AA4499'],
QualLight: ['#77AADD', '#EE8866', '#EEDD88', '#FFAABB', '#99DDFF', '#44BB99', '#BBCC33', '#AAAA00'],
Sunset: ['#364B9A', '#4A7BB7', '#6EA6CD', '#98CAE1', '#C2E4EF', '#EAECCC', '#FEDA8B', '#FDB366', '#F67E4B', '#DD3D2D', '#A50026'],
BuRd: ['#2166AC', '#4393C3', '#92C5DE', '#D1E5F0', '#F7F7F7', '#FDDBC7', '#F4A582', '#D6604D', '#B2182B'],
PRGn: ['#762A83', '#9970AB', '#C2A5CF', '#E7D4E8', '#F7F7F7', '#D9F0D3', '#ACD39E', '#5AAE61', '#1B7837'],
YlOrBr: ['#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FB9A29',
'#EC7014', '#CC4C02', '#993404', '#662506'],
Iridescent: ['#FEFBE9', '#FCF7D5', '#F5F3C1', '#EAF0B5', '#DDECBF',
'#D0E7CA', '#C2E3D2', '#B5DDD8', '#A8D8DC', '#9BD2E1',
'#8DCBE4', '#81C4E7', '#7BBCE7', '#7EB2E4', '#88A5DD',
'#9398D2', '#9B8AC4', '#9D7DB2', '#9A709E', '#906388',
'#805770', '#684957', '#46353A'],
RainbowPuRd: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
'#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
'#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
'#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
'#DF4828', '#DA2222'],
RainbowPuBr: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
'#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
'#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
'#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
'#DF4828', '#DA2222', '#B8221E', '#95211B', '#721E17',
'#521A13'],
RainbowWhRd: ['#E8ECFB', '#DDD8EF', '#D1C1E1', '#C3A8D1', '#B58FC2',
'#A778B4', '#9B62A7', '#8C4E99', '#6F4C9B', '#6059A9',
'#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC', '#549EB3',
'#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
'#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39',
'#E78C35', '#E67932', '#E4632D', '#DF4828', '#DA2222'],
RainbowDiscrete: ['#E8ECFB', '#D9CCE3', '#D1BBD7', '#CAACCB', '#BA8DB4',
'#AE76A3', '#AA6F9E', '#994F88', '#882E72', '#1965B0',
'#437DBF', '#5289C7', '#6195CF', '#7BAFDE', '#4EB265',
'#90C987', '#CAE0AB', '#F7F056', '#F7CB45', '#F6C141',
'#F4A736', '#F1932D', '#EE8026', '#E8601C', '#E65518',
'#DC050C', '#A5170E', '#72190E', '#42150A']
})Plot = import("https://esm.sh/@observablehq/plot@0.6.17")
co2rr = transpose(co2rr_)
toNumber = v => v == null || v === "" ? NaN : Number(v)
data = co2rr.map(d => ({
...d,
Product: d.Product || "Unknown",
FE: toNumber(d.FE),
J: toNumber(d.J),
Year: toNumber(d.Year),
Stability: toNumber(d.Stability),
Efull: toNumber(d.Efull),
Ecathode: toNumber(d.Ecathode),
REType: d["RE type"] || "NA"
}))
products = Array.from(new Set(data.map(d => d.Product))).sort(d3.ascending)
scheme = tol.QualMuted
yLabels = ({
FE: "Faradaic efficiency, FE (%)",
J: "Current density, J (mA cm-2)"
})
colorLabels = ({
FE: "Faradaic efficiency, FE (%)",
J: "Current density, J (mA cm-2)",
Stability: "Stability, ln(h)"
})Source Data
This page traces representative CO2RR measurements by publication year.
sourceData = data.filter(d =>
d.Product === selectedProduct &&
Number.isFinite(d.Year) &&
Number.isFinite(d[yVar]) &&
Number.isFinite(d[colorVar])
).map(d => ({
...d,
ColorValue: colorVar === "Stability" ? Math.log(d.Stability) : d[colorVar]
}))
yearDomain = d3.extent(data.filter(d => Number.isFinite(d.Year)), d => d.Year)
yearTicks = d3.range(yearDomain[0], yearDomain[1] + 1, 1)
colorDomain = d3.extent(sourceData, d => d.ColorValue)
formatMaybe = (label, value, suffix = "") => {
if (value == null || value === "" || (typeof value === "number" && !Number.isFinite(value))) return null
return `${label}: ${value}${suffix}`
}
fullTooltip = d => [
formatMaybe("Product", d.Product),
formatMaybe("Year", d.Year),
formatMaybe("FE", d.FE, "%"),
formatMaybe("J", d.J, " mA cm-2"),
formatMaybe("Efull", d.Efull, " V"),
formatMaybe("Ecathode", d.Ecathode, " V"),
d.REType !== "NA" ? `RE type: ${d.REType}` : null,
formatMaybe("Stability", d.Stability, " h"),
d.Reference ? `Reference: ${d.Reference}` : null
].filter(Boolean).join("\n")sourcePlot = {
const legendDomain = colorDomain[0] === colorDomain[1]
? [colorDomain[0] - 1, colorDomain[1] + 1]
: colorDomain
const legendWidth = 450
const legendHeight = 24
const legendTicks = d3.ticks(legendDomain[0], legendDomain[1], 5)
const legendFormat = d3.format(",")
const gradientId = `source-color-ramp-${colorVar}`
const colorRamp = d3.scaleLinear()
.domain(legendDomain)
.range(["blue", "green"])
const sourceLegend = html`<div class="source-color-legend">
<div class="source-color-legend-label">${colorLabels[colorVar]}</div>
<svg width="${legendWidth}" height="64" viewBox="0 0 ${legendWidth} 64" role="img" aria-label="${colorLabels[colorVar]} color legend">
<defs>
<linearGradient id="${gradientId}" x1="0%" x2="100%" y1="0%" y2="0%">
${d3.range(0, 1.01, 0.1).map(t => svg`<stop offset="${t * 100}%" stop-color="${colorRamp(legendDomain[0] + t * (legendDomain[1] - legendDomain[0]))}"></stop>`)}
</linearGradient>
</defs>
<rect x="0" y="8" width="${legendWidth}" height="${legendHeight}" fill="url(#${gradientId})"></rect>
${legendTicks.map(t => {
const x = legendWidth * ((t - legendDomain[0]) / (legendDomain[1] - legendDomain[0]))
return svg`<g transform="translate(${x}, 32)">
<line y2="10"></line>
<text y="28">${legendFormat(t)}</text>
</g>`
})}
</svg>
</div>`
const copyReference = async text => {
if (!text) return
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement("textarea")
textarea.value = text
textarea.style.position = "fixed"
textarea.style.opacity = "0"
document.body.appendChild(textarea)
textarea.select()
document.execCommand("copy")
textarea.remove()
}
const plot = Plot.plot({
style: { fontFamily: "Times New Roman, Times, serif", fontSize: 12 },
width,
height: 520,
marginLeft: 70,
marginRight: 30,
marginTop: 20,
marginBottom: 45,
x: { label: "Year", domain: yearDomain, ticks: yearTicks, grid: true, line: true, tickFormat: d3.format(".0f") },
y: { label: yLabels[yVar], grid: true, line: true },
color: {
label: colorLabels[colorVar],
domain: colorDomain,
range: ["blue", "green"],
legend: false,
className: "color-legend"
},
marks: [
Plot.dot(sourceData, {
x: "Year",
y: yVar,
fill: "ColorValue",
stroke: "white",
strokeWidth: 0.7,
r: 10,
fillOpacity: 0.85
}),
Plot.tip(sourceData, Plot.pointer({
x: "Year",
y: yVar,
title: fullTooltip
}))
]
})
d3.select(plot)
.selectAll("circle")
.style("cursor", d => d?.Reference ? "copy" : null)
.on("click", (event, d) => copyReference(d?.Reference))
return html`<div class="source-plot-output">${sourceLegend}${plot}</div>`
}