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/
Related
I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level inside values. For the most part, my code works, but for some reason, the j index in the anonymous function for the fill returns an array of repeated circle instead of returning the parent of the current element. I believe this may have something to do with the way I created the svg and selected the elements, but I can't figure it out. Below is an excerpt of my code that shows how I created the svg, the line path and the circles.
var svgb = d3.select("body")
.append("svg")
.attr("id","svg-b")
.attr("width", width)
.attr("height", height)
var gameb = svgb.selectAll(".gameb")
.data(games)
.enter()
.append("g")
.attr("class", "gameb");
gameb.append("path")
.attr("class", "line")
.attr("d", function(d) {return line_count(d.values); })
.style("stroke", function(d) { return color(d.name); })
.style("fill", "none");
gameb.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3)
.style("fill", function(d,i,j) {console.log(j)
return color(games[j].name);});
j (or more accurately, the third parameter) will always be the nodes in the selection (the array of circles here), not the parent. If you want the parent datum you can use:
.attr("fill", function() {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
return color(datum.name);
})
Note that using ()=> instead of function() will change the this context and the above will not work.
However, rather than coloring each circle independently, you could use a or the parent g to color the circles too:
gameb.append("g")
.style("fill", function(d) { return color(d.name); })
.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3);
Here we add an intermediate g (though we could use the original parent with a few additional modifications), apply a fill color to it, and then the parent g will color the children circles for us. The datum is passed on to this new g behind the scenes.
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 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.
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 a noob with d3.js. I am using topoJSON data to render maps and so far it's been working great. Now I want to overlay some data such as text or circles on top of each country/region and I am hitting a wall. I have code similar to this:
var countries = g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("path")
.attr("d", path)
.style("fill", colorize)
.attr("class", "country")
.on("click", clicked)
which properly renders my map. In order to overlay some circles on it, I do the following:
countries
.append("circle")
.attr("r", function(d, i, j) {
return 10; // for now
})
// getCentroid below is a function that returns the
// center of the poligon/path bounding box
.attr("cy", function(d, i, j) { return getCentroid(countries[0][j])[0]})
.attr("cx", function(d, i, j) { return getCentroid(countries[0][j])[1]})
.style("fill", "red")
Which is pretty cumbersome (specially the way it accesses the countries array), but it succeeds at appending a circle for each path representing a country. The problem is that the circle exists in the SVG markup, but doesn't show up at all in the document. I am obviously doing something wrong, but I am at a loss of what is it.
The problem is that you're appending the circle elements to path elements, which you can't do in SVG. You need to append them to the parent g elements. The code would look something like this.
var countries = g.selectAll("g.countries")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("g")
.attr("id", "countries");
countries.append("path")
.datum(function(d) { return d; })
.attr("d", path)
// etc
countries.append("circles")
// etc