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>`function interval(range = [], options = {}) {
const [min = 0, max = 1] = range;
const {
step = .001,
label = null,
value = [min, max],
format = ([start, end]) => `${start} ... ${end}`,
color,
width,
theme,
__ns__ = randomScope(),
} = options;
const css = `
#${__ns__} {
display: flex;
align-items: baseline;
flex-wrap: wrap;
max-width: 100%;
width: auto;
flex-direction: column;
}
@media only screen and (min-width: 30em) {
#${__ns__} {
flex-wrap: nowrap;
width: 15rem;
}
}
#${__ns__} .label {
flex-shrink: 0;
font-weight: bold;
line-height: normal;
}
#${__ns__} .form {
display: flex;
width: 100%;
}
#${__ns__} .range {
flex-shrink: 1;
width: 100%;
}
#${__ns__} .range-slider {
width: 100%;
margin-bottom: .3em;
margin-top: .3em;
}
#${__ns__} .range-output {
font-size: .85em;
line-height: 1;
}
`;
const $range = rangeInput({min, max, value: [value[0], value[1]], step, color, width, theme});
const $output = html`<output>`;
const $view = html`<div id="${__ns__}">
${label == null ? '' : html`<div class="label">${label}</div>`}
<div class="form">
<div class="range">
${$range}<div class=range-output>${$output}</div>
</div>
</div>
${html`<style>${css}`}
</div>
`;
const update = () => {
const content = format([$range.value[0], $range.value[1]]);
if(typeof content === 'string') $output.value = content;
else {
while($output.lastChild) $output.lastChild.remove();
$output.appendChild(content);
}
};
$range.oninput = update;
update();
return Object.defineProperty($view, 'value', {
get: () => $range.value,
set: ([a, b]) => {
$range.value = [a, b];
update();
},
});
}
function rangeInput(options = {}) {
const {
min = 0,
max = 100,
step = 'any',
value: defaultValue = [min, max],
color,
width,
theme = theme_Flat,
} = options;
const controls = {};
const scope = randomScope();
const clamp = (a, b, v) => v < a ? a : v > b ? b : v;
const input = html`<input type="range" min="${min}" max="${max}" step="${step}">`;
const dom = html`<div class="${scope} range-slider" style="${styleAttrs({
color,
width: cssLength(width)
})}">
${controls.track = html`<div class="range-track">
${controls.zone = html`<div class="range-track-zone">
${controls.range = html`<div class="range-select" tabindex=0>
${controls.min = html`<div class="thumb thumb-min" tabindex=0>`}
${controls.max = html`<div class="thumb thumb-max" tabindex=0>`}
`}
`}
`}
${html`<style>${theme.replace(/:scope\b/g, '.'+scope)}`}
</div>`;
let value = [], changed = false;
Object.defineProperty(dom, 'value', {
get: () => [...value],
set: ([a, b]) => {
value = sanitize(a, b);
updateRange();
},
});
const sanitize = (a, b) => {
a = isNaN(a) ? min : ((input.value = a), input.valueAsNumber);
b = isNaN(b) ? max : ((input.value = b), input.valueAsNumber);
return [Math.min(a, b), Math.max(a, b)];
}
const updateRange = () => {
const ratio = v => (v - min) / (max - min);
dom.style.setProperty('--range-min', `${ratio(value[0]) * 100}%`);
dom.style.setProperty('--range-max', `${ratio(value[1]) * 100}%`);
};
const dispatch = name => {
dom.dispatchEvent(new Event(name, {bubbles: true}));
};
const setValue = (vmin, vmax) => {
const [pmin, pmax] = value;
value = sanitize(vmin, vmax);
updateRange();
if(pmin === value[0] && pmax === value[1]) return;
dispatch('input');
changed = true;
};
setValue(...defaultValue);
const handlers = new Map([
[controls.min, (dt, ov) => {
const v = clamp(min, ov[1], ov[0] + dt * (max - min));
setValue(v, ov[1]);
}],
[controls.max, (dt, ov) => {
const v = clamp(ov[0], max, ov[1] + dt * (max - min));
setValue(ov[0], v);
}],
[controls.range, (dt, ov) => {
const d = ov[1] - ov[0];
const v = clamp(min, max - d, ov[0] + dt * (max - min));
setValue(v, v + d);
}],
]);
const pointer = e => e.touches ? e.touches[0] : e;
const on = (e, fn) => e.split(' ').map(e => document.addEventListener(e, fn, {passive: false}));
const off = (e, fn) => e.split(' ').map(e => document.removeEventListener(e, fn, {passive: false}));
let initialX, initialV, target, dragging = false;
function handleDrag(e) {
if(!e.buttons && !e.touches) {
handleDragStop();
return;
}
dragging = true;
const w = controls.zone.getBoundingClientRect().width;
e.preventDefault();
handlers.get(target)((pointer(e).clientX - initialX) / w, initialV);
}
function handleDragStop(e) {
off('mousemove touchmove', handleDrag);
off('mouseup touchend', handleDragStop);
if(changed) dispatch('change');
}
invalidation.then(handleDragStop);
dom.ontouchstart = dom.onmousedown = e => {
dragging = false;
changed = false;
if(!handlers.has(e.target)) return;
on('mousemove touchmove', handleDrag);
on('mouseup touchend', handleDragStop);
e.preventDefault();
e.stopPropagation();
target = e.target;
initialX = pointer(e).clientX;
initialV = value.slice();
};
controls.track.onclick = e => {
if(dragging) return;
changed = false;
const r = controls.zone.getBoundingClientRect();
const t = clamp(0, 1, (pointer(e).clientX - r.left) / r.width);
const v = min + t * (max - min);
const [vmin, vmax] = value, d = vmax - vmin;
if(v < vmin) setValue(v, v + d);
else if(v > vmax) setValue(v - d, v);
if(changed) dispatch('change');
};
return dom;
}
function randomScope(prefix = 'scope-') {
return prefix + (performance.now() + Math.random()).toString(32).replace('.', '-');
}
cssLength = v => v == null ? null : typeof v === 'number' ? `${v}px` : `${v}`
styleAttrs = attrs => Object.entries(attrs)
.filter(([, value]) => value != null)
.map(([key, value]) => `${key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}: ${value};`)
.join(" ")
theme_Flat = `
:scope {
color: #3b99fc;
width: 100%;
}
:scope {
position: relative;
display: inline-block;
--thumb-size: 16px;
--thumb-radius: calc(var(--thumb-size) / 2);
margin: 2px;
vertical-align: middle;
}
:scope .range-track {
box-sizing: border-box;
position: relative;
height: 7px;
background-color: hsl(0, 0%, 82%);
overflow: visible;
border-radius: 4px;
padding: 0 var(--thumb-radius);
}
:scope .range-track-zone {
box-sizing: border-box;
position: relative;
}
:scope .range-select {
box-sizing: border-box;
position: relative;
left: var(--range-min);
width: calc(var(--range-max) - var(--range-min));
cursor: ew-resize;
background: currentColor;
height: 7px;
border: inherit;
}
:scope .range-select:before {
content: "";
position: absolute;
width: 100%;
height: var(--thumb-size);
left: 0;
top: calc(2px - var(--thumb-radius));
}
:scope .range-select:focus,
:scope .thumb:focus {
outline: none;
}
:scope .thumb {
box-sizing: border-box;
position: absolute;
width: var(--thumb-size);
height: var(--thumb-size);
background: #fcfcfc;
top: -4px;
border-radius: 100%;
border: 1px solid hsl(0, 0%, 55%);
cursor: ew-resize;
margin: 0;
}
:scope .thumb:active {
box-shadow: inset 0 var(--thumb-size) #0002;
}
:scope .thumb-min {
left: calc(-1px - var(--thumb-radius));
}
:scope .thumb-max {
right: calc(-1px - var(--thumb-radius));
}
`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),
Stability: toNumber(d.Stability),
Year: toNumber(d.Year)
}))
.filter(d => Number.isFinite(d.FE) || Number.isFinite(d.J) || d.Product !== "Unknown")
plotData = data.filter(d => Number.isFinite(d.FE) && Number.isFinite(d.J))
scheme = tol.QualMuted
default_color = scheme[0]
yearExtent = d3.extent(data.filter(d => Number.isFinite(d.Year)), d => d.Year)
durationExtent = d3.extent(data.filter(d => Number.isFinite(d.Stability)), d => d.Stability)
logDurationExtent = durationExtent.map(Math.log)
radiusScale = (value, extent) => {
if (!Number.isFinite(value) || extent[0] === extent[1]) return 5
return 4 + 8 * ((value - extent[0]) / (extent[1] - extent[0]))
}
overviewPlotWidth = width < 700 ? width : 0.5 * width
yearRangeInput = () => interval(yearExtent, {
step: 1,
label: "Year",
format: ([start, end]) => `${start} - ${end}`,
width: "100%",
color: "#3b99fc"
})CO2RR Experimental Database
CO\(_2\) reduction reaction (CO\(_2\)RR) is an important electrochemical route for converting carbon dioxide into value-added chemicals and fuels using renewable electricity. It offers a promising strategy to mitigate carbon emissions while storing intermittent renewable energy in chemical bonds. Since early studies on electrochemical CO\(_2\) conversion, the field has progressed from fundamental investigations of reaction pathways and catalyst selectivity to the development of high-performance catalysts, gas-diffusion electrodes, and flow-cell systems capable of industrially relevant operation.
This website summarizes the development of representative CO\(_2\)RR studies by tracking two key performance metrics: current density and Faradaic efficiency. These metrics reflect, respectively, the reaction rate and product selectivity, and together provide a concise overview of how CO\(_2\)RR performance has advanced over time.
displayScheme = [
[204, 102, 119],
[221, 204, 119],
[ 68, 170, 153],
[136, 204, 238],
]
displayConfig = ([
{ index: 0, value: new Set(data.map(d => d.Product)).size, label: "Products", icon: "diagram-3" },
{ index: 1, value: d3.min(data, d => d.Year), label: "First Year", icon: "calendar3" },
{ index: 2, value: d3.max(data, d => d.Year), label: "Latest Year", icon: "calendar-check" },
{ index: 3, value: `${d3.max(data, d => d.Stability)} h`, label: "Max Duration", icon: "hourglass-split" },
])
displayBox = ({ index, value, label, icon }) => {
const color = displayScheme[index]
const color_str = `${color[0]}, ${color[1]}, ${color[2]}`
const borderColor = `rgb(${color_str})`
const backgroundColor = `rgba(${color_str}, 0.4)`
return html`<div class="callout callout-style-default callout-note no-icon callout-empty-content callout-titled callout-display" style="border-left-color: ${borderColor};">
<div class="callout-header d-flex align-content-center" style="background-color: ${backgroundColor};">
<div class="callout-title-container flex-fill">
<span class="display-box">
<span class="display-icon"><i class="bi-${icon}" role="img" aria-hidden="true"></i></span>
<span class="display-content">
<span class="display-var">${label}</span>
<span class="display-val">${value ?? "NA"}</span>
</span>
</span>
</div>
</div>
</div>`
}
boxes = displayConfig.map(displayBox)
html`<div class="display-box-container">${boxes}</div>`viewof countYearRange = yearRangeInput()
viewof countMinLogDuration = Inputs.range(logDurationExtent, { label: "Min duration (ln h)", step: 0.1, value: logDurationExtent[0] })
marginLeft = 110
filteredCountData = data.filter(d =>
d.Year >= countYearRange[0] &&
d.Year <= countYearRange[1] &&
d.Stability >= Math.exp(countMinLogDuration)
)
Plot.plot({
style: { fontFamily: "Times New Roman, Times, serif", fontSize: 12 },
width: overviewPlotWidth,
height: 360,
marginLeft,
marginTop: 20,
marginBottom: 40,
x: { label: "Sample count", axis: "bottom", grid: true, line: true },
y: { label: null, tickSize: 0, tickPadding: 4 },
color: { range: scheme },
marks: [
Plot.barX(filteredCountData, Plot.groupY(
{ x: "count" },
{
y: "Product",
fill: "Product",
sort: { y: "x", reverse: true },
inset: 1
}
)),
Plot.textX(filteredCountData, Plot.groupY(
{ x: "count", text: "count" },
{
y: "Product",
dx: 4,
textAnchor: "start",
fill: "currentColor",
sort: { y: "x", reverse: true }
}
)),
Plot.ruleX([0])
]
})
productOptions = new Map([["All products", "All"], ...Array.from(new Set(data.map(d => d.Product))).sort(d3.ascending).map(d => [d, d])])
viewof selectedProduct = Inputs.select(productOptions, { label: "Product" })
viewof sizeVar = Inputs.select(new Map([["Year", "Year"], ["Duration", "Stability"]]), { label: "Size", value: "Year" })
viewof selectedYearRange = yearRangeInput()
viewof minLogDuration = Inputs.range(logDurationExtent, { label: "Min duration (ln h)", step: 0.1, value: logDurationExtent[0] })
filteredPlotData = plotData.filter(d =>
(selectedProduct === "All" || d.Product === selectedProduct) &&
d.Year >= selectedYearRange[0] &&
d.Year <= selectedYearRange[1] &&
d.Stability >= Math.exp(minLogDuration)
)
sizeExtent = sizeVar === "Stability"
? d3.extent(plotData.filter(d => Number.isFinite(d.Stability) && d.Stability > 0), d => Math.log(d.Stability))
: d3.extent(plotData.filter(d => Number.isFinite(d.Year)), d => d.Year)
pointRadius = d => sizeVar === "Stability"
? radiusScale(Math.log(d.Stability), sizeExtent)
: radiusScale(d.Year, sizeExtent)
Plot.plot({
style: { fontFamily: "Times New Roman, Times, serif", fontSize: 12 },
width: overviewPlotWidth,
height: 360,
marginTop: 20,
marginBottom: 45,
marginLeft: 60,
x: { label: "Faradaic efficiency, FE (%)", grid: true, line: true },
y: { label: "Current density, J (mA cm-2)", grid: true, line: true },
r: { type: "identity" },
color: {
legend: true,
label: "Product",
range: scheme,
className: "color-legend"
},
marks: [
Plot.dot(filteredPlotData, {
x: "FE",
y: "J",
fill: "Product",
stroke: "white",
strokeWidth: 0.6,
r: pointRadius,
fillOpacity: 0.85
}),
Plot.tip(filteredPlotData, Plot.pointer({
x: "FE",
y: "J",
title: d => [
`Product: ${d.Product}`,
`FE: ${d.FE}%`,
`J: ${d.J} mA cm-2`,
Number.isFinite(d.Year) ? `Year: ${d.Year}` : null,
Number.isFinite(d.Stability) ? `Stability: ${d.Stability} h` : null,
d.Reference ? `Reference: ${d.Reference}` : null
].filter(Boolean).join("\n")
}))
]
})