|
class Calendar{ |
|
constructor(selection, data, config = {}) { |
|
let self = this; |
|
this.selection = selection; |
|
this.data = data; |
|
|
|
// Graph configuration |
|
this.cfg = { |
|
'margin': {'top': 30, 'right': 30, 'bottom': 10, 'left': 50}, |
|
'key': 'key', |
|
'datefield': 'date', |
|
'dateformat': '%d-%m-%Y', // https://github.com/d3/d3-time-format/blob/master/README.md#locale_format |
|
'title': false, |
|
'source': false, |
|
'rectsize': 10, |
|
'colorScale': d3.interpolateRdBu, |
|
'emptycolor': '#EEE', |
|
'year': false, |
|
'mondaystart': false, |
|
'weekdayformat': '%a', |
|
'monthformat': '%b', |
|
}; |
|
Object.keys(config).forEach(function(key) { |
|
if(config[key] instanceof Object && config[key] instanceof Array === false){ |
|
Object.keys(config[key]).forEach(function(sk) { |
|
self.cfg[key][sk] = config[key][sk]; |
|
}); |
|
} else self.cfg[key] = config[key]; |
|
}); |
|
|
|
this.cfg.width = parseInt(this.selection.node().offsetWidth) - this.cfg.margin.left - this.cfg.margin.right, |
|
this.cfg.height = parseInt(this.selection.node().offsetHeight)- this.cfg.margin.top - this.cfg.margin.bottom; |
|
|
|
this.extentdates = d3.extent(this.data, function(d){ return d[self.cfg.datefield]}); |
|
this.year = self.cfg.year ? self.cfg.year : + self.extentdates[0].substr(0,4); |
|
this.cfg.rectsize = this.cfg.width/53 < this.cfg.height/7 ? this.cfg.width/53 : this.cfg.height/7; |
|
this.dayCalc = this.cfg.mondaystart ? function(d) { return (d.getDay() + 6) % 7; } : function(d) { return d.getDay(); } |
|
this.weekCalc = this.cfg.mondaystart ? d3.timeFormat("%W") : d3.timeFormat("%U"); |
|
this.cScale = d3.scaleSequential(this.cfg.colorScale); |
|
|
|
this.weekDay = d3.timeFormat(self.cfg.weekdayformat); |
|
this.monthName = d3.timeFormat(self.cfg.monthformat); |
|
|
|
|
|
this.initGraph(); |
|
} |
|
initGraph() { |
|
var self = this; |
|
|
|
this.cScale.domain(d3.extent(this.data, function(d){ return +d[self.cfg.key]}).reverse()) |
|
|
|
this.svg = this.selection.append('svg') |
|
.attr("class", "chart calendar") |
|
.attr("viewBox", "0 0 "+(this.cfg.width + this.cfg.margin.left + this.cfg.margin.right)+" "+(this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom)) |
|
.attr("width", this.cfg.width + this.cfg.margin.left + this.cfg.margin.right) |
|
.attr("height", this.cfg.height + this.cfg.margin.top + this.cfg.margin.bottom); |
|
|
|
this.g = this.svg.append("g") |
|
.attr("transform", "translate(" + (self.cfg.margin.left) + "," + (self.cfg.margin.top) + ")"); |
|
|
|
// TITLE |
|
if(self.cfg.title){ |
|
this.svg.append('text') |
|
.attr('class', 'title label') |
|
.attr('text-anchor', 'middle') |
|
.attr('transform', 'translate('+ (self.cfg.width/2) +',20)') |
|
.text(self.cfg.title) |
|
} |
|
|
|
// SOURCE |
|
if(self.cfg.source){ |
|
this.svg.append('text') |
|
.attr('class', 'source label') |
|
.attr('transform', 'translate('+ (self.cfg.margin.left) +','+(self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom - 5)+')') |
|
.html(self.cfg.source) |
|
|
|
} |
|
|
|
this.rects = this.g.selectAll("rect") |
|
.data(function(d) { return d3.timeDays(new Date(self.year, 0, 1), new Date(self.year + 1, 0, 1)); }) |
|
.enter().append("rect") |
|
.attr("width", self.cfg.rectsize) |
|
.attr("height", self.cfg.rectsize) |
|
.attr("x", function(d) { return self.weekCalc(d) * self.cfg.rectsize; }) |
|
.attr("y", function(d) { return self.dayCalc(d) * self.cfg.rectsize; }) |
|
.attr("fill", self.cfg.emptycolor) |
|
|
|
var nesteddata = d3.nest() |
|
.key(function(d) { return d[self.cfg.datefield]; }) |
|
.rollup(function(d) { return +d[0][self.cfg.key]; }) |
|
.object(this.data); |
|
|
|
this.rects.filter(function(d) { return d.yyyymmdd() in nesteddata; }) |
|
.attr("fill", function(d) { return self.cScale(nesteddata[d.yyyymmdd()]); }) |
|
.append("title") |
|
.text(function(d) { return d.yyyymmdd() + ": " + nesteddata[d.yyyymmdd()]; }); |
|
|
|
|
|
self.drawLabels(); |
|
} |
|
|
|
drawLabels() { |
|
var self = this; |
|
|
|
var j_start = this.cfg.mondaystart ? 1 : 0; |
|
|
|
for(var j = j_start; j < j_start+7; j++){ |
|
self.g.append("text") |
|
.attr("class", 'label') |
|
.style("text-anchor", "end") |
|
.attr("dy", self.cfg.rectsize * (j - j_start) + self.cfg.rectsize*0.7) |
|
.attr("dx", "-1em") |
|
.text(self.weekDay(new Date(2018, 0, j))); |
|
} |
|
for(var j = 0; j < 12; j++){ |
|
self.g.append("text") |
|
.attr("class", 'label') |
|
.style("text-anchor", "middle") |
|
.attr("dy", -5) |
|
.attr("dx", (j*self.cfg.rectsize*4.4) + (self.cfg.rectsize*2.2)) |
|
.text(self.monthName(new Date(2018, j, 1))); |
|
} |
|
} |
|
|
|
} |
|
|
|
Date.prototype.yyyymmdd = function(joinchar='-') { |
|
var mm = this.getMonth() + 1; |
|
var dd = this.getDate(); |
|
|
|
return [this.getFullYear(), |
|
(mm>9 ? '' : '0') + mm, |
|
(dd>9 ? '' : '0') + dd |
|
].join(joinchar); |
|
}; |