Skip to content

Instantly share code, notes, and snippets.

@Saigesp
Last active October 19, 2018 18:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Saigesp/a0b505c3d0e42cfbdf39b0d2c19ada0c to your computer and use it in GitHub Desktop.
Save Saigesp/a0b505c3d0e42cfbdf39b0d2c19ada0c to your computer and use it in GitHub Desktop.
D3v4 linear calendar
a0b505c3d0e42cfbdf39b0d2c19ada0c

Linear Calendar Comparison

D3 implementation of linear calendar

See the demo and more charts from d3graphs repository

Features:

  • Object oriented approach
  • Responsive

Requires:

  • D3 v4+

Default options:

{
    'margin': {'top': 30, 'right': 20, 'bottom': 40, 'left': 40},
    'year': 'key',
    'start': 'start',
    'end': 'end',
    'domain': ['01-01-1900', '12-31-1900'],
    'dateformat': '%d-%m-%Y',
    'scaleformat': '%B',
    'color': 'steelblue',
    'title': false,
    'source': false,
    'radius': 3,
    'stroke': 2,
}
class LinearCalendar{
constructor(selection, data, config = {}) {
let self = this;
this.selection = selection;
this.data = data;
// Graph configuration
this.cfg = {
'margin': {'top': 30, 'right': 20, 'bottom': 40, 'left': 40},
'year': 'key',
'start': 'start',
'end': 'end',
'domain': ['01-01-1900', '12-31-1900'],
'dateformat': '%d-%m-%Y', // https://github.com/d3/d3-time-format/blob/master/README.md#locale_format
'scaleformat': '%B',
'color': 'steelblue',
'title': false,
'source': false,
'radius': 3,
'stroke': 2,
};
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.parseTime = d3.timeParse(this.cfg.dateformat);
this.formatTime = d3.timeFormat(this.cfg.dateformat);
this.formatTimeLeg = d3.timeFormat('%d %B');
this.yScale = d3.scaleBand().rangeRound([0, self.cfg.height]).padding(1);
this.xScale = d3.scaleTime().range([0, this.cfg.width]);
this.initGraph();
}
initGraph() {
var self = this;
this.data.forEach(function(d){
d.jsdateStart = self.parseTime(d[self.cfg.start]);
d.jsdateEnd = self.parseTime(d[self.cfg.end]);
});
this.yScale.domain(this.data.map(function(d){ return +d[self.cfg.year]}))
this.xScale.domain([self.parseTime(self.cfg.domain[0]), self.parseTime(self.cfg.domain[1])]);
this.svg = this.selection.append('svg')
.attr("class", "chart calendar-linear")
.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) + ")");
this.yGrid = this.g.append("g")
.attr("class", "grid grid--y")
.call(self.make_y_gridlines()
.tickSize(-self.cfg.width)
.tickFormat(""));
this.g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + this.cfg.height + ")")
.call(d3.axisBottom(self.xScale)
.tickFormat(d3.timeFormat(self.cfg.scaleformat)));
this.g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(self.yScale));
// 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)
}
// ITEM GROUP
this.itemg = this.g.selectAll('.itemgroup')
.data(this.data)
.enter().append('g')
.attr('class', 'itemgroup')
.attr('transform', function(d, i){
return 'translate(0,'+ self.yScale(d[self.cfg.year]) +')';
})
this.itemg.append('line')
.attr('y1', 0)
.attr('x1', function(d){
return self.xScale(d.jsdateStart);
})
.attr('y2', 0)
.attr('x2', function(d){
return self.xScale(d.jsdateEnd);
})
.attr('stroke', self.cfg.color)
.attr('stroke-width', self.cfg.stroke)
this.itemg.append('circle')
.attr('class', 'start')
.attr('cy', 0)
.attr('cx', function(d){
return self.xScale(d.jsdateStart);
})
.attr('r', self.cfg.radius)
.attr('stroke-width', 3)
.attr('fill', self.cfg.color);
this.itemg.append('circle')
.attr('class', 'end')
.attr('cy', 0)
.attr('cx', function(d){
return self.xScale(d.jsdateEnd);
})
.attr('r', self.cfg.radius)
.attr('stroke-width', 3)
.attr('fill', self.cfg.color);
// MOUSE INDICATOR
this.mouseover = this.g.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', self.cfg.width)
.attr('height', self.cfg.height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
this.mousegroup = this.g.append('g')
.attr('class', 'mousegroup')
.attr('pointer-events', 'none')
.style("opacity", "0");
this.mouseline = this.mousegroup.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.attr("shape-rendering", "crispEdges")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", 0.3)
.attr("d", "M0,"+ (self.cfg.height - self.yScale.step()) + " 0," + self.yScale.step())
this.mousetext = this.mousegroup.append("text")
.attr('text-anchor', 'middle')
.attr('class', 'label')
.style('font-size', '10px')
.attr('y', self.yScale.step() -2);
this.mouseover.on('mouseover', function(){
self.mousegroup.style('opacity', '1')
}).on('mouseout', function(){
self.mousegroup.style('opacity', '0')
}).on('mousemove', function(){
var mouse = d3.mouse(this);
var tim = self.formatTime(self.xScale.invert(mouse[0]));
self.mousegroup.attr("transform", "translate("+mouse[0]+",0)")
self.mousetext.text(self.formatTimeLeg(self.xScale.invert(mouse[0])))
self.itemg.selectAll('circle')
.attr('stroke', 'trasparent')
self.itemg.filter(function(d){ return d.inicio == tim; })
.selectAll('.start')
.attr('stroke', self.cfg.color)
self.itemg.filter(function(d){ return d.fin == tim; })
.selectAll('.end')
.attr('stroke', self.cfg.color)
})
}
// gridlines in y axis function
make_y_gridlines() {
return d3.axisLeft(this.yScale);
}
}
<html>
<head>
<meta charset="utf-8">
</head>
<style>
.chart .axis .domain {
display: none;
}
.chart .axis--y .tick line{
display: none;
}
.chart .grid line {
opacity: 0.1;
}
.chart .grid path {
fill: transparent;
stroke: transparent;
}
.label {
font-family: sans-serif;
font-size: 12px;
cursor: default;
}
.chart .source {
fill: #7a7a7a;
font-size: 10px;
}
.chart .source a {
text-decoration: underline;
}
.chart .title {
font-weight: 700;
}
</style>
<body>
<div id="exports" style="width: 960px; height: 440px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="d3.linearcalendar.js"></script>
<script>
d3.csv('vendimia.csv', function(data) {
data.forEach(function(d){
d.inicio = d.inicio.substring(0, d.inicio.length - 5);
d.fin = d.fin.substring(0, d.fin.length - 5);
});
d3.timeFormatDefaultLocale({
"decimal": ".",
"thousands": ",",
"grouping": [3],
"currency": ["$", ""],
"dateTime": "%a %b %e %X %Y",
"date": "%m/%d/%Y",
"time": "%H:%M:%S",
"periods": ["AM", "PM"],
"days": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
"months": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
"shortMonths": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"]
});
var grafico = new LinearCalendar(d3.select('#exports'), data, {
'year': 'anyo',
'start': 'inicio',
'end': 'fin',
'domain': ['01-08', '31-12'],
'color': '#A01897',
'dateformat': '%d-%m',
'title': 'Fechas de vendimia de la D.O. Ribera del Duero',
'source': '<a href="https://www.riberadelduero.es/comunicacion-promocion/estadisticas/vendimia">Consejo Regulador de la Denominación de Origen de Ribera del Duero</a>',
})
})
</script>
</body>
</html>
anyo inicio fin
1989 20-09-1989 16-10-1989
1990 20-09-1990 12-10-1990
1991 04-10-1991 20-10-1991
1992 06-10-1992 26-10-1992
1993 08-10-1993 02-11-1993
1994 21-09-1994 14-10-1994
1995 23-09-1995 17-10-1995
1996 23-09-1996 29-10-1996
1997 23-09-1997 04-11-1997
1998 25-09-1998 06-11-1998
1999 28-09-1999 17-11-1999
2000 26-09-2000 12-11-2000
2001 17-09-2001 31-10-2001
2002 26-09-2002 30-10-2002
2003 11-09-2003 25-10-2003
2004 18-09-2004 15-11-2004
2005 05-09-2005 27-10-2005
2006 04-09-2006 27-10-2006
2007 27-09-2007 03-11-2007
2008 01-10-2008 17-11-2008
2009 09-09-2009 27-10-2009
2010 20-09-2010 03-11-2010
2011 10-09-2011 28-10-2011
2012 12-09-2012 01-11-2012
2013 25-09-2013 18-11-2013
2014 10-09-2014 30-10-2014
2015 07-09-2015 14-10-2015
2016 22-09-2016 07-11-2016
2017 08-09-2017 20-10-2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment