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
The Map Data:
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]}...]
The Tooltip Problem:
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");
})
});
});
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).
Accessing the datum's properties:
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).
Opacity vs Visibility:
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.
Related
I'm working on a project where I'm making multiple D3 stacked bar charts. The idea is that when a button is pressed, the plot will reload with a different dataset, similar to the code that is shown here.
My issue is with modifying this code to make the bar charts stacked. I'm not too familiar with the update functionality in D3 (I've never learned about it), so I've been trying to just append more "rect" objects to the "u" variable. It will load in correctly the first time (with all the "rect" objects where I'd expect), but whenever the update method is recalled on a button click all that gets drawn is the second iteration of the append "rect" calls. If anyone knows how to work this code into stacked bar chart functionality, I'd greatly appreciate it.
For reference, this is what I've been trying
u
.enter()
.append("rect") // Add a new rect for each new elements
.merge(u) // get the already existing elements as well
.transition() // and apply changes to all of them
.duration(1000)
.attr("x", function(d) { return x(d.group); })
.attr("y", function(d) { return y(d.value1); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.value1); })
.attr("fill", "#69b3a2")
u
.enter()
.append("rect") // Add a new rect for each new elements
.merge(u) // get the already existing elements as well
.transition() // and apply changes to all of them
.duration(1000)
.attr("x", function(d) { return x(d.group); })
.attr("y", function(d) { return y(d.value2 + d.value1); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.value1); })
.attr("fill", "#69b3a2")
I have a map and a matching legend on my website. As the user selects different values from a select list, the map is updated and in the same function, the legend should be updated with new values. As the map actualization works properly, the values of the legend stay the same even in the console are logged the right values if I log the variables.
This is the function that draws the legend:
color_domain = [wert1, wert2, wert3, wert4, wert5];
ext_color_domain = [0, wert1, wert2, wert3, wert4, wert5];
console.log(ext_color_domain);
legend_labels = ["< "+wert1, ""+wert1, ""+wert2, ""+wert3, ""+wert4, "> "+wert5];
color = d3.scale.threshold()
.domain(color_domain)
.range(["#85db46", "#ffe800", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
console.log(legend_labels); //gives the right legend_labels but doesn't display them correctly
};
Sadly even the map is updated with new colors they're colored with the old thresholds. This is the way the map is colored:
svg.append("g")
.attr("class", "id")
.selectAll("path")
.data(topojson.feature(map, map.objects.immoscout).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.id]);
})
This is tough to answer without a complete, working code sample but...
You are not handling the enter, update, exit pattern correctly. You never really update existing elements, you are only re-binding data and entering new ones.
Say you've called your legend function once already, now you have new data and you do:
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
This re-binds the data and computes an enter selection. It says, hey d3, what data elements are new? For those new ones, you then append a g. Further:
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
Again, this is operating on those newly entered elements only. The ones that already existed on the page aren't touched at all.
Untested code, but hopefully it points you in the right direction:
// selection of all enter, update, exit
var legend = svg.selectAll("g.legend")
.data(ext_color_domain); //<-- a key function would be awesome here
legend.exit().remove(); //<-- did the data go away? remove the g bound to it
// ok, what data is coming in? create new elements;
var legendEnter = legend.enter().append("g")
.attr("class", "legend");
legendEnter.append("rect");
legendEnter.append("text");
// ok, now handle our updates...
legend.selectAll("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.selectall("text")
...
There's some really great tutorials on this; and it's confusing as hell, but it's the foundation of d3.
An example that helps you get started with updating d3 (d3, v4):
const line = svg.selectAll('line').data(d3Data); // binds things
line.exit().remove(); // removes old data
line.enter()
.append('line') // add new lines for new items on enter
.merge(line) // <--- this will make the updates to the lines
.attr('fill', 'none')
.attr('stroke', 'red');
I am trying to do the obvious thing of getting the arrowhead colors of my directed graph's links to match the edge colors. Surprisingly I have not found a complete solution for doing this, although this older post seems like an excellent starting point. I would be fine with adapting that solution to work as outlined below, or if there is a superior method for creating arrowheads that achieves this effect I would be most thankful.
First, I have a linear gradient color function to color my edges by property like this:
var gradientColor = d3.scale.linear().domain([0,1]).range(["#08519c","#bdd7e7"]);
Then, like that previous post I have a function for adding markers:
function marker (color) {
var reference;
svg.append("svg:defs").selectAll("marker")
.data([reference])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15) // This sets how far back it sits, kinda
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(#" + reference + ")"; };
And then the links definition I have is this one based on the Curved Links example.
var link = svg.selectAll(".link")
.data(bilinks)
.enter().append("path")
.attr("class", "link")
.style("fill", "none")
.style("opacity", "0.5")
.style("stroke-width", "2")
.style("stroke", function(d) { return gradientColor(d[3]); } )
.attr("marker-end", marker( "#FFCC33" ) );
This DOES NOT work as written; the browser gives me an "Uncaught TypeError: Cannot read property '5' of undefined" (where 'd[5]' refers to the fifth property in a list of properties that the links have). The problem is clearly passing the data function to the marker function in this case. If I feed in a static color like "#FFCC33" then the arrowheads DO change color (now). Unfortunately the person who posted this "marker function" solution 1.5 years ago didn't include the bit about passing the color to the marker function at all.
I don't know how to feed in the link's color properly. Ideally I would be able to use a reference to the color of the link that the arrowhead is attached to rather than inputting the same color function (because eventually I'm going to be coloring the links via different schemes based on button presses).
I've created a JS Fiddle that includes all the necessary bits to see and solve the problem. Currently I'm passing a static color to the markers, but it should be whatever is the color of the link it is attached to. I've also included features for another question on properly positioning the arrowheads and edge tails.
I don't believe you're able to define a single SVG marker and change it's colour. Instead you need to define the marker many times (1 for each colour that you need to use). There's a nice example that recently popped up onto the D3 website.
The way this works, is by having lots if different markers, each defining the colour of the marker. Here's a screenshot of all the markers that are defined:
Then this particular example, cycles the CSS classes on the paths. The particular colored marker that each path is using is defined within the CSS class that's being applied to a path at any given time.
I've modified your example to add a new marker per path (and changed the colors slightly in the gradient to prove that it's working). Here's what I've got:
var width = 960,
height = 500;
var color = d3.scale.category20();
var gradientColor = d3.scale.linear().domain([0, 15]).range(["#ff0000", "#0000ff"]);
var force = d3.layout.force()
.linkDistance(10)
.linkStrength(2)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var defs = svg.append("svg:defs");
d3.json("http://bost.ocks.org/mike/miserables/miserables.json", function (error, graph) {
if (error) throw error;
function marker(color) {
defs.append("svg:marker")
.attr("id", color.replace("#", ""))
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15) // This sets how far back it sits, kinda
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(" + color + ")";
};
var nodes = graph.nodes.slice(),
links = [],
bilinks = [];
graph.links.forEach(function (link) {
var s = nodes[link.source],
t = nodes[link.target],
i = {}, // intermediate node
linkValue = link.value // for transfering value from the links to the bilinks
;
nodes.push(i);
links.push({
source: s,
target: i
}, {
source: i,
target: t
});
bilinks.push([s, i, t, linkValue]);
});
force.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(bilinks).enter().append("path")
.attr("class", "link")
.style("fill", "none")
.style("opacity", "0.5")
.style("stroke-width", "2")
.each(function(d) {
var color = gradientColor(d[3]);
console.log(d[3]);
d3.select(this).style("stroke", color)
.attr("marker-end", marker(color));
});
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return 2 + d.group;
})
.style("opacity", 0.5)
.style("fill", function (d) {
return color(d.group);
});
node.append("title")
.text(function (d) {
return d.name;
});
force.on("tick", function () {
link.attr("d", function (d) {
return "M" + d[0].x + "," + d[0].y + "S" + d[1].x + "," + d[1].y + " " + d[2].x + "," + d[2].y;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I'm working on a d3.js pie chart application. I am trying to develop the functionality that when you click on the legend rectangles, it toggles the slice on/off as well as the fill inside the legend rectangle.
http://jsfiddle.net/Qh9X5/3136/
legend
Rects
.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function(d, i){ return i * 20;})
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d, i) {
return methods.color(i);
})
.style("stroke", function(d, i) {
return methods.color(i);
})
.on('click', function(d, i){
onLegendClick(d, i);
})
Here's one way to solve your problem:
One change required in your code is to use key functions, so that d3 matches the filtered data to the corresponding DOM node. Labels seem to be a proper key in your dataset.
Simply use:
.data(this.piedata, function(d) { return d.data.label});
instead of
.data(this.piedata);
Then, in your OnLegendClick function, you want to select all the legend's rect and all the svg arcs matching with the clicked element.
Workflow is :
select the DOM elements
match with the selected data
apply changes
Here's how to do it:
function onLegendClick(dt){
d3.selectAll('rect').data([dt], function(d) { return d.data.label}).style("opacity", function(d) {return Math.abs(1-d3.select(this).style("opacity"))})
d3.selectAll('.pie').data([dt], function(d) { return d.data.label}).style("opacity", function(d) {return Math.abs(1-d3.select(this).style("opacity"))})
}
I let you adjust the "toggle" feature. You might also want to change the texts in addition to the arcs, for this use another selection.
Updated jsfiddle: http://jsfiddle.net/Qh9X5/3138/
first of all sorry for the long title, I couldn't come up with a better one to explain my problem.
What I have is a scatterplot, with circles that have different ids based on the d.FamilyName column in my csv. Some circles share the same id.
On the right of that scatterplot, I set up a div, containing a list of all the d.Familyname values, nested to have them show just once. The id of every text is also set by d.FamilyName.
To improve readability of the scatterplot, since it has many values, I was planning to add an event listener on mouseover on the text, which should then modify the radius of the circles sharing the same id as the text.
Every circle is plotted inside the var circle and the text is plotted inside a div, here's the code for the circle and the text part:
var circle = svg.append("g")
.attr("id", "circles")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.SquadraturaInterna_o); })
.attr("cy", function (d) { return y(d.SquadraturaEsterna_o); })
.attr("r", 2)
After the radius attribute there's an event listener to show other values of the specified circle (the name and coordinates) but they are not relevant to my problem I think.
The text part:
d3.select("#elenco")
.select("#value")
.selectAll("text")
.data(nested)
.enter()
.append("p")
.append("text")
.attr("id", function (i) { return (i).key; })
.text(function (i) { return (i).key; })
.on("mouseover", function (d, i) {
if (this.id == circle.id)
{d3.select("circle")
.attr("r", 5);
}
else {d3.select("circle").attr("r", 1);}
;})
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2);
});
The problem is of course on the if statement in the mouseover.
Any hint on how to solve this would be really appreciated, thanks!
EDIT: Thanks to #Lars' reply I was able to make this work with a little edit to his code, like so:
.on("mouseover", function (d) {
var sel = this.id;
circle.filter(function() { return this.id === sel; }).attr("r", 5); })
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2); }
);
As an alternative to #musically_ut's approach, you can also use the .filter() function to modify only the elements you want.
.on("mouseover", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 5);
})
.on("mouseout", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 1);
})
I think you are looking for this:
.on("mouseover", function (d, i) {
circles.attr('r', function (circle_d, i) {
return (d.id == circle_d.id) ? 5 : 1;
});
})
In D3, the this in the accessor functions refers to the DOM element. If you wanted to compare the id of the DOM element with circle's data's ids, then you could do something like this kind: d3.select(this).attr('id') == circle_d.id (cache d3.select(this).attr('id') for performance reasons).