Public
Edited
Apr 3, 2023
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
mainTitle = "Tracking change in quantity of 2 SARS-CoV-2 genes since " + d3.timeFormat('%b %e')(earliestDateUnformatted)
Insert cell
ylabel = "Quantity of RNA, normalized"
Insert cell
xlabel = "Date of Sample"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
x_weeks = num_weeks <= 6 ? 1 : num_weeks <= 12 ? 2 : num_weeks <= 20 ? 4 : 8
Insert cell
smoothing = "_trimmed5"
Insert cell
Insert cell
totalwidth = 600
Insert cell
totalheight = 300
Insert cell
leftMargin = 100
Insert cell
num_weeks = 6
Insert cell
humanReadableLocationNames = ({
'Palo Alto': 'Palo Alto',
'San Jose': 'San Jose',
'Oceanside': 'Oceanside (Western San Francisco)',
'Silicon Valley': 'Silicon Valley Clean Water',
'CODIGA': 'CODIGA (Stanford University)',
'Davis': 'Davis',
'Gilroy': 'Gilroy',
'Sunnyvale': 'Sunnyvale',
'Sacramento': 'Sacramento',
'UC Davis': 'UC Davis',
'Merced': 'Merced',
'Modesto': 'Modesto',
'Southeast San Francisco': '"Southeast" (Eastern San Francisco)',
})
Insert cell
cutoffDates = ({
'Palo Alto': '2020-11-18',
'San Jose': '2020-11-17',
'Oceanside': '2020-12-10',
'Silicon Valley': '2020-12-10',
'CODIGA': '2021-07-29',
'Davis': '2020-11-25',
'Gilroy': '2020-12-03',
'Sunnyvale': '2020-12-02',
'Sacramento': '2020-12-10',
'UC Davis': '2021-05-01',
'Merced': '2021-10-20',
'Modesto': '2021-10-20',
'Southeast San Francisco': '2022-05-20'
})
Insert cell
//metrics = ["N_PMMoV","S_PMMoV"]
metrics = ["N_PMMoV","S_PMMoV"]
Insert cell
metricsAllVariants = ["N_PMMoV","S_PMMoV"]
Insert cell
metricsToDefineYGridlines = metricsAllVariants // for covid use N & S to define gridlines, for other pathogens will import "with" to overright this with appropriate metric
Insert cell
gridlineColor = "#101b63"
Insert cell
ndLimits = ({
Sacramento: 0.00000123,
"Silicon Valley": 0.00000031,
Oceanside: 0.00000162,
Gilroy: 0.000000294,
Sunnyvale: 0.000000557,
Davis: 0.000000965,
"Palo Alto": 0.000000703,
"San Jose": 0.000000467,
CODIGA: 0.0000026
})
Insert cell
metricsMeta = ({
N_PMMoV: { label: "N gene - all variants / PMMoV", color: "#7fa2db" },
N: { label: "N gene - all variants", color: "#7fa2db" },

S_PMMoV: { label: "S gene - all variants / PMMoV", color: "#3865b0" },
S: { label: "S gene - all variants", color: "#3865b0" },
Delta_156157_PMMoV: {label: "Delta / PMMoV", color: "#45d991"},
Delta_156157: {label: "Delta", color: "#45d991"},
Del_143145_PMMoV: { label: "BA.1 Omicron / PMMoV", color: "#ffb14e"},
Del_143145: { label: "BA.1 Omicron", color: "#ffb14e"},
BA_2_LPPA24S_PMMoV: { label: "(BA.2 + BA.4 + BA.5 Omicron) / PMMoV", color: "#eb5f34"},
BA_2_LPPA24S: { label: "(BA.2 + BA.4 + BA.5 Omicron)", color: "#eb5f34"},
BA_4_ORF1a_Del_141143_PMMoV: { label: "BA.4 Omicron / PMMoV", color: "#cd34b5"},
BA_4_ORF1a_Del_141143: { label: "BA.4 Omicron", color: "#cd34b5"},

HV_6970_Del_PMMoV: { label: "(BA.4 + BA.5 + BQ.* Omicron)/PMMoV", color: "#9d02d7"},
HV_6970_Del: { label: "(BA.4 + BA.5 + BQ.* Omicron)", color: "#9d02d7"},

BA_2_75_S_147E_S_152R_PMMoV: {label: "BA 2.75 Omicron / PMMoV", color: "#FF1175"}, // BA 2.75
BA_2_75_S_147E_S_152R: {label: "BA 2.75 Omicron", color: "#FF1175"},

XBB_bkpt_PMMoV: {label: "XBB Omicron / PMMoV", color: "#764FFF"},
XBB_bkpt: {label: "XBB Omicron", color: "#764FFF"},

})

