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),
Year: toNumber(d.Year),
Stability: toNumber(d.Stability),
Efull: toNumber(d.Efull),
Ecathode: toNumber(d.Ecathode),
REType: d["RE type"] || "NA"
}))
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)
products = Array.from(new Set(data.map(d => d.Product))).sort(d3.ascending)
potentialTypes = ["Efull", "Ecathode vs SCE", "Ecathode vs RHE", "Ecathode vs Ag/AgCl"]
scheme = tol.QualMuted
potentialRows = data.flatMap(d => {
const rows = []
if (Number.isFinite(d.Efull)) {
rows.push({ ...d, PotentialType: "Efull", Potential: d.Efull })
}
if (Number.isFinite(d.Ecathode)) {
if (d.REType === "RHE") {
rows.push({ ...d, PotentialType: "Ecathode vs RHE", Potential: d.Ecathode })
} else if (d.REType === "Ag/AgCl") {
rows.push({ ...d, PotentialType: "Ecathode vs Ag/AgCl", Potential: d.Ecathode })
} else if (d.REType === "SCE") {
rows.push({ ...d, PotentialType: "Ecathode vs SCE", Potential: d.Ecathode })
}
}
return rows
})
radiusScale = (value, extent) => {
if (!Number.isFinite(value) || extent[0] === extent[1]) return 5
return 4 + 8 * ((value - extent[0]) / (extent[1] - extent[0]))
}
yearRangeInput = () => interval(yearExtent, {
step: 1,
label: "Year",
format: ([start, end]) => `${start} - ${end}`,
width: "100%",
color: "#3b99fc"
})
axisOptions = new Map([
["Potential (V)", "Potential"],
["Faradaic efficiency, FE (%)", "FE"],
["Current density, J (mA cm-2)", "J"],
["Stability, ln(h)", "Stability"]
])
sizeOptions = new Map([
["Year", "Year"],
["Duration", "Stability"],
["Faradaic efficiency, FE", "FE"],
["Current density, J", "J"]
])Potential Dependence
This page shows the dependence of current density and Faradaic efficiency on potential. Different studies may report the potential using \(E_\mathrm{hull}\) or \(E_\mathrm{cathode}\), with reference electrodes including SCE, RHE, and Ag/AgCl.
viewof selectedYearRange = yearRangeInput()
viewof minLogDuration = Inputs.range(logDurationExtent, { label: "Min duration (ln h)", step: 0.1, value: logDurationExtent[0] })
viewof selectedProducts = Inputs.checkbox(products, { value: products })
viewof selectedPotentialTypes = Inputs.checkbox(potentialTypes, { value: potentialTypes })
viewof sizeVar = Inputs.select(sizeOptions, { label: "Size", value: "Year" })
viewof xVar = Inputs.select(axisOptions, { label: "X axis", value: "Potential" })
viewof yVar = Inputs.select(axisOptions, { label: "Y axis", value: "FE" })
usesPotentialAxis = xVar === "Potential" || yVar === "Potential"
valueForAxis = (d, v) => v === "Stability" ? Math.log(d.Stability) : d[v]
hasFiniteAxisValue = (d, v) => Number.isFinite(valueForAxis(d, v))
baseFilter = d =>
selectedProducts.includes(d.Product) &&
d.Year >= selectedYearRange[0] &&
d.Year <= selectedYearRange[1] &&
d.Stability >= Math.exp(minLogDuration)
filteredPotentialRows = potentialRows.filter(d =>
baseFilter(d) &&
selectedPotentialTypes.includes(d.PotentialType) &&
hasFiniteAxisValue(d, xVar) &&
hasFiniteAxisValue(d, yVar)
)
filteredSingleRows = data.filter(d =>
baseFilter(d) &&
hasFiniteAxisValue(d, xVar) &&
hasFiniteAxisValue(d, yVar)
)
plotRows = usesPotentialAxis ? filteredPotentialRows : filteredSingleRows
sizeExtent = sizeVar === "Stability"
? d3.extent(data.filter(d => Number.isFinite(d.Stability) && d.Stability > 0), d => Math.log(d.Stability))
: d3.extent(data.filter(d => Number.isFinite(d[sizeVar])), d => d[sizeVar])
pointRadius = d => sizeVar === "Stability"
? radiusScale(Math.log(d.Stability), sizeExtent)
: radiusScale(d[sizeVar], sizeExtent)
axisLabels = ({
Potential: "Potential (V)",
FE: "Faradaic efficiency, FE (%)",
J: "Current density, J (mA cm-2)",
Stability: "Stability, ln(h)"
})
potentialTitle = type => {
if (type === "Efull") return html`<span><i>E</i><sub>full</sub></span>`
const suffix = type.replace("Ecathode", "")
return html`<span><i>E</i><sub>cathode</sub>${suffix}</span>`
}
formatTooltipValue = (d, v) => {
const value = valueForAxis(d, v)
if (!Number.isFinite(value)) return null
if (v === "Potential") return `Potential: ${value.toFixed(3)} V`
if (v === "FE") return `FE: ${d.FE}%`
if (v === "J") return `J: ${d.J} mA cm-2`
if (v === "Stability") return `Stability: ${d.Stability} h`
return null
}
tooltipFor = d => [
`Product: ${d.Product}`,
formatTooltipValue(d, xVar),
xVar !== yVar ? formatTooltipValue(d, yVar) : null,
usesPotentialAxis ? `Potential type: ${d.PotentialType}` : null,
xVar !== "FE" && yVar !== "FE" && Number.isFinite(d.FE) ? `FE: ${d.FE}%` : null,
xVar !== "J" && yVar !== "J" && Number.isFinite(d.J) ? `J: ${d.J} mA cm-2` : null,
Number.isFinite(d.Year) ? `Year: ${d.Year}` : null,
xVar !== "Stability" && yVar !== "Stability" && Number.isFinite(d.Stability) ? `Stability: ${d.Stability} h` : null,
d.REType !== "NA" ? `RE type: ${d.REType}` : null,
d.Reference ? `Reference: ${d.Reference}` : null
].filter(Boolean).join("\n")
plotForRows = (rows, { title = "", width = 460, height = 320 } = {}) => html`<div class="potential-plot-panel">
${title === "" ? "" : html`<h3>${title}</h3>`}
${Plot.plot({
style: { fontFamily: "Times New Roman, Times, serif", fontSize: 12 },
width,
height,
marginLeft: 60,
marginRight: 20,
marginTop: 15,
marginBottom: 45,
x: { label: axisLabels[xVar], grid: true, line: true },
y: { label: axisLabels[yVar], grid: true, line: true },
r: { type: "identity" },
color: {
label: "Product",
range: scheme,
legend: true,
className: "color-legend"
},
marks: [
Plot.dot(rows, {
x: d => valueForAxis(d, xVar),
y: d => valueForAxis(d, yVar),
fill: "Product",
stroke: "white",
strokeWidth: 0.6,
r: pointRadius,
fillOpacity: 0.85
}),
Plot.tip(rows, Plot.pointer({
x: d => valueForAxis(d, xVar),
y: d => valueForAxis(d, yVar),
title: tooltipFor
}))
]
})}
</div>`
plotForPotential = type => {
const rows = plotRows.filter(d => d.PotentialType === type)
return plotForRows(rows, { title: potentialTitle(type) })
}
visibleTypes = potentialTypes.filter(t => selectedPotentialTypes.includes(t))
usesPotentialAxis
? html`<div class="potential-plot-grid">${visibleTypes.map(plotForPotential)}</div>`
: html`<div class="potential-single-plot">${plotForRows(plotRows, { width: 640, height: 420 })}</div>`Filter by…
NoteProduct
NotePotential Type
NoteSize
NoteX Axis
NoteY Axis