2023-11-24 16:51:40 +01:00
|
|
|
//Requirement: - D3v5
|
|
|
|
// - jquery
|
|
|
|
// data input: var data = [{"count":0,"date":"2023-11-20","day":0,"hour":0}
|
|
|
|
// based on gist nbremer/62cf60e116ae821c06602793d265eaf6
|
|
|
|
|
|
|
|
// container_id = #heatmapweekhour
|
|
|
|
|
|
|
|
const create_heatmap_week_hour = (container_id, data, options) => {
|
|
|
|
|
|
|
|
|
|
|
|
var days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
|
|
|
|
times = d3.range(24);
|
|
|
|
|
|
|
|
var margin = {
|
|
|
|
top: 80,
|
|
|
|
right: 50,
|
|
|
|
bottom: 20,
|
|
|
|
left: 50
|
|
|
|
};
|
|
|
|
|
|
|
|
var width = Math.max(Math.min(window.innerWidth, 1000), 500) - margin.left - margin.right - 20,
|
|
|
|
gridSize = Math.floor(width / times.length),
|
|
|
|
height = gridSize * (days.length + 2);
|
|
|
|
|
|
|
|
var heatmap_font_size = width * 62.5 / 900;
|
|
|
|
|
|
|
|
//SVG container
|
|
|
|
var svg = d3.select(container_id)
|
|
|
|
.append("svg")
|
|
|
|
.attr("width", width + margin.left + margin.right)
|
|
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
|
|
.append("g")
|
|
|
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
|
|
|
|
|
|
|
// create a tooltip
|
|
|
|
const tooltip = d3.select(container_id)
|
|
|
|
.append("div")
|
|
|
|
.style("opacity", 0)
|
|
|
|
.attr("class", "tooltip")
|
|
|
|
.style("background-color", "white")
|
|
|
|
.style("border", "solid")
|
|
|
|
.style("border-width", "2px")
|
|
|
|
.style("border-radius", "5px")
|
|
|
|
.style("padding", "5px")
|
|
|
|
|
|
|
|
// Three function that change the tooltip when user hover / move / leave a cell
|
|
|
|
const mouseover = function(d) {
|
|
|
|
tooltip.style("opacity", 1)
|
|
|
|
d3.select(this)
|
|
|
|
.style("stroke", "black")
|
|
|
|
//.style("stroke-opacity", 1)
|
|
|
|
|
2024-01-29 10:30:53 +01:00
|
|
|
var xPosition = d3.mouse(this)[0] + margin.left;
|
|
|
|
var yPosition = d3.mouse(this)[1] + margin.top + window.scrollY + 100;
|
|
|
|
|
2023-11-24 16:51:40 +01:00
|
|
|
tooltip.html(d.date + " " + d.hour + "-" + (d.hour + 1) + "h: <b>" + d.count + "</b> messages")
|
2024-01-29 10:30:53 +01:00
|
|
|
.style("left", xPosition + "px")
|
|
|
|
.style("top", yPosition + "px");
|
2023-11-24 16:51:40 +01:00
|
|
|
}
|
|
|
|
const mouseleave = function(d) {
|
|
|
|
tooltip.style("opacity", 0)
|
|
|
|
d3.select(this)
|
|
|
|
.style("stroke", "white")
|
|
|
|
//.style("stroke-opacity", 0.8)
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
//////////////////////////// Draw Heatmap /////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
var colorScale = d3.scaleLinear()
|
|
|
|
.domain([0, d3.max(data, function (d) {
|
|
|
|
return d.count;
|
|
|
|
}) / 2, d3.max(data, function (d) {
|
|
|
|
return d.count;
|
|
|
|
})])
|
|
|
|
.range(["#FFFFF6", "#3E9583", "#1F2D86"])
|
|
|
|
//.interpolate(d3.interpolateHcl);
|
|
|
|
|
|
|
|
var dayLabels = svg.selectAll(".dayLabel")
|
|
|
|
.data(days)
|
|
|
|
.enter().append("text")
|
|
|
|
.text(function (d) {
|
|
|
|
return d;
|
|
|
|
})
|
|
|
|
.attr("x", 0)
|
|
|
|
.attr("y", function (d, i) {
|
|
|
|
return i * gridSize;
|
|
|
|
})
|
|
|
|
.style("text-anchor", "end")
|
|
|
|
.attr("transform", "translate(-36, -11)")
|
|
|
|
.attr("class", function (d, i) {
|
|
|
|
return ((i >= 0 && i <= 4) ? "dayLabel mono axis axis-workweek" : "dayLabel mono axis");
|
|
|
|
})
|
|
|
|
.style("font-size", heatmap_font_size + "%");
|
|
|
|
|
|
|
|
var timeLabels = svg.selectAll(".timeLabel")
|
|
|
|
.data(times)
|
|
|
|
.enter().append("text")
|
|
|
|
.text(function (d) {
|
|
|
|
return d;
|
|
|
|
})
|
|
|
|
.attr("x", function (d, i) {
|
|
|
|
return i * gridSize;
|
|
|
|
})
|
|
|
|
.attr("y", 0)
|
|
|
|
.style("text-anchor", "middle")
|
|
|
|
.attr("transform", "translate(-" + gridSize / 2 + ", -36)")
|
|
|
|
.attr("class", function (d, i) {
|
|
|
|
return ((i >= 8 && i <= 17) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis");
|
|
|
|
})
|
|
|
|
.style("font-size", heatmap_font_size + "%");
|
|
|
|
|
|
|
|
var heatMap = svg.selectAll(".hour")
|
|
|
|
.data(data)
|
|
|
|
.enter().append("rect")
|
|
|
|
.attr("x", function (d) {
|
|
|
|
return (d.hour - 1) * gridSize;
|
|
|
|
})
|
|
|
|
.attr("y", function (d) {
|
|
|
|
return (d.day - 1) * gridSize;
|
|
|
|
})
|
|
|
|
.attr("class", "hour bordered")
|
|
|
|
.attr("width", gridSize)
|
|
|
|
.attr("height", gridSize)
|
|
|
|
.style("stroke", "white")
|
|
|
|
.style("stroke-opacity", 0.6)
|
|
|
|
.style("fill", function (d) {
|
|
|
|
return colorScale(d.count);
|
|
|
|
})
|
|
|
|
.on("mouseover", mouseover)
|
|
|
|
.on("mouseleave", mouseleave);
|
|
|
|
|
|
|
|
//Append title to the top
|
|
|
|
svg.append("text")
|
|
|
|
.attr("class", "title")
|
|
|
|
.attr("x", width / 2)
|
|
|
|
.attr("y", -60)
|
|
|
|
.style("text-anchor", "middle")
|
|
|
|
.text("Chat Messages");
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
//////////////// Create the gradient for the legend ///////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
//Extra scale since the color scale is interpolated
|
|
|
|
var countScale = d3.scaleLinear()
|
|
|
|
.domain([0, d3.max(data, function (d) {
|
|
|
|
return d.count;
|
|
|
|
})])
|
|
|
|
.range([0, width])
|
|
|
|
|
|
|
|
//Calculate the variables for the temp gradient
|
|
|
|
var numStops = 10;
|
|
|
|
countRange = countScale.domain();
|
|
|
|
countRange[2] = countRange[1] - countRange[0];
|
|
|
|
countPoint = [];
|
|
|
|
for (var i = 0; i < numStops; i++) {
|
|
|
|
countPoint.push(i * countRange[2] / (numStops - 1) + countRange[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
//Create the gradient
|
|
|
|
svg.append("defs")
|
|
|
|
.append("linearGradient")
|
|
|
|
.attr("id", "legend-heatmap")
|
|
|
|
.attr("x1", "0%").attr("y1", "0%")
|
|
|
|
.attr("x2", "100%").attr("y2", "0%")
|
|
|
|
.selectAll("stop")
|
|
|
|
.data(d3.range(numStops))
|
|
|
|
.enter().append("stop")
|
|
|
|
.attr("offset", function (d, i) {
|
|
|
|
return countScale(countPoint[i]) / width;
|
|
|
|
})
|
|
|
|
.attr("stop-color", function (d, i) {
|
|
|
|
return colorScale(countPoint[i]);
|
|
|
|
});
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////// Draw the legend ////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
var legendWidth = Math.min(width * 0.8, 400);
|
|
|
|
//Color Legend container
|
|
|
|
var legendsvg = svg.append("g")
|
|
|
|
.attr("class", "legendWrapper")
|
|
|
|
.attr("transform", "translate(" + (width / 2) + "," + (gridSize * days.length) + ")"); // 319
|
|
|
|
|
|
|
|
//Draw the Rectangle
|
|
|
|
legendsvg.append("rect")
|
|
|
|
.attr("class", "legendRect")
|
|
|
|
.attr("x", -legendWidth / 2)
|
|
|
|
.attr("y", 0)
|
|
|
|
//.attr("rx", hexRadius*1.25/2)
|
|
|
|
.attr("width", legendWidth)
|
|
|
|
.attr("height", 10)
|
|
|
|
.style("fill", "url(#legend-heatmap)");
|
|
|
|
|
|
|
|
//Append title
|
|
|
|
legendsvg.append("text")
|
|
|
|
.attr("class", "legendTitle")
|
|
|
|
.attr("x", 0)
|
|
|
|
.attr("y", -10)
|
|
|
|
.style("text-anchor", "middle")
|
|
|
|
.text("Number of Messages");
|
|
|
|
|
|
|
|
//Set scale for x-axis
|
|
|
|
var xScale = d3.scaleLinear()
|
|
|
|
.range([-legendWidth / 2, legendWidth / 2])
|
|
|
|
.domain([0, d3.max(data, function (d) {
|
|
|
|
return d.count;
|
|
|
|
})]);
|
|
|
|
|
|
|
|
//Define x-axis
|
|
|
|
var xAxis = d3.axisBottom(xScale)
|
|
|
|
//.orient("bottom")
|
|
|
|
.ticks(5);
|
|
|
|
//.tickFormat(formatPercent)
|
|
|
|
|
|
|
|
//Set up X axis
|
|
|
|
legendsvg.append("g")
|
|
|
|
.attr("class", "axis")
|
|
|
|
.attr("transform", "translate(0," + (10) + ")")
|
|
|
|
.call(xAxis);
|
|
|
|
|
|
|
|
|
|
|
|
// return svg ???
|
|
|
|
// return svg
|
|
|
|
|
|
|
|
}
|