This scatterplot is step 2 of my blog on Using a D3 Voronoi grid to improve a chart’s interactive experience in which the tooltip and the event are both triggered by an invisible Voronoi grid that lies over the scatterplot. This is used to illustrate the fact that you still need one more step, and that is to attach the tooltip to the circle that represents the seed of each Voronoi cell, which you can see in the next example
You can find all of the steps here
////////////////////////////////////////////////////////////
//////////////////////// Set-up ////////////////////////////
////////////////////////////////////////////////////////////
//Quick fix for resizing some things for mobile-ish viewers
var mobileScreen = ($( window ).innerWidth() < 500 ? true : false);
//Scatterplot
var margin = {left: 30, top: 20, right: 20, bottom: 20},
width = Math.min($("#chart").width(), 800) - margin.left - margin.right,
height = width*2/3;
var svg = d3.select("#chart").append("svg")
.attr("width", (width + margin.left + margin.right))
.attr("height", (height + margin.top + margin.bottom));
var wrapper = svg.append("g").attr("class", "chordWrapper")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//////////////////////////////////////////////////////
///////////// Initialize Axes & Scales ///////////////
//////////////////////////////////////////////////////
var opacityCircles = 0.7;
//Set the color for each region
var color = d3.scale.ordinal()
.range(["#EFB605", "#E58903", "#E01A25", "#C20049", "#991C71", "#66489F", "#2074A0", "#10A66E", "#7EB852"])
.domain(["Africa | North & East", "Africa | South & West", "America | North & Central", "America | South",
"Asia | East & Central", "Asia | South & West", "Europe | North & West", "Europe | South & East", "Oceania"]);
//Set the new x axis range
var xScale = d3.scale.log()
.range([0, width])
.domain([100,2e5]); //I prefer this exact scale over the true range and then using "nice"
//.domain(d3.extent(countries, function(d) { return d.GDP_perCapita; }))
//.nice();
//Set new x-axis
var xAxis = d3.svg.axis()
.orient("bottom")
.ticks(2)
.tickFormat(function (d) { //Difficult function to create better ticks
return xScale.tickFormat((mobileScreen ? 4 : 8),function(d) {
var prefix = d3.formatPrefix(d);
return "$" + prefix.scale(d) + prefix.symbol;
})(d);
})
.scale(xScale);
//Append the x-axis
wrapper.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(xAxis);
//Set the new y axis range
var yScale = d3.scale.linear()
.range([height,0])
.domain(d3.extent(countries, function(d) { return d.lifeExpectancy; }))
.nice();
var yAxis = d3.svg.axis()
.orient("left")
.ticks(6) //Set rough # of ticks
.scale(yScale);
//Append the y-axis
wrapper.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + 0 + "," + 0 + ")")
.call(yAxis);
//Scale for the bubble size
var rScale = d3.scale.sqrt()
.range([mobileScreen ? 1 : 2, mobileScreen ? 10 : 16])
.domain(d3.extent(countries, function(d) { return d.GDP; }));
////////////////////////////////////////////////////////////
/////////////////// Scatterplot Circles ////////////////////
////////////////////////////////////////////////////////////
//Initiate a group element for the circles
var circleGroup = wrapper.append("g")
.attr("class", "circleWrapper");
//Place the country circles
circleGroup.selectAll("countries")
.data(countries.sort(function(a,b) { return b.GDP > a.GDP; })) //Sort so the biggest circles are below
.enter().append("circle")
.attr("class", function(d,i) { return "countries " + d.CountryCode; })
.style("opacity", opacityCircles)
.style("fill", function(d) {return color(d.Region);})
.attr("cx", function(d) {return xScale(d.GDP_perCapita);})
.attr("cy", function(d) {return yScale(d.lifeExpectancy);})
.attr("r", function(d) {return rScale(d.GDP);});
//////////////////////////////////////////////////////////////
//////////////////////// Voronoi /////////////////////////////
//////////////////////////////////////////////////////////////
//Initiate the voronoi function
//Use the same variables of the data in the .x and .y as used in the cx and cy of the circle call
//The clip extent will make the boundaries end nicely along the chart area instead of splitting up the entire SVG
//(if you do not do this it would mean that you already see a tooltip when your mouse is still in the axis area, which is confusing)
var voronoi = d3.geom.voronoi()
.x(function(d) { return xScale(d.GDP_perCapita); })
.y(function(d) { return yScale(d.lifeExpectancy); })
.clipExtent([[0, 0], [width, height]]);
//Initiate a group element to place the voronoi diagram in
var voronoiGroup = wrapper.append("g")
.attr("class", "voronoiWrapper");
//Create the Voronoi diagram
voronoiGroup.selectAll("path")
.data(voronoi(countries)) //Use vononoi() with your dataset inside
.enter().append("path")
.attr("d", function(d, i) { return "M" + d.join("L") + "Z"; })
.datum(function(d, i) { return d.point; })
//Give each cell a unique class where the unique part corresponds to the circle classes
.attr("class", function(d,i) { return "voronoi " + d.CountryCode; })
.style("stroke", "#2074A0") //I use this to look at how the cells are dispersed as a check
.style("stroke-opacity", 0.5)
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", showTooltip)
.on("mouseout", removeTooltip);
//////////////////////////////////////////////////////
///////////////// Initialize Labels //////////////////
//////////////////////////////////////////////////////
//Set up X axis label
wrapper.append("g")
.append("text")
.attr("class", "x title")
.attr("text-anchor", "end")
.style("font-size", (mobileScreen ? 8 : 12) + "px")
.attr("transform", "translate(" + width + "," + (height - 10) + ")")
.text("GDP per capita [US $] - Note the logarithmic scale");
//Set up y axis label
wrapper.append("g")
.append("text")
.attr("class", "y title")
.attr("text-anchor", "end")
.style("font-size", (mobileScreen ? 8 : 12) + "px")
.attr("transform", "translate(18, 0) rotate(-90)")
.text("Life expectancy");
///////////////////////////////////////////////////////////////////////////
///////////////////////// Create the Legend////////////////////////////////
///////////////////////////////////////////////////////////////////////////
if (!mobileScreen) {
//Legend
var legendMargin = {left: 5, top: 10, right: 5, bottom: 10},
legendWidth = 160,
legendHeight = 270;
var svgLegend = d3.select("#legend").append("svg")
.attr("width", (legendWidth + legendMargin.left + legendMargin.right))
.attr("height", (legendHeight + legendMargin.top + legendMargin.bottom));
var legendWrapper = svgLegend.append("g").attr("class", "legendWrapper")
.attr("transform", "translate(" + legendMargin.left + "," + legendMargin.top +")");
var rectSize = 16, //dimensions of the colored square
rowHeight = 22, //height of a row in the legend
maxWidth = 125; //widht of each row
//Create container per rect/text pair
var legend = legendWrapper.selectAll('.legendSquare')
.data(color.range())
.enter().append('g')
.attr('class', 'legendSquare')
.attr("transform", function(d,i) { return "translate(" + 0 + "," + (i * rowHeight) + ")"; });
//Append small squares to Legend
legend.append('rect')
.attr('width', rectSize)
.attr('height', rectSize)
.style('fill', function(d) {return d;});
//Append text to Legend
legend.append('text')
.attr('transform', 'translate(' + 25 + ',' + (rectSize/2) + ')')
.attr("class", "legendText")
.style("font-size", "11px")
.attr("dy", ".35em")
.text(function(d,i) { return color.domain()[i]; });
}//if !mobileScreen
else {
d3.select("#legend").style("display","none");
}
///////////////////////////////////////////////////////////////////////////
/////////////////// Hover functions of the circles ////////////////////////
///////////////////////////////////////////////////////////////////////////
//Show the tooltip on the hovered over circle
function showTooltip(d) {
$(this).popover({
placement: 'auto top', //place the tooltip above the item
container: '#chart', //the name (class or id) of the container
trigger: 'manual',
html : true,
content: function() { //the html content to show inside the tooltip
return "<span style='font-size: 11px; text-align: center;'>" + d.Country + "</span>"; }
});
$(this).popover('show');
}//function showTooltip
//Hide the tooltip when the mouse moves away
function removeTooltip() {
//Hide the tooltip
$('.popover').each(function() {
$(this).remove();
});
}//function removeTooltip
//iFrame handler
var pymChild = new pym.Child();
pymChild.sendHeight()
setTimeout(function() { pymChild.sendHeight(); },5000);
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Scatterplot with Voronoi</title>
<!-- D3.js -->
<script src="//d3js.org/d3.v3.js"></script>
<!-- Pym.js - iframe height handler for the Blog -->
<script src="pym.min.js"></script>
<!-- jQuery -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<!-- Open Sans & CSS -->
<link href='//fonts.googleapis.com/css?family=Open+Sans:700,400,300' rel='stylesheet' type='text/css'>
<style>
body {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-weight: 400;
color: #525252;
text-align: center;
}
html, body {
width:auto;
height:auto;
}
.axis path,
.axis line {
fill: none;
stroke: #B3B3B3;
shape-rendering: crispEdges;
}
.axis text {
font-size: 10px;
fill: #6B6B6B;
}
.popover {
pointer-events: none;
}
.legendTitle {
fill: #1A1A1A;
color: #1A1A1A;
text-anchor: middle;
font-size: 10px;
}
.legendText {
fill: #949494;
text-anchor: start;
font-size: 9px;
}
@media (min-width: 500px) {
.col-sm-3, .col-sm-9 {
float: left;
}
.col-sm-9 {
width: 75%;
}
.col-sm-3 {
width: 25%;
}
}
</style>
</head>
<body>
<div id="cont" class="container-fluid text-center">
<div class="row scatter">
<h5 style="color: #3B3B3B;">Life expectancy versus GDP per Capita</h5>
<h6 style="color: #A6A6A6;">Voronoi - Tooltip attached to Voronoi cells</h6>
<div class="col-sm-9">
<div id="chart"></div>
</div>
<div id = "legend" class="col-sm-3">
<div class="legendTitle" style="font-size: 12px;">REGION</div>
<div id="legend"></div>
</div>
</div>
</div>
<script src="worldbank.js"></script>
<script src="script.js"></script>
</body>
</html>
/*! pym.js - v0.4.4 - 2015-07-16 */
!function(a){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&module.exports?module.exports=a():window.pym=a.call(this)}(function(){var a="xPYMx",b={},c=function(a){var b=new RegExp("[\\?&]"+a.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]")+"=([^&#]*)"),c=b.exec(location.search);return null===c?"":decodeURIComponent(c[1].replace(/\+/g," "))},d=function(a,b){return"*"===b.xdomain||a.origin.match(new RegExp(b.xdomain+"$"))?!0:void 0},e=function(b,c,d){var e=["pym",b,c,d];return e.join(a)},f=function(b){var c=["pym",b,"(\\S+)","(.+)"];return new RegExp("^"+c.join(a)+"$")},g=function(){for(var a=document.querySelectorAll("[data-pym-src]:not([data-pym-auto-initialized])"),c=a.length,d=0;c>d;++d){var e=a[d];e.setAttribute("data-pym-auto-initialized",""),""===e.id&&(e.id="pym-"+d);var f=e.getAttribute("data-pym-src"),g=e.getAttribute("data-pym-xdomain"),h={};g&&(h.xdomain=g),new b.Parent(e.id,f,h)}};return b.Parent=function(a,b,c){this.id=a,this.url=b,this.el=document.getElementById(a),this.iframe=null,this.settings={xdomain:"*"},this.messageRegex=f(this.id),this.messageHandlers={},c=c||{},this._constructIframe=function(){var a=this.el.offsetWidth.toString();this.iframe=document.createElement("iframe");var b="",c=this.url.indexOf("#");c>-1&&(b=this.url.substring(c,this.url.length),this.url=this.url.substring(0,c)),this.url.indexOf("?")<0?this.url+="?":this.url+="&",this.iframe.src=this.url+"initialWidth="+a+"&childId="+this.id+"&parentUrl="+encodeURIComponent(window.location.href)+b,this.iframe.setAttribute("width","100%"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("marginheight","0"),this.iframe.setAttribute("frameborder","0"),this.el.appendChild(this.iframe),window.addEventListener("resize",this._onResize)},this._onResize=function(){this.sendWidth()}.bind(this),this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c<this.messageHandlers[a].length;c++)this.messageHandlers[a][c].call(this,b)},this.remove=function(){window.removeEventListener("message",this._processMessage),window.removeEventListener("resize",this._onResize),this.el.removeChild(this.iframe)},this._processMessage=function(a){if(d(a,this.settings)&&"string"==typeof a.data){var b=a.data.match(this.messageRegex);if(!b||3!==b.length)return!1;var c=b[1],e=b[2];this._fire(c,e)}}.bind(this),this._onHeightMessage=function(a){var b=parseInt(a);this.iframe.setAttribute("height",b+"px")},this._onNavigateToMessage=function(a){document.location.href=a},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlers[a].push(b)},this.sendMessage=function(a,b){this.el.getElementsByTagName("iframe")[0].contentWindow.postMessage(e(this.id,a,b),"*")},this.sendWidth=function(){var a=this.el.offsetWidth.toString();this.sendMessage("width",a)};for(var g in c)this.settings[g]=c[g];return this.onMessage("height",this._onHeightMessage),this.onMessage("navigateTo",this._onNavigateToMessage),window.addEventListener("message",this._processMessage,!1),this._constructIframe(),this},b.Child=function(b){this.parentWidth=null,this.id=null,this.parentUrl=null,this.settings={renderCallback:null,xdomain:"*",polling:0},this.messageRegex=null,this.messageHandlers={},b=b||{},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlers[a].push(b)},this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c<this.messageHandlers[a].length;c++)this.messageHandlers[a][c].call(this,b)},this._processMessage=function(a){if(d(a,this.settings)&&"string"==typeof a.data){var b=a.data.match(this.messageRegex);if(b&&3===b.length){var c=b[1],e=b[2];this._fire(c,e)}}}.bind(this),this._onWidthMessage=function(a){var b=parseInt(a);b!==this.parentWidth&&(this.parentWidth=b,this.settings.renderCallback&&this.settings.renderCallback(b),this.sendHeight())},this.sendMessage=function(a,b){window.parent.postMessage(e(this.id,a,b),"*")},this.sendHeight=function(){var a=document.getElementsByTagName("body")[0].offsetHeight.toString();this.sendMessage("height",a)}.bind(this),this.scrollParentTo=function(a){this.sendMessage("navigateTo","#"+a)},this.navigateParentTo=function(a){this.sendMessage("navigateTo",a)},this.id=c("childId")||b.id,this.messageRegex=new RegExp("^pym"+a+this.id+a+"(\\S+)"+a+"(.+)$");var f=parseInt(c("initialWidth"));this.parentUrl=c("parentUrl"),this.onMessage("width",this._onWidthMessage);for(var g in b)this.settings[g]=b[g];return window.addEventListener("message",this._processMessage,!1),this.settings.renderCallback&&this.settings.renderCallback(f),this.sendHeight(),this.settings.polling&&window.setInterval(this.sendHeight,this.settings.polling),this},g(),b});