I am creating a population map of the US. I have the map and legend working, but I made a drop down menu that allows me to filter by race. This works and changes the data on the map and legend, but I am having an issue where the legend text shows the new range, and all previous ranges as I change the drop down menu. Is there a way to have it so that it doesn't overlap the previous text but instead shows only the one correct text? I am using D3 with React.
This is the code to my legend:
svg.append("g")
.attr("transform", "translate(580,20)")
.append(() => Legend(color, {title: `2019 Population (x10^${exp})`, width: 300,tickFormat: ".1f"}))
The code to my color variable:
const color = d3.scaleQuantile([start/divider,end/divider], d3.schemeYlOrRd[9])
start and end change depending on how big the population sizes are
Edit/Update:
I found a work around to solving my issue, I'm just not sure if its best practice or if there's another way of doing it. I gave it a background color so that the previous info gets covered up
code:
svg.append("g")
.attr("transform", "translate(580,20)")
.append(() => Legend(color, {title: `2019 Population (x10^${exp})`, width: 300,tickFormat: ".1f"}))
.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "grey")
svg.append("g")
.attr("transform", "translate(580,20)")
.append(() => Legend(color, {title: `2019 Population (x10^${exp})`, width: 300,tickFormat: ".1f"}))
Every time you do svg.append('g').transform(...).append(() => Legend()) you are adding a new <g> element.
Instead store the <g> selection in a variable and then remove its contents before adding a new legend.
const legendG = svg.append('g').attr('transform', 'translate(580, 20)')
function renderLegend(legendOptions) {
legendG.selectAll('*').remove() // removes everything inside
legendG.append(() => Legend(legendOptions))
}
// now call the `renderLegend` function every time you change something from the menu
And to answer your question if what you did is a good practice. I would say it is not, because you're just adding elements every time the user chooses something in the menu and it's just taking up more and more memory for storing the DOM. Not good for performance.
Related
I have the following adapted d3.js visual and I'm unable to work out why the transition does not fire. The ars should rotate around to different sizes when the radio button is clicked.
It seems that clicking the radio button is changing the titles of the arc (if I hover the cursor over each)
Is this section of code to blame?
// check if svg group already exists
var svg = d3.select("#sunGroup");
if (svg.empty()) {
var svg = d3.select("#sunBurst")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")",
id: "sunGroup"
});
}
Here is the full (not) working example:
https://plnkr.co/edit/hpvREU?p=preview
This is the transition I'm trying to hold onto: plnkr.co/edit/NnQUAp?p=preview
What I trying to do is move the logic at line 128 (starting d3.selectAll("input").on("change", function change() {...) out of this function
An easy fix to your problem is to remove all children of the SVG whenever you switch data types:
d3.select("#sunBurst").selectAll('*').remove()
That way, you are binding the new data to new elements. You can also remove the svg.empty() and d3.select('#sunGroup') code. This simple fix lets you switch between pie charts, and is in the spirit of the code you currently have. Here's the users pie chart.
However, this may not be the best way to do what you're trying to achieve. As a reference, see Mike Bostock's General Update Pattern series (link is to first in the series) for how to update your SVG.
I can't seem to figure out how to update bar labels when I re-sort ranking data; essentially the label names will all remain the same, but their order will change.
Originally I have:
// University Names
labelsContainer = chart.append('g')
.attr('transform', 'translate(' + (uniLabel - barLabelPadding) + ',' + (gridLabelHeight + topMargin) + ')')
.selectAll('text')
.data(sortedData)
.enter()
.append('text')
.attr('x', xoffset)
.attr('y', yText)
.attr('stroke', 'none')
.attr('fill', 'black')
.attr("dy", ".35em") // vertical-align: middle
.attr('text-anchor', 'end')
.text(barLabel);
I sort the data differently, which I still call sortedData. The rectangles and rest of the graph updates successfully...save for the labels (which I have on only one rectangle bar column.)
In a new function I tried:
// update University Names (this overwrites, however... I want to select the existing label instead of appending text on top of the original text)
labelsContainer = chart.append('g')
.attr('transform', 'translate(' + (uniLabel - barLabelPadding) + ',' + (gridLabelHeight + topMargin) + ')')
.selectAll('text')
.data(sortedData)
.enter() // using transition ... or selecting the group ... does not allow the new text to appear!
.append('text')
.attr('x', xoffset)
.attr('y', yText)
.attr('stroke', 'none')
.attr('fill', 'black')
.attr("dy", ".35em") // vertical-align: middle
.attr('text-anchor', 'end')
.text(barLabel);
The issue here is that this just adds the new (correct) labels on top of the existing ones, instead of replacing them.
Using transition() I'm able to update the rest of the graph, but not the labels.
Any ideas of how to fix? Happy to provide more info/context if need be...
UPDATE 12/24: JSFiddle: http://jsfiddle.net/myhrvold/BVB2d/
JSFiddle showing transition, but with labels being overwritten: http://jsfiddle.net/myhrvold/BVB2d/embedded/result/
I know that by appending, I'm overwriting; but in attempting to replace, nothing happens and the original text remains, so the idea here is that I'm showing that I am at least generating the correct new labels and putting them in the right place...it's just that I'm not substituting them from my original labels...
You're completely repeating your code when you update your data -- including the chart.append('g') which creates a new group and then adds text labels to it. Because you've just created this as a new group, when you select inside it you can't select any of the labels you created the first time, so instead you end up creating all new labels.
To fix: first, as #musically_ut suggested, give each of your groups a unique class name. Then, in your update method select this group and the text elements it contains using chart.select(g.univerity-labels-container).selectAll("text"). However, you'll find you still have problems because you've got everything chained to an enter(); since you don't expect any new elements to be added when sorting, just take out that line. *
That should get it working, but the program is still painfully complex for what you're trying to do. For starters, all of this could work a lot better as an HTML table which would handle a lot of the layout for you. More importantly, you could save a lot of work if, instead of grouping elements by column you grouped them by row. That way, you would only have to join the data once, to the group, instead of doing separate data joins for each variable. If I have a chance in the next few days, I might try to write up a from-the-ground up explanations of how to approach this. In the meantime, google "d3 sortable table" for a couple examples, or look at the source code for this NYT graphic by Mike Bostock.
*For updating with an enter() step, I find most tutorials don't describe the update process very clearly, but I wrote up a step-by-step breakdown here yesterday.
In the process of learning D3.js.
Is it possible using a force layout to place a circle within another circle shape as per the picture. I am hoping to transition between a single circle per node to a display showing two circles per node. The size of the effective donut is used to illustrate another variable in the data.
Is this possible?
You don't even need to use anything other than a basic svg circle, as you find in most examples. Just bind the data to it, apply a stroke, and set the stroke-width attr to your other variable. Or r - otherVar, I'm sure you can figure that part out.
If this doesn't satisfy, build your own shape. The 'g' svg element is a container element, and lets you build whatever you like. Add two circles to a g, fill them how you like. Make sure to add them in the right order, since svg has no concept of 'on top', things just get painted in the order that you add them.
edit: okay, quick demo so you can learn some syntax. I didn't add any comments but hopefully the code is very verbose and straightforward. Find it here.
d3/svg is something that you have to just bash your head against for a while. I highly recommend spending some time creating a sandbox environment where you can quickly test new things, save, refresh browser to see results. Minimizing that turnaround time is key.
Thanks to roippi I was able to create a group containing two circle shapes.
var nodeCircles = svg.selectAll("g")
.data(nodes);
// Outer circle
var outer = nodeCircles
.enter()
.append("circle")
.attr("class", "node_circle")
.attr("r", function(d) { return d.radius_plus; })
.style("fill", function(d) { return d.color_plus; })
.style("opacity", 0);
// Inner circle
var inner = nodeCircles
.enter()
.append("circle")
.attr("class", "node_circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; })
.style("stroke", function(d) { return d3.rgb(d.color).darker(2); })
.on("mouseover", mouseOver)
.on("mouseout", mouseOut)
.call(force.drag);
Outer circle visibility is toggled via a button.
As mentioned, I use a desktop based IDE to run/test visualisation languages. Currently the IDE supports studies written in D3.js, Raphael, Processin.js, Paper.js and Dygraphs. Picture below...
I have a dataset of 100 numbers, and within an SVG I create a bunch of text objects to display those numbers using the code below:
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
console.log(output_format(d));
return output_format(d);
This works perfectly. However, if I try to create a title later on (outside of my d3.csv brackets) with this code:
svg.append("text")
.text("Actual Labels")
.attr("x", w/1.92)
.attr("y", top_gap/1.5)
.attr("class", "title");
Then the first datapoint gets erased, and does not even display in console.log(output_format(d));. What is happening here and how do I fix this?
What happens is that your single text element is appended first because the other code has to wait for the AJAX request. So when you're appending the remainder of your text elements, one is already there. This existing text element is selected by selectAll("text") and then matched with the data in dataset. By default, d3 matches data based on the index -- the first element in the array matches the first element that is already there and is therefore not in the .enter() selection which you operate on.
The easiest way to fix this is to give the text labels that you append dynamically a different class and select based on that. That is, your code for appending the dynamic labels would look like
svg.selectAll("text.label")
.data(dataset)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
console.log(output_format(d));
return output_format(d);
});
No other changes should be required.
After fiddling around for several hours now, I still cannot make labels work in my D3 Sunburst layout. Here's how it looks like:
http://codepen.io/anon/pen/BcqFu
I tried several approaches I could find online, here's a list of examples I tried with, unfortunately all failed for me:
[cannot post link list because of new users restriction]
and of course the coffee flavour wheel: http://www.jasondavies.com/coffee-wheel/
At the moment i fill the slices with a title tag, only to have it displayed when hovering over the element. For that I'm using this code:
vis.selectAll("path")
.append("title")
.text(function(d) { return d.Batch; });
Is there something similar I could use to show the Batch number in the slice?
--
More info: Jason Davies uses this code to select and add text nodes:
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style(...)
...
While the selection for his wheel gives back 97 results (equaling the amount of path tags) I only get one and therefore am only able to display one label (the one in the middle)
Needs some finessing but the essential piece to get you started is:
var labels = vis.selectAll("text.label")
.data(partition.nodes)
.enter().append("text")
.attr("class", "label")
.style("fill", "black")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.text(function(d, i) { return d.Batch;} );
You can see it here
The trick is that in addition to making sure you are attaching text nodes to the appropriate data you also have to tell them where to go (the transform attribute with the handy centroid function of the arc computer).
Note that I do not need vis.data([json]) because the svg element already has the data attached (when you append the paths), but I still have to associate the text selection with the nodes from each partition.
Also I class the text elements so that they will not get confused with any other text elements you may want to add in the future.