//["#ffb14e","#fa8775","#ea5f94","#cd34b5","#9d02d7", "#FF1175"]
Insert cell
Insert cell
mutable temp = []
Insert cell
tempchart = plotOverview("Oceanside", params)
Insert cell
params = ({cutoffdate: "2023-01-01", ndlimit: -.0001, showdetailtooltip: true})
Insert cell
plotOverview = function (l, params) {
// todo: revisit all "params" (put in place for monkeypox issues near 0)
const ndLimit = params?.ndlimit ? params.ndlimit : ndLimits[l];

let cutoffDate = params?.cutoffdate ? new Date(params.cutoffdate) : new Date(cutoffDates[l]);

let data = rawdata.filter(
(d) => (d.Plant == l) & (d.date >= new Date(earliestDate))
);
let dates = d3.map(data, (d) => d.date);
let dataPoints = data.filter((d) => d.date < cutoffDate);
let dataLines = data.filter((d) => d.date >= cutoffDate);
let dataByDate = d3.group(data, (d) => d.dateformatted);

let dateExtent = [new Date(earliestDate), new Date()];
let props = {
margin: {
top: 80,
right: 180,
bottom: 60,
left: leftMargin
}
};
props.chartHeight = totalheight - props.margin.top - props.margin.bottom;
props.chartWidth = totalwidth - props.margin.right - props.margin.left;
let dayMultiple =
props.chartWidth /
d3.timeDay.count(new Date(dateExtent[0]), new Date(dateExtent[1]));

let twoWeeksAgo =
data[
data
.map((e) => timeFormat(e.date))
.indexOf(timeFormat(d3.timeDay.offset(new Date(), -14)))
];

let timelabels = {
full: "2 weeks",
abbv: "2w"
};

// if no data 14 days ago, try looking backwards
let countup = 14
while (typeof twoWeeksAgo == "undefined" & countup <= 30) {
twoWeeksAgo =
data[
data
.map((e) => timeFormat(e.date))
.indexOf(timeFormat(d3.timeDay.offset(new Date(), -1 * countup)))
];
timelabels = {
full: countup + " days",
abbv: countup + "d"
};
countup++
}

// if no data 14-30 days ago, try looking forward
let countdown = 14
while (typeof twoWeeksAgo == "undefined" & countdown >= 7) {
twoWeeksAgo =
data[
data
.map((e) => timeFormat(e.date))
.indexOf(timeFormat(d3.timeDay.offset(new Date(), -1 * countdown)))
];
timelabels = {
full: countdown + " days",
abbv: countdown + "d"
};
countdown = countdown - 1;
}
let twoWeeksAgoMean = d3.mean(metricsToDefineYGridlines.map((e) => twoWeeksAgo[e]));


let minValue = log
? d3.min(data, (d) => d3.min(metrics.map((metric) => d[metric])))
: 0;
let maxValue = d3.max(data, (d) => d3.max(metrics.map((metric) => d[metric])));
let ydomain = [
d3.min([minValue, twoWeeksAgoMean / 2]),
maxValue == 0 ? .000001 : maxValue
];

let xScale = d3.scaleLinear().domain(dateExtent).range([0, props.chartWidth]);
let yScale = log ? d3.scaleLog() : d3.scaleLinear();

const svg = d3
.create("svg")
.attr("width", props.chartWidth + props.margin.left + props.margin.right)
.attr("height", props.chartHeight + props.margin.top + props.margin.bottom);

let chart = svg
.append("g")
.attr(
"transform",
"translate(" + props.margin.left + "," + props.margin.top + " )"
);

yScale.domain(ydomain).range([props.chartHeight, 0]);

// axis
let formatMyDates = d3.timeFormat("%b %e");
let formatMyDatesYear = d3.timeFormat("%b %e, %Y");
let xAxis = d3
.axisBottom(xScale)
.tickValues(d3.timeDay.range(dateExtent[0], dateExtent[1], 7 * x_weeks))
.tickFormat((d) => formatMyDates(d))
.tickSize(10);

let intervalLabels = [
minValue == 0 ? "0" : "",
"half " + timelabels.abbv + " ago",
timelabels.abbv + " ago"
];
let intervalValues = [minValue, twoWeeksAgoMean / 2, twoWeeksAgoMean];
if (twoWeeksAgoMean * 2 < ydomain[1]) {
intervalValues.push(twoWeeksAgoMean * 2);
intervalLabels.push("twice " + timelabels.abbv + " ago");
}

if (twoWeeksAgoMean == 0) {
intervalLabels = ["2w ago"];
intervalValues = [0];
}

if (metrics.filter(x => metricsToDefineYGridlines.includes(x)).length == 0 ||
typeof(twoWeeksAgoMean) == 'undefined') {
intervalLabels = [];
intervalValues = [];
}

let yAxis = d3
.axisLeft(yScale)
.tickValues(intervalValues)
.tickFormat((d, i) => {
return intervalLabels[i];
});

chart.append("g").call(yAxis).selectAll("text").attr("fill", "#101b63");
chart
.append("g")
.attr("transform", "translate(0," + props.chartHeight + ")")
.call(xAxis);

chart.selectAll(".domain").attr("stroke", "#d6d6d6");
chart.selectAll(".tick").selectAll("line").attr("stroke", "#d6d6d6");

// title
chart
.append("text")
.attr("transform", "translate(0,-38)")
.attr("font-size", "20px")
.text(humanReadableLocationNames[l]);

chart
.append("text")
.attr("transform", "translate(0,-20)")
.attr("font-size", "14px")
.text(mainTitle);

chart
.append("text")
.attr(
"transform",
"translate(" +
props.chartWidth / 2 +
"," +
(props.chartHeight + props.margin.top - 40) +
")"
)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text(xlabel);

chart
.append("text")
.attr(
"transform",
"rotate(270)translate(" + (-1 * props.chartHeight) / 2 + ",-80)"
)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.text(ylabel);


// gridlines
chart
.append("g")
.selectAll("line")
.data(intervalValues.slice(1, intervalValues.length))
.join("line")
.attr("x1", xScale.range()[0])
.attr("x2", xScale.range()[1])
.attr("y1", (d) => yScale(d))
.attr("y2", (d) => yScale(d))
.attr("stroke", gridlineColor)
.attr("stroke-width", 1)
.attr("stroke-dasharray", (d, i) => {
if (i == 1) {
return "5,2";
}
return "1,3";
});

let gridlines = chart
.append("g")
.attr(
"transform",
"translate(" + xScale(twoWeeksAgo.date) + "," + yScale.range()[1] + ")"
);

// vertical for two weeks ago
gridlines
.append("line")
.attr("y1", yScale.range()[0])
.attr("stroke", "#ababab");

gridlines.append("circle").attr("r", 2).attr("fill", "#757575");

gridlines
.append("text")
.attr("x", 2)
.attr("y", 3)
.attr("font-size", "10px")
.attr("fill", "#757575")
.text(formatMyDates(twoWeeksAgo.date) + " (" + timelabels.abbv + " ago)");


// the data curves

chart
.append("g")
.selectAll("g")
.data(metrics)
.join("path")
.attr("d", (metric) => {
return d3
.line()
.y((d) => yScale(d[metric]))
.defined((d) => isFinite(yScale(d[metric])))
.x((d) => xScale(d.date))(dataLines);
})
.attr("fill", "none")
.attr("stroke", (metric) => {
return metricsMeta[metric]["color"];
})
.attr("stroke-width", 3)
.attr("opacity", 0.75)
.attr("class", "lines");

// the data points [for early history when sampling was not yet supposed to be daily]
for (const [i] in metrics) {

// points
chart
.append("g")
.selectAll("g")
.data(dataPoints.filter((d) => isFinite(yScale(d[metrics[i]]))))
.join("circle")
.attr("stroke", "none")
.attr("fill", metricsMeta[metrics[i]]["color"])
.attr("cx", (d) => xScale(d.date))
.attr("cy", (d) => yScale(d[metrics[i]]))
.attr("r", 3);

// hover points
let lasttwoweeks = chart.append("g").selectAll("g")
.data(data.filter(d => !isNaN(d[metrics[i]])));

lasttwoweeks
.join("circle")
.attr("stroke", "black")
.attr("fill", metricsMeta[metrics[i]]["color"])
.attr("cx", (d) => xScale(d.date))
.attr("cy", (d) => yScale(d[metrics[i]]))
.attr("class", (d) => "date_" + d.dateformatted + " tooltipPoints")
.attr("r", 5)
.attr("opacity", 0);

/*
lasttwoweeks.join('line')
.attr('stroke', metricsMeta[metrics[i]]['color'])
.attr('x1', d => xScale(d.date))
.attr('y1', d => yScale(d[metrics[i]]))
.attr('x2', d => xScale(new Date(offset2weeks(d.dateformatted))))
.attr('y2', d => {
let twoweeksago = dataByDate.get(offset2weeks(d.dateformatted));
if(twoweeksago){
return yScale(twoweeksago[0][metrics[i]])}
})
.attr('class', d => "date_" + d.dateformatted + ' tooltipPoints')
.attr('stroke-width', 2)
.attr('opacity', 0)
*/
}

// Add Labels to lines (assuming last value is in the lines group)
let yLabelPlacements = [];

// todo: better handle for empty dataLines
let lastvalue = dataLines.length > 0 ? dataLines[dataLines.length - 1]
: dataPoints[dataPoints.length - 1];

metrics.forEach((m) => {
// todo: better handle for empty dataLines
let nonNAN = dataLines.length > 0 ? dataLines.filter(d => !isNaN(d[m])) :
dataPoints.filter(d => !isNaN(d[m]));
let value = nonNAN[nonNAN.length - 1]
if(value){
yLabelPlacements.push({
metric: m,
missing: false,
pixel: value[m] == 0 ? yScale(0) - 8 : yScale(value[m]),
xpixel: xScale(value.date)
});
} else {
yLabelPlacements.push({
metric: m,
missing: true,
pixel: yScale(0) - 8,
xpixel: xScale.range()[1]
});
}
});

// adjust yLabel location to avoid overflow
yLabelPlacements = yLabelPlacements.sort((a, b) => a.pixel - b.pixel);
if (yLabelPlacements.length > 1) {
if (yLabelPlacements[1].pixel - yLabelPlacements[0].pixel < 10) {
yLabelPlacements[0].pixel = yLabelPlacements[1].pixel - 10;
}
if (yLabelPlacements.length > 2) {
if (yLabelPlacements[2].pixel - yLabelPlacements[1].pixel < 10) {
yLabelPlacements[2].pixel = yLabelPlacements[1].pixel + 10;
}
}
}

chart
.append("g")
.selectAll("text")
.data(yLabelPlacements)
.join("text")
.attr(
"transform",
(yLabel, i) =>
"translate(" +
(2 + yLabel.xpixel) +
"," +
(yLabel.pixel + 4) +
")"
)
.attr("fill", (yLabel) => metricsMeta[yLabel.metric].color)
.attr("x", 2)
.attr("font-size", "10px")
.text((yLabel) => yLabel.missing ?
'No Data: ' + metricsMeta[yLabel.metric].label :
metricsMeta[yLabel.metric].label);

// show a mark on x-axis for all dates we have data for
chart
.append("g")
.selectAll("line")
.data(dates)
.join("line")
.attr("x1", (d) => xScale(d) + 0.5)
.attr("x2", (d) => xScale(d) + 0.5)
.attr("y1", yScale.range()[0] + 3)
.attr("y2", yScale.range()[0])
.attr("stroke", "black")
.attr("stroke-width", 2);

// for capturing mouseover events
let datesFormatted = dates.map((d) => timeFormat(d));
chart
.append("g")
.selectAll("line")
.data(d3.timeDays(dateExtent[0], d3.timeDay.offset(dateExtent[1], 1), 1))
.join("rect")
.attr("x", (d) => xScale(d) - dayMultiple / 2)
.attr("width", dayMultiple + 0.2)
.attr("y1", yScale.range()[0] + 3)
.attr("height", yScale.range()[0] - yScale.range()[1])
.attr("opacity", 0)
/*.attr('opacity', d => {
if(datesFormatted.indexOf(timeFormat(d)) == -1){
return .4
};
return 0
})*/
.attr("fill", "grey")
.attr("stroke", "none")
.attr("cursor", "pointer")
.on("mouseover", showTooltip)
.on("mousemove", moveTooltip)
.on("mouseout", hideTooltip);

const gradientId = "gradient-vertical-fade"; //+ Math.random();
const gradientSel = chart
.append("linearGradient")
.attr("id", gradientId)
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 1)
.attr("y2", 0);

gradientSel
.append("stop")
.attr("offset", "50%")
.attr("stop-color", "rgba(200,200,200,1)");

gradientSel
.append("stop")
.attr("offset", "70%")
.attr("stop-color", "rgba(200,200,200,.8)");

gradientSel
.append("stop")
.attr("offset", "100%")
.attr("stop-color", "rgba(200,200,200,.1)");

chart
.append("rect")
.attr("width", props.chartWidth)
.attr("height", props.chartHeight - yScale(ndLimit))
.attr("y", yScale(ndLimit))
.attr("fill", `url(#${gradientId})`);

function moveTooltip(event, d) {
tooltip
//.style('top', (event.pageY - event.layerY + props.margin.top - 10) + 'px')
.style("top", event.pageY + 30 + "px")
.style("left", event.pageX + "px");

for (const [i] in metrics) {
let ltw = d3
.selectAll(".lasttwoweeksmark_" + metrics[i])
.attr("transform", (d) => console.log(d));
/*
ltw.select("circle")
.attr('cx', xScale(datavalue.date))
.attr('cy', yScale(datavalue[metrics[i]]))
*/
}
}

function showTooltip(event, d) {
d3.selectAll(".date_" + timeFormat(d)).attr("opacity", 1);

if(params?.showdetailtooltip) {
tooltip
.html(`<div>${d}</div>`)
.style("visibility", "visible");
} else {
tooltip
.html(`<div>${formatMyDatesYear(d)}</div>`)
.style("visibility", "visible");
}
}

