sjackson
sjackson

Reputation: 81

How to get day of week on left side of D3 calendar heat map

I'm trying to create a calendar heatmap with D3, very similar to the Github contribution calendar.

I can't get the day of week to align correctly. It seems to repeat for every month and doesn't have correct margins or alignment. I only want the days to display once, on the left side of the calendar.

Just like this:

enter image description here

Here is what mine looks like:

enter image description here

Here is my code:

<style>
#calendar {
  margin: 20px;
}
.month {
  margin-right: 8px;
}
.month-name {
  font-size: 85%;
  fill: #777;
  font-family: Muli, san-serif;
}
.day.hover {
  stroke: #6d6E70;
  stroke-width: 2;
}
.day.focus {
  stroke: #ffff33;
  stroke-width: 2;
}
</style>
<div style="text-align:center;" id="calendar"></div>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>

function drawCalendar(dateData){

  var weeksInMonth = function(month){
    var m = d3.timeMonth.floor(month)
    return d3.timeWeeks(d3.timeWeek.floor(m), d3.timeMonth.offset(m,1)).length;
  }

  //var minDate = new Date(2018, 12, 31);
  var minDate = d3.min(dateData, function(d) { return new Date(2018, 12, 1 ) });
  //var minDate = d3.min(dateData, function(d) { return new Date(d.day) });
  console.log(minDate);
  //var maxDate = new Date(2019, 11, 30);
  var maxDate = d3.max(dateData, function(d) { return new Date(2019, 11, 30 ) });
  console.log(maxDate);

  var cellMargin = 2,
      calY=10,//offset of calendar in each group
      xOffset=-5,
      dayName = ['Su','Mo','Tu','We','Th','Fr','Sa'],
      cellSize = 20;

  var day = d3.timeFormat("%w"),
      week = d3.timeFormat("%U"),
      format = d3.timeFormat("%Y-%m-%d"),
      titleFormat = d3.utcFormat("%a, %d-%b"),
      monthName = d3.timeFormat("%B"),
      months= d3.timeMonth.range(d3.timeMonth.floor(minDate), maxDate);

  var svg = d3.select("#calendar").selectAll("svg")
    .data(months)
    .enter().append("svg")
    .attr("class", "month")
    .attr("height", ((cellSize * 7) + (cellMargin * 8) + 20) ) // the 20 is for the month labels
    .attr("width", function(d) {
      var columns = weeksInMonth(d);
      return ((cellSize * columns) + (cellMargin * (columns + 1)));
    })
    .append("g")


  svg.append("text")
    .attr("class", "month-name")
    .attr("y", (cellSize * 7) + (cellMargin * 8) + 15 )
    .attr("x", function(d) {
      var columns = weeksInMonth(d);
      return (((cellSize * columns) + (cellMargin * (columns + 1))) / 2);
    })
    .attr("text-anchor", "middle")
    .text(function(d) { return monthName(d); })


 //create day labels
        var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
        var dayLabels=svg.append("g").attr("id","dayLabels")
        days.forEach(function(d,i)    {
            dayLabels.append("text")
            .attr("class","dayLabel")
            .attr("x",xOffset)
            .attr("y",function(d) { return calY+(i * cellSize); })
            .text(d);
        })



  var rect = svg.selectAll("rect.day")
    .data(function(d, i) { return d3.timeDays(d, new Date(d.getFullYear(), d.getMonth()+1, 1)); })
    .enter().append("rect")
    .attr("class", "day")
    .attr("width", cellSize)
    .attr("height", cellSize)
    .attr("rx", 3).attr("ry", 3) // rounded corners
    .attr("fill", '#eaeaea') // default light grey fill
    .attr("y", function(d) { return (day(d) * cellSize) + (day(d) * cellMargin) + cellMargin; })
    .attr("x", function(d) { return ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellSize) + ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellMargin) + cellMargin ; })
    .on("mouseover", function(d) {
      d3.select(this).classed('hover', true);
    })
    .on("mouseout", function(d) {
      d3.select(this).classed('hover', false);
    })
    .datum(format);

  rect.append("title")
    .text(function(d) { return titleFormat(new Date(d)); });

  var lookup = d3.nest()
    .key(function(d) { return d.day; })
    .rollup(function(leaves) {
      return d3.sum(leaves, function(d){ return parseInt(d.count); });
    })
    .object(dateData);

  var scale = d3.scaleLinear()
    .domain(d3.extent(dateData, function(d) { return parseInt(d.count); }))
    .range([0.2,1]); // the interpolate used for color expects a number in the range [0,1] but i don't want the lightest part of the color scheme

  rect.filter(function(d) { return d in lookup; })
    .style("fill", function(d) { return d3.interpolateYlGn(scale(lookup[d])); })
    .select("title")
    .text(function(d) { return titleFormat(new Date(d)) + ":  " + lookup[d]; });

}

d3.csv("dates.csv", function(response){
  drawCalendar(response);
})


</script>

There is also an input csv file that contains the following values:

day,count
2019-05-12,171
2019-06-17,139
2019-05-02,556
2019-04-10,1
2019-05-04,485
2019-03-27,1
2019-05-26,42
2019-05-25,337
2019-05-23,267
2019-05-05,569
2019-03-31,32
2019-03-25,128
2019-05-13,221
2019-03-30,26
2019-03-15,3
2019-04-24,10
2019-04-27,312
2019-03-20,99
2019-05-10,358
2019-04-01,15
2019-05-11,199
2019-07-06,744
2019-05-08,23
2019-03-28,98
2019-03-29,64
2019-04-30,152
2019-03-21,148
2019-03-19,20
2019-05-07,69
2019-04-29,431
2019-04-25,330
2019-04-28,353
2019-04-18,9
2019-01-10,1
2019-01-09,2
2019-03-26,21
2019-05-27,18
2019-04-19,10
2019-04-06,1
2019-04-12,214
2019-05-03,536
2019-07-03,3
2019-06-16,1
2019-03-24,138
2019-04-26,351
2019-04-23,14
2019-05-01,19
2019-07-05,523
2019-05-22,3
2019-05-09,430
2019-05-24,472
2019-04-11,172
2019-03-17,7
2019-05-14,10
2019-05-06,449
2019-07-04,295
2019-05-15,12
2019-03-23,216
2019-03-18,47
2019-03-22,179

Upvotes: 0

Views: 499

Answers (1)

Mikkel
Mikkel

Reputation: 7777

Typically you allow for a margin in your SVG, something like this:

  const margin = { top: 10, right: 20, bottom: 10, left: 5 }
  const svg = d3
      .select('#chart')
      .append('svg')
      .attr('width', 900 + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

Basically you create an SVG element that is bigger than your drawing area, and then you move (translate) the chart in by the margins. Then your axis can appear in the margin

Upvotes: 0

Related Questions