Skip to content

Instantly share code, notes, and snippets.

@wrobstory
Last active November 16, 2024 13:38
Show Gist options
  • Save wrobstory/7612013 to your computer and use it in GitHub Desktop.
Save wrobstory/7612013 to your computer and use it in GitHub Desktop.
D3 Brush and Tooltip Complete

Example for Cooperative Brushing and Tooltips in D3.

The completed chart, with both tooltips and brushing working cooperatively. You can start a brush-zoom on either the background or a data point.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.point {
fill: #2f225d;
stroke: #afa2dc;
}
.selected {
fill: #afa2dc;
stroke: #2f225d;
}
.axis {
font: 10px sans-serif;
}
p {
font: 12px sans-serif;
margin: 0 0 2px 0;
padding: 0;
}
.clear-button {
font: 14px sans-serif;
cursor: pointer;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
d3.helper = {};
d3.helper.tooltip = function(){
var tooltipDiv;
var bodyNode = d3.select('body').node();
function tooltip(selection){
selection.on('mouseover.tooltip', function(pD, pI){
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltip').remove();
// Append tooltip
tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'tooltip')
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 40)+'px',
'background-color': '#d8d5e4',
width: '65px',
height: '30px',
padding: '5px',
position: 'absolute',
'z-index': 1001,
'box-shadow': '0 1px 2px 0 #656565'
});
var first_line = '<p>X-Value: ' + pD.index + '</p>'
var second_line = '<p>Y-Value: ' + pD.value + '</p>'
tooltipDiv.html(first_line + second_line)
})
.on('mousemove.tooltip', function(pD, pI){
// Move tooltip
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 40)+'px'
});
})
.on('mouseout.tooltip', function(pD, pI){
// Remove tooltip
tooltipDiv.remove();
});
}
tooltip.attr = function(_x){
if (!arguments.length) return attrs;
attrs = _x;
return this;
};
tooltip.style = function(_x){
if (!arguments.length) return styles;
styles = _x;
return this;
};
return tooltip;
};
var data = [];
var values = [];
for (var i = 2; i <= 50; i++) {
var val = Math.floor(Math.random() * (50 - 5 + 1) + 5);
data.push({index: i, value: val});
values.push(val);
}
var margin = {top: 20, right: 20, bottom: 60, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width])
.domain([0, 50]);
var y = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(values) + 5]);
var brush = d3.svg.brush()
.x(x)
.on("brush", brushmove)
.on("brushend", brushend);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(11);
var svg = d3.select("body").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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "brush")
.call(brush)
.selectAll('rect')
.attr('height', height);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height + 20);
points = svg.selectAll(".point")
.data(data)
.enter().append("circle")
.attr("class", "point")
.attr("clip-path", "url(#clip)")
.attr("r", function(d){return Math.floor(Math.random() * (20 - 5 + 1) + 5);})
.attr("cx", function(d) { return x(d.index); })
.attr("cy", function(d) { return y(d.value); })
.call(d3.helper.tooltip());
points.on('mousedown', function(){
brush_elm = svg.select(".brush").node();
new_click_event = new Event('mousedown');
new_click_event.pageX = d3.event.pageX;
new_click_event.clientX = d3.event.clientX;
new_click_event.pageY = d3.event.pageY;
new_click_event.clientY = d3.event.clientY;
brush_elm.dispatchEvent(new_click_event);
});
function brushmove() {
var extent = brush.extent();
points.classed("selected", function(d) {
is_brushed = extent[0] <= d.index && d.index <= extent[1];
return is_brushed;
});
}
function brushend() {
get_button = d3.select(".clear-button");
if(get_button.empty() === true) {
clear_button = svg.append('text')
.attr("y", 460)
.attr("x", 825)
.attr("class", "clear-button")
.text("Clear Brush");
}
x.domain(brush.extent());
transition_data();
reset_axis();
points.classed("selected", false);
d3.select(".brush").call(brush.clear());
clear_button.on('click', function(){
x.domain([0, 50]);
transition_data();
reset_axis();
clear_button.remove();
});
}
function transition_data() {
svg.selectAll(".point")
.data(data)
.transition()
.duration(500)
.attr("cx", function(d) { return x(d.index); });
}
function reset_axis() {
svg.transition().duration(500)
.select(".x.axis")
.call(xAxis);
}
</script>
@LukeTheWalker
Copy link

Thanks, this would be very useful, but with the newest version of d3 I cannot seem to create the right synthetic event, could you please update this if you have time? I couldn't manage to get it working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment