pubs = await d3.tsv("https://raw.githubusercontent.com/Nanostring-Biostats/ScratchSpaceDatasets/refs/heads/main/browser_bytes/publication_tracker/spatial_publications.tsv", d3.autoType)
manifest = await d3.csv("https://raw.githubusercontent.com/Nanostring-Biostats/ScratchSpaceDatasets/refs/heads/main/browser_bytes/publication_tracker/manifest.csv")
updatedDateStr = {
if (manifest && manifest.length > 0 && manifest[0].Last_Updated_UTC) {
const ts = manifest[0].Last_Updated_UTC.replace(' ', 'T') + "Z";
const d = new Date(ts);
const formatter = new Intl.DateTimeFormat(navigator.language, { month: 'short', day: 'numeric', year: 'numeric' });
return formatter.format(d);
}
return 'Unknown date';
}
// Convert publication date string to year, filtering out pre-2008
pubs_clean = pubs.map(d => {
let y = "N/A";
const pd = d["Publication Date"];
if (pd) {
if (pd instanceof Date) {
// 1. D3 auto-typed it correctly as a Date object
y = pd.getFullYear();
} else if (typeof pd === 'number') {
// 2. D3 auto-typed a year-only string (e.g., "2015") as a raw number
y = pd;
} else if (typeof pd === 'string') {
// 3. It's a string. Safely extract the first 4 consecutive digits
const match = pd.match(/\d{4}/);
if (match) y = parseInt(match[0]);
}
}
return {
...d,
Year: Number.isInteger(y) ? y : "N/A"
}
}).filter(d => d.Year !== "N/A" && d.Year >= 2008)
Editorial Volume 26.1
PUBLICATION
Pulse
Explore the research landscape and publication trends driving spatial biology.
EXPLORE NEW INNOVATIONSview_quilt Platform Metrics
description
library_books
Global
lock_open
Ratio
oa_percent = {
if (filteredLinePubs.length === 0) return 0; // Prevents a divide-by-zero error if no boxes are checked
const oa = filteredLinePubs.filter(d => String(d["Open Access"]).toLowerCase() === 'true').length;
return Math.round((oa / filteredLinePubs.length) * 100);
}
html`<h4 class="text-5xl font-extrabold text-slate-900 tracking-tight mb-2">${oa_percent}%</h4>`Open Access Ratio
oa_ring = {
// Quarto initializes 'this' as an empty object on the first run.
// We must explicitly check if it has a tagName to ensure it's a real DOM element.
const isFirstRender = !(this && this.tagName);
// Either create a new SVG or reuse the existing one
const node = isFirstRender ? d3.create("svg").node() : this;
const svg = d3.select(node);
if (isFirstRender) {
svg.attr("class", "w-full h-full transform -rotate-90 pointer-events-none")
.attr("viewBox", "0 0 36 36");
// 1. Draw the base gray ring
svg.append("path")
.attr("d", "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831")
.attr("fill", "none")
.attr("stroke", "#e2e8f0")
.attr("stroke-width", "3");
// 2. Draw the orange active ring
svg.append("path")
.attr("class", "active-ring")
.attr("d", "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831")
.attr("fill", "none")
.attr("stroke", "#f97316")
.attr("stroke-width", "3")
.attr("stroke-dasharray", "0, 100")
.style("transition", "stroke-dasharray 1s cubic-bezier(0.4, 0, 0.2, 1)"); // Smooth CSS easing
// 3. Trigger the initial animation slightly after the component mounts
setTimeout(() => {
d3.select(node).select(".active-ring").attr("stroke-dasharray", `${oa_percent}, 100`);
}, 50);
} else {
// 4. REACTIVE UPDATE: If the SVG already exists, smoothly change the dasharray
svg.select(".active-ring").attr("stroke-dasharray", `${oa_percent}, 100`);
}
return node;
}Top Institutions
domain
top_institutions = {
// Extract and split comma-separated institutions
const allInsts = filteredLinePubs.flatMap(d => (d.Institutions || "").split(",").map(i => i.trim()).filter(i => i !== ""));
const instCounts = d3.rollup(allInsts, v => v.length, d => d);
// Sort and take top 10
const sorted = Array.from(instCounts, ([name, count]) => ({name, count})).sort((a,b) => b.count - a.count);
return {
totalActive: instCounts.size,
top10: sorted.slice(0, 10),
maxCount: sorted.length ? sorted[0].count : 1
};
}
html`
<div class="flex flex-col gap-2 w-full group/card cursor-default mt-auto">
${top_institutions.top10.map((inst, i) => `
<div class="flex items-center gap-3 text-xs font-mono text-slate-400 transition-all duration-300 ${i >= 5 ? 'h-0 opacity-0 overflow-hidden group-hover/card:h-[18px] group-hover/card:opacity-100 group-hover/card:mt-1' : 'h-[18px]'}">
<span>${i < 9 ? '0' : ''}${i+1}</span>
<div class="flex-grow h-2 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full rounded-full ${i===0 ? 'bg-[#2a4534]' : i===1 ? 'bg-[#f97316]' : 'bg-slate-400'}" style="width: ${(inst.count / top_institutions.maxCount) * 100}%"></div>
</div>
<span class="font-bold text-slate-700 w-auto truncate max-w-[250px] text-right" title="${inst.name}">${inst.name}</span>
</div>
`).join('')}
<div class="text-[10px] text-center text-slate-400 mt-2 italic group-hover/card:hidden">Hover to expand list</div>
</div>
`Top Types
category
top_types = {
// Use filteredLinePubs so types react to platform selection
const allTypes = filteredLinePubs.map(d => d.Type || "Unknown");
const typeCounts = d3.rollup(allTypes, v => v.length, d => d);
// Sort and take top 10
const sorted = Array.from(typeCounts, ([name, count]) => ({name, count})).sort((a,b) => b.count - a.count);
return {
totalActive: typeCounts.size,
top10: sorted.slice(0, 10),
maxCount: sorted.length ? sorted[0].count : 1
};
}
html`
<div class="flex flex-col gap-2 w-full group/card cursor-default mt-auto">
${top_types.top10.map((typeObj, i) => `
<div class="flex items-center gap-3 text-xs font-mono text-slate-400 transition-all duration-300 ${i >= 5 ? 'h-0 opacity-0 overflow-hidden group-hover/card:h-[18px] group-hover/card:opacity-100 group-hover/card:mt-1' : 'h-[18px]'}">
<span>${i < 9 ? '0' : ''}${i+1}</span>
<div class="flex-grow h-2 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full rounded-full ${i===0 ? 'bg-sky-500' : i===1 ? 'bg-violet-500' : 'bg-slate-400'}" style="width: ${(typeObj.count / top_types.maxCount) * 100}%"></div>
</div>
<span class="font-bold text-slate-700 w-auto truncate max-w-[250px] text-right capitalize" title="${typeObj.name}">${typeObj.name.replace('-', ' ')}</span>
</div>
`).join('')}
<div class="text-[10px] text-center text-slate-400 mt-2 italic group-hover/card:hidden">Hover to expand list</div>
</div>
`html`
<div class="w-full">
<h2 class="text-4xl font-bold text-slate-900 mb-3 flex items-center gap-3">
<span class="material-symbols-outlined text-primary text-4xl">show_chart</span>
Publications by Year
</h2>
<div class="flex flex-col xl:flex-row gap-6 justify-between items-start xl:items-end w-full mt-4">
<div class="flex-1 max-w-5xl">
<p class="text-slate-400 text-sm flex items-center gap-1 font-medium">
<span class="material-symbols-outlined text-sm">schedule</span> Last updated from OpenAlex: ${updatedDateStr}
</p>
</div>
</div>
</div>
`viewof selectedPlatforms = Inputs.checkbox(
["CellScape", "CosMx", "GeoMx", "AtoMx", "nCounter"],
{value: ["CellScape","CosMx", "GeoMx", "AtoMx", "nCounter"]}
)filteredLinePubs = pubs_clean.filter(d => {
if (selectedPlatforms.length === 0) return false;
let keep = false;
if (selectedPlatforms.includes("CellScape") && d.Has_CellScape === 'True') keep = true;
if (selectedPlatforms.includes("CosMx") && d.Has_CosMx === 'True') keep = true;
if (selectedPlatforms.includes("GeoMx") && d.Has_GeoMx === 'True') keep = true;
if (selectedPlatforms.includes("AtoMx") && d.Has_AtoMx === 'True') keep = true;
if (selectedPlatforms.includes("nCounter") && d.Has_nCounter === 'True') keep = true;
return keep;
})
// Calculate ALL TIME top papers for the right-hand panel
allTimeTopPapers = [...filteredLinePubs]
.sort((a,b) => (b["Cited By"] || 0) - (a["Cited By"] || 0))
.slice(0, 3)
// Generate cumulative data and capture top papers per year
chartData = {
// 1. Determine the timeline bounds
const minYear = 2008;
// Use the actual data we are plotting to find the max year
const years = filteredLinePubs.map(d => d.Year).filter(Number.isInteger);
const maxYear = years.length > 0 ? Math.max(...years) : 2026;
let cumulative = 0;
const result = [];
// 2. Calculate pre-2008 baseline correctly from the PLOTTED dataset
const preBase = filteredLinePubs.filter(d => d.Year < minYear).length;
cumulative = preBase;
// 3. Loop through years to build the cumulative total
for (let y = minYear; y <= maxYear; y++) {
// Get papers for this specific year
const pubsInYear = filteredLinePubs.filter(d => d.Year === y);
// Increment the total
cumulative += pubsInYear.length;
// Sort and grab top 3 by citations for the "Top Chart" tooltips
const topItems = [...pubsInYear]
.sort((a, b) => (parseInt(b["Cited By"]) || 0) - (parseInt(a["Cited By"]) || 0))
.slice(0, 3);
result.push({
YearVal: y,
Year: new Date(y, 0, 1),
Cumulative: cumulative,
YearlyCount: pubsInYear.length, // Useful for debugging
TopPapers: topItems
});
}
return result;
}
mutable selectedYearVal = null
selectedYearData = {
// If null, return nothing so the UI defaults to All Time
if (selectedYearVal === null || !chartData || chartData.length === 0) return null;
return chartData.find(d => d.YearVal === selectedYearVal);
}
// Interactive Plot
{
const minYear = 2008;
const maxYear = Math.max(...pubs_clean.map(d => d.Year).filter(y => Number.isInteger(y)));
const minDate = new Date(minYear, 0, 1);
const maxDate = new Date(maxYear, 0, 1);
const node = Plot.plot({
width: 900,
height: 400,
marginTop: 20,
marginRight: 40,
marginBottom: 30,
marginLeft: 80,
style: {
background: "transparent",
fontFamily: "Space Grotesk, sans-serif",
fontSize: "14px"
},
x: { type: "time", grid: true, label: null, domain: [minDate, maxDate], insetRight: 20 },
y: { grid: true, label: "Cumulative Publications" },
marks: [
Plot.lineY(chartData, {x: "Year", y: "Cumulative", stroke: "#0077c8", strokeWidth: 4, curve: "catmull-rom"}),
Plot.areaY(chartData, {x: "Year", y: "Cumulative", fill: "url(#gradient-blue)", fillOpacity: 0.2, curve: "catmull-rom"}),
// Visible indicator for the selected year
Plot.dot([selectedYearData].filter(Boolean), {
x: "Year",
y: "Cumulative",
r: 10,
fill: "#f97316", // Made it orange to pop
stroke: "white",
strokeWidth: 4,
pointerEvents: "none"
})
]
});
const svg = d3.select(node);
// Make SVG responsive
svg.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 900 400")
.attr("preserveAspectRatio", "xMidYMid meet");
// Allow cursor to indicate interactivity
svg.style("cursor", "pointer");
// Add gradient def
svg.append("defs").append("linearGradient")
.attr("id", "gradient-blue")
.attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%")
.selectAll("stop")
.data([{offset: "0%", color: "#0077c8"}, {offset: "100%", color: "transparent"}])
.join("stop")
.attr("offset", d => d.offset)
.attr("stop-color", d => d.color);
// Bind click event directly to SVG to set nearest year
svg.on("click", function(event) {
const coords = d3.pointer(event, svg.node());
const x = coords[0];
const innerWidth = 900 - 50 - 40;
const plotX = Math.max(0, Math.min(innerWidth, x - 50));
const ratio = plotX / innerWidth;
const clickedTime = minDate.getTime() + ratio * (maxDate.getTime() - minDate.getTime());
const clickedYear = new Date(clickedTime).getFullYear();
// snap to nearest existing chartData year
if (chartData.length > 0) {
const closest = chartData.reduce((prev, curr) => {
return (Math.abs(curr.YearVal - clickedYear) < Math.abs(prev.YearVal - clickedYear)) ? curr : prev;
});
mutable selectedYearVal = closest.YearVal;
}
});
svg.on("dblclick", function() {
mutable selectedYearVal = null;
});
return node;
}activePapersList = selectedYearVal === null ? allTimeTopPapers : (selectedYearData ? selectedYearData.TopPapers : [])
html`
<div class="mb-4">
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-widest mb-1 flex items-center justify-between">
Top Cited Papers
<span class="text-primary font-black bg-blue-50 px-2 py-0.5 rounded-md">${selectedYearVal === null ? "All Time" : selectedYearVal}</span>
</h3>
<p class="text-[11px] text-slate-400 italic">
${selectedYearVal === null
? "Displaying top cited publications across all years. Click anywhere on the chart line to view a specific year."
: "Double-click the chart to return to all-time top cited publications."}
</p>
</div>
${activePapersList.length > 0
? `<div class="flex flex-col gap-4">
${activePapersList.map((p, i) => `
<a href="${p.DOI}" target="_blank" class="group block bg-slate-50 hover:bg-slate-100 rounded-xl p-4 border border-slate-100 transition-colors">
<div class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full ${i===0 ? 'bg-amber-100 text-amber-700' : 'bg-slate-200 text-slate-600'} flex items-center justify-center font-bold text-xs shrink-0">${i+1}</div>
<div>
<h4 class="text-slate-900 font-bold text-sm group-hover:text-primary transition-colors line-clamp-3 leading-snug">${p.Title}</h4>
<div class="text-[11px] text-slate-500 mt-2 flex flex-col gap-1">
<span class="font-serif italic line-clamp-1">${p.Journal}</span>
<span class="font-mono text-emerald-700 font-bold">${p["Cited By"]} Citations</span>
</div>
</div>
</div>
</a>
`).join('')}
</div>`
: `<div class="flex-grow flex items-center justify-center text-slate-400 text-sm h-full italic text-center p-4">No publications found for this selection.</div>`
}`
info
Note: This is a sample query, not an exhaustive list. It was generated semi-automatically by searching specific terms like "CellScape", "CosMx", "AtoMx", "GeoMx", and "nCounter" that were found in the title, abstract, and full text (if applicable). It may miss publications using broader terminology or publications not found on openAlex and it may include unrelated publications. If you notice a publication that is missing or is included erroneously, please let us know.
verified
See Additional Publications