Unlisted
Edited
Oct 16, 2023
Paused
1 fork
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
downloadbutton = {
const dl = download(
() =>
new Blob(
[
d3.csvFormat(
facts.allFiltered().map(d => {
let res = {};
res["Treatment (per arm)"] = d.arms
.map((e, i) => `(${i + 1}) ${e["Treatment name"]}`)
.join(" vs ")
.replace(/\+/g, " + ");
res["Sample size"] = d["Total sample size"];
res["Severity at enrollment"] = String(d["Type of patients"])
.trim()
.replace(/ at enrollment$/, "")
.replace(/ disease$/, "");
res["Sponsor/Funder"] = d["Funding"];
res["Reg. number"] = d["Trial registration number"];
res["Full text link"] = d["Full text link"];
return res;
})
)
],
{
type: "text/csv"
}
),
`covid-nma-export-${d3.utcFormat("%Y%m%d-treatments")(new Date())}.csv`,
"Download the data"
);
d3.select(dl)
.style("position", "absolute")
.style("right", "2px")
.style("top", "5px")
.style("font-size", "10px")
.style("font-family", "sans-serif");
return dl;
}
Insert cell
FULLSCREEN = fullscreen({
breakLayoutAtCell: breakAfter,
hideAfter: hideAfter,
left: 53,
right: 42,
button: html`<button style="background: steelblue; color: white; font-size: 16px; /*position:fixed;right:20px;top:0;*/">Fullscreen`,
fsButtonText: "x"
})
Insert cell
W0 = (help, 1200) // viewof W0 = slider({ min: 1000, max: 2000, value: 1200 })
Insert cell
{
map;
//d3.select("main.mw8").classed("mw8", false);
d3.selectAll("#loader").remove();
}
Insert cell
Insert cell
Insert cell
dataText = {
const charset = "utf-8"; // "iso-8859-1"
const url = document.location.search.match(/test/)
? "https://covid-nma.com/dataviz/data/database_test.csv"
: "https://covid-nma.com/dataviz/data/database_valid.csv",
cors_proxy = "https://cors-anywhere.herokuapp.com/"; //"https://observable-cors.glitch.me/";

try {
return await fetch(url)
.then((response) => response.arrayBuffer())
.then((buffer) => new TextDecoder(charset).decode(buffer))
.then((d) => d.replace(/^/, "")); // remove UTF8-BOM
} catch (e) {
return await fetch(`${cors_proxy}${url}`)
.then((response) => response.arrayBuffer())
.then((buffer) => new TextDecoder(charset).decode(buffer))
.then((d) => d.replace(/^/, "")); // remove UTF8-BOM
// return await FileAttachment("Database_covid_studies_registered@2.csv").text();
}
}
Insert cell
allData = (reset, csvparser(fixCharset(dataText)))
Insert cell
csvparser = dataText.match(/^.*\t.*\t.*\t.*\t.*\t.*\t/)
? d3.tsvParse
: dataText.match(/;.*;.*;.*;.*;.*;/)
? d3.dsvFormat(";").parse
: d3.csvParse
Insert cell
arms = allData
.filter(
(d) => d["Registration date"].trim() && !d["Trial ID"].trim().match(/^#/)
)
.filter((d) => d["Study design"] != "nonRCT")
.filter(
(d) =>
!d["Treatment category"].match("(^|/)Vaccine(/|$)") &&
!d["Treatment category"].match("(^|/)Vaccine comparator(/|$)")
)
.map((d) => {
d.Published = d["Publication date"] == "No" ? "Not published" : "Published";
if (!("Treatment category" in d))
d["Treatment category"] = d["Pharmacological treatment"]; // temp column renaming
// Reorder treatment types so that Treat1+Treat2 <=> Treat2+Treat1
d.treatment_type_ordered = d["Treatment type"]
.toLowerCase()
.split("+")
.sort()
.join("+");
// Recapitalize treatment types
d.treatment_type_ordered =
d.treatment_type_ordered.charAt(0).toUpperCase() +
d.treatment_type_ordered.slice(1);
return d;
})
Insert cell
// the reference date is in the first row of comments of the csv file, in the second column
date_reference = {
const d = allData.filter(d => d["Trial ID"].trim().match(/^#/)).pop();
if (!d) return null;
return timeParse(d["Trial registration number"]);
}
Insert cell
function fixCharset(text) {
text = text.replace(/�/g, "◎");
return text;
}
Insert cell
timeParse = {
const f1 = d3.timeParse("%Y-%m-%d"),
f2 = d3.timeParse("%d/%m/%Y"),
f3 = d3.timeParse("%m/%d/%Y");
return x => f1(x.substring(0, 10)) || (f2(x) > new Date() ? f3(x) : f2(x));
}
Insert cell
facts = Object.assign(
crossfilter(
d3
.groups(arms, d => d["Trial ID"])
.map(d => Object.assign({ arms: d[1] }, d[1][0]))
),
{ dispatch: d3.dispatch("redraw", "reset") }
)
Insert cell
// for re-export into the database
reExport = {
const fmt = d3.timeFormat("%Y-%m-%d");
return arms.map(d => {
const countryCodes = identifyCountries(d).join(";");
return {
"Trial ID": d["Trial ID"],
countryCodes,
// Countries: d["Countries"],
"Recruitment status": d["Recruitment status"].replace(
"Not Recruiting",
"Not recruiting"
),
"Registration date": fmt(timeParse(d["Registration date"]))
};
});
}
Insert cell
Insert cell
severityLabels = new Map([
["0", "no restriction"],
["1", "mild"],
["2", "mild/moderate"],
["3", "moderate"],
["4", "moderate/severe"],
["5", "moderate/severe/critical"],
["6", "severe"],
["7", "severe/critical"],
["8", "critical"],
["N/A", "N/A"]
])
Insert cell
studyAimOrder = d3.scaleOrdinal(
["Prevention", "Treatment", "Post treatment"],
d3.range(10)
)
Insert cell
colors = ({ phantom: ['#ccc'], solid: ['steelblue'] })
Insert cell
function horizontalHistogram(accessor, options, invalidation) {
const { container, view, reset } = createView(options);

const uid = DOM.uid();
d3.select(container)
.attr("id", uid.id)
.append("style")
.text(`#${uid.id} g.row text {fill: black;}`);

const x = facts.dimension(accessor, options.multivalued),
y = x.group().reduceSum(reducer),
w = Math.min(width, options.width || 650);

const chart = new dc.RowChart(view)
.width(w)
.height(options.height || 330)
.rowsCap(options.rowsCap || 13)
.othersLabel(options.othersLabel || "Others")
.gap(0)
.dimension(x)
.group(y)
.ordinalColors(options.colors || d3.schemeSet1)
.label(d => d.key.replace(/^(\d+|N[/]A)\. /, ""))
.ordering(options.ordering == "key" ? d => d.key : d => -d.value)
.on("postRender", () =>
d3
.select(view)
.select("g.axis")
.selectAll(".legent")
.data([numberLabel])
.join("text")
.attr("class", "legent")
.style("fill", "#000")
.attr("x", w - 75)
.attr("dy", -4)
.attr("text-anchor", "end")
.text(d => d)
);

chart.xAxis().tickFormat(integerFormat);

if (reset) reset.on("click", () => (chart.filter(null), chart.redrawGroup()));
invalidate(invalidation, chart);

return container;
}
Insert cell
function ordinalHistogram(accessor, options, invalidation) {
const { container, view, reset } = createView(options);
const uid = DOM.uid();
d3.select(container).attr("id", uid.id);

if (options.rotateX)
d3.select(container).append("style").text(`
#${uid.id} .axis.x .tick text {
text-anchor: end;
transform: rotate(-90deg) translate(-10px,-14px);
}
`);

const dim = facts.dimension(accessor, options.multivalued),
group = dim.group().reduceSum(reducer),
factsall = crossfilter(facts.all()),
static_group = factsall
.dimension(accessor, options.multivalued)
.group()
.reduceSum(reducer),
chart = new dc.CompositeChart(view);
if (reset) reset.on("click", () => (chart.filter(null), chart.redrawGroup()));
invalidate(invalidation, chart);

const w = options.width || 315,
h = options.height || 175,
values = [...new Set(facts.all().map(accessor))].sort(options.sort);

const xScale = d3.scaleBand().domain(values),
margins = options.margin || {
top: 10,
right: 10,
bottom: 40,
left: 40
},
gap =
(w - margins.right - margins.left) / (xScale.domain().length - 1.5) / 4;

// no click on phantoms
d3.select(container).append("style").text(`
#${uid.id} g.sub._0 { pointer-events: none; }
`);

const charts = [
dc
.barChart(chart)
.group(static_group)
.colors(colors.phantom)
.centerBar(false)
.barPadding(0.2)
.brushOn(false),
dc
.barChart(chart)
.dimension(null)
.colors(colors.solid)
.centerBar(false)
.barPadding(0.2)
.brushOn(false)
];

chart
.width(w)
.height(h)
.margins(margins)
.x(xScale)
.xUnits(dc.units.ordinal)
.dimension(dim) // the values across the x axis
.group(group) // the values on the y axis
.transitionDuration(300)
.compose(charts)
._rangeBandPadding(0.2)
.brushOn(false)
.on("postRender", () =>
d3
.select(container)
.select("g.axis")
.selectAll(".legent")
.data([numberLabel])
.join("text")
.attr("class", "legent")
.style("fill", "#000")
.attr("dx", 5)
.attr("dy", 8)
.attr("y", -h + margins.top + margins.bottom)
.attr("text-anchor", "start")
.text(d => d)
);

chart.xAxis().tickFormat(v => v);
chart.yAxis().tickFormat(integerFormat);

return container;
}
Insert cell
facts.all()
Insert cell
function timeBarChart(accessor, options, invalidation) {
const { container, view, reset } = createView(options);
const uid = DOM.uid();
d3.select(container).attr("id", uid.id);

if (options.rotateX)
d3.select(container).append("style").text(`
#${uid.id} .axis.x .tick text {
text-anchor: end;
transform: rotate(-90deg) translate(-10px,-14px);
}
`);

if (options.barWidth)
d3.select(container).append("style").text(`
#${uid.id} .bar {
width: ${options.barWidth}px;
}
`);

const dim = facts.dimension(accessor, options.multivalued),
group = dim.group().reduceSum(reducer),
factsall = crossfilter(facts.all()),
static_group = factsall
.dimension(accessor, options.multivalued)
.group()
.reduceSum(reducer),
chart = new dc.CompositeChart(view);
invalidate(invalidation, chart);
if (reset) reset.on("click", () => (chart.filter(null), chart.redrawGroup()));

const h = options.height || 200,
margins = options.margins || { top: 10, right: 10, bottom: 20, left: 45 };
const charts = [
dc
.barChart(chart)
.group(static_group)
.colors(colors.phantom)
.gap(options.gap || 2)
.centerBar(options.centerBar || false)
.brushOn(false),
dc
.barChart(chart)
.colors(colors.solid)
.gap(options.gap || 2)
.centerBar(options.centerBar || false)
.brushOn(false)
];

let domain = d3.extent(facts.all(), accessor);
domain = [
domain[0].setDate(domain[0].getDate() - 5),
domain[1].setDate(domain[1].getDate() + 5)
];
const scaleX = d3.scaleTime().domain(domain);

chart
.xAxis()
.tickFormat((d, i) => (i % 2 === 0 ? d3.timeFormat("%b %y")(d) : ""));

chart
.width(options.width || 500)
.height(h)
.x(scaleX)
.xUnits(
// to get days!!
() => (scaleX.domain()[1] - scaleX.domain()[0]) / (24 * 3600 * 1000)
)
.margins(margins)
.dimension(dim) // the values across the x axis
.group(group) // the values on the y axis
.transitionDuration(500)
.compose(charts)
.on("postRedraw", () => {
// alert the dispatch crowd
facts.dispatch.call("redraw", null, "DC");
})
.on("postRender", () => {
d3.select(container)
.select("g.axis")
.selectAll(".legent")
.data([numberLabel])
.join("text")
.attr("class", "legent")
.style("fill", "#000")
.attr("dx", 5)
.attr("dy", 8)
.attr("y", -h + margins.top + margins.bottom)
.attr("text-anchor", "start")
.text(d => d);
});

return container;
}
Insert cell
function logBarChart(accessor, options, invalidation) {
const { container, view, reset } = createView(options);
const uid = DOM.uid();
d3.select(container).attr("id", uid.id);

if (options.rotateX)
d3.select(container).append("style").text(`
#${uid.id} .axis.x .tick text {
text-anchor: end;
transform: rotate(-90deg) translate(-10px,-14px);
}
`);

const reducer = () => 1;

const dim = facts.dimension(accessor, options.multivalued),
group = dim.group().reduceSum(reducer),
factsall = crossfilter(facts.all()),
static_group = factsall
.dimension(accessor, options.multivalued)
.group()
.reduceSum(reducer),
chart = new dc.CompositeChart(view);
invalidate(invalidation, chart);
if (reset) reset.on("click", () => (chart.filter(null), chart.redrawGroup()));

const h = options.height || 200,
margins = options.margins || { top: 10, right: 10, bottom: 20, left: 35 };
const charts = [
dc
.barChart(chart)
.group(static_group)
.colors(colors.phantom)
.gap(options.gap || 2)
.brushOn(false),
dc
.barChart(chart)
.colors(colors.solid)
.gap(options.gap || 2)
.brushOn(false)
];

const scaleX = d3.scaleLinear().domain(d3.extent(facts.all(), accessor));

chart.xAxis().tickFormat(d => {
const a = d3.format("~s")(Math.pow(10, d));
if (["1", "5"].includes(a[0])) return a;
});

chart
.width(options.width || 500)
.height(h)
.x(scaleX)
.xUnits(() => 35)
.margins(margins)
.dimension(dim) // the values across the x axis
.group(group) // the values on the y axis
.transitionDuration(500)
.compose(charts)
.on("postRedraw", () => {
// alert the dispatch crowd
facts.dispatch.call("redraw", null, "DC");
})
.on("postRender", () => {
d3.select(container)
.select("g.axis")
.selectAll(".legent")
.data([/*numberLabel*/ "# of studies"])
.join("text")
.attr("class", "legent")
.style("fill", "#000")
.attr("dx", 5)
.attr("dy", 8)
.attr("y", -h + margins.top + margins.bottom)
.attr("text-anchor", "start")
.text(d => d);
});

return container;
}
Insert cell
integerFormat = {
const small = d3.format(",d"),
large = d3.format(",.2s"),
mix = d => (d < 1000 ? small(d) : large(d));
mix.small = small; // integerFormat.small
return mix;
}
Insert cell
function createCheckboxes({
accessor,
title,
width,
invalidation,
visibility
}) {
const dim = facts.dimension(accessor),
group = dim.group().reduceSum(reducer),
values = group
.all()
.map((d) => ({ key: d.key, value: d.value }))
.sort((a, b) => d3.descending(a.value, b.value)),
uid = "check" + dim.id();

invalidation &&
invalidation.then(() => {
dim.filterAll().dispose();
facts.dispatch
.on("redraw.view" + dim.id(), null)
.on("reset.view" + dim.id(), null);
});

let filter = null;

const { container, view, reset } = createView({ width, title, reset: true }),
form = d3
.select(view)
.append("div")
.append("form")
.html(
`${values
.map(
(d, i) =>
` <label>
<input type=checkbox checked value="${i}" />
<small${addTitle(d.key)}>${
d.key
} <span id="${uid}_${i}"></span> ${
addTitle(d.key) != "" ? "<span class='help-tip'></span>" : ""
}</small>
</label>`
)
.join("<br>")}
`
)
.on("change", () => {
const select = Array.prototype.filter
.call(form.node(), (d) => d.checked)
.map((i) => values[+i.value].key);
dim.filter(
(filter =
select.length > 0 && select.length < values.length
? (d) => select.includes(d)
: null)
);
facts.dispatch.call("redraw");
});

if (reset) reset.attr("href", "#void").on("click", () => (resetF(), false));

redraw();

facts.dispatch
.on("redraw.view" + dim.id(), redraw)
.on("reset.view" + dim.id(), resetF);

return container;

function redraw() {
const filteredValues = new Map(group.all().map((d) => [d.key, d.value]));
const label = patientsButton
? ["patients", "patient"]
: ["studies", "study"];
values.forEach((d, i) => {
const num = filteredValues.get(d.key),
aff =
num === d.value
? `(${integerFormat.small(d.value)} ${label[+(d.value == 1)]})`
: `(${integerFormat.small(num)}/${integerFormat.small(d.value)})`;
form.select(`#${uid}_${i}`).html(aff);
});
reset.style("visibility", filter ? "visible" : "hidden");
}

function resetF() {
form.selectAll('input[type="checkbox"]').property("checked", true);
dim.filter((filter = null));
facts.dispatch.call("redraw");
}
}
Insert cell
function addTitle(key) {
const titles = {
Suspended: "The study has stopped early but may start again.",
Terminated:
"The study has stopped early and will not start again. Participants are no longer being examined or treated.",
Recruiting: "The study is currently recruiting participants.",
"Not recruiting":
"The study has not started recruiting participants OR is ongoing, and participants are receiving an intervention or being examined, but potential participants are not currently being recruited or enrolled.",
"No longer available":
"Expanded access was available for this intervention previously but is not currently available and will not be available in the future.",
Completed:
"The study has ended normally, and participants are no longer being examined or treated (that is, the last participant's last visit has occurred).",
Withdrawn:
"The study stopped early, before enrolling its first participant.",
Published:
"This include studies published in peer-reviewed journals and preprints.",
"Variants of Concern (VOC)":
"We consider currently designated variants of concern : Alpha, Beta, Gamma, Delta, Omicron."
};
const a = titles[key];
if (a) return ` title="${a}"`;
return "";
}
Insert cell
function createMap({ facts, invalidation, visibility }) {
const height = (Math.min(WIDTH, 954) * 0.55) | 0;
projection.fitExtent(
[
[margin, margin],
[WIDTH - margin, height - margin]
],
{
type: "Sphere"
}
);

const dim = dimCountries, // dimCountries = facts.dimension(identifyCountries, MULTIVALUED),
group = dim.group().reduceCount(), // not reducer
values = group
.all()
.slice()
.sort((a, b) => d3.descending(a.value, b.value)),
id = dim.id();

invalidation &&
invalidation.then(() => {
dim.filterAll().dispose();
facts.dispatch.on("redraw.view" + id, null).on("reset.view" + id, null);
});
facts.dispatch.on("redraw.view" + id, redraw).on("reset.view" + id, reset);

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, WIDTH, height])
.style("overflow", "visible")
.attr("id", "map"),
path = d3.geoPath().projection(projection);

let _circles,
_ghost_circles,
_links,
_labels,
_legend,
_sphere,
_countries,
_activeCountries;

let filter = null;

const ghost_values = new Map(
crossfilter(facts.all())
.dimension(dim.accessor, MULTIVALUED)
.group()
.reduceCount() // not reducer
.all()
.map((d) => [d.key, d.value])
),
active = [...centroids.values()]
.filter((d) => (d.total = ghost_values.get(d.iso) || 0))
.sort((a, b) => d3.descending(a.total, b.total));

// CREATE
function render() {
svg
.html("")
.append("defs")
.append("style")
.attr("type", "text/css")
.text(styleSvg);

_sphere = svg
.append("g")
.attr("id", "sphere")
.append("path")
.datum({ type: "Sphere" });

_countries = svg
.append("g")
.attr("id", "land")
.append("path")
.datum(countries)
.attr("d", path);

_activeCountries = svg
.append("g")
.attr("id", "activeCountries")
.selectAll("path")
.data(
countries.features.filter((d) => allCountries.has(d.properties.adm0_a3))
)
.join("path");

_links = svg.append("g").attr("id", "links");

const onClick = function (event, d) {
filter = filter && filter.includes(d.iso) ? null : [d.iso];
dim.filter(filter && ((d) => filter.includes(d)));
facts.dispatch.call("redraw");
d3.select(this).classed("selected", !!filter);
svg.select("#tooltip").attr("transform", `translate(-1000,-1000)`);
},
onMouseover = function () {
d3.select(this).classed("hover", true);
},
onMouseout = function () {
d3.select(this).classed("hover", false);
};

_ghost_circles = svg
.append("g")
.attr("id", "ghostCircles")
.selectAll("path")
.data(active)
.join("path");

_circles = svg
.append("g")
.attr("id", "circles")
.selectAll("path")
.data(active)
.join("path");

_circles
.merge(_ghost_circles)
.on("click", onClick)
.on("mouseover", onMouseover)
.on("mouseout", onMouseout);

_labels = svg
.append("g")
.attr("id", "labels")
.attr("fill", "white")
.selectAll("text")
.data(active)
.join("text");

_legend = svg.append("g").attr("id", "legend");

svg.call(addTooltip, _circles, _links);

svg.call(
zoom(projection)
.filter(() => viewof zoomButton.value)
.on("zoom.render", () => {
project();
redraw(true);
})
.on("end.render", () => {
project();
redraw(true);
})
);

project();
redraw();
}

// UPDATE PROJECTION
function project() {
_sphere.attr("d", path);
_countries.attr("d", path);
_activeCountries.attr("d", path);
}

// UPDATE
function redraw(immediate) {
// We group "by hand"
const trialCountries = facts.allFiltered().map(dim.accessor),
values = d3.rollup(
trialCountries.flat(),
(v) => v.length,
(d) => d
);

r.range([2, baseRadius * Math.sqrt(projection.scale())]);

_circles
.transition()
.duration(immediate ? 0 : 500)
.attr("d", (d) =>
path.pointRadius(values.get(d.iso) ? r(values.get(d.iso)) : 0)(d)
);

const ghost_values = d3.rollup(
facts.all().map(dim.accessor).flat(),
(v) => v.length,
(d) => d
);
_ghost_circles
.transition()
.duration(immediate ? 0 : 500)
.attr("d", (d) => path.pointRadius(r(ghost_values.get(d.iso) || 0))(d));

// links
const links = getLinks(trialCountries);

_links
.selectAll("path")
.data(links)
.join("path")
.attr("stroke", fillLinks)
.attr("fill", "none")
.attr("stroke-width", (d) => d.value / 8)
.attr("d", (d) =>
path({
type: "LineString",
coordinates: [
centroids.get(d.source).centroid,
centroids.get(d.target).centroid
]
})
);

_labels
.text((d) => {
d.trials = values.get(d.iso) || 0;
return r(d.trials) > 14 ? `${d.iso}: ${d.trials}` : d.trials;
})
.attr("transform", (d) => `translate(${projection(d.centroid)})`)
.style("opacity", (d) => (r(d.trials) > 6 ? 1 : 0));

//
_legend.call(legend, { width: WIDTH, height });
}

function reset() {
dim.filter((filter = null));
redraw();
}

render();

return html`<details open><summary class=h4>Map</summary>${svg.node()}`;
}
Insert cell
Insert cell
dimCountries = facts.dimension(identifyCountries, MULTIVALUED)
Insert cell
bycontinent = d3.rollup(
facts
.all()
.map(identifyCountries)
.map(l => l.map(d => continents.get(d)))
.flat(),
v => v.length,
d => d
)
Insert cell
continents = new Map([...centroids].map(d => [d[0], d[1].properties.CONTINENT]))
Insert cell
severityColorScale = d3
.scaleSequential(t => d3.interpolateBlues(t / 2))
.domain([1, 8])
Insert cell
styleSvg = `
#sphere { fill: #fff; stroke: #000; stroke-width: 0; }

#land { fill: ${fillLand}; stroke: #ccc; stroke-width: .5; }
#activeCountries { fill: ${fillActiveCountry}; stroke: #ccc; stroke-width: .5; }

#map text { font-family: sans-serif; font-size: 9px; text-anchor: middle; dominant-baseline: middle; pointer-events: none; }
#map text.legend { font-size: 12px }


#ghostCircles { fill: #ccc; fill-opacity: .4 }
#circles { fill: ${fillCircles}; fill-opacity: 1; stroke: white; }

#map path.hover { stroke-width: 2px }
#map path.selected { stroke-width: 3px }

#tooltip text { text-anchor: start; }


`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
r = d3.scaleSqrt().domain([0, 500])
Insert cell
baseRadius = 3.5
Insert cell
function legend(selection, { width, height }) {
const W = 180,
H = 250;
if (!selection.select("g").size()) {
const g = selection
.append("g")
.attr("transform", `translate(${width - W / 2}, ${height - H})`);
if (true)
g.append("rect")
.attr("x", -W / 2)
.attr("y", H - 100)
.attr("width", W)
.attr("height", H)
.attr("fill", "rgba(255,255,255,0.8)");
g.append("text")
.classed("legend", true)
.text("Clinical Trials")
.attr("dy", H - 58);
g.append("text")
.text("per country")
.classed("legend", true)
.attr("dy", H - 45);
g.append("g").attr("id", "container");

g.append("path")
.attr("transform", `translate(0,${H - 10})`)
.attr("fill", "none")
.attr("stroke", fillLinks)
.attr("d", "M-50,0l100,0");
g.append("text")
.classed("legend", true)
.text("Partnerships")
.attr("dy", H - 20);
}

const c = selection
.select("#container")
.selectAll("g")
.data([20, 50, 100, 200].reverse())
.join(
enter => {
const g = enter
.append("g")
.attr("transform", `translate(0, ${H - 75})`);
g.append("circle")
.attr("r", d => r(d))
.attr("cy", d => -r(d))
.attr("fill", fillCircles)
.attr("stroke", "white");
g.append("text")
.attr("fill", "white")
.text(d => d)
.attr("dy", d => -1.9 * r(d) + r(4));
},
update => {
update
.select("circle")
.attr("r", d => r(d))
.attr("cy", d => -r(d));
update.select("text").attr("dy", d => -1.9 * r(d) + r(4));
}
);
}
Insert cell
function addTooltip(svg, _circles, _links) {
const tooltip = svg.append("g").attr("id", "tooltip");

svg.on("touchmove mousemove", function(event) {
const m = d3.pointer(event);

const data = _circles.data(),
xy = data.map(d => projection(d.centroid));

const i = d3.scan(xy.map(d => Math.hypot(m[0] - d[0], m[1] - d[1]))),
dist = Math.hypot(m[0] - xy[i][0], m[1] - xy[i][1]),
d = data[i];

if (d.trials && dist < r(d.trials) + 3) {
const partners = [
...new Set(
_links
.selectAll("path")
.data()
.map(l => [l.source, l.target])
.filter(l => l.includes(d.iso))
.flat()
.sort()
.filter(l => l !== d.iso)
.map((l, i) => (!((i + 1) % 5) ? "\n" : "") + l)
)
],
partnersLines = partners.length
? `Partner${partners.length > 1 ? "s" : ""}: ${partners
.join(", ")
.trim()}`
: "";

tooltip
.attr(
"transform",
`translate(${xy[i][0]}, ${xy[i][1] + r(d.trials) + 1})`
)
.call(
callout,
`${d.properties.NAME} ${d.trials} trial${d.trials > 1 ? "s" : ""}${
d.trials < d.total ? ` (on ${d.total} total)\n` : "\n"
}${partnersLines}`.trim()
);
} else tooltip.call(callout, null);
});

svg.on("touchend mouseleave", () => tooltip.call(callout, null));
}
Insert cell
Insert cell
// https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_countries.geojson
countries = {
yield { type: "FeatureCollection", features: [] };
yield await FileAttachment("110m_countries.geojson")
.json()
.then(
d => (
(d.features = d.features.filter(d => d.properties.iso_a3 !== "ATA")), d
)
);
}
Insert cell
// https://raw.githubusercontent.com/visionscarto/some-geo-data/master/population-centroids.geojson
centroids = FileAttachment("population-centroids.geojson.txt")
.json()
.then(
d =>
new Map(
d.features.map(d => [
d.properties.ADM0_A3,
Object.assign(d, {
centroid: d3.geoCentroid(d),
iso: d.properties.ADM0_A3
})
])
)
)
Insert cell
WIDTH = width >= W0 ? Math.floor(width * 0.49) : width // for fullscreen
Insert cell
Insert cell
viewof patientsButton = checkbox(["Show number of patients"])
Insert cell
viewof fullTableButton = checkbox(["Show full table"])
Insert cell
reducer = patientsButton ? d => +d["Total sample size"] || 0 : d => 1
Insert cell
numberLabel = patientsButton ? "# of patients" : "# of studies"
Insert cell
margin = -10
Insert cell
projection = (reset, d3.geoBertin1953().precision(0.1))
Insert cell
allCountries = new Set(
facts
.allFiltered()
.map(identifyCountries)
.flat()
)
Insert cell
function identifyCountries(d) {
return d.Countries.replace(/Not reported/, "")
.replace(/Korea, Republic of/g, "Korea")
.replace(/Russian/g, "Russian Federation")
.replace(/Federation Federation/g, "Federation")
.replace(/Iran, Islamic Republic of/g, "Iran")
.replace(/United States Minor Outlying Islands/g, "United States")
.replace(/◎/g, " ")
.split(/[,;\/]/g)
.filter(d => d)
.map(d => String(d).trim())
.map(visionscarto.country_name_to_iso3)
.filter(d => d);
}
Insert cell
Insert cell
function createView(options = {}) {
const container = d3
.create("div")
.style("margin-bottom", "2em")
.style("min-width", `${options.width || 315}px`),
view = container.append("details").attr("open", "open"),
summary = view
.append("summary")
.attr("class", "h4")
.html(options.title || ""),
reset =
options.reset &&
summary
.append("a")
.attr("href", "#void")
.attr("class", "reset")
.style("font-size", "small")
.style("margin-left", "0.6em")
.html("Reset"); // &times

if (options.description) container.append("small").html(options.description);

return { container: container.node(), view: view.node(), reset };
}
Insert cell
function update() {
// this little dance re-renders everything properly when a cell changes
dc.renderAll();
dc.filterAll();
dc.renderAll();

return html`<small><i>Updated ${timeFormat(Date.now())}`;
}
Insert cell
timeFormat = d3.timeFormat("%Y-%m-%dT%H:%M:%S")
Insert cell
d3 = require("d3@6", "d3-geo-projection@2")
Insert cell
crossfilter = require("crossfilter2")
Insert cell
dc = require("https://unpkg.com/dc@4.2.0/dist/dc.js")
Insert cell
dc.config.defaultColors(d3.schemeSet1)
Insert cell
invalidate = (invalidation, chart, chartGroup) =>
invalidation &&
invalidation.then(() => {
chart.filter(null);
chart.redrawGroup();
dc.deregisterChart(chart, chartGroup);
})
Insert cell
// https://stackoverflow.com/questions/46563249/dc-js-dealing-with-entries-with-multi-valued-attributes
MULTIVALUED = true
Insert cell
Insert cell
import { checkbox } from "@jashkenas/inputs"
Insert cell
callout = (g, value) => {
if (!value) return g.style('display', 'none')
g
.style('display', null)
.style('pointer-events', 'none')
.style('font', '10px sans-serif')
const path = g.selectAll('path')
.data([null])
.join('path')
.attr('fill', 'white')
.attr('stroke', 'black')
const text = g.selectAll('text')
.data([null])
.join('text')
.call(text => text
.selectAll('tspan')
.data((value + '').split(/\n/))
.join('tspan')
.attr('x', 0)
.attr('y', (d, i) => `${i * 1.1}em`)
.style('font-weight', (_, i) => i ? null : 'bold')
.text(d => d)

)
const {x, y, width: w, height: h} = text.node().getBBox()
text.attr('transform', `translate(${-w / 2},${15 - y})`)
path.attr('d', `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`)
}
Insert cell
import { zoom } with { d3 } from "@fil/map-pan-zoom"
Insert cell
style = html`
<!-- DC.css + observablehq styles -->
<link rel="stylesheet" href="${await FileAttachment("app@1.css").url()}">
`
Insert cell
Insert cell
Insert cell
Insert cell
// import { fullscreen } from "@severo/two-columns-layout-in-fullscreen-mode"
function fullscreen({
className = 'custom-fullscreen',
style = null,
breakLayoutAtCell = 2,
hideAfter = 9999,
left = 50,
right = 50,
button,
fsButtonText = "x"
} = {}) {
if (!document.location.host.match(/static.observableusercontent.com/)) {
d3.select(title.parentElement).style("display", "none");
d3.select("main.mw8").classed("mw8", false);
}

// Superfluous bling.
const buttonStyle =
style != null
? style
: 'font-size:1rem;font-weight:bold;padding:8px;background:hsl(50,100%,90%);border:5px solid hsl(40,100%,50%); border-radius:4px;box-shadow:0 .5px 2px 1px rgba(0,0,0,.2);cursor: pointer';

button = button || html`<button style="${buttonStyle}">Toggle fullscreen!`;

const buttonText = d3.select(button).html();

// Vanilla version for standards compliant browsers.
if (document.documentElement.requestFullscreen) {
button.onclick = () => {
const parent = document.documentElement;
const cleanup = () => {
if (document.fullscreenElement) return;
parent.classList.remove(className);
d3.select(button).html(buttonText);
document.removeEventListener('fullscreenchange', cleanup);
};
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
parent.requestFullscreen().then(() => {
parent.classList.add(className);
d3.select(button).html(fsButtonText);
// Can't use {once: true}, because event fires too late.
document.addEventListener('fullscreenchange', cleanup);
});
}
};
}

// Because Safari is the new IE.
// Note: let as in https://observablehq.com/@mootari/fullscreen-layout-demo.
// The button will not toggle between states
else {
// = FileAttachment("screenfull.js") .then(require)
const screenfull = require('screenfull@4.2.0/dist/screenfull.js').catch(
() => window['screenfull']
);
// We would need some debouncing, in case screenfull isn't loaded
// yet and user clicks frantically. Then again, it's Safari.
button.onclick = () => {
screenfull.then(sf => {
const parent = document.documentElement;
sf.request(parent).then(() => {
const cleanup = () => {
if (sf.isFullscreen) return;
parent.classList.remove(className);
d3.select(button).html(buttonText);
sf.off('change', cleanup);
};
parent.classList.add(className);
d3.select(button).html(fsButtonText);
sf.on('change', cleanup);
});
});
};
}

// Styles don't rely on the :fullscreen selector to avoid interfering with
// Observable's fullscreen mode. Instead a class is applied to html during
// fullscreen.

return html`
${button}
<style>
html.${className} {
padding: 0 0px 0 8px;
}
html.${className} body {
margin: 0;
padding: 0;
}
html.${className} body, html.${className} body > div:not(.observablehq) {
overflow: auto;
height: 100%;
width: 100%;
}
html.${className} body, html.${className} body > div:not(.observablehq) {
display: block;
background: white;
height: 100%;
width: auto;
overflow: auto;
position: relative;
}
.observablehq::before {
height: 0;
}
html.${className} .observablehq {
margin-left: ${left}vw;
width: ${(WIDTH * right) / 50}px;
margin-top: 0 !important;
margin-bottom: 0 !important;
min-height: 0 !important;
/*max-height: 100%;*/
/*overflow: auto;*/
padding: .5rem;
box-sizing: border-box;
}
html.${className} .observablehq:nth-of-type(-n+${breakLayoutAtCell}) {
float: left;
width: ${(WIDTH * left) / 50}px;
margin-left: 0;
}

html.${className} .observablehq:nth-of-type(n+${hideAfter}) {
display: none;
}
summary.h4 { font-size: 1rem; height: 1.5rem; }

`;
}
Insert cell
clamp = (x, lo, hi) => (x < lo ? lo : x > hi ? hi : x)
Insert cell
getMonday = d => {
d = new Date(d);
var day = d.getDay(),
diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
return new Date(d.setDate(diff));
}
Insert cell
import { download } from "@mbostock/lazy-download"
Insert cell
Insert cell
d3.rollup(
facts.all(),
v => v.map(d => d.Countries + " | " + identifyCountries(d).join(", ")),
d => identifyCountries(d).length
)
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more