Overlay a smaller, stylized rect over each resize element in the brush.
Last active
November 24, 2015 03:30
-
-
Save kendopunk/c4b189475344a660e36b to your computer and use it in GitHub Desktop.
Simple D3.js Brush Handles
This file contains 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> | |
<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 * .2), | |
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 * .8), | |
margins = { | |
main: { | |
top: 20, | |
right: 20, | |
bottom: 10, | |
left: 50 | |
}, | |
brush: { | |
top: 20, | |
right: 20, | |
bottom: 10, | |
left: 50 | |
} | |
}, | |
scales = { | |
main: { | |
xScale: null, | |
yScale: null | |
}, | |
brush: { | |
xScale: null, | |
yScale: null | |
} | |
}, | |
seriesFn = { | |
main: null, | |
brush: null | |
}, | |
svg; | |
/** | |
* 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 + ')'; | |
}); | |
contextBrush = d3.svg.brush() | |
.on('brush', onBrush); | |
// generate random timeseries data | |
chartData = randomDataGenerator(); | |
} | |
function onBrush() { | |
// 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); | |
}); | |
} | |
/** | |
* 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 * 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