function hideTooltip(event, d) {
d3.selectAll("." + "tooltipPoints").attr("opacity", 0);

tooltip.html(``).style("visibility", "hidden");
}

return svg.node();
}
Insert cell
tooltip = d3
.select('body')
.append('div')
.attr('class', 'scan-tooltip')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.style('padding', '5px')
.style('background', 'white')
.style('border-radius', '4px')
.style('border', "1px solid grey")
.style('opacity', ".9")
.style('left', '50%')
.style('transform', 'translateX(-50%)')
.style('color', 'black');
Insert cell
html`<style>
.hidden {
display: none;
}
</style>
`
Insert cell
md`### Supporting functions`
Insert cell
offset2weeks = function(date){
return timeFormat(d3.timeDay.offset(new Date(date), -14))
}

Insert cell
earliestDate = timeFormat(earliestDateUnformatted)
Insert cell
earliestDateUnformatted = d3.timeDay.offset(new Date(), -(7 * num_weeks))
Insert cell
timeParse = d3.timeParse("%Y-%m-%d")
Insert cell
parseData = (d) => {
d.datetime = d.Date.replace(' ', 'T')
if (new Date(d.datetime) >= new Date(earliestDate)) {
let r = {
date: timeParse(d.datetime.substring(0, 10)),
dateformatted: d.datetime.substring(0, 10),
};
if (new Date(d.datetime) >= new Date(cutoffDates[d.Plant])) {
metrics.forEach((metric) => (r[metric] = +d[metric + smoothing]));
} else {
metrics.forEach((metric) => (r[metric] = +d[metric]));
}
r["Plant"] = d.Plant;
return r;
}
}
Insert cell
timeFormat = d3.timeFormat('%Y-%m-%d');
Insert cell
omicron_color = "#448388"
Insert cell
Insert cell
mutable mutVar = []
Insert cell
rawdata = d3.csv(url, parseData)
Insert cell
test = d3.csv(url)
Insert cell
[...new Set(test.map((d) => d.Plant))]
Insert cell
test.map(d => d.N_PMMoV_trimmed5)
Insert cell
rawdata.filter((d) => d.Plant == "Oceanside")
Insert cell
url = "https://storage.googleapis.com/soe-wwd-data/dashboard.csv?no_cache=" + Math.random()
//"https://storage.googleapis.com/soe-wwd-data/4462.csv?" + Math.random()
Insert cell
Insert cell
Select = Inputs.Select
Insert cell
Inputs = require("@observablehq/inputs@0.7.17/dist/inputs.umd.min.js")
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more