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/ae534a5cedfa284086717f1814bafe81 to your computer and use it in GitHub Desktop.
Save Saigesp/ae534a5cedfa284086717f1814bafe81 to your computer and use it in GitHub Desktop.
D3v4 Slopegraph
ae534a5cedfa284086717f1814bafe81

Slopegraph

D3 implementation of slopegraph

See the demo and more charts from d3graphs repository

Features:

  • Object oriented approach
  • Responsive
  • Resizable
  • Updatable

Requires:

  • D3 v4+

Default options:

{
    'margin': {'top': 40, 'right': 160, 'bottom': 60, 'left': 160},
    'key': '',
    'currentkey': '',
    'color': '#1f77b4',
    'defaultcolor': '#AAA',
    'opacity': 0.5,
    'radius': 3,
    'title': false,
    'source': false,
    'labels': false,
    'transition': {'duration': 550}
}
class SlopeGraph {
constructor(selection, data, config = {}) {
let self = this;
this.selection = selection;
this.data = data;
// Graph configuration
this.cfg = {
'margin': {'top': 40, 'right': 160, 'bottom': 60, 'left': 160},
'key': '',
'currentkey': '',
'color': '#1f77b4',
'defaultcolor': '#AAA',
'opacity': 0.5,
'radius': 3,
'title': false,
'source': false,
'labels': false,
'transition': {'duration': 550}
};
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.yScale = d3.scaleLinear().rangeRound([this.cfg.height, 0]);
window.addEventListener("resize", function(){ self.draw() });
this.initGraph();
}
initGraph() {
let self = this;
this.yScale.domain([
d3.min(this.data, function(d) { return d.start < d.end ? d.start*0.9 : d.end*0.9 ; }),
d3.max(this.data, function(d) { return d.start > d.end ? d.start*1.1 : d.end*1.1 ; })
]);
// SVG
this.svg = this.selection.append('svg')
.attr("class", "chart slopegraph");
this.g = this.svg.append("g")
.attr("transform", "translate(" + (self.cfg.margin.left) + "," + (self.cfg.margin.top) + ")");
// VERTICAL AXIS
this.startAxis = this.g.append('line')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('class', 'axis axis--start')
.attr('stroke', 'black')
this.endAxis = this.g.append('line')
.attr('class', 'axis axis--end')
.attr('stroke', 'black')
.attr('y1', 0)
// TITLE
if(self.cfg.title) this.title = this.svg.append('text').attr('class', 'title label').attr('text-anchor', 'middle').text(self.cfg.title)
// SOURCE
if(self.cfg.source) this.source = this.svg.append('text').attr('class', 'source label').html(self.cfg.source)
// END-START LABELS
if(self.cfg.labels){
this.startl = this.g.append('text')
.attr('class', 'label')
.attr('text-anchor', 'middle')
.attr('y', self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom -12)
.text(self.cfg.labels[0])
this.endl = this.g.append('text')
.attr('class', 'label')
.attr('text-anchor', 'middle')
.attr('y', self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom -12)
.text(self.cfg.labels[1])
}
// LINES
this.lin = this.g.selectAll(".line--group")
.data(this.data)
this.lineg = this.lin
.enter().append('g')
.attr("class", "line--group");
this.lines = this.lineg.append('line')
.attr("class", "line")
.attr('stroke', function(d, i){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor;
})
.style("stroke-width", function(d){
return d[self.cfg.key] == self.cfg.currentkey ? '2px' : '1px';
})
.style("opacity", self.cfg.opacity)
// POINTS
this.startg = this.lineg.append('g')
.attr('class', 'point--group point--group__start')
.classed('current', function(d){ return d[self.cfg.key] == self.cfg.currentkey })
this.endg = this.lineg.append('g')
.attr('class', 'point--group point--group__end')
.classed('current', function(d){ return d[self.cfg.key] == self.cfg.currentkey })
.attr('transform', 'translate('+self.cfg.width+',0)')
this.startg.append('circle')
.attr('fill', function(d){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor
})
.attr('r', self.cfg.radius)
this.endg.append('circle')
.attr('fill', function(d){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor
})
.attr('r', self.cfg.radius)
//LABELS
this.startg.append('text')
.attr('class', 'label')
.attr('text-anchor', 'end')
.attr('y', 3)
.attr('x', -5)
.text(function(d){
return d[self.cfg.key] +' '+ d.start
})
this.endg.append('text')
.attr('class', 'label')
.attr('text-anchor', 'start')
.attr('y', 3)
.attr('x', 5)
.text(function(d){
return d.end + ' ' + d[self.cfg.key]
})
self.draw()
}
draw(){
var self = this;
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.svg
.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.startAxis
.attr('y2', self.cfg.height)
this.endAxis
.attr('x1', this.cfg.width)
.attr('x2', this.cfg.width)
.attr('y2', self.cfg.height)
// TITLE
if(self.cfg.title) this.title.attr('transform', 'translate('+ ((self.cfg.width/2) + self.cfg.margin.left) +',20)')
// SOURCE
if(self.cfg.source) this.source.attr('transform', 'translate('+ (self.cfg.margin.left) +','+(self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom - 5)+')')
// END-START LABELS
if(self.cfg.labels){
this.startl.attr('y', self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom -12)
this.endl.attr('x', self.cfg.width).attr('y', self.cfg.height + self.cfg.margin.top + self.cfg.margin.bottom -12)
}
this.yScale.rangeRound([this.cfg.height, 0]);
self.update()
}
getData(){
return this.data;
}
add(data){
this.data = this.data.concat(data)
this.update()
}
remove(filter){
var self = this;
this.data.forEach(function(d,i){
let c = 0
Object.keys(filter).forEach(function(key) {
if(filter[key] == d[key]) c++
})
if(c == Object.keys(filter).length){
self.data.splice(i,1)
}
})
this.update()
}
update(){
var self = this;
var t = d3.transition().duration(self.cfg.transition.duration);
this.yScale.domain([
d3.min(this.data, function(d) { return d.start < d.end ? d.start*0.9 : d.end*0.9 ; }),
d3.max(this.data, function(d) { return d.start > d.end ? d.start*1.1 : d.end*1.1 ; })
]);
this.lin = this.g.selectAll(".line--group")
.data(this.data, function(d){ return d[self.cfg.key]})
// EXIT
this.lin.exit().transition(t)
.style("opacity", function(){ return 0; })
.remove();
// UPDATE
this.startg = this.g.selectAll('.point--group__start')
.transition(t)
.attr('transform', function(d){ return 'translate(0,'+self.yScale(d.start)+')'})
this.endg = this.g.selectAll('.point--group__end')
.transition(t)
.attr('transform', function(d){ return 'translate('+self.cfg.width+','+self.yScale(d.end)+')'})
this.lines = this.lin.selectAll('.line')
.transition(t)
.attr("x1", 0)
.attr("x2", this.cfg.width)
.attr("y1", function(d){ return self.yScale(d.start)})
.attr("y2", function(d){ return self.yScale(d.end)})
// ENTER
var news = this.lin
.enter().append('g')
.attr("class", "line--group")
news.append('line')
.attr("class", "line")
.attr('stroke', function(d, i){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor;
})
.style("opacity", self.cfg.opacity)
.transition(t)
.attr("x1", 0)
.attr("x2", this.cfg.width)
.attr("y1", function(d){ return self.yScale(d.start)})
.attr("y2", function(d){ return self.yScale(d.end)})
var gstart = news.append('g')
.attr('class', 'point--group point--group__start')
gstart
.transition(t)
.attr('transform', function(d){ return 'translate(0,'+self.yScale(d.start)+')'})
var gend = news.append('g')
.attr('class', 'point--group point--group__end')
.attr('transform', 'translate('+self.cfg.width+',0)')
gend
.transition(t)
.attr('transform', function(d){ return 'translate('+self.cfg.width+','+self.yScale(d.end)+')'})
gstart.append('circle')
.attr('fill', function(d){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor
})
.attr('r', self.cfg.radius)
gend.append('circle')
.attr('fill', function(d){
return d[self.cfg.key] == self.cfg.currentkey ? self.cfg.color : self.cfg.defaultcolor
})
.attr('r', self.cfg.radius)
gstart.append('text')
.attr('class', 'label')
.attr('text-anchor', 'end')
.attr('y', 3)
.attr('x', -5)
.text(function(d){
return d[self.cfg.key] +' '+ d.start
})
gend.append('text')
.attr('class', 'label')
.attr('text-anchor', 'start')
.attr('y', 3)
.attr('x', 5)
.text(function(d){
return d.end + ' ' + d[self.cfg.key]
})
}
};
[
{
"name": "Málaga",
"start": 5355,
"end": 5855
},
{
"name": "Totalán",
"start": 6160,
"end": 6510
},
{
"name": "Rincón de la Victoria",
"start": 3029,
"end": 5138
},
{
"name": "Moclinejo",
"start": 2116,
"end": 2904
},
{
"name": "Macharaviaya",
"start": 3503,
"end": 4408
}]
<html>
<head>
<meta charset="utf-8">
</head>
<style>
.chart .axis{
shape-rendering: crispEdges;
}
.chart .label {
font-family: sans-serif;
font-size: 10px;
cursor: default;
}
.chart .line {
fill: transparent;
stroke-width: 1px;
}
.chart .grid line {
opacity: 0.1;
}
.chart .grid path {
fill: transparent;
stroke: transparent;
}
.chart .source {
fill: #7a7a7a;
font-size: 10px;
}
.chart .source a {
text-decoration: underline;
}
.chart .title {
font-weight: 700;
}
/* SLOPEGRAPH */
.slopegraph .point--group {
opacity: 0;
transition: opacity 200ms ease;
}
.slopegraph .point--group.current {
opacity: 1;
}
.slopegraph:hover .point--group {
opacity: 1;
}
</style>
<body>
<div id="evolution" style="width: 100%; height: 400px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="d3.slopegraph.js"></script>
<script>
d3.json('data.json', function(data) {
var evolution = new SlopeGraph(d3.select('#evolution'), data, {
'key': 'name',
'currentkey': 'Málaga',
'yscaleformat': '.0s',
'color': ['#3161e2'],
'title': 'Población de Málaga',
'source': 'Fuente: INE',
'labels': ['2000', '2015'],
})
setTimeout(function(){
evolution.add([{
"name": "Sevilla",
"start": 8355,
"end": 9321
},{
"name": "Madrid",
"start": 11355,
"end": 7805
}])
}, 2000);
setTimeout(function(){
evolution.remove({
"name": "Madrid",
})
}, 4000);
setTimeout(function(){
evolution.add([{
"name": "Barcelona",
"start": 18355,
"end": 20855
}])
}, 6000);
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment