Skip to content

Instantly share code, notes, and snippets.

@alanshaw
Last active May 16, 2016 14:46
Show Gist options
  • Save alanshaw/917af0db2ebb5d97c43cde00ef47f413 to your computer and use it in GitHub Desktop.
Save alanshaw/917af0db2ebb5d97c43cde00ef47f413 to your computer and use it in GitHub Desktop.
d3 line graph
<!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">&lt;</button> <button type=button data-delta="1">&gt;</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