Skip to content

Instantly share code, notes, and snippets.

@Saigesp Saigesp/.block
Last active Oct 19, 2018

Embed
What would you like to do?
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
You can’t perform that action at this time.