d3.js update bar chart labels after sorting data - javascript

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.

Related

D3 map legend issue

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.

Mouse out event not working properly in JavaScript D3

I am working with D3 Charts, and by all work I have completed the chart and did some enhancement as well.
What I am doing
I have one function which shows tooltip, whenever I hover over any bar I am showing some info to the user.
My chart shows live data, so if any new data is coming it automatically creates a new bar.
SO when I hover over any bar then data is showing fine and perfect
My issue
So as I mentioned above in my case the data comes after some interval continuously, so I am getting data and adding it to previous data
The issue is when i Hover the last bar the tooltip shows fine, and when the new data comes so now two tooltips showing and it goes on and on
But in my coding I am writing code to remove the tooltip on mouseout, but still getting this issue
My code
This is what I am doing
.on("mousemove", function (event, d) {
// this whole code is when I hover that perticular bar
d3.select(this)
.transition()
.duration("50")
.attr("opacity", 0.6)
.attr("x", (a) => xScaleBars(a.timeline) - 3)
.attr("width", xScaleBars.bandwidth() + 6)
.style("filter", "url(#glow)");
div.transition().duration(50).style("opacity", 1);
div
.html(
`</text><text"></br> value : ${d.dataToShow}
<br/>
</text><text"></br> Month : ${d.month}
`
)
.style("left", event.pageX - 58 + "px")
.style("top", event.pageY - 140 + "px");
})
This above code is when I hove rover
Below is the code when I do mouseout
.on("mouseout", function (d, i) {
// this is when I move cursor out of that bar
d3.select(this)
.transition()
.duration("50")
.attr("width", xScaleBars.bandwidth())
.attr("x", (a) => xScaleBars(a.timeline))
.style("filter", "none")
.attr("opacity", "1");
div.transition().duration("50").style("opacity", 0);
})
I have tried display none as well as visibility property, but nothing works
I don't know what I am doing wrong
is My code right or wrong this also I am not getting properly
I have put all of my code in code sandbox Please have a look
PS: I am using use Effect with set Timeout for each 1 second to make it update every second
I have checked all the related suggestion by stack overflow, but none helped me out may be I am wrong but I did not found the solution.
Edit / Update
The answer suggested below is not working, if I have more than once chart
I used d3.selectAll(".tooltipCHart").remove();
So when there are two charts on hover of first chart's bar it shows tooltip, but when I hover over the other chart it is not showing
Please have a look here
here in above code sandbox in first chart it does not show tooltip but in second it is showing
Vivek, I have gone through the code and found out that most of the code written is fine. Besides that the reason of multiple tooltips is that you are rendering everything whenever data changes, so you are appending the tooltip div everytime, it means below code will execute multiple times which display multiple tooltips in case where our code did not perform mouseout execution. You can check this by inspecting elements.
var div = d3
.select("body")
.append("div")
.attr("class", "tooltipCHart")
.style("opacity", 0);
Simple solution will be to remove all the tooltips before appending a new one.
d3.selectAll(".tooltipCHart").remove();
It will resolve the issue of multiple tooltips.
But for your chart my confusion is whether is it a good idea to show tooltip or not as the data is always moving toward left so it will be difficult to keep the tooltip for long time.

D3 - force layout, circle within circle

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...

Adding a title to my SVG window erases one datapoint? (d3)

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.

Cannot make labels work on zoomable d3 sunburst

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.

Categories