timesHeatMap = (
data,
xTitle,
yTitle,
topTitle,
palette_x,
palette_y,
xOrdering,
yOrdering,
xLabel = null,
yLabel = null
) => {
const margin = { top: 10, right: 20, bottom: 40, left: 40 };
const innerMargin = { top: 10, right: 10 };
const w = width - margin.left - margin.right;
const h = 450 - margin.top - margin.bottom;
const chartBody = document.createElement("div");
chartBody.id = "chartBody";
const svg = d3.create("svg").attr("viewBox", [0, 0, w, h]);
const tooltip = d3
.create("div")
.style("visibility", "hidden")
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("position", "absolute")
.style("padding", "5px")
.text("im a tooltip");
const legend = basicLegend(xLabel, yOrdering, palette_y);
const xGroups = xOrdering
? [...new Set(data.map((d) => d.x))].sort((a, b) => {
return xOrdering.indexOf(a) - xOrdering.indexOf(b);
})
: [...new Set(data.map((d) => d.x))].sort();
const yGroups = yOrdering
? [...new Set(data.map((d) => d.y))].sort((a, b) => {
return yOrdering.indexOf(a) - yOrdering.indexOf(b);
})
: [...new Set(data.map((d) => d.y))];
let targetData;
let selectedRects = new Set();
const x = d3
.scaleBand()
.domain(xGroups)
.range([margin.left, w - margin.right])
.padding(0.05);
const xAxis = d3.axisBottom(x).tickSize(0);
svg
.append("text")
.attr("text-anchor", "end")
.attr("x", w)
.attr("y", h - 5)
.text(xTitle);
let topBarCategory;
svg
.append("g")
.attr("id", "x-axis")
.attr("transform", `translate(0, ${h - margin.bottom})`)
.call(xAxis)
.select(".domain")
.remove();
const yAxisTitle = svg
.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 28) //due to rotation, y is now x and vice versa
.attr("x", -margin.top)
.text(yTitle);
const y = d3
.scaleBand()
.domain(yGroups)
.range([margin.top, h - margin.bottom])
.padding(0.05);
const yAxis = d3.axisLeft(y).tickSize(0);
svg
.append("g")
.attr("id", "y-axis")
.attr("transform", `translate( ${margin.left}, 0)`)
.call(yAxis)
.select(".domain")
.remove();
// svg
// .select("#y-axis")
// .selectAll(".tick")
// .on("click", (e) => {
// showTemporal(e.target.__data__);
// e.target.style = "color: red; font-weight: bold;";
// })
// .on("mouseover", (e) => {
// e.target.style = "color: red; font-weight: bold;";
// })
// .on("mouseout", (e) => {
// e.target.style = "color: black; font-weight: normal;";
// });
const clip = svg
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", w + 5)
.attr("height", h + 10)
.attr("x", margin.left - 5)
.attr("y", 0);
const topX = d3.scaleTime().range([margin.left, w - margin.right]);
const topXAxis = d3.axisBottom(topX);
const topXSVGAxis = svg.append("g");
topXSVGAxis
.attr("id", "top-x-axis")
.attr("transform", `translate(0, ${(h - margin.bottom) / 2 - 20})`)
.call(topXAxis)
.style("opacity", 0)
.select(".domain")
.remove();
let brush = d3.brushX();
const topY = d3.scaleLinear().range([margin.top, margin.top]);
const topYAxis = d3.axisLeft(topY);
const topYSVGAxis = svg.append("g");
topYSVGAxis
.attr("id", "top-y-axis")
.attr("transform", `translate( ${margin.left}, 0)`)
.call(topYAxis)
.style("opacity", 0)
.select(".domain")
.remove();
const topYAxisTitle = svg
.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.style("opacity", 0)
.attr("y", margin.left - 28) //due to rotation, y is now x and vice versa
.attr("x", -margin.top)
.text(topTitle);
const temporalDots = svg.append("g").attr("clip-path", "url(#clip)");
let linefunc = d3.line().curve(d3.curveBasis);
const lineGroup = svg.append("g").attr("clip-path", "url(#clip)");
const color = d3
.scaleSequentialLog()
.interpolator(d3.interpolate("white", "orange"))
.domain(d3.extent(data, (d) => d.avg_value));
// const color = xGroups.map((d) => {
// let xGroup = data.filter((w) => w.x == d);
// let xExtent = d3.extent(xGroup, (n) => n.avg_value);
// let xRange = xExtent[1] - xExtent[0];
// return {
// group: d,
// func: d3
// .scaleSequential()
// .interpolator(d3.interpolate("white", "orange"))
// .domain([xExtent[0] - xRange / 2, xExtent[1]])
// };
// });
const topColorY = d3.scaleOrdinal().domain(yOrdering).range(palette_y);
const topColorX = d3.scaleOrdinal().domain(xOrdering).range(palette_x);
const rectGroup = svg.append("g");
rectGroup
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d) => x(d.x) + 5)
.attr("y", (d) => y(d.y))
.attr("width", x.bandwidth() - 5)
.attr("height", y.bandwidth())
.style("stroke", "#ddd")
.style("opacity", 0.8)
.style("fill", (d) => color(d.avg_value ? d.avg_value : 0))
// .style("fill", (d) =>
// color.filter((n) => n.group == d.x)[0].func(d.avg_value ? d.avg_value : 0)
// )
.attr("class", (d) => `rect-${d.x}-${d.y}-${d.avg_value}`)
.on("mouseover", (d) => {
tooltip.style("visibility", "visible");
d.target.style.stroke = "black";
})
.on("mouseleave", (d) => {
tooltip.style("visibility", "hidden");
if (!selectedRects.has(d.target.getAttribute("class"))) {
d.target.style.stroke = "#ddd";
}
})
.on("mousemove", (d) => {
tooltip
.html(`Value: ${d.target.getAttribute("class").split("-")[3]}%`)
.style("left", d.offsetX + 10 + "px")
.style("top", d.offsetY + 10 + "px");
})
.on("click", (d) => {
let currClass = d.target.getAttribute("class");
if (selectedRects.has(currClass)) {
selectedRects.delete(currClass);
} else if (selectedRects.size == 6) {
tooltip
.html(`Maximum of 6 cells selected.`)
.style("left", d.offsetX + 10 + "px")
.style("top", d.offsetY + 10 + "px");
} else {
selectedRects.add(currClass);
}
console.log(selectedRects);
if (selectedRects.size == 0) {
resetchart();
} else {
showTemporal(d.target.__data__);
}
});
let idleTimeout;
function idled() {
idleTimeout = null;
}
const exitBtn = d3
.create("div")
.attr("id", "exitBtn")
.attr(
"style",
"position: absolute; top: 10px; right: 0px; width: 20px; height: 20px; border-radius: 2px; background-color: #eee; display: flex; justify-content: center; align-items: center; cursor: pointer;"
)
.text("x")
.style("visibility", "hidden")
.on("click", () => {
resetchart();
})
.on("mouseover", (d) => {
tooltip.style("position", "fixed");
tooltip.style("visibility", "visible");
})
.on("mouseleave", (d) => {
tooltip.style("position", "absolute");
tooltip.style("visibility", "hidden");
})
.on("mousemove", (d) => {
tooltip
.html(`Clear selection`)
.style("left", d.clientX + 10 + "px")
.style("top", d.clientY + 10 + "px");
});
chartBody.appendChild(exitBtn.node());
function resetchart() {
selectedRects.clear();
x.range([margin.left, w - margin.right]);
y.range([margin.top, h - margin.bottom]);
//svg.select("#x-rightBar-axis").remove();
//svg.select("#y-topBar-axis").remove();
svg.select("#y-axis").selectAll("text").style =
"color: black; font-weight: normal";
topY.range([margin.top, margin.top]);
topYSVGAxis.style("opacity", 0);
topXSVGAxis.style("opacity", 0);
topYAxisTitle.style("opacity", 0);
rectGroup.selectAll("rect").style("stroke", "#ddd");
temporalDots.selectAll("circle").remove();
lineGroup.selectAll("path").remove();
exitBtn.style("visibility", "hidden");
updateMaterials();
yAxisTitle.transition().duration(1000).attr("x", -margin.top);
}
function showTemporal(target) {
temporalDots.selectAll("circle").remove();
lineGroup.selectAll("path").remove();
temporalDots.selectAll(".brush").remove();
exitBtn.style("visibility", "visible");
targetData = [].concat(
...data
.filter((d) => selectedRects.has(`rect-${d.x}-${d.y}-${d.avg_value}`))
.map((d) => d.temporal)
);
rectGroup.selectAll("rect").stroke = "none";
rectGroup.selectAll("rect").attr("stroke", (d) => {
return d.y == target.y && d.x == target.x ? "black" : "none";
});
y.range([(h - margin.bottom) / 2, h - margin.bottom]);
topY
.range([(h - margin.bottom) / 2 - 20, margin.top])
.domain(d3.extent(targetData, (d) => d.y));
topYAxis.tickSizeInner(-(w - margin.left)).tickSizeOuter(3);
topX
.range([margin.left, w - margin.right])
.domain(d3.extent(targetData, (d) => d.x));
topXAxis.tickSizeOuter(3);
topXSVGAxis.style("opacity", 1);
topYSVGAxis.style("opacity", 1);
topYAxisTitle.style("opacity", 1);
temporalDots
.selectAll("circle")
.data(targetData)
.enter()
.append("circle")
.attr("class", (d) => "dot-" + target)
.attr("cx", (d) => topX(d.x))
.attr("cy", (d) => topY(d.y))
.attr("r", 4)
.attr("fill", (d) => topColorY(d.label))
//.attr("stroke", (d) => topColorX(d.category))
.attr("stroke-width", 3)
.style("opacity", 0.7);
linefunc = linefunc.x((d) => topX(d.x)).y((d) => topY(d.rolling_y));
console.log(targetData);
console.log(d3.group(targetData, (d) => `${d.category}-${d.label}`));
lineGroup
.selectAll("path")
.data(d3.group(targetData, (d) => `${d.category}-${d.label}`))
.enter()
.append("path")
.attr("class", (d) => "line-" + d[0])
.attr("fill", "none")
.attr("stroke", (d) => {
console.log(d[0].split("-")[1]);
console.log(topColorY(d[0].split("-")[1]));
return topColorY(d[0].split("-")[1]);
})
.attr("stroke-width", 3)
.attr("d", (d) => {
return linefunc(d[1]);
})
.on("mouseover", (d) => {
tooltip.style("visibility", "visible");
})
.on("mouseleave", (d) => {
tooltip.style("visibility", "hidden");
})
.on("mousemove", (event, d) => {
tooltip
.style("left", event.offsetX + 10 + "px")
.style("top", event.offsetY + 10 + "px")
.text(event.target.getAttribute("class").split("-")[1]);
});
// .style("background-color", "white")
// .style("border", "solid")
// .style("border-width", "2px")
// .style("border-radius", "5px")
// .style("position", "absolute")
// .style("padding", "5px")`
brush
.extent([
[margin.left, 0],
[w, (h - margin.bottom) / 2 - 20]
])
.on("end", updateChart);
temporalDots.append("g").attr("class", "brush").call(brush);
updateMaterials();
}
function updateChart() {
const extent = d3.brushSelection(this);
//deHighlightChart();
// Reset to initial bounds if timeout or selection was zero. Otherwise, zoom to
// specified x boundaries.
if (!extent) {
if (!idleTimeout) return (idleTimeout = setTimeout(idled, 350));
topX.domain(d3.extent(targetData, (d) => d.x));
} else {
topX.domain([topX.invert(extent[0]), topX.invert(extent[1])]);
temporalDots.select(".brush").call(brush.move, null);
}
updateMaterials();
}
function updateMaterials() {
svg.select("#x-axis").transition().duration(1000).call(xAxis);
svg.select("#y-axis").transition().duration(1000).call(yAxis);
svg.selectAll(".domain").remove();
yAxisTitle
//.select("text")
.transition()
.duration(1000)
.attr("x", -margin.top - (h - margin.bottom) / 2);
lineGroup.selectAll("path").style("opacity", 0);
rectGroup
.selectAll("rect")
.transition("rectMove")
.duration(1000)
.attr("x", (d) => x(d.x) + 5)
.attr("y", (d) => y(d.y))
.attr("width", x.bandwidth() - 5)
.attr("height", y.bandwidth());
topYSVGAxis
.transition()
.duration(1000)
.call(topYAxis)
.selectAll(".tick")
.selectAll("line")
.style("stroke", "lightgrey");
topXSVGAxis
.transition()
.duration(1000)
.call(topXAxis)
.selectAll(".tick")
.selectAll("line")
.style("stroke", "lightgrey");
temporalDots
.selectAll("circle")
.transition("temporalDotsMove")
.duration(1000)
.attr("cx", (d) => topX(d.x))
.attr("cy", (d) => topY(d.y));
lineGroup
.selectAll("path")
.transition("pathmovement")
.duration(1000)
.style("opacity", 1)
.attr("d", (d) => linefunc(d[1]))
.attr("stroke", (d) => topColorY(d[0].split("-")[1]));
//brush.clear();
}
chartBody.appendChild(svg.node());
chartBody.appendChild(legend);
chartBody.appendChild(tooltip.node());
return chartBody;
}