D3 Line Generator Handling Multiple Arrays - json

I have 3 arrays within a JSON file of the following structure:
I am trying to parse a JSON file of coordinate data into something that can be read by D3 path/line generators. I first had to find a way to ensure the values were actual numbers and not strings. A full discussion can be found here:
D3/JS mapping a JSON data callback
That discussion prompted me to not only consider formatting the JSON data via .map() to numbers, but also consider a nested/zipped format that the line generator can make sense of. That is really the target I've been after all along. As depicted above, My JSON has 3 arrays, xs, ys and id. id only governs color, and takes 1 of 3 values (0,1,2). I was recommended this approach:
var obj = raw_json[0];
var data = obj.id.map((id, i) => [+id, +obj.xs[i], +obj.ys[i]]);
My line generator function is:
var valueLine = d3.svg.line()
.x(function(d) {return xScale(d.xs);})
.y(function(d) {return yScale(d.ys)});
However I am getting tens of thousands of errors, and unfortunately I do not have that much experience with parsing related issues and I am not sure how to proceed.
Full block & JSON here.

Glad to see you took my advice on restructuring your data as we are moving in the right direction. I suggested that you should convert your three separate arrays into one array of individual arrays per point to make the use of the line generator more easy and to eliminate the need for cross-array reads to collect data for the points.
This time, though, you are not accessing your values correctly. By using function(d) { return xScale(d.xs); } you are assuming that your points were represented by objects having properties xs and ys. My suggested approach however got rid of these properties by storing the information into arrays. There are basically two ways around this:
Adjust you path generator's .x() and .y() callbacks while keeping your data structure.
var obj = raw_json[0];
var data = obj.id.map((id, i) => [+id, +obj.xs[i], +obj.ys[i]]);
// Remember, that d is a point's array [id, x, y]
var valueLine = d3.svg.line()
.x(function(d) { return xScale(d[1]); })
.y(function(d) { return yScale(d[2]); });
If you prefer to store your points' data in objects instead, another solution would be to adjust how your data is built.
// Structure data into an array of objects {id, x, y}
var obj = raw_json[0];
var data = obj.id.map((id, i) => ({
id: +id,
x: +obj.xs[i],
y: +obj.ys[i]
}));
// Keep your code as d now has properties x and y.
var valueLine = d3.svg.line()
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); });

I think you aren't modeling correctly the data to use to use with data(data).enter() and/or d3.svg.line(). As you defined your valueLine function it expects an array of objects with xs and ys properties and you are feeding it with an array of arrays.
Try it out manually to see how it works
valueLine([{xs: 1, ys: 1}, {xs: 3, ys: 5}, {xs: 12, ys: 21}])
and see the generated svg path:
"M100,400L300,-400L1200,-3600"
So maybe you can change how you are preparing your data for something like :
var data = obj.id.map((id,i) => ({id: +id, xs:+obj.xs[i], ys: +obj.ys[i]}));

Related

Multiple Variable Assignment in Javascript/GAS - Is this the most compact way to do it?

Ok so I have a spreadsheet which we extract a 2d array of values from.
But really I want one variable per line of this 2d array.
The following code does work... but is this the best way to do it?
function testAssignments(){
config = ss.getRange("C2:C6").getValues();//2D Array
result = []
config.forEach(x => result.push(x[0]))
var [a,b,c,d,e] = result;
console.log(a,b,c,d,e);
}
I also tried the line config.forEach(x=> x=x[0]) but that didn't work for some reason.
Use .flat instead of .forEach and .push. If you want a different variable name for each element, there isn't a better way.
const [a,b,c,d,e] = ss.getRange("C2:C6").getValues().flat();//1D Array
//or
const [[a],[b],[c],[d],[e]] = ss.getRange("C2:C6").getValues();

How to add legend for a bar chart with different colors in dc.js?

Below is the code snippet for a barchart with colored bars:
var Dim2 = ndx.dimension(function(d){return [d.SNo, d.something ]});
var Group2 = Dim2.group().reduceSum(function(d){ return d.someId; });
var someColors = d3.scale.ordinal().domain(["a1","a2","a3","a4","a5","a6","a7","a8"])
.range(["#2980B9","#00FFFF","#008000","#FFC300","#FF5733","#D1AEF1","#C0C0C0","#000000"]);
barChart2
.height(250)
.width(1000)
.brushOn(false)
.mouseZoomable(true)
.x(d3.scale.linear().domain([600,800]))
.elasticY(false)
.dimension(Dim2)
.group(Group2)
.keyAccessor(function(d){ return d.key[0]; })
.valueAccessor(function(d){return d.value; })
.colors(someColors)
.colorAccessor(function(d){return d.key[1]; });
How do I add a legend to this chart?
Using composite keys in crossfilter is really tricky, and I don't recommend it unless you really need it.
Crossfilter only understands scalars, so even though you can produce dimension and group keys which are arrays, and retrieve them correctly, crossfilter is going to coerce those arrays to strings, and that can cause trouble.
Here, what is happening is that Group2.all() iterates over your data in string order, so you get keys in the order
[1, "a1"], [10, "a3"], [11, "a4"], [12, "a5"], [2, "a3"], ...
Without changing the shape of your data, one way around this is to sort the data in your legendables function:
barChart2.legendables = function() {
return Group2.all().sort((a,b) => a.key[0] - b.key[0])
.map(function(kv) {
return {
chart: barChart2,
name: kv.key[1],
color: barChart2.colors()(kv.key[1]) }; }) };
An unrelated problem is that dc.js takes the X domain very literally, so even though [1,12] contains all the values, the last bar was not shown because the right side ends right at 12 and the bar is drawn between 12 and 13.
So:
.x(d3.scale.linear().domain([1,13]))
Now the legend matches the data!
Fork of your fiddle (also with dc.css).
EDIT: Of course, you want the legend items unique, too. You can define uniq like this:
function uniq(a, kf) {
var seen = [];
return a.filter(x => seen[kf(x)] ? false : (seen[kf(x)] = true));
}
Adding a step to legendables:
barChart2.legendables = function() {
var vals = uniq(Group2.all(), kv => kv.key[1]),
sorted = vals.sort((a,b) => a.key[1] > b.key[1] ? 1 : -1);
// or in X order: sorted = vals.sort((a,b) => a.key[0] - b.key[0]);
return sorted.map(function(kv) {
return {
chart: barChart2,
name: kv.key[1],
color: barChart2.colors()(kv.key[1]) }; }) };
Note that we're sorting by the string value of d.something which lands in key[1]. As shown in the comment, sorting by x order (d.SNo, key[0]) is possible too. I wouldn't recommend sorting by y since that's a reduceSum.
Result, sorted and uniq'd:
New fiddle.

D3 reusable multi-line chart with JSON data

I'm trying to do some re-factoring on my charts to make them re-usable using this as a guide: http://bost.ocks.org/mike/chart/
I'm having problems drawing the lines in my multi-line graph though - specifically passing the data to the x and y values. If I hard code the element names it works, but if I try to use the xValue and yValue objects this does not work. I'm assuming that this is because I'm trying to call a function within the parameter of an other object, but I'm not sure how to get around this. In the exmaple Mike uses d[0] and d[1], but this won't work with JSON data (or I'm not sure how to make it work).
I've posted this JSFiddle so you can see the code. The problem lines are 125 to 131 which in turn is being called from line 165.
var main_line = d3.svg.line()
.interpolate("cardinal")
// Hard coding the elements works
//.x(function(d) { return main_x(d.date); })
//.y(function(d) { return main_y(d.buildFixTime); });
// Passing xValue and yValue does not work
.x(function(d) { return main_x(xValue); })
.y(function(d) { return main_y(yValue); });
http://jsfiddle.net/goodspeedj/fDyLY/
Thank you in advance.
You need to redefine your accessor method within .x() and .y(). The accessor method defines the way that a datum is pulled out of the data that is bound to the selection that you call the line generator on.
Suppose you have a relatively flat data structure such as the following.
data = [{x : 1, y : 2}, {x:1, y:3}, {x:4, y:5}];
You then bind the data to a selection with the following statement
d3.select("body").datum(data).append("path").attr("d",lineGenerator);
Quite a bit is going on underneath this statement. I'll give you a bit more of a walkthrough after showing you a commonly used example.
The important aspect to understand is that similarly to other calls in d3 such as
var exampleRectangles = d3.select("body")
.data(data).enter()
.append("rect")
.attr("width",2)
.attr("height", 3)
.attr("x",function(datum){return datum.x}) // pay attention to this line
.attr("y",0);
d3 is implicitly iterating over each element in your data. For each datum in your data array, in this case there is a total of three datum, you are going to add a rectangle to the dom.
In the line that I tell you to pay attention to you notice that you're defining an anonymous (unnamed) function. What is that datum parameter coming from? It's implicitly being passed to your anonymous function.
So each rectangle has it's own corresponding datum {x : 1, y : 2}, {x:1, y:3}, {x:4, y:5} respectively. Each rectangle's x coordinate is defined by the respective datum.x attribute. Under the sheets, d3 is implicitly looping over the data array that you've defined. A similar approach to the example d3 code could be written as above.
for (var i = 0; i < data.length; i++)
{
d3.select("body").append("rect")
.attr("width",2)
.attr("height", 3)
.attr("x",data[i].x)
.attr("y",0);
}
This follows from the notion of data driven documents (d3). For each item added (a rectangle in the above example a piece of data is tied to it. In the above example you see that there is something kind of similar to your .x() and .y() accessor functions :
.attr("x",function(datum){return datum.x})
This function is telling d3 how to filter over the total datum that's being passed to the .attr() accessor method.
So, you need to determine which data you need to get a hold of to make your .attr("d", lineGenerator)call make sense. The difference between your.datum(data)call and the typical.data(data)call is that instead of parceling the data that's being passed to.data(data)`, the whole array is given as a single piece of data to the line generator function (similar to main_line(data), wherein it will again implicitly loop over the points to construct your path.
So, what you need to do is determine what a single datum will be defined as for your function to operate on.
I'm not going to define that as I don't seem to know quite which information you are operating on, but I would hazard a guess at something like.
.x(xAccessor)
.y(yAccessor)
function xAccessor(datum)
{
return xScale(datum._id.month);
}
function yAccessor(datum)
{
return yScale(datum.buildFixTime);
}
The way you have it set up, xValue and yValue are functions; you have to actually execute them on something to get a value back.
.x(function(d) { return main_x( xValue(d) ); })
.y(function(d) { return main_y( yValue(d) ); });
If you weren't using a scale, you could use
.x(xValue)
.y(yValue);
but only because if you pass in a function d3 executes it for you with the data as a parameter. And that only works for d3 methods that expect functions as possible input -- the scale functions expect data values as input.
I wrote a long piece work for another user last week that you may find useful, explaining methods that accept functions as parameters.

merging old data with new data

I want the old nodes in the force layout to keep their position/momentum/fixedness, when i update the graph with new nodes.
The update adds and removes nodes and links, but the server only sends {name: _}, and doesn't know about the rest, i.e. {x: _, y: _, px: _, py: _, fixed: _}.
Now, I'm manually merging each:
var names = {};
// stores old nodes, indexed by _.name
function merge(nodes) {
for (var i=0; i < nodes.length; i++) {
var next = nodes[i];
var prev = names[nodes[i].name]; // defaults to null
var node = $.extend(true, {}, next, prev); // merge old into new
nodes[i] = node;
names[node.name] = node;
}
return nodes;
}
d3.json(function(error, graph){
merge(graph.nodes)
force.nodes(graph.nodes);
...
}
Is there a better way to do this? Maybe some d3 function that takes the two data (i.e. the old and the new) and outputs one (to be used as the new), that's called by the data join? selection.data() only takes a key.
This example replaces the nodes, rather than merging them.
Related: Problems adding and removing nodes in a force-layout using d3.js
D3 doesn't provide any functionality to do what you're looking for, but you've already implemented merge, which does the same thing. Basically, you need to modify the data structures that you gave to force.links() and force.nodes(). There is no need to tell the force layout to use them explicitly after making the changes.

error in reading csv file to plot graph

i am just not able to read a csv file .I want to display a graph for it.
I am getting error:
TypeError: n is undefined
please help me out!!
d3.csv("example.csv", function(dataset){
var svg=d3.select("body").append("svg").attr("width",w).attr("height",h);
var xScale=d3.scale.ordinal().domain(d3.range(dataset.length)).rangeRoundBands([0,w],0.05);
var yScale=d3.scale.linear().domain([0,d3.max(dataset.value)]).range([0,h]);
svg.selectAll("rect").data(dataset).enter().append("rect").attr({x:function(d,i) {return xScale(i);}, y:function(d){
return h-yScale(d);}, width:xScale.rangeBand(),height:function(d){return yScale(d);},fill:function(d){return "rgb(0,0,"+(d.value*10)+")";}});
d3.select("svg").selectAll("text").data(dataset).enter().append("text").text(function(d) {return d.value;}).attr("x",function(d,i){
return xScale(i)+xScale.rangeBand()/2;}).attr("y",function(d){return h-yScale(d)+14;}).attr("font-family","sans-serif").attr("font-size","10px").attr
("fill","white").attr("text-anchor","middle");
d3.select("p").on("click",function(){
var numValues=dataset.length;
dataset=[];
for(var i=0;i<numValues;i++)
{var newNumber=Math.floor(Math.random()*25);
dataset.push(newNumber,newNumber);
}
yScale.domain([0,d3.max(dataset)]);
svg.selectAll("rect").data(dataset).transition().delay(function(d,i){return i/dataset.length*1000;})
.duration(500).attr("y",function(d){return h-yScale(d); }).attr("height",function(d) {return yScale(d);}).attr("fill",function(d){
return "rgb(0,0,"+(d.value*10)+")";});
svg.selectAll("text").data(dataset).transition().delay(function(d,i){return i/dataset.length*1000;}).duration(500).text(function(d){return d.value;})
.attr("x",function(d,i){return xScale(i)+xScale.rangeBand()/2;}).attr("y",function(d) {return h-yScale(d)+14;})
.attr("font-family","sans-serif").attr("font- size","10px").attr("fill","white").attr("text-anchor","middle")
;});
});
here is my csv file
names,value
john,78
brad,105
amber,103
james,2
dean,74
pat,45
matt,6
andrew,18
ashley,15
As mentioned by cuckovic there are quite a few errors in your code, not to mention that you seem to be using different styles to achieve similar things which is confusing. Anyway there are 3 fundamental things that are casuing you trouble, the first is your dataset. The value column of the csv is being read as a string. You need to convert it to a number by:
dataset.forEach(function (d,i) {
d.value = +d.value;
});
The next issue you have is the yScale where you have set the range to .range([0,h]);. This is the wrong way round for an svg viewport in which the y direction starts from top and increases towards the bottom. So you need to swap the 0 and h around in the range.
The next thing to address is the difference between d and d.value. When you bind data through the data() operator you are generally binding an array. In this case it is an array of objects. So after you have bound the data, d refers to each element of that array which in this case is an object containing a name and a value. This can be seen if you console.log your dataset. When d is passed to yScale it doesn't know what to do with it as it is not a number, what you really want to do is to pass d.value to yScale. So replace your d's with d.value.
Finally, the last part of your code starting at d3.select("p") does not seem to add anything.
I'd recommend reading Scott Murray's tutorials, particularly this one if you haven't already.