Last active
May 16, 2016 14:46
-
-
Save alanshaw/917af0db2ebb5d97c43cde00ef47f413 to your computer and use it in GitHub Desktop.
d3 line graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font: 10px sans-serif; | |
width: 960px; | |
height: 500px; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.y.axis .tick line { | |
opacity: 0.1; | |
} | |
.bundle.axis path { | |
display: none; | |
} | |
.bundle.axis .tick line { | |
stroke: steelblue; | |
stroke-width: 6px; | |
cursor: pointer; | |
} | |
.bundle.axis .tick text { | |
display: none; | |
} | |
.line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 1.5px; | |
} | |
.d3-tip { | |
line-height: 1; | |
font-weight: bold; | |
padding: 12px; | |
background: rgba(0, 0, 0, 0.8); | |
color: #fff; | |
border-radius: 2px; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.d3-tip: after { | |
box-sizing: border-box; | |
display: inline; | |
font-size: 10px; | |
width: 100%; | |
line-height: 1; | |
color: rgba(0, 0, 0, 0.8); | |
content: "\25BC"; | |
position: absolute; | |
text-align: center; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n: after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
} | |
</style> | |
<body> | |
<button type=button data-delta="-1"><</button> <button type=button data-delta="1">></button> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script> | |
<script> | |
// [{date, engagement, bundle?}], Node | |
function renderGraph (data, container) { | |
const margin = {top: 20, right: 20, bottom: 30, left: 50} | |
const width = container.clientWidth - margin.left - margin.right | |
const height = container.clientHeight - margin.top - margin.bottom | |
const x = d3.time.scale().range([0, width]) | |
const y = d3.scale.linear().range([height, 0]) | |
const xAxis = d3.svg.axis() | |
.scale(x) | |
.orient('bottom') | |
const yAxis = d3.svg.axis() | |
.scale(y) | |
.orient('left') | |
.innerTickSize(-width) | |
.outerTickSize(0) | |
const bundleItems = data.filter((d) => !!d.bundle) | |
const bundleXAxis = d3.svg.axis() | |
.scale(x) | |
.orient('bottom') | |
.tickValues(bundleItems.map((d) => d.date)) | |
.tickFormat((d) => bundleItems.find((b) => b.date === d).bundle.title) | |
.tickSize(20, 20) | |
const line = d3.svg.line() | |
.x((d) => x(d.date)) | |
.y((d) => y(d.engagement)) | |
let svg = d3.select(container).select('svg > g') | |
if (svg.empty()) { | |
svg = d3.select(container).append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom) | |
.append('g') | |
.attr('transform', `translate(${margin.left}, ${margin.top})`) | |
} | |
x.domain(d3.extent(data, (d) => d.date)) | |
y.domain([0, 100]) | |
// y.domain(d3.extent(data, (d) => d.engagement)) | |
let xAxisG = svg.select('.x.axis') | |
if (xAxisG.empty()) { | |
xAxisG = svg.append('g').attr('class', 'x axis') | |
} | |
xAxisG | |
.attr('transform', `translate(0, ${height})`) | |
.transition() | |
.call(xAxis) | |
let yAxisG = svg.select('.y.axis') | |
if (yAxisG.empty()) { | |
yAxisG = svg.append('g').attr('class', 'y axis') | |
yAxisG.append('text') | |
.attr('transform', 'rotate(-90)') | |
.attr('y', 6) | |
.attr('dy', '.71em') | |
.style('text-anchor', 'end') | |
.text('Engagement (%)') | |
} | |
yAxisG.transition().call(yAxis) | |
let bundleXAxisG = svg.select('.bundle.x.axis') | |
if (bundleXAxisG.empty()) { | |
bundleXAxisG = svg.append('g').attr('class', 'bundle x axis') | |
} | |
bundleXAxisG | |
.attr('transform', `translate(0, ${height - 5})`) | |
.transition() | |
.call(bundleXAxis) | |
const tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html((d) => `<div>${bundleItems.find((b) => b.date === d).bundle.title}</div>`) | |
svg.call(tip) | |
d3.selectAll('.bundle.x.axis .tick') | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide) | |
let engagementPath = svg.select('.line') | |
if (engagementPath.empty()) { | |
engagementPath = svg.append('path').attr('class', 'line') | |
} | |
engagementPath | |
.datum(data) | |
.transition() | |
.attr('d', line) | |
} | |
function fakeData () { | |
let engagement = 0 | |
const data = [] | |
for (var i = 700; i > 0; i--) { | |
if (Math.random() > 0.4) { | |
engagement = Math.min(100, engagement + Math.random()) | |
} else { | |
engagement = Math.max(0, engagement - Math.random()) | |
} | |
data.push({ | |
date: new Date(Date.now() - (1000 * 60 * 60 * 24 * i)), | |
engagement, | |
bundle: Math.random() > 0.98 ? {title: `Bundle jumble ${i}`} : null | |
}) | |
} | |
return data | |
} | |
function dataSlice (data, monthOffset = 0) { | |
const daysOffset = monthOffset * 31 | |
const from = data.length - 180 + daysOffset | |
const to = data.length + daysOffset | |
return data.slice(from, to) | |
} | |
const data = fakeData() | |
const buttons = [].slice.call(document.querySelectorAll('button')) | |
buttons.forEach((b) => b.addEventListener('click', onButtonClick.bind(b))) | |
let monthOffset = 0 | |
function onButtonClick () { | |
const delta = parseInt(this.getAttribute('data-delta'), 10) | |
monthOffset = Math.min(0, monthOffset + delta) | |
renderGraph(dataSlice(data, monthOffset), document.querySelector('body')) | |
} | |
renderGraph(dataSlice(data), document.querySelector('body')) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment