Skip to content

Instantly share code, notes, and snippets.

@kendopunk
Last active Nov 24, 2015
Embed
What would you like to do?
D3.js Brush "tooltips"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="pragma" content="no-cache" />
<style type="text/css">
.axis path, .axis line {
fill: none;
stroke: black;
stroke-width: 1;
shape-rendering: crispEdges;
}
.axis text {
font-size: 9px;
font-family: sans-serif;
fill: #555;
pointer-events: none;
}
.axisPartial path {
fill: none;
stroke: black;
stroke-width: 1;
shape-rendering: crispEdges;
}
.axisPartial line {
display: none;
}
.axisPartial text {
display: none;
}
</style>
</head>
<body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
var canvasWidth = 900, canvasHeight = 500;
var axes = {
main: {
x: null,
y: null
},
brush: {
x: null,
y: null
}
},
brushChartHeight = Math.floor(canvasHeight * .25),
chartData = [{
name: 'A',
color: '#00f',
data: []
}, {
name: 'B',
color: '#090',
data: []
}, {
name: 'C',
color: '#f00',
data: []
}, {
name: 'D',
color: '#a0522d',
data: []
}],
clipId = 'myClipId',
clipPath,
contextBrush,
gMain,
gMainXAxis,
gMainYAxis,
gBrush,
gBrushXAxis,
gBrushYAxis,
mainChartHeight = Math.floor(canvasHeight * .75),
margins = {
main: {
top: 20,
right: 20,
bottom: 10,
left: 50
},
brush: {
top: 20,
right: 20,
bottom: 20,
left: 50
}
},
scales = {
main: {
xScale: null,
yScale: null
},
brush: {
xScale: null,
yScale: null
}
},
seriesFn = {
main: null,
brush: null
},
svg,
tipFormat = d3.time.format('%b %d');
/**
* magic happens here
*/
initChart();
draw();
/**
* Wrapper function for drawing components
*/
function draw() {
setScales();
handlePaths();
callAxes();
setBrush();
}
function handlePaths() {
seriesFn.main = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return scales.main.xScale(d.timestamp);
})
.y(function(d) {
return scales.main.yScale(d.value);
});
seriesFn.brush = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return scales.brush.xScale(d.timestamp);
})
.y(function(d) {
return scales.brush.yScale(d.value);
});
gMain.selectAll('path.main')
.data(chartData)
.enter()
.append('path')
.attr('clip-path', 'url(#' + clipId + ')')
.attr('class', 'main')
.style('fill', 'none')
.style('stroke', function(d) {
return d.color;
})
.attr('d', function(d) {
return seriesFn.main(d.data);
});
gBrush.selectAll('path.brush')
.data(chartData)
.enter()
.append('path')
.attr('class', 'brush')
.style('fill', 'none')
.style('opacity', .6)
.style('stroke', function(d) {
return d.color;
})
.attr('d', function(d) {
return seriesFn.brush(d.data);
});
}
function callAxes() {
gMainXAxis.transition()
.attr('transform', function() {
var x = 0, y = scales.main.yScale(0);
return 'translate(' + x + ',' + y + ')';
})
.call(axes.main.x);
gMainYAxis.transition()
.attr('transform', function() {
var x = margins.main.left, y = 0;
return 'translate(' + x + ',' + y + ')';
})
.call(axes.main.y);
gBrushXAxis.transition()
.attr('transform', function() {
var x = 0, y = mainChartHeight + scales.brush.yScale(0);
return 'translate(' + x + ',' + y + ')';
})
.call(axes.brush.x);
gBrushYAxis.transition()
.attr('transform', function() {
var x = margins.brush.left, y = mainChartHeight;
return 'translate(' + x + ',' + y + ')';
})
.call(axes.brush.y);
}
/**
* initialize SVG and group containers
*/
function initChart() {
svg = d3.select('body')
.append('svg')
.attr('width', canvasWidth)
.attr('height', canvasHeight);
clipPath = svg.append('defs')
.append('clipPath')
.attr('id', clipId)
.append('rect')
.attr('x', margins.main.left)
.attr('y', 0)
.attr('width', function() {
return canvasWidth - margins.main.right - margins.main.left;
})
.attr('height', function() {
return mainChartHeight;
});
gMain = svg.append('svg:g');
gMainXAxis = svg.append('svg:g')
.attr('class', 'axis');
gMainYAxis = svg.append('svg:g')
.attr('class', 'axis');
gBrushXAxis = svg.append('svg:g')
.attr('class', 'axisPartial');
gBrushYAxis = svg.append('svg:g')
.attr('class', 'axisPartial');
gBrush = svg.append('svg:g')
.attr('class', 'x brush')
.attr('transform', function() {
var x = 0, y = mainChartHeight;
return 'translate(' + x + ', ' + y + ')';
});
gBrush.append('text')
.attr('class', 'tipLeft')
.attr('x', 0)
.attr('y', function() {
return brushChartHeight - 10;
})
.attr('text-anchor', 'middle')
.style('font-size', '9px')
.style('font-family', 'sans-serif')
.style('opacity', 0)
.style('fill', 'blue')
.text('');
gBrush.append('text')
.attr('class', 'tipRight')
.attr('x', function() {
return canvasWidth - margins.brush.left - margins.brush.right;
})
.attr('y', function() {
return brushChartHeight - 10;
})
.attr('text-anchor', 'middle')
.style('font-size', '9px')
.style('font-family', 'sans-serif')
.style('opacity', 0)
.style('fill', 'blue')
.text('');
contextBrush = d3.svg.brush()
.on('brush', onBrushing)
.on('brushend', onBrushEnd);
// generate random timeseries data
chartData = randomDataGenerator();
}
function onBrushing() {
var ext = contextBrush.extent();
// adjust main xScale domain
scales.main.xScale.domain(contextBrush.empty() ? scales.main.xScale.domain() : contextBrush.extent());
gMainXAxis.call(axes.main.x);
// change x calculation function and apply
seriesFn.main.x(function(d) {
return scales.main.xScale(d.timestamp);
});
gMain.selectAll('path.main')
.attr('d', function(d) {
return seriesFn.main(d.data);
});
// tip left
gBrush.selectAll('text.tipLeft')
.attr('x', scales.brush.xScale(ext[0]))
.style('opacity', .7)
.text(function() {
return tipFormat(ext[0]);
});
// tip right
gBrush.selectAll('text.tipRight')
.attr('x', scales.brush.xScale(ext[1]))
.style('opacity', .7)
.text(function() {
return tipFormat(ext[1]);
});
}
function onBrushEnd() {
gBrush.selectAll('text')
.style('opacity', 0)
.text('');
}
/**
* generate random data
*/
function randomDataGenerator() {
var cp = JSON.parse(JSON.stringify(chartData)),
d = new Date();
d.setHours(12);
d.setMinutes(0);
d.setSeconds(0);
cp.forEach(function(item) {
for(i=0; i<15; i++) {
var rnd = Math.random();
var signage = rnd <= .5 ? -1 : 1;
item.data.push({
timestamp: d.getTime() + (86400000 * 5 * i),
value: Math.floor(Math.random() * 15) * signage
});
}
});
return cp;
}
function setBrush() {
// calculate min/max timestamps
var minTs = d3.min(chartData[0].data, function(d) {
return d.timestamp;
});
var maxTs = d3.max(chartData[0].data, function(d) {
return d.timestamp;
});
contextBrush.x(scales.brush.xScale).extent([minTs, maxTs]);
gBrush.call(contextBrush)
.selectAll('rect')
.attr('y', margins.brush.top)
.attr('height', function() {
return brushChartHeight - margins.brush.top - margins.brush.bottom;
});
gBrush.selectAll('rect.extent')
.style('fill', '#ccc')
.style('fill-opacity', .3)
.style('stroke', '#999')
.style('stroke-width', .5);
gBrush.selectAll('g.resize')
.selectAll('rect')
.attr('rx', 3)
.attr('ry', 3)
.attr('width', 5)
.style('visibility', 'visible')
.style('fill', '#019ed5')
.style('fill-opacity', .7)
.transition()
.each('end', function() {
var slider = d3.select(this);
d3.select(this.parentNode).append('rect')
.attr('class', 'handle')
.attr('x', parseInt(slider.attr('x')) - 1)
.attr('y', function() {
return margins.brush.top + Math.floor((brushChartHeight - margins.brush.top - margins.brush.bottom)/4);
})
.attr('rx', 3)
.attr('ry', 3)
.attr('width', parseInt(slider.attr('width')) + 2)
.attr('height', function() {
return Math.floor((brushChartHeight - margins.brush.top - margins.brush.bottom)/2);
})
.style('fill', 'white')
.style('stroke', '#008b8b')
.style('stroke-width', 1)
.style('opacity', 1);
});
}
/**
* set X/Y scales for main and brush
*/
function setScales() {
var minX, maxX, minY, maxY;
var _x, _X, _y, _Y;
//////////////////////////////
// get min/max X and Y values
//////////////////////////////
chartData.forEach(function(cd) {
_x = cd.data.map(function(m) {
return m.timestamp;
}).reduce(function(prev, curr) {
return prev < curr ? prev : curr;
});
_X = cd.data.map(function(m) {
return m.timestamp;
}).reduce(function(prev, curr) {
return prev > curr ? prev : curr;
});
_y = cd.data.map(function(m) {
return m.value;
}).reduce(function(prev, curr) {
return prev < curr ? prev : curr;
});
_Y = cd.data.map(function(m) {
return m.value;
}).reduce(function(prev, curr) {
return prev > curr ? prev : curr;
});
// find absolute max/min values
if(minX === undefined) {
minX = _x;
} else {
minX = Math.min(_x, minX);
}
if(maxX === undefined) {
maxX = _X;
} else {
maxX = Math.max(_X, maxX);
}
if(minY === undefined) {
minY = _y;
} else {
minY = Math.min(_y, minY);
}
if(maxY === undefined) {
maxY = _Y;
} else {
maxY = Math.max(_Y, maxY);
}
});
// adjust Y for negative values
maxY = Math.max(Math.abs(minY), Math.abs(maxY));
if(maxY == 0) { maxY = 5; }
minY = -maxY;
//////////////////////////////
// Main X/Y scales
//////////////////////////////
scales.main.xScale = d3.time.scale()
.domain([minX, maxX])
.range([margins.main.left, canvasWidth - margins.main.right]);
axes.main.x = d3.svg.axis()
.scale(scales.main.xScale)
.orient('bottom');
scales.main.yScale = d3.scale.linear()
.domain([
minY - Math.abs(minY * .1),
maxY + Math.abs(maxY * .1)
])
.range([margins.main.top, mainChartHeight - margins.main.bottom]);
axes.main.y = d3.svg.axis()
.scale(scales.main.yScale)
.orient('left')
.ticks(10);
//////////////////////////////
// Brush X/Y Scales
//////////////////////////////
scales.brush.xScale = d3.time.scale()
.domain([minX, maxX])
.range([margins.brush.left, canvasWidth - margins.brush.right]);
axes.brush.x = d3.svg.axis()
.scale(scales.brush.xScale)
.orient('bottom');
scales.brush.yScale = d3.scale.linear()
.domain([
minY - Math.abs(minY * .1),
maxY + Math.abs(maxY * .1)
])
.range([margins.brush.top, brushChartHeight - margins.brush.bottom]);
axes.brush.y = d3.svg.axis()
.scale(scales.brush.yScale)
.orient('left')
.ticks(10);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment