Object Consistency when updating shape attributes - javascript

I'm very new to D3 and Javascript, so forgive me if my code looks a little ugly or poorly organized.
I have been working on a plot that utilizes 3 metrics: an x and y axis, and the radius of the circle as a data metric for the plot. The data I am reading is a two dimensional array, with each row being a different metric, and each column being a new data point. I have successfully implemented a method to change the radius of the circle dynamically by picking a different metric from a drop box, but this was after struggling endlessly with a very particular issue - my data was being assigned to the wrong circle!
When I initially create my circles, I first use sort() to sort the circles in descending order from the default radius metric (in my code, its "impactcpu"). This was done to fix an issue where larger circles that were drawn after smaller circles were obstructing the smaller circles, so I wanted to "paint" the largest circles first.
I was able to get past this issue by first sorting my calculated data array before assignign it to the circles, which preserved the default order. However, I am now trying to do something similar with my X and Y axis. While my dropdown menu is correctly assigning metric values to circles, it is doing so to the WRONG circles. I have yet to figure out a solution to this issue, as re-sorting the array before assignign is like I was doing for the radius isn't working (which I expected). Are there any suggestions as to how I could ensure the right data point is assigned to the correct circle? Preferably one that wouldn't require an overhaul of the rest of my code :)
Please take a look at my jsfiddle for an example of my above situation:
http://jsfiddle.net/kingernest/YDQR4/3/
Example of how I am creating my circles initially:
var circles = svg.selectAll("circle")
.data(dataset, function(d) { return d.id })
.enter()
.append("circle")
.sort(function(a, b){ //Sort by radius size, helps reduce obstruction of circles
return d3.descending(a[14], b[14]);
})
.attr("cx", function(d){ //x axis is Req IO, col index 9
return xScale(d[9]);
})
.attr("cy", function(d){ //y axis is Req CPU, col index 8
return yScale(d[8]);
})
.attr("r", function(d){ //radius is based off Impact CPU, col 14
console.log("Rad: " + d[14])
return d[14] * 1.5;
})
.attr("class", "dataCircle")
etc
How I am currently altering my radius:
function changeRad() {
console.log(this.value);
var myRadData = [];
var index = metricHash[this.value];
var weight; //to adjust data to fit appropriately in graph
switch(this.value)
{
case "impactcpu":
weight = 1.5;
break;
case "spool":
weight = .0000001; //spool is normally a very large value
break;
case "pji":
weight = 8;
break;
case "unnecio":
weight = 12;
break;
case "duration":
weight = .0002;
break;
default: alert("Invalid value: " + this.value);
break;
}
for(var i=0; i < dataset.length; i++)
{
console.log(dataset[i][index]);
myRadData.push(dataset[i][index] * weight);
}
myRadData.sort(function(a,b){return b-a});
d3.selectAll("circle")
.data(myRadData)
.transition().duration(500)
.attr("r", function(d){
return d;
});
circles.data(dataset); //reassign old data set (with all data values)
}

There are two things I see with your code:
Inconsistent key function on your data binding - you correctly use it on your initial creation of the circles(.data(dataset, function(d) { return d.id })), but do not reference it when updating them, adding the same key on the updates will make sure that you are updating the same elements.
DOM sorting - Your use of selection.sort when initially creating your circles seems logical and appropriate(.sort(function(a, b){ return d3.descending(a[14], b[14]); })) I would recommending extending this to your update functions, rather than re-binding data.
I have made these quick updates to your code and it appears to solve your issues:
http://jsfiddle.net/AbHfk/3/

I'm not sure I grasp your entire code (it's quite long), but I think the solution lies along these lines:
1 - When you initially create the circles, use a keys function. Good, you're doing this:
.data(dataset, function(d) { return d.id; })
2 - Give the circles an ID attribute using the same function:
.attr("ID", function(d) { return d.id; })
3 - Then when you need to modify a particular circle individually you can select it like so:
svg.select('#' + myCircleID).attr('blahblah', somevalue)
I also notice that you've lost the ID attribute as you build up the myRadData array. This will prevent the code from joining them to the correct circles. Since you have an ID attribute at the beginning, you're better off using the keys function throughout, rather than trying to use sorting to make things line up.
If you want a more specific answer I think you need to boil the example down to the simplest possible form that reproduces the issue.

Related

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

Add points (x,y) to a d3 multi-line graph with time scale x-axis

I'm currently attempting to build a a multi-line graph with a d3.time.scale() for the x-axis.
I'm trying to add circles to each point on lines of the graph, but have been unsuccessful thus far.
When I do something like:
.attr('cx', function(d){ return x(d.price) })
I get a negative number.
I was thinking of setting up another scale (pointsScale) to handle this but have been largely unsuccessful.
What am I doing wrong?
Please refer to my JSBin for the code.
You're running into a few issues here:
Since you made the x-axis a time-scale, I'm guessing that you actually want price to be the y variable, while date is the x variable. That's why x(d.price) is negative - d3 is trying to interpret the prices as dates, which doesn't end up making much sense. So replace your line of code above with this: .attr('cy', function(d){ return y(d.price) })
In order to actually have circles be visible, they need to have three parameters set: cx, cy, and r. Since d3 already knows that your x axis is a time scale, you can set cx with .attr('cx', function(d){ return x(d.date) }). You can make r be whatever radius you want for the circles. Just choose one, or it will default to 0 and you won't be able to see the circles. .attr('r', 4), for instance, would set the radius to a perfectly visible value of 4.
You're drawing the circles before you draw the lines. As a result, the lines get drawn over the circles and it looks kind of weird. So move the circle code to after the line code if you want to avoid that.
Putting it all together, this is roughly what the code to create your circles should look like, and it should go after you declare var paths:
var circles = company.selectAll('circle')
.data(function(d){ return d.values; })
.enter().append('circle')
.attr('cy', function(d){
return y(d.price);}) //Price is the y variable, not the x
.attr('cx', function(d){
return x(d.date);}) //You also need an x variable
.attr('r',4); //And a radius - otherwise your circles have
//radius 0 and you can't see them!
Updated jsbin:
http://jsbin.com/gorukojoxu/edit?html,console,output

Drawing multiple sets of multiple lines on a d3.js chart

I have a d3 chart that displays two lines showing a country's imports and exports over time. It works fine, and uses the modular style described in 'Developing a D3.js Edge' so that I could quite easily draw multiple charts on the same page.
However, I now want to pass in data for two countries and draw imports and exports lines for both of them. After a day of experimentation, and getting closer to making it work, I can't figure out how to do this with what I have. I've successfully drawn multi-line charts with d3 before, but can't see how to get there from here.
You can view what I have here: http://bl.ocks.org/philgyford/af4933f298301df47854 (or the gist)
I realise there's a lot of code. I've marked with "Hello" the point in script.js where the lines are drawn. I can't work out how to draw those lines once for each country, as opposed to just for the first one, which is what it's doing now.
I'm guessing that where I'm applying data() isn't correct for this usage, but I'm stumped.
UPDATE: I've put a simpler version on jsfiddle: http://jsfiddle.net/philgyford/RCgaL/
The key to achieving what you want are nested selections. You first bind the entire data to the SVG element, then add a group for each group in the data (each country), and finally get the values for each line from the data bound to the group. In code, it looks like this (I've simplified the real code here):
var svg = d3.select(this)
.selectAll('svg')
.data([data]);
var g = svg.enter().append('svg').append('g');
var inner = g.selectAll("g.lines").data(function(d) { return d; });
inner.enter().append("g").attr("class", "lines");
inner.selectAll("path.line.imports").data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports')
.attr("d", function(d) { return imports_line(d); });
The structure generated by this looks like svg > g > g.lines > path.line.imports. I've omitted the code for the export line here -- that would be below g.lines as well. Your data consists of a list of key-value pairs with a list as value. This is mirrored by the SVG structure -- each g.lines corresponds to a key-value pair and each path to the value list.
Complete demo here.
The point is that you're thinking to imperative. That's why you have so much code. I really can't put it better than Mike Bostock, you have to start Thinking with Joins:
svg.append("circle")
.attr("cx", d.x)
.attr("cy", d.y)
.attr("r", 2.5);
But that’s just a single circle, and you want many circles: one for each data point. Before you bust out a for loop and brute-force it, consider this mystifying sequence from one of D3’s examples.
Here data is an array of JSON objects with x and y properties, such as: [{"x": 1.0, "y": 1.1}, {"x": 2.0, "y": 2.5}, …].
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 2.5);
I'll leave translating this example to the "from one line to many lines" as an excerxise.

How can I fix this bar chart sorting?

Here is a JSFiddle of the issue I'm currently facing.
Basically, I have an array of objects that I use to draw some bars. After sorting the array, I try to update the bars' y positions accordingly but that does not work.
So, this is the scale I use to draw the bars:
var yScale = d3.scale.ordinal()
.domain(d3.range(0, numberOfBars))
.rangeBands([0, numberOfBars * barHeight]);
So if I have 3 bars and a bar is 40px high, then I'm mapping 0-3 => 0-120px.
Next, I have a function that uses this scale to return the right y position:
var y = function(d, i) {
return yScale(i);
};
After drawing the bars using this y function, I then sort the data array and try to redraw the bars:
barsContainer.selectAll('.bar')
.data(chartData.users)
.transition()
.duration(750)
.delay(delay)
.attr('y', y); // Not working. I thought this would order the bars.
//.attr('y', 120); // This works though. It moves all the bars to this y.
This is where I'm stumped. Since I reordered the array (chartData.users), and since the bars are "joined" with the data, shouldn't the bars change their y according to the data's new position in the array?
So I figured out what the problem was. Since I was sorting an array of objects, D3.js couldn't figure out by itself which array objects matched which DOM objects. So I had to create a key function to identify the objects:
var key = function(d) { return d.id }
Then, all I had to do was call data() using this key function and then order(), so the DOM order matches the array order:
barsContainer.selectAll('.bar')
.data(chartData.users, key)
.order()
.transition()
.delay(delay)
.duration(750)
.attr('y', y);
Here's the working JSFiddle.
Your Fiddle is very long. I'd recommend paring it back to the bare minimum and seeking help with that specific point.
For example, try testing
console.log(chartData.users)
immediately after you've declared chartData. It appears to be identical to the sorted one, so it might not be the sorting that's at fault. If you do some more work yourself and then ask a more specific, concise question, you'll probably get more answers.
Good luck with D3!

Categories

Resources