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

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