Skip to content

Instantly share code, notes, and snippets.

@mwburke
Last active December 29, 2017 02:41
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 mwburke/9873c09ac6c21d6ac9153e54892cf5ec to your computer and use it in GitHub Desktop.
Save mwburke/9873c09ac6c21d6ac9153e54892cf5ec to your computer and use it in GitHub Desktop.
D3 Slopegraph
All data taken from https://www.kdnuggets.com/2017/05/poll-analytics-data-science-machine-learning-software-leaders.html/2
[{"Tool":"Python","Users":1516,"After":52.6,"Before":45.8, "Comments": "Python is growing in popularity"},{"Tool":"R","Users":1502,"After":52.1,"Before":49.0},{"Tool":"SQL","Users":1006,"After":34.9,"Before":35.5},{"Tool":"RapidMiner","Users":946,"After":32.8,"Before":32.6, "Comments": "13.6% of users only use RapidMiner"},{"Tool":"Excel","Users":810,"After":28.1,"Before":33.6},{"Tool":"Spark","Users":654,"After":22.7,"Before":21.6},{"Tool":"Anaconda","Users":629,"After":21.8,"Before":16.0},{"Tool":"Tensorflow","Users":581,"After":20.2,"Before":6.8, "Comments": "Tensorflow is growing greatly among users since public release"},{"Tool":"scikit-learn","Users":561,"After":19.5,"Before":17.2},{"Tool":"Hadoop","Users":431,"After":15.0,"Before":22.1},{"Tool":"Java","Users":399,"After":13.8,"Before":16.8},{"Tool":"Scala","Users":214,"After":7.4,"Before":6.2}]
<!DOCTYPE html>
<html>
<head>
<title>D3 Slopegraph</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=650, user-scalable=yes">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link rel="stylesheet" href="style.css">
<!-- move this into your css file -->
</head>
<body>
<div id="container">
<p>Data Science Tools: <b>2016</b> to <b>2017</b>
<br><i>% Usage</i></p>
<div id="chart">
</div>
<p>Source: <a href="https://www.kdnuggets.com/2017/05/poll-analytics-data-science-machine-learning-software-leaders.html">KDnuggets Analytics/Data Science 2017 Software Poll</a></p>
</div>
<script src="slopegraph.js"></script>
</body>
</html>
d3.json('data.json', function(error, data) {
if (error) throw error;
// Label each point as increasing/decreasing above thresholds
// or just little to no change
var arrayLength = data.length;
for (var i = 0; i < arrayLength; i++) {
change = data[i]['After'] - data[i]['Before'];
if (change < -3) {
data[i]['Change'] = 'negative';
} else if (change > 5) {
data[i]['Change'] = 'positive';
} else {
data[i]['Change'] = 'neutral';
}
}
var opts = {
width: 600,
height: 500,
margin: {top: 20, right: 100, bottom: 30, left: 150}
};
// Calculate area chart takes up out of entire svg
var chartHeight = opts.height - opts.margin.top - opts.margin.bottom;
var chartWidth = opts.width - opts.margin.left - opts.margin.right;
var svg = d3.select('#chart')
.append('svg')
.attr('width', opts.width)
.attr('height', opts.height);
// Create scale for positioning data correctly in chart
var vertScale = d3.scaleLinear().domain([6, 53]).range([opts.margin.bottom, chartHeight]);
// Labels overlap, need to precompute chart height placement
// and adjust to avoid label overlap
// First, calculate the right and left side chart placements
for (var i = 0; i < arrayLength; i++) {
data[i]['AfterY'] = vertScale(data[i]['After']);
data[i]['BeforeY'] = vertScale(data[i]['Before']);
}
// Next, use a basic heuristic to pull labels up or down
// If next item is too close to next one, move label up
// If next item is too close and item above has been moved up, keep the same value,
// and move next value down
data.sort(function(a, b) {
return b.Before-a.Before;
})
for (var i = 1; i < (arrayLength - 1); i++) {
if ((data[i]['BeforeY']-data[i+1]['BeforeY']) < 15) {
if ((data[i-1]['BeforeY']-data[i]['BeforeY']) < 15) {
data[i+1]['BeforeY'] = data[i+1]['BeforeY'] - 10;
} else {
data[i]['BeforeY'] = data[i]['BeforeY'] + 10;
}
}
}
data.sort(function(a, b) {
return b.After-a.After;
})
console.log(data);
for (var i = 1; i < (arrayLength - 1); i++) {
if ((data[i]['AfterY']-data[i+1]['AfterY']) < 15) {
if ((data[i-1]['AfterY']-data[i]['AfterY']) < 15) {
data[i+1]['AfterY'] = data[i+1]['AfterY'] - 10;
} else {
data[i]['AfterY'] = data[i]['AfterY'] + 10;
}
} else if ((data[i-1]['AfterY']-data[i]['AfterY']) < 15) {
data[i]['AfterY'] = data[i]['AfterY'] - 10;
}
}
data.sort(function(a, b) {
return b.Before-a.Before;
})
// Create slopegraph labels
svg.selectAll('text.labels')
.data(data)
.enter()
.append('text')
.text(function(d) {
return d.Tool
})
.attr('class', function(d) {
return 'label ' + d.Change;
})
.attr('text-anchor', 'end')
.attr('x', opts.margin.left * .6)
.attr('y', function(d) {
return opts.margin.top + chartHeight - d.BeforeY;
})
.attr('dy', '.35em');
// Create slopegraph left value labels
svg.selectAll('text.leftvalues')
.data(data)
.enter()
.append('text')
.attr('class', function(d) {
return d.Change;
})
.text(function(d) {
return Math.round(d.Before) + '%'
})
.attr('text-anchor', 'end')
.attr('x', opts.margin.left * .85)
.attr('y', function(d) {
return opts.margin.top + chartHeight - d.BeforeY;
})
.attr('dy', '.35em');
// Create slopegraph left value labels
svg.selectAll('text.rightvalues')
.data(data)
.enter()
.append('text')
.attr('class', function(d) {
return d.Change;
})
.text(function(d) {
return Math.round(d.After) + '%'
})
.attr('text-anchor', 'start')
.attr('x', chartWidth + opts.margin.right)
.attr('y', function(d) {
return opts.margin.top + chartHeight - d.AfterY;
})
.attr('dy', '.35em');
// Create slopegraph lines
svg.selectAll('line.slope-line')
.data(data)
.enter()
.append('line')
.attr('class', function(d) {
return 'slope-line ' + d.Change;
})
.attr('x1', opts.margin.left)
.attr('x2', chartWidth + opts.margin.right * 0.75)
.attr('y1', function(d) {
return opts.margin.top + chartHeight - vertScale(d.Before);
})
.attr('y2', function(d) {
return opts.margin.top + chartHeight - vertScale(d.After);
});
// Create slopegraph left circles
svg.selectAll('line.left-circle')
.data(data)
.enter()
.append('circle')
.attr('class', function(d) {
return d.Change;
})
.attr('cx', opts.margin.left)
.attr('cy', function(d) {
return opts.margin.top + chartHeight - vertScale(d.Before);
})
.attr('r', 6);
// Create slopegraph right circles
svg.selectAll('line.left-circle')
.data(data)
.enter()
.append('circle')
.attr('class', function(d) {
return d.Change;
})
.attr('cx',chartWidth + opts.margin.right * 0.75)
.attr('cy', function(d) {
return opts.margin.top + chartHeight - vertScale(d.After);
})
.attr('r', 6);
// Create bottom area denoting years
svg.append("line")
.attr('x1', opts.margin.left)
.attr('x2', opts.margin.left)
.attr('y1', opts.height - opts.margin.bottom)
.attr('y2', opts.height - opts.margin.bottom * 0.7)
.attr('stroke', 'grey')
.attr('stroke-width', '2px');
svg.append("line")
.attr('x1', chartWidth + opts.margin.right * 0.75)
.attr('x2', chartWidth + opts.margin.right * 0.75)
.attr('y1', opts.height - opts.margin.bottom)
.attr('y2', opts.height - opts.margin.bottom * 0.7)
.attr('stroke', 'grey')
.attr('stroke-width', '2px');
svg.append("line")
.attr('x1', opts.margin.left)
.attr('x2', chartWidth + opts.margin.right * 0.75)
.attr('y1', opts.height - opts.margin.bottom * 0.7)
.attr('y2', opts.height - opts.margin.bottom * 0.7)
.attr('stroke', 'grey')
.attr('stroke-width', '2px');
svg.append('text')
.text('2016')
.attr('class', 'neutral')
.attr('x', opts.margin.left)
.attr('y', opts.height - opts.margin.bottom * 0.05)
.attr('text-anchor', 'start');
svg.append('text')
.text('2017')
.attr('class', 'neutral')
.attr('x', chartWidth + opts.margin.right * 0.75)
.attr('y', opts.height - opts.margin.bottom * 0.05)
.attr('text-anchor', 'end');
// Get y values of notes and add notes to viz
var pythonY = data.filter(function (ind) {
return ind.Tool == 'Python';
});
var rapidMinerY = data.filter(function (ind) {
return ind.Tool == 'RapidMiner';
});
var tensorflowY = data.filter(function (ind) {
return ind.Tool == 'Tensorflow';
});
svg.selectAll('text-comments')
.data(data)
.enter()
.append('text')
.text(function(d) {
return d.Comments;
})
.attr('class', 'neutral')
.attr('text-anchor', 'start')
.attr('x', chartWidth + opts.margin.right)
.attr('y', function(d) {
return opts.margin.top + chartHeight - d.AfterY;
})
.attr('dy', '.25em')
.call(wrap, opts.margin.right);
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", chartWidth + opts.margin.left).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", chartWidth + opts.margin.left).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
});
function round10(x) {
return Math.round(x / 10) * 10;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
line {
stroke-width: 3px;
stroke-opacity: 0.7;
}
line.positive {
stroke: #1b4fa3;
stroke-width: 5px;
stroke-opacity: 0.8;
}
line.negative {
stroke: #991616;
stroke-width: 5px;
stroke-opacity: 0.8;
}
line.neutral {
stroke: grey;
}
text.positive {
fill: #1b4fa3;
font-weight: 600;
}
text.negative {
fill: #991616;
font-weight: 600;
}
text.neutral {
fill: grey;
}
.label {
font-weight: 600;
}
circle.positive {
fill: #1b4fa3;
}
circle.negative {
fill: #991616;
}
circle.neutral {
fill: grey;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment