I’m, pretty new to D3 and I am trying to build a heat-map in D3. It has different years. Each year has its own day scale(on x-axis) and year label should also be shown but shared month-scale(on y-axis).
My sample data structure is as follows –
const data = [
{
year: 2023,
data: [
{ month: "Jan", day: 1, value: 10 },
{ month: "Jan", day: 2, value: 20 },
{ month: "Jan", day: 3, value: 30 },
{ month: "Feb", day: 1, value: 10 },
{ month: "Feb", day: 2, value: 20 },
{ month: "Feb", day: 3, value: 30 },
{ month: "Mar", day: 1, value: 10 },
{ month: "Mar", day: 2, value: 20 },
{ month: "Mar", day: 3, value: 30 },
],
},
{
group: 2024,
data: [
{ month: "Jan", day: 1, value: 40 },
{ month: "Jan", day: 2, value: 30 },
{ month: "Feb", day: 1, value: 25 },
{ month: "Feb", day: 2, value: 20 },
{ month: "Mar", day: 1, value: 50 },
{ month: "Mar", day: 2, value: 23 },
],
},
];
For example:
- For year 2023 -> day scale domain is 1,2,3
- For year 2024 -> day scale domain is 1,2
- year scale below day scale is 2023,2024
- Y-scale is Jan,Feb,Mar
Following is my code:
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const Heatmap = ({ data }) => {
const svgRef = useRef(null);
useEffect(() => {
const svg = d3.select(svgRef.current);
const margin = { top: 40, right: 30, bottom: 30, left: 80 };
const width = 1500 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const totalDaySpace = width;
const fixedDayWidth = totalDaySpace / 360;
const boundsWidth = width - margin.right - margin.left;
const boundsHeight = height - margin.top - margin.bottom;
const yScale = d3
.scaleBand()
.domain(months)
.range([boundsHeight, margin.top])
.padding(0.1);
const nestedData = d3.group(data, (d) => d.year);
const yearScale = d3
.scaleBand()
.domain(Array.from(nestedData.keys()))
.range([0, boundsWidth])
.padding(0);
// .padding((width - margin.left - margin.right) / (nestedData.size * 1.1));
svg
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
g.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(yScale).tickSize(0))
.call((g) => g.select(".domain").remove());
g.append("g")
.attr("class", "year-axis")
.call(d3.axisTop(yearScale).tickSize(0))
.call((g) => g.select(".domain").remove());
function dynamicDomain(daysPerYear, year) {
console.log(totalDaySpace, fixedDayWidth, "****");
// fixedDayWidth * daysPerYear.length
const dayScale = d3
.scaleBand()
.domain(daysPerYear.map((d) => d.day))
.range([0, fixedDayWidth * daysPerYear.length])
.padding(0);
// console.log(daysPerYear.map((d) => dayScale(d.day)));
// Render day axis for current year
g.append("g")
.attr("class", "day-axis")
.attr("transform", `translate(${yearScale(year)}, 0)`)
.call(d3.axisBottom(dayScale).tickSize(0))
.call((g) => g.select(".domain").remove());
// Render heatmap rectangles for current year
const rects = g
.selectAll(`.day-rect-${year}`)
.data(daysPerYear)
.enter()
.append("rect")
.attr("class", "day-rect")
.attr("x", function (k) {
return yearScale(k.year) + dayScale(k.day);
})
// .attr("x", (d) => yearScale(year) + dayScale(d.day))
.attr("y", (d) => yScale(d.month))
.attr("width", dayScale.bandwidth())
.attr("height", yScale.bandwidth())
.style("fill", (d) => getColor(d.value));
}
nestedData.forEach((yearData, year) => {
dynamicDomain(yearData, year);
});
function getColor(value) {
const colorScale = d3
.scaleLinear()
.domain([0, 100])
.range(["#fff", "#f00"]);
return colorScale(value);
}
}, [data]);
return (
<svg
ref={svgRef}
width={1500}
height={400}
// style={{ backgroundColor: "yellow" }}
/>
);
};
export default Heatmap;
I did show some gaps between each year in the heatmap – which is kind of placing multiple heatmaps side-by-side. But I’m struggling with width issues between years in getting them properly aligned. This is the output of how it looks now – Heatmap Output