function appendOrSelect(parent, type, className) {
return parent.selectAll(`.${className}`)._groups[0][0]
? parent.select(`.${className}`)
: parent.append(type).attr("class", className);
}
function createChartLayers(parent) {
let chartLayer = appendOrSelect(parent, "g", "chart-layer");
let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer");
let axisX = appendOrSelect(axisLayer, "g", "axis-x");
let axisY = appendOrSelect(axisLayer, "g", "axis-y");
let titleLayer = appendOrSelect(chartLayer, "g", "title-layer");
let dataLayer = appendOrSelect(chartLayer, "g", "data-layer");
let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer");
}
function createChartSettings(parent, dataArray, pad) {
[topPad, rightPad, bottomPad, leftPad] = pad;
let width = Number(parent.style("width").split("px")[0]);
let height = Number(parent.style("height").split("px")[0]);
let numDays = dataArray.length;
let barWidth = (width - leftPad - rightPad) / numDays;
return {
width: width,
height: height,
numDays: numDays,
barWidth: barWidth,
pad: {
top: topPad,
right: rightPad,
bottom: bottomPad,
left: leftPad,
},
};
}
//Scale functions
function getMaxSum(dataArray, metricArray) {
let maxTotal = 0;
dataArray.forEach((day) => {
let dayTotal = 0;
metricArray[2].forEach((metric) => {
dayTotal += day[metricArray[0]][metricArray[1]][metric];
});
maxTotal = Math.max(dayTotal, maxTotal);
});
return maxTotal;
}
function getYScale(chart, max) {
return d3
.scaleLinear()
.domain([0, Math.max(max * 1.05, 1)])
.range([chart.height - chart.pad.bottom, chart.pad.top]);
}
function getXScale(chart, data) {
return d3
.scaleLinear()
.domain([0, chart.numDays - 1])
.range([chart.pad.left, chart.width - chart.pad.right]);
}
function getTimeScale(chart, data) {
let timeRange = d3.extent(data, (d) => d.date);
let before = timeRange[0];
before.setHours(0, 0, 0);
let after = timeRange[1];
after.setHours(0, 0, 0);
// console.log(before, after);
return d3
.scaleTime()
.domain([before, after])
.range([chart.pad.left, chart.width - chart.pad.right]);
}
//Line chart functions
function getLine(xScale, yScale, zero = false) {
return d3
.line()
.x((d, i) => xScale(i))
.y((d, i) => yScale(zero ? 0 : d[1]));
}
//Hover dot functions
function createDots(
fullData,
layer,
xScale,
yScale,
areaColorScale,
lineColorScale,
dotColorScale
) {
fullData.forEach((stack, index) => {
let D = layer.selectAll(`.circle-${index}`).data(stack.data);
let Dplus = D.enter().append("circle");
Dplus.append();
Dplus.attr("class", `circle-${index}`)
.attr("dot", (d, i) => i)
.attr("cx", (d, i) => xScale(i))
.attr("cy", (d) => yScale(0))
.attr("r", 5)
.attr("fill", dotColorScale(fullData[index].key))
.attr("stroke", lineColorScale(fullData[index].key))
.attr("stroke-width", 1)
.attr("opacity", 0)
.transition()
.duration(1000)
.attr("cy", (d) => yScale(d[1]));
});
}
function updateDots(
fullData,
layer,
xScale,
yScale,
areaColorScale,
lineColorScale,
dotColorScale
) {
layer.selectAll("*").remove();
fullData.forEach((stack, index) => {
let D = layer.selectAll(`.circle-${index}`).data(stack.data);
D.exit().remove();
D.attr("class", `circle-${index}`)
.attr("cx", (d, i) => xScale(i))
.attr("fill", areaColorScale(fullData[index].key))
.attr("stroke", lineColorScale(fullData[index].key))
.attr("opacity", 0.3)
.transition()
.duration(1000)
.attr("cy", (d) => yScale(d[1]));
D.enter()
.append("circle")
.attr("class", `circle-${index}`)
.attr("dot", (d, i) => i)
.attr("cx", (d, i) => xScale(i))
.attr("cy", (d) => yScale(0))
.attr("r", 5)
.attr("fill", dotColorScale(fullData[index].key))
.attr("stroke", lineColorScale(fullData[index].key))
.attr("stroke-width", 1)
.attr("opacity", 0)
.transition()
.duration(1000)
.attr("cy", (d) => yScale(d[1]));
});
}
function enterLegend(parent, data, chart, areaColorScale, lineColorScale) {
parent.selectAll("*").remove();
let newData = data.slice().reverse();
let Leg = parent.selectAll("g").data(newData);
let LegLayer = Leg.enter()
.append("g")
.attr("transform", (d, i) => `translate(${chart.pad.left}, ${i * 20 + chart.pad.top})`);
LegLayer.append("rect")
.attr("fill", (d) => areaColorScale(d))
.attr("width", 14)
.attr("height", 14)
.attr("stroke", (d) => lineColorScale(d))
.attr("stroke-width", 0)
.attr("rx", 2);
//.attr('shape-rendering', 'crispEdges')
LegLayer.append("text")
.text((d) =>
d
.toLowerCase()
.split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
.join(" ")
)
.attr("x", 19)
.attr("y", 12)
.attr("font-size", 14)
.attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`);
}
//Area chart functions
function getArea(xScale, yScale, zero = false) {
return d3
.area()
.x((d, i) => xScale(i))
.y0((d) => yScale(zero ? 0 : d[0]))
.y1((d) => yScale(zero ? 0 : d[1]));
}
function enterArea(selection, area, areaZero, areaColorScale) {
selection
.enter()
.append("path")
.attr("class", "area")
.attr("d", (d) => areaZero(d.data))
.attr("fill", (d, i) => areaColorScale(d.key))
.transition()
.duration(1000)
.attr("d", (d) => area(d.data));
return selection;
}
function updateArea(selection, area, dur) {
selection
.transition()
.duration(dur)
.attr("d", (d) => area(d.data));
}
function exitArea(selection, areaZero, dur) {
selection
.exit()
.lower()
.attr("opacity", 1)
.transition()
.duration(1000)
.attr("d", (d) => areaZero(d.data))
.attr("opacity", 1)
.remove();
}
//Resize functions
function debounce(func) {
var timer;
return function (event) {
if (timer) clearTimeout(timer);
timer = setTimeout(func, 10, event);
};
}
function addResize(parent, data, metricArray, latestDate) {
window.addEventListener(
"resize",
debounce(() => {
resizeNewCurveChart(parent, data, metricArray, latestDate);
})
);
}
function makeButton(buttonParent, text, index, selection, parent, data, metricArray, latestDate) {
toggle = "button-off";
if (metricArray[index] === selection) {
toggle = "button-on";
}
if (index === 2 && metricArray[2][0] === "cases" && text === "Total") {
toggle = "button-on";
}
if (index === 2 && metricArray[2][0] !== "cases" && text === "Breakdown") {
toggle = "button-on";
}
buttonParent
.append("button")
.text(text)
.on("click", (d) => {
metricArray[index] = selection;
if (metricArray[2][0] !== "cases") {
metricArray[2] = ["active", "recovered", "deaths"];
}
if (metricArray[1] !== "cumulative") {
metricArray[2] = metricArray[2].filter((e) => e !== "active");
}
//if (metricArray[2][0] !== 'cases' && metricArray[1] !== 'cumulative') {metricArray[2] = ['deaths', 'recovered']}
if (parent === "#covid-canada-date-chart") {
chartInput = metricArray;
}
updateNewCurveChart(
parent,
data,
[metricArray[0], metricArray[1], metricArray[2]],
latestDate
);
})
.attr("class", `chart-button ${toggle}`);
}
function checkDate(date, latestDate) {
let d1 = new Date(date);
let d2 = new Date(latestDate);
let dayDiff = (d2.setHours(0, 0, 0, 0) - d1.setHours(0, 0, 0, 0)) / (1000 * 3600 * 24);
//let check = date.setHours(0,0,0,0) == latestDate.setHours(0,0,0,0);
//console.log(date, latestDate, check)
return dayDiff;
}
//Constants
const op = 0.55;
let ds = -10;
let dl = -10;
const colorCategories = ["cases", "deaths", "recovered", "active"];
const areaColors = [`#fadca2`, `#a1a0a0`, `#efb88f`, `#76c1cf`];
const dotColors = [`#ffeecf`, `#d4d4d4`, `#ffe3cf`, `#bdf3fc`];
const lineColors = [
`rgb(244, 187, 63)`,
`hsla(0, 0%, ${32.5 + dl}%, 1`,
`hsla(27, 74.2%,${52.9 + dl}%, 1)`,
`hsla(191, 71.6%,${38.6 + dl}%, 1)`,
];
const chartPadding = [10, 50, 30, 20];
const dur = 1000;
function createNewCurveChart(parent, data, metricArray, latestDate) {
let dataArray = data.tracking;
let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate);
//console.log(data.properties.PRENAME, dateAdjustment)
dataArray = dataArray.filter((day, i) => i > 35 + dateAdjustment);
let container = d3.select(parent);
let titleDiv = appendOrSelect(container, "div", "title-div");
if (parent === "#covid-canada-date-chart") {
titleDiv.html(
`${data.properties.PRENAME} (click map for provincial stats)`
);
} else {
titleDiv.html(`${data.properties.PRENAME}`);
}
let buttonDiv = appendOrSelect(container, "div", "button-div");
let buttonGroup1 = buttonDiv.append("div").attr("class", "button-group");
let buttonGroup2 = buttonDiv.append("div").attr("class", "button-group");
let buttonGroup3 = buttonDiv.append("div").attr("class", "button-group");
makeButton(buttonGroup1, "Total", 2, ["cases"], parent, data, metricArray, latestDate);
makeButton(
buttonGroup1,
"Breakdown",
2,
["active", "recovered", "deaths"],
parent,
data,
metricArray,
latestDate
);
makeButton(buttonGroup2, "Cumulative", 1, "cumulative", parent, data, metricArray, latestDate);
makeButton(buttonGroup2, "New", 1, "new", parent, data, metricArray, latestDate);
makeButton(buttonGroup2, "7-day avg", 1, "average", parent, data, metricArray, latestDate);
makeButton(buttonGroup3, "Raw", 0, "raw", parent, data, metricArray, latestDate);
makeButton(buttonGroup3, "/100K", 0, "per100k", parent, data, metricArray, latestDate);
let svg = appendOrSelect(container, "svg", "curve-svg");
svg.attr("width", "100%").attr("height", "250");
let chart = createChartSettings(svg, dataArray, chartPadding);
let chartLayer = appendOrSelect(svg, "g", "chart-layer");
let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer");
let axisX = appendOrSelect(axisLayer, "g", "axis-x");
let axisY = appendOrSelect(axisLayer, "g", "axis-y");
let titleLayer = appendOrSelect(chartLayer, "g", "title-layer");
let dataLayer = appendOrSelect(chartLayer, "g", "data-layer");
let areaLayer = appendOrSelect(dataLayer, "g", "area-layer");
let lineLayer = appendOrSelect(dataLayer, "g", "line-layer");
let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer");
let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer");
let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer");
let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer");
let max = getMaxSum(dataArray, metricArray);
let yScale = getYScale(chart, max);
let xScale = getXScale(chart);
let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors);
let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors);
let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors);
let timeScale = getTimeScale(chart, dataArray);
// console.log(dataArray);
let xAxis = d3
.axisBottom()
.scale(timeScale)
.tickFormat(function (date) {
if (d3.timeYear(date) < date) {
return d3.timeFormat("%b")(date);
} else {
return d3.timeFormat("%Y")(date);
}
});
axisX
.call(xAxis)
.attr("transform", `translate(0, ${chart.height - chart.pad.bottom})`)
.attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`);
let yAxis = d3.axisRight().scale(yScale).ticks(6);
axisY
.call(yAxis)
.attr("transform", `translate(${chart.width - chart.pad.right}, 0)`)
.attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`);
let breakdown = [];
metricArray[2].forEach((metric) => {
breakdown.push([metricArray[0], metricArray[1], metric]);
});
const stack = d3
.stack()
.keys(breakdown)
.value((d, key) => d[key[0]][key[1]][key[2]]);
const stackedValues = stack(dataArray);
let stackArray = [];
stackedValues.forEach((stack, i) => {
let key = metricArray[2][i];
stackArray.push({
key: key,
data: stack,
});
});
let area = getArea(xScale, yScale);
let areaZero = getArea(xScale, yScale, true);
let line = getLine(xScale, yScale);
let lineZero = getLine(xScale, yScale, true);
let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key);
enterArea(A, area, areaZero, areaColorScale);
let L = lineLayer.selectAll(".line").data(stackArray, (d) => d.key);
L.enter()
.append("path")
.attr("class", "line")
.attr("d", (d) => lineZero(d.data))
.attr("stroke", (d, i) => lineColorScale(d.key))
.attr("stroke-width", 1)
.attr("fill", "none")
.transition()
.duration(1000)
.attr("d", (d) => line(d.data));
let H = hoverLayer.selectAll("rect").data(dataArray);
function toolTip(e, d, i) {
tooltipLayer.selectAll("*").remove();
dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1);
const [x, y] = d3.pointer(e);
// console.log(e, d, i);
let newData = metricArray[2].slice().reverse();
newData.forEach((metric, j) => {
tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 10 + j * 15 - 0.5 * metricArray[2].length * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.text(`${metric}: ${d[metricArray[0]][metricArray[1]][metric]}`);
});
}
H.enter()
.append("rect")
.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2)
.attr("y", yScale(max * 1.05))
.attr("width", (d, i) => xScale(i + 1) - xScale(i))
.attr("height", yScale(0))
.attr("shape-rendering", "crispEdges")
.attr("opacity", 0)
.on("mousemove", function (e, d, i) {
tooltipLayer.selectAll("*").remove();
dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1);
const [x, y] = d3.pointer(e);
let newData = metricArray[2].slice().reverse();
let rect = tooltipLayer
.append("rect")
.attr("fill", "white")
.attr("stroke", "#444")
.attr("shape-rendering", "crispEdges")
.style("pointer-events", "none");
let dateText = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(`${d.date.toLocaleDateString()}`);
let tipWidth = dateText.node().getComputedTextLength();
let tipHeight = 15;
newData.forEach((metric, j) => {
let text = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(
`${metric}: ${d[
metricArray[0]
][metricArray[1]][metric].toLocaleString()}`
);
tipWidth = Math.max(tipWidth, text.node().getComputedTextLength());
tipHeight += 15;
});
rect
.attr("width", tipWidth + 10)
.attr("height", tipHeight + 10)
.attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5)
.attr("y", y - (tipHeight + 10) / 2 - 2.5);
})
.on("mouseout", function (d, i) {
dotLayer.selectAll(`[dot]`).attr("opacity", 0);
tooltipLayer.selectAll("*").remove();
});
createDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale);
enterLegend(legendLayer, metricArray[2], chart, areaColorScale, lineColorScale);
addResize(parent, data, metricArray, latestDate);
}
function updateNewCurveChart(parent, data, metricArray, latestDate) {
let dataArray = data.tracking;
let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate);
dataArray = dataArray.filter((day, i) => i > 35 - dateAdjustment);
let container = d3.select(parent);
let titleDiv = appendOrSelect(container, "div", "title-div");
titleDiv.text(data.properties.PRENAME);
let buttonDiv = appendOrSelect(container, "div", "button-div");
buttonDiv.selectAll("*").remove();
let buttonGroup1 = buttonDiv.append("div").attr("class", "button-group");
let buttonGroup2 = buttonDiv.append("div").attr("class", "button-group");
let buttonGroup3 = buttonDiv.append("div").attr("class", "button-group");
makeButton(buttonGroup1, "Total", 2, ["cases"], parent, data, metricArray, latestDate);
makeButton(
buttonGroup1,
"Breakdown",
2,
["active", "recovered", "deaths"],
parent,
data,
metricArray,
latestDate
);
makeButton(buttonGroup2, "Cumulative", 1, "cumulative", parent, data, metricArray, latestDate);
makeButton(buttonGroup2, "New", 1, "new", parent, data, metricArray, latestDate);
makeButton(buttonGroup2, "7-day avg", 1, "average", parent, data, metricArray, latestDate);
makeButton(buttonGroup3, "Raw", 0, "raw", parent, data, metricArray, latestDate);
makeButton(buttonGroup3, "/100K", 0, "per100k", parent, data, metricArray, latestDate);
let svg = appendOrSelect(container, "svg", "curve-svg");
//svg.on('click', d => updateNewCurveChart(parent, data, ['total']))
let chart = createChartSettings(svg, dataArray, chartPadding);
let chartLayer = appendOrSelect(svg, "g", "chart-layer");
let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer");
let axisX = appendOrSelect(axisLayer, "g", "axis-x");
let axisY = appendOrSelect(axisLayer, "g", "axis-y");
let titleLayer = appendOrSelect(chartLayer, "g", "title-layer");
let dataLayer = appendOrSelect(chartLayer, "g", "data-layer");
let areaLayer = appendOrSelect(dataLayer, "g", "area-layer");
let lineLayer = appendOrSelect(dataLayer, "g", "line-layer");
let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer");
let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer");
let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer");
let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer");
let max = getMaxSum(dataArray, metricArray);
let yScale = getYScale(chart, max);
let xScale = getXScale(chart);
let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors);
let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors);
let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors);
let timeScale = getTimeScale(chart, dataArray);
let xAxis = d3
.axisBottom()
.scale(timeScale)
.tickFormat(function (date) {
if (d3.timeYear(date) < date) {
return d3.timeFormat("%b")(date);
} else {
return d3.timeFormat("%Y")(date);
}
});
axisX.transition().duration(1000).call(xAxis);
let yAxis = d3.axisRight().scale(yScale).ticks(6);
axisY.transition().duration(1000).call(yAxis);
let breakdown = [];
metricArray[2].forEach((metric) => {
breakdown.push([metricArray[0], metricArray[1], metric]);
});
const stack = d3
.stack()
.keys(breakdown)
.value((d, key) => d[key[0]][key[1]][key[2]]);
const stackedValues = stack(dataArray);
let stackArray = [];
stackedValues.forEach((stack, i) => {
let key = metricArray[2][i];
stackArray.push({
key: key,
data: stack,
});
});
let area = getArea(xScale, yScale);
let areaZero = getArea(xScale, yScale, true);
let line = getLine(xScale, yScale);
let lineZero = getLine(xScale, yScale, true);
let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key);
exitArea(A, areaZero, dur);
updateArea(A, area, dur);
enterArea(A, area, areaZero, areaColorScale);
let L = lineLayer.selectAll(".line").data(stackArray, (d) => d.key);
L.exit()
.lower()
.attr("opacity", 1)
.transition()
.duration(1000)
.attr("d", (d) => lineZero(d.data))
.attr("opacity", 0)
.remove();
L.transition()
.duration(1000)
.attr("d", (d) => line(d.data));
L.enter()
.append("path")
.attr("class", "line")
.attr("d", (d) => lineZero(d.data))
.attr("stroke", (d, i) => lineColorScale(d.key))
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("opacity", 1)
.transition()
.duration(1000)
.attr("d", (d) => line(d.data))
.attr("opacity", 1);
let H = hoverLayer.selectAll("rect").data(dataArray);
H.exit().remove();
H.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2)
.attr("y", yScale(max * 1.05))
.attr("width", (d, i) => xScale(i + 1) - xScale(i))
.attr("height", yScale(0))
.on("mousemove", function (e, d, i) {
tooltipLayer.selectAll("*").remove();
dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1);
const [x, y] = d3.pointer(e);
let newData = metricArray[2].slice().reverse();
let rect = tooltipLayer
.append("rect")
.attr("fill", "white")
.attr("stroke", "#444")
.attr("shape-rendering", "crispEdges")
.style("pointer-events", "none");
let dateText = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(`${d.date.toLocaleDateString()}`);
let tipWidth = dateText.node().getComputedTextLength();
let tipHeight = 15;
newData.forEach((metric, j) => {
let text = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(
`${metric}: ${d[
metricArray[0]
][metricArray[1]][metric].toLocaleString()}`
);
tipWidth = Math.max(tipWidth, text.node().getComputedTextLength());
tipHeight += 15;
});
rect
.attr("width", tipWidth + 10)
.attr("height", tipHeight + 10)
.attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5)
.attr("y", y - (tipHeight + 10) / 2 - 2.5);
})
.on("mouseout", function (d, i) {
dotLayer.selectAll(`[dot]`).attr("opacity", 0);
tooltipLayer.selectAll("*").remove();
});
H.enter()
.append("rect")
.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2)
.attr("y", yScale(max * 1.05))
.attr("width", (d, i) => xScale(i + 1) - xScale(i))
.attr("height", yScale(0))
.attr("shape-rendering", "crispEdges")
.attr("opacity", 0)
.on("mousemove", function (e, d, i) {
tooltipLayer.selectAll("*").remove();
dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1);
const [x, y] = d3.pointer(e);
// console.log(e, d, i);
let newData = metricArray[2].slice().reverse();
let rect = tooltipLayer
.append("rect")
.attr("fill", "white")
.attr("stroke", "#444")
.attr("shape-rendering", "crispEdges")
.style("pointer-events", "none");
let dateText = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(`${d.date.toLocaleDateString()}`);
let tipWidth = dateText.node().getComputedTextLength();
let tipHeight = 15;
newData.forEach((metric, j) => {
let text = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(
`${metric}: ${d[
metricArray[0]
][metricArray[1]][metric].toLocaleString()}`
);
tipWidth = Math.max(tipWidth, text.node().getComputedTextLength());
tipHeight += 15;
});
rect
.attr("width", tipWidth + 10)
.attr("height", tipHeight + 10)
.attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5)
.attr("y", y - (tipHeight + 10) / 2 - 2.5);
})
.on("mouseout", function (d, i) {
dotLayer.selectAll(`[dot]`).attr("opacity", 0);
tooltipLayer.selectAll("*").remove();
});
updateDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale);
enterLegend(legendLayer, metricArray[2], chart, areaColorScale, lineColorScale);
addResize(parent, data, metricArray, latestDate);
}
function resizeNewCurveChart(parent, data, metricArray, latestDate) {
let dataArray = data.tracking;
let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate);
dataArray = dataArray.filter((day, i) => i > 35 + dateAdjustment);
let container = d3.select(parent);
let svg = appendOrSelect(container, "svg", "curve-svg");
let chart = createChartSettings(svg, dataArray, chartPadding);
let chartLayer = appendOrSelect(svg, "g", "chart-layer");
let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer");
let axisX = appendOrSelect(axisLayer, "g", "axis-x");
let axisY = appendOrSelect(axisLayer, "g", "axis-y");
let titleLayer = appendOrSelect(chartLayer, "g", "title-layer");
let dataLayer = appendOrSelect(chartLayer, "g", "data-layer");
let areaLayer = appendOrSelect(dataLayer, "g", "area-layer");
let lineLayer = appendOrSelect(dataLayer, "g", "line-layer");
let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer");
let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer");
let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer");
let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer");
let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors);
let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors);
let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors);
let max = getMaxSum(dataArray, metricArray);
let yScale = getYScale(chart, max);
let xScale = getXScale(chart);
let timeScale = getTimeScale(chart, dataArray);
// console.log(chart);
let xAxis = d3
.axisBottom()
.scale(timeScale)
.tickFormat(function (date) {
if (d3.timeYear(date) < date) {
return d3.timeFormat("%b")(date);
} else {
return d3.timeFormat("%Y")(date);
}
});
axisX.call(xAxis).attr("transform", `translate(0, ${chart.height - chart.pad.bottom})`);
let yAxis = d3.axisRight().scale(yScale).ticks(6);
axisY.call(yAxis).attr("transform", `translate(${chart.width - chart.pad.right}, 0)`);
let breakdown = [];
metricArray[2].forEach((metric) => {
breakdown.push([metricArray[0], metricArray[1], metric]);
});
const stack = d3
.stack()
.keys(breakdown)
.value((d, key) => d[key[0]][key[1]][key[2]]);
const stackedValues = stack(dataArray);
let stackArray = [];
stackedValues.forEach((stack, i) => {
let key = metricArray[2][i];
stackArray.push({
key: key,
data: stack,
});
});
let area = getArea(xScale, yScale);
let line = getLine(xScale, yScale);
let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key);
updateArea(A, area, 0);
let L = lineLayer
.selectAll(".line")
.data(stackArray, (d) => d.key)
.attr("d", (d) => line(d.data));
let H = hoverLayer.selectAll("rect").data(dataArray);
H.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2)
.attr("y", yScale(max * 1.05))
.attr("width", (d, i) => xScale(i + 1) - xScale(i))
.attr("height", yScale(0))
.on("mousemove", function (e, d, i) {
tooltipLayer.selectAll("*").remove();
dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1);
const [x, y] = d3.pointer(e);
// console.log(e, d, i);
let newData = metricArray[2].slice().reverse();
let rect = tooltipLayer
.append("rect")
.attr("fill", "white")
.attr("stroke", "#444")
.attr("shape-rendering", "crispEdges")
.style("pointer-events", "none");
let dateText = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(`${d.date.toLocaleDateString()}`);
let tipWidth = dateText.node().getComputedTextLength();
let tipHeight = 15;
newData.forEach((metric, j) => {
let text = tooltipLayer
.append("text")
.attr("x", x < chart.width / 2 ? x + 25 : x - 15)
.attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15)
.attr("text-anchor", x < chart.width / 2 ? "start" : "end")
.style("pointer-events", "none")
.html(
`${metric}: ${d[
metricArray[0]
][metricArray[1]][metric].toLocaleString()}`
);
tipWidth = Math.max(tipWidth, text.node().getComputedTextLength());
tipHeight += 15;
});
rect
.attr("width", tipWidth + 10)
.attr("height", tipHeight + 10)
.attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5)
.attr("y", y - (tipHeight + 10) / 2 - 2.5);
})
.on("mouseout", function (d, i) {
dotLayer.selectAll(`[dot]`).attr("opacity", 0);
tooltipLayer.selectAll("*").remove();
});
updateDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale);
}