diff --git a/var/www/static/js/d3/heatmap_week_hour.js b/var/www/static/js/d3/heatmap_week_hour.js new file mode 100644 index 00000000..f41e8985 --- /dev/null +++ b/var/www/static/js/d3/heatmap_week_hour.js @@ -0,0 +1,224 @@ +//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) + + tooltip.html(d.date + " " + d.hour + "-" + (d.hour + 1) + "h: " + d.count + " messages") + } + 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 + +}