Reputation: 37
I am making a world map of businesses linked to their performance rating: so each business will be represented by a point, that has a tooltip with the performance (and other info.) I'm using the map example here
pointData = {
"businessName": businessName,
"location": location,
"performance": currperformance
}
pointsData.push(pointData);
Therefore the pointsData JSON object is of the form
[{"business":"b1","location":[long1, lat1]},{"businessName":"b2","location":[long2, lat2]}...
]
I can display the map with points and the same tooltip perfectly until I try to have differing tooltips. Many D3 examples that I've researched with dynamic tooltips are only applicable to charts - and my struggle is to append the JSON data for the tooltip on each SVG circle on a map.
Here is my attempt thus far, which displays no points and shows no console errors (Adding in the .each(function (d, i) {..}
doesn't draw the parts anymore but it's necessary for linking each location to it's subsequent business and performance rating.)
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json", function (error, topo) {
if (error) throw error;
gBackground.append("g")
.attr("id", "country")
.selectAll("path")
.data(topojson.feature(topo, topo.objects.countries).features)
.enter().append("path")
.attr("d", path);
gBackground.append("path")
.datum(topojson.mesh(topo, topo.objects.countries, function (a, b) { return a !== b; }))
.attr("id", "country-borders")
.attr("d", path);
//Tooltip Implementation
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style('opacity', 0)
.style('position', 'absolute')
.style('padding', '0 10px');
gPoints.selectAll("circle")
.each(function (d, i) {
this.data(pointsData.location).enter()
.append("circle")
.attr("cx", function (d, i) { return projection(d)[0]; })
.attr("cy", function (d, i) { return projection(d)[1]; })
.attr("r", "10px")
.style("fill", "yellow")
.on('mouseover', function (d) {
tooltip.transition()
.style('opacity', .9)
.style('background', 'black')
.text("Business" + pointsData.businessName + "performance" + pointsData.performance)
.style('left', (d3.event.pageX - 35) + 'px')
.style('top', (d3.event.pageY - 30) + 'px')
})
.on('mouseout', function (d) {
tooltip.transition()
.style("visibility", "hidden");
})
});
});
Upvotes: 1
Views: 1325
Reputation: 38231
The enter selection does what you are trying to do without the use of .each()
. Bostock designed D3 to join data to elements so that:
Instead of telling D3 how to do something, tell D3 what you want. You want the circle elements to correspond to data. You want one circle per datum. Instead of instructing D3 to create circles, then, tell D3 that the selection "circle" should correspond to data. This concept is called the data join (Thinking with Joins).
I suggest that you take a look at some examples on the enter, update, and exit selections. Though, it is possible that you were originally doing this with the plain circles (and identical tooltips):
svg.selectAll("circle")
.data([[long,lat],[long,lat]])
.enter()
.append("circle")
.attr("cx", function(d,i) { return projection(d)[0]; })
.attr("cy", function(d,i) { return projection(d)[1]; })
If you were, then it is just a matter of accessing the datum for additional properties. If not, then it is a matter of properly using an enter selection. In any event, here is a possible implementation using an enter selection and the data format you specified:
var pointsData = [
{ "businessName": "Business A",
"location": [50,100],
"performance": "100" },
{ "businessName": "Business B",
"location": [100,50],
"performance": "75"
},
{ "businessName": "Business C",
"location": [150,150],
"performance": "50"
}];
var svg = d3.select("body")
.append("svg")
.attr("width",300)
.attr("height",300);
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style('opacity', 0)
.style('position', 'absolute')
.style('padding', '0 10px');
svg.selectAll("circle") // empty selection
.data(pointsData) // data to bind to selection
.enter() // the enter selection holds new elements in the selection
.append("circle") // append a circle for each new element
// Manage the style of each circle based on its datum:
.attr("cx",function(d) { return d.location[0]; })
.attr("cy",function(d) { return d.location[1]; })
.attr("r",10)
.on("mouseover", function(d) {
tooltip.transition()
.style('opacity', .9)
.style('background', 'steelblue')
.text("Business: " + d.businessName + ". performance " + d.performance)
.style('left', (d3.event.pageX - 35) + 'px')
.style('top', (d3.event.pageY - 30) + 'px')
.duration(100);
})
.on("mouseout",function(d) {
tooltip.transition()
.style("opacity", "0")
.duration(50);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
The original selection svg.selectAll("circle")
is empty. When binding data to this empty selection, the enter
selection will hold one item for each item in the data array that does not have a corresponding element in the selection (in this case, a circle, and since there are none, the enter array has one item for each item in the data array). We then append one element for each item in the enter selection, and stylize it based on the properties of each datum.
Note that this required a few modifications from your original code (I've also skipped a projection to make it a more concise snippet).
I imagine that your initial dataset looked like this: [[long,lat], [long,lat], [long,lat]]
. When accessing each datum from this dataset, you could center a circle like:
.attr("cx", function(d,i) { return projection(d)[0]; })
.attr("cy", function(d,i) { return projection(d)[1]; })
Those are the lines you used above in your example. However, your datum now looks like your variable pointData
in your example code, so you need to modify it to look like:
.attr("cx", function(d,i) { return projection(d.location)[0]; })
.attr("cy", function(d,i) { return projection(d.location)[1]; })
I've also accessed the appropriate property of each datum for the text of the tooltip, rather than accessing data that is not bound to each element (even if it comes from the same source).
You set the opacity of the tooltip to be zero initially, then modify it to be 0.9 on mouseover: .style('opacity', .9)
, rather than toggling visibility (which only the mouseout function does) change opacity back to zero on mouseout.
Upvotes: 1