Scatterplot update: same number of data points, but new data points - javascript

Apologies in advance that my question does not include code, but rather is a high level question on D3 and how to build my app correctly. I will attempt to make my question as clear and concise as possible:
I am building a React / D3 app that creates a scatter graph of NBA team logos, that allows users to click buttons to choose variables for the X and Y axis. The user can also filter the graph to include only certain teams (those in a particular division of the NBA).
Here is a quick demo gif of the app that features the main problem I am having:
.
.
.
.
and here is the link to my app for anyone interested.
What is working correctly
When I change the X or Y axis button (2nd half of the gif), the team logos correctly slide to their new locations.
What is working incorrectly
When I change the division (1st half of gif), it changes the 5 team logos that are showing, which is correct. However, the animation (which I show a few times in the gif) is incorrect. The old logos should simply disappear in place, and the new logos should simply appear in place. Instead, the logos change and slide.
I understand why the animation is doing this - D3 sees 5 data points before the update, and 5 data points after the update, but doesn't distinguish that my points are unique (different team logos). Since the updated data points have new (x,y) locations (different stats for each team), it simply animates the old logos to the locations of the new logos.
My proposed fix
I think the structure of my app is holding be back with regards to fixing this. Currently, I have a container component that loads the data, filters the teams (based on the division selected), and then passes the filtered data (an array of objects with the team stats) into a graph component that creates the logo scatter graph.
If, on the other hand, I pass the full object (of all 30 teams) to the graph component, then could I fix this problem by simply having D3 change the "fill" of the markers to transparent when they are filtered out? This way, there are always 30 logos being plotted, although 25 would be invisible, and the logos displaying should animate correctly.
Thanks in advance for your thoughts!
Edit: please let me know if the post is unclear in any way and I will try to clarify. I try to avoid posting Qs without code, but this is a fairly high level Q that focuses on how the D3 general update pattern works, and how I can build a graph with a specific animation that works within the general update pattern framework.
Edit2: The radio buttons are built in the container component here. Using my API to grab the data from my database, and then using these radio buttons to filter the data, are all done in the container component. I am considering bringing these radio buttons into the graph component and building them with D3. I think I may have to.
Edit3: Should have shared earlier, here is the D3 code that makes these markers:
const update = svg.select('g.points')
.selectAll("rect")
.data(graphData);
// Second exit and remove
update.exit().remove();
// Third Update
update
.enter()
.append("rect")
.attr("transform", d => `translate(${(xScale(+d[xColname]) - logoRadius)}, ${(yScale(+d[yColname]) - logoRadius)})`)
.attr("x", 0).attr("y", 0)
.attr("height", logoRadius)
.attr("width", logoRadius)
.attr("fill", d => `url(#teamlogo-${d.teamAbbrev})`)
.attr("opacity", 0.95);
// And Fourth transition
update
.transition()
.duration(750)
.delay((d, i) => i * 15)
.attr("transform", d => `translate(${(xScale(+d[xColname]) - logoRadius)}, ${(yScale(+d[yColname]) - logoRadius)})`)
.attr("x", 0).attr("y", 0)
.attr("height", logoRadius)
.attr("width", logoRadius)
.attr("fill", d => `url(#teamlogo-${d.teamAbbrev})`)
.attr("opacity", 0.95);

The issue here seems to be just the lack of a key function during the data join.
You asked in the comments section:
Doesn't the D3 general update pattern just look for the number of objects in the data array?
If you set up a key function the answer is no. The thing is that...
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element. (source)
So, if you don't set up a key function, because you have always just 5 teams you don't effectively have working enter and exit selections when you change the division, but just an update one: as you're binding the data by their order, D3 thinks that Chicago Bulls and Atlanta Hawks are the same team.
Solution: set up a key function.
It can be simple as using the team abbreviation (supposing they are unique):
const update = svg.select('g.points')
.selectAll("rect")
.data(graphData, function(d){
return d.teamAbbrv;
});
PS: Just a question not related to your problem: why are you appending rects here? Since you have logoRadius, doesn't appending circles seem more natural? On top of that, the data representation would be more accurate, since the center of the circle, regardless its size, is at the correct datum coordinate. That's not the case with a rectangle, in which the coordinates (x, y) represent its top left corner.

Related

How to change the link color of a sankey in d3.js based off a boolean variable?

