plotOverview = function (l, params) {
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();
}