I'm trying to create a sankey diagram using D3 in observable based off a CSV. I would like to partition the sankey based off a boolean column of the CSV, similar to this picture:
partitioned on male/female
I'm currently trying to create visualization identically to this code but with my own dataset. The only difference is in my energy array I have my variable where each object has a 1.0 or 0.0 for the partition. How can I get this variable to be reflected in the colors?
I've tried adding in a "partioned" option in the drop down and then creating a
if (linkColor === "scaled")
option where I could change the .attr("stroke", "color") but I don't think I got close.
The only thing I know is that typically you change the link color with .attr("stroke", "color").
I'm very new to this all so please let me know if I can provide more information or if you have any ideas.

How to format array of values to form a distribution for google-charts histogram?

I have 1000 values in no particular order but I'd like to format them into a normal distribution to plot on a histogram using google-charts.
I've tried using d3.js and I got it working just based off some examples but it looks extremely ugly and I don't have enough time to learn d3 in and out to get the results I want. Google-charts visual format are great.
The problem is google-charts expects data in a format where each value has a name along with headers. So when I organized it into this:
'dsSample1': [
['price', 'number'],
['price', 11386.057139142767],
['price', 27659.397260273952],
['price', 44159.39726027395],
...
from
'dsSample2': [
11386.057139142767,
27659.397260273952,
44159.39726027395,
28026.04112639835,
...
google charts works, but I get the following:
This is as close as I've come to getting it working in d3: https://jsfiddle.net/0jtrq17x/1/. It works but it's extremely ugly.
I've managed to arrange the array data into bins using some d3 code but it is imcompatible with google-charts and I don't know to make it compatible, and also don't know how to format the data so it plays nice with google-charts histogram
this code
var values = this.hypo.dsSample2.map(x => {
return x + 128608.42487322348
})
var max = d3.max(values)
var min = d3.min(values)
var x = d3.scale.linear()
.domain([min, max])
.range([0, 800]);
var histGenerator = d3.layout.histogram()
.bins(x.ticks(100))
(values)
this.data1 = histGenerator
returns this array transformation
My problem is I don't know how to massage my array of data so I can get something like this in google-charts:
there are two data formats for the google charts version.
a single series format, with the names,
or a multi-series format, with just the numbers.
it is ok to use the multi-series format with a single series.
so, assigning names is not required.
but you will have to convert each value to its own array.
'dsSample1': [
[11386.057139142767],
[27659.397260273952],
[44159.39726027395],
...
from
'dsSample2': [
11386.057139142767,
27659.397260273952,
44159.39726027395,
...
you can use the map method to format the data.
dsSample.map(function (value) {
return [value];
});
see following fiddle...
https://jsfiddle.net/x684f1vs/
I know you have decided against D3, but since your question is still tagged with d3.js, I will post an answer using D3 anyways :)
I have made an updated JSFiddle, with an adaption of your code:
https://jsfiddle.net/w7r80cfo/1/
In short, to manipulate this histogram, look to the following lines:
1038 and 1039 to change the dimensions (width and height respectively) of the visualization. The values given are in pixels.
1049 to change the number of buckets for you histogram. Currently it is set to 100.
1083 to change the width of the individual bars. Currently, I've set it to 0.25 of the space calculated for each bar. If you e.g. change 0.25 to 1 the bars will be so wide, they will be drawn right next to each other.
1085 to change the color of the bars. Currently they are given a darker shade of red the higher number of values they represent. If you want e.g. just blue, change the line to .attr("fill", "steelblue")
Play around with these values and see if you can get to a chart that is close to what you want.
To elaborate a bit on the changes I've made, they consist mainly of the following:
Line 1038: lowered the width to 600.
Line 1073: updated to position the visualization correctly:
.attr('transform', `translate(${margin.left},${margin.top})`);
Line 1083: lowered the width of the bars by multiplying by 0.25:
.attr("width", (x(data[0].dx) - x(0)) * 0.25)
Other than that I have removed the following code to remove the text labels, as they indeed made the chart look messy:
bar.append("text")
.attr("dy", ".75em")
.attr("y", -12)
.attr("x", (x(data[0].dx) - x(0)) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return "$" + d3.format(",.2f")(d.x); });
Besides this, I have added an y axis and changed the way the axes are drawn in order to make them look a bit nicer. I can go into detail about these changes, but I think they are of lesser interest to your goal.
Hope this helps!

D3 exit selecting wrong data

I'm trying to build kind of real time graph using D3.js. Code is available at https://plnkr.co/edit/hrawv8CTBIsJf2QWTBMb?p=preview.
The source data represent user authentication results from different organizations. For each organization there is a name, ok count and fail count. The graph should be dynamically (getting the data in loop) updated based on data.
The code is based on https://bl.ocks.org/mbostock/3808234.
There are few problems and few things i'm not sure about.
exit function only selects the red bars based on data update:
// JOIN new data with old elements
// specify function for data matching - correct?
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
// EXIT old elements not present in new data
// this works somehow strange
// it does select all red boxes
boxes.exit().transition(t).remove();
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
This obviously breaks the graph quite a lot (see plunker). I need the exit to select only bars, which are not available in data file anymore. See example below.
initial state of data file:
inst_name,ok,fail
inst1,24,-1
inst2,23,-3
...
updated state of data file:
inst_name,ok,fail
inst1,26,-1
inst14,22,-4
...
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I've read, that new data are matched against older using index. I've specified that inst_name should be used:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
Is this necessary (I've used it everywhere when inserting data)?
Also the transition for removing the elements does not work. What is the problem?
I'm also not sure if specifying data is necessary when adding new bars:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
.....
// add new element in new data
svg.selectAll(".blue")
.data(data, function(d) { // is this necessary ?
return d.inst_name;
}) // use function for new data matching against inst_name, necessary?
.enter().append("rect")
.transition(t)
.attr("class", function(d) {
return "blue box "
})
.attr("x", function(d) {
return x(d.inst_name);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.ok);
})
.attr("height", function(d) {
return height - y(d.ok + min);
})
Thanks for help.
EDIT
The underlying data get changed by script (this was not written clearly in the original post), so it can change independently of the graph state. The data should be only growing.
You've asked a lot of questions.
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
First, you build two sets of bars (blue [ok] and red [fail]). When you data bind these you give them the same key function, which identifies them by inst_name. You then do your data update, which now selects all the bars at once with:
svg.selectAll(".box")
You again data-bind with the same key function. Your data has 10 values in the array but you just selected 20 bars. The second 10 bars exit (the red ones) because to d3 they are not in your 10 data-points
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I don't see that in your plunker, you are giving it the same data over and over.
Also the transition for removing the elements does not work. What is the problem?
You haven't given the transition anything to do. It'll run it, then at the end remove the rects. What you need is something for it to transition, like "height":
boxes.exit().transition(t).attr('height', 0).remove();
This will shrink them to 0 height.
So how do we clean up your code?
First, I would operate on g elements each one paired to an item in your data array. You then place both bars in the g that belong to that data point. Take a look here, I've started to clean-up your code (incomplete, though, hopefully it gets you going).

Not able to get the Zoom/Pan and States name appear in India Map - D3?

I am new to D3.js. However after being practiced the examples of this site, I tried to play ahead with Mr.John Coogan's map given here. The output that I found in his site is as under
But when I am trying to do the same thing by placing his .js,css,.json and index.html in plunker it is coming as
Problems
a) No States are getting displayed
b) Zoom and Pan is not working
In another word, at this point of time I am looking only for the Indian map to work exactly as the example shown by Mr. Coogan.
What needs to be done for this?
Here's the working plunk: http://plnkr.co/1EqpIFecwJmkbvypTyQD?p=preview
You needed to uncomment this line:
.call(d3.behavior.zoom().on("zoom", redraw))
on line 40 of the index.html in your plunk, and then zoom and pan will work.
The state colors (based on wealth) are not showing because of various, more complex errors. The error shown in the console (svg is not defined referencing line 78) is just the start (you need to replace svg with india, which is defined).
In fact the whole original gist your example is based on is really just a work in progress, but most of the answers for how to fix it can be found in this thread from the google group, from Mike Bostock himself.
Essentially, the json data loads asynchronously, so need to be loaded in series.
// load the two JSON files in series
d3.json("states.json", function(states) {
d3.json("wealth.json", function(wealthdata) {
...
});
});
Then you can just apend the relevant colorbrewer CSS class when you first create each path:
india.selectAll("path")
.data(states.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) {
return "q" + quantize(wealthdata[d.id]) + "-9";
})
.attr("d", path);
But you also need to define the quantize scale, range...:
var quantize = d3.scale.quantize()
.range(d3.range(9));
... and domain (which you can only do once the data has been loaded:
quantize.domain([0, d3.max(d3.values(wealthdata))]);

Circles aren't updating in a multiline graph

I have a D3.js multiline graph with circles added on every path peak. When I update my graph, the paths update just fine with the new data but the circles don't seem to get updated at all. Here's my code: http://jsbin.com/eMuQOHoV/3/edit
Does anyone know what I'm doing wrong?
You need to update the data point circles in the same way that you've created them. In particular, you're using a nested selection when creating them, but not when updating. This means that the data won't get matched correctly on update and nothing happens.
The code for the update should look like this.
var sel = svg.selectAll('.series')
.data(sources);
sel.select('path')
.transition()
// etc
// update circles
sel.selectAll('.datapoint')
.data(function (d) {
return d.values;
})
// etc
Complete jsbin here.

Categories

Resources