I am trying to display a beautiful line graph using D3. The problem I have is with the format of the data.
I have the following data (as an example):
var data = [
{
label: "name",
data: [[14444123, 0.012321312],
[14444123, 0.012321312],
[14444123, 0.012321312], ...]
},{
label: "another name",
data: [[14444123, 0.012321312],
[14444123, 0.012321312],
[14444123, 0.012321312], ...]
}
];
Each entry contains the name of it as well as a data attribute with array of points (each point is represented as an array, where item[0] is x timestamp and item[1] is the value).
My problem is that it is not working correctly.
This is the D3 code I have as of now:
var w = options.width,
h = options.height,
p = options.padding,
x = d3.scale.linear()
.domain([0, 1])
.range([0, w]),
y = d3.scale.linear()
.domain([options.ydomainstart, options.ydomainend])
.range([h, 0]);
var vis = d3.select(options.element)
.data(data)
.append("svg:svg")
.attr("width", w + p * 2)
.attr("height", h + p * 2)
.append("svg:g");
vis.append("svg:line")
.attr("stroke", '#808080')
.attr("x1", p)
.attr("x2", p)
.attr("y1", 0)
.attr("y2", h - p);
vis.append("svg:line")
.attr("stroke", '#808080')
.attr("x1", p)
.attr("x2", w)
.attr("y1", h - p)
.attr("y2", h - p);
var rules = vis.selectAll("g.rule")
.data(data)
.enter()
.append("svg:text")
.attr("x", w - p)
.attr("y", function(d, i) { return 15 + i*12; })
.attr("text-anchor", "end")
.attr("font-size", 12)
.attr("fill", function(d, i) { return defaultColors[i % 5]; })
.text(function(d) { return d.label;});
var lines = rules.data(function(d, i) {
return d.data;
})
.append("svg:path")
.attr("stroke", function(d, i) { return defaultColors[i % 5]; })
.attr("d", d3.svg.line()
.x(function(d) {
return x(d[0]);
})
.y(function(d) {
return y(d[1]);
}));
The problem I have appears in this part of the code:
.x(function(d) {
return x(d[0]);
})
.y(function(d) {
return y(d[1]);
}));
The data inside 'd' is NOT the point array [x, y] but instead each value inside each array.
Meaning, on first item, d contains the x coordinate, on second item, it has the y coordinate, on third item, it contains the x coordinate on next point and so on.
It's like it's recursively going into the array, and then again for each value inside.
I have no idea how to fix this.
There’s a few problems here.
First, you’re appending an svg:path element to an svg:text element. It seems to me like you’re trying to create an svg:g element with the class "rule", but your code defines the selection rules as a set of svg:text elements. Create the svg:g elements first, and then append svg:text elements:
var rules = vis.selectAll("g.rule")
.data(data)
.enter().append("svg:g")
.attr("class", "rule");
rules.append("svg:text")
…
The second problem is that the data operator is evaluated once per group, rather than once per element. See the section "Operating on Selections" in the API reference for more details. You have one svg:svg element in vis, so you have one group in rules, and so your data function is only called once:
function(d, i) {
return d.data;
}
Then, the resulting data elements are mapped to the rules selection… which already have defined data from the previous selectAll and append when they were created.
The simple fix is to use the map operator rather than the data operator, which is evaluated once per element rather than once per group.
rules.append("svg:path")
.map(function(d) { return d.data; })
.attr("d", d3.svg.line()
…
Alternatively, you could pass the data directly to the line generator, but that requires you declaring the line generator ahead of time rather than inlining it:
var line = d3.svg.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
rules.append("svg:path")
.attr("d", function(d) { return line(d.data); })
…
Hope this helped!
Related
I have the following code, where I read a file, and then use d3.map to get the unique user_id values to create column labels of a heat map:
d3.tsv("data_heatmap.tsv", function(d) {
console.log(d.user_id);
return {
col : +d.u_id,
row : +d.d_id,
value : +d.count,
user_id: +d.user_id,
};
var colLabels = svg.append("g").selectAll(".colLabelg")
.data(d3.map(data, function(d) {return d.user_id;}.keys())
.enter().append("text")
.text(function(d) {return d;}
)
}
This code works fine, but I need to use the col variable to change the positions of the .colLabelg elements created. I tried the following but it does not work:
var colLabels = svg.append("g").selectAll(".colLabelg")
.data(d3.map(data, function(d) {return d.user_id;}.keys())
.enter().append("text")
.text(function(d) {return d;}
).data(d3.map(data, function(d){return d.u_id;}).keys())
.enter().attr("x", 0).attr("y", function(d) {
return d * cellSize;
})
But, it gives an error:
What would be the solution to this problem?
For example, I need to calculate a Math.sqrt of my data for each attr, how can I calculate only one time the Math.sqrt(d)?
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
return Math.sqrt(d) + 1
})
.attr("cy", function(d) {
return Math.sqrt(d) + 2
})
.attr("rx", function(d) {
return Math.sqrt(d) + 3
})
.attr("ry", function(d) {
return Math.sqrt(d) + 4
});
Has any elegant/performative mode? I'm thinking this way:
var aux;
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
aux = Math.sqrt(d);
return aux + 1
})
.attr("cy", function(d) {
return aux + 2
})
.attr("rx", function(d) {
return aux + 3
})
.attr("ry", function(d) {
return aux + 4
});
An underestimated feature of D3 is the concept of local variables which were introduced with version 4. These variables allow you to store information on a node (that is the reason why it is called local) independent of the data which might have been bound to that node. You don't have to bloat your data to store additional information.
D3 locals allow you to define local state independent of data.
Probably the major advantage of using local variables over other approaches is the fact that it smoothly fits into the classic D3 approach; there is no need to introduce another loop whereby keeping the code clean.
Using local variables to just store a pre-calculated value is probably the simplest use case one can imagine. On the other hand, it perfectly illustrates what D3's local variables are all about: Store some complex information, which might require heavy lifting to create, locally on a node, and retrieve it for later use further on in your code.
Shamelessly copying over and adapting the code from Gerardo's answer the solution can be implemented like this:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var roots = d3.local(); // This is the instance where our square roots will be stored
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
return roots.set(this, Math.sqrt(d)) * 3; // Calculate and store the square root
})
.attr("cy", function(d) {
return roots.get(this) * 3; // Retrieve the previously stored root
})
.attr("rx", function(d) {
return roots.get(this) + 3; // Retrieve the previously stored root
})
.attr("ry", function(d) {
return roots.get(this) + 4; // Retrieve the previously stored root
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably, the most idiomatic way for doing this in D3 is using selection.each, which:
Invokes the specified function for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]).
So, in your case:
circle.each(function(d){
//calculates the value just once for each datum:
var squareRoot = Math.sqrt(d)
//now use that value in the DOM element, which is 'this':
d3.select(this).attr("cx", squareRoot)
.attr("cy", squareRoot)
//etc...
});
Here is a demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.each(function(d) {
var squareRoot = Math.sqrt(d);
d3.select(this)
.attr("cx", function(d) {
return squareRoot * 3
})
.attr("cy", function(d) {
return squareRoot * 3
})
.attr("rx", function(d) {
return squareRoot + 3
})
.attr("ry", function(d) {
return squareRoot + 4
});
})
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Another common approach in D3 codes is setting a new data property in the first attr method, and retrieving it latter:
.attr("cx", function(d) {
//set a new property here
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
//retrieve it here
return d.squareRoot * 3
})
//etc...
That way you also perform the calculation only once per element.
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100).map(function(d) {
return {
value: d
}
});
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
return d.squareRoot * 3
})
.attr("rx", function(d) {
return d.squareRoot + 3
})
.attr("ry", function(d) {
return d.squareRoot + 4
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: by the way, your solution with var aux will not work. Try it and you'll see.
UPDATE: So, I was able to get the data into my cx/cy data properly (spits out the correct values), but the element gives me an error of NaN.
So I get 3 circle elements with only the radius defined.
Original post:
I have a question about rendering dots in on a d3 line chart. I will try to give every part of the relevant code (and the data structure, which is slightly more complicated).
So, currently, 1 black dot renders in the top left corner of my chart, but nowhere else. It looks like I am getting 3 constants when I console.log the cx and cy return functions. What am I doing wrong?
Cities is currently an array that returns 3 objects.
Each object has the following structure:
Object {
name: 'string',
values: array[objects]
}
values array is as follows:
objects {
Index: number,
Time: date in a particular format
}
Okay. Relevant code time:
var points = svg.selectAll('dot')
.data(cities);
console.log('Points is :', points)
points
.enter().append('circle')
// .data(function(d) {console.log("data d is :", d); return d})
.data(cities)
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
.attr('cy', function(d) {
return y(d.values.forEach(function(c) {
console.log("cy is: ", c.Index)
return c.Index;
}))
})
.attr('r', dotRadius());
points.exit().remove();
// points.attr('class', function(d,i) { return 'point point-' + i });
d3.transition(points)
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
.attr('cy', function(d) {
return y(d.values.forEach(function(c) {
console.log("cy is: ", c.Index)
return c.Index;
}))
})
.attr('r', dotRadius())
You need a nested selection here.
This:
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
is totally invalid. One, attr is expecting a single value to set, you are trying to get it to process an array of values. Two, forEach by design only returns undefined. Just not going to work.
You should be doing something like this:
var g = svg.selectAll(".groupOfPoint") //<-- top level selection
.data(cities)
.enter().append("g")
.attr("class", "groupOfPoint");
g.selectAll(".point") //<-- this is the nested selection
.data(function(d){
return d.values; //<-- we are doing a circle for each value
})
.enter().append("circle")
.attr("class", "point")
.attr('cx', function(d){
return x(d.Time);
})
.attr('cy', function(d){
return y(d.Index);
})
.attr('r', 5)
.style('fill', function(d,i,j){
return color(j);
});
Since you seem to be building off of this example, I've modified it here to be a scatter plot instead of line.
i want to add a multidimensional array of lines to my SVG with the D3 library. But somehow the lines don´t show up. There is no error message from javascript so i guess i can not be totally wrong but something is missing. I tried to use the description of Mike Bostock as an example http://bost.ocks.org/mike/nest/
I have an array of lines that looks like that:
var datasetPolylines = [[[-10849.0, 1142.0, -10720.0, 454.0],[x1, y1, x2, y2],[x1, y1, x2, y2]...],[polyline],[polyline]...];
For every Line there are 4 points in the array for x and y values of the line.
Now i try to add them to my mainsvg like that:
d3.select("#mainsvg").selectAll("g")
.data(datasetPolylines)
.enter()
.append("g")
.selectAll("line")
.data(function (d) {return d;})
.enter()
.append("line")
.attr("x1", function(d, i) {
return xScale(i[0]);
})
.attr("y1", function(d, i) {
return yScale(i[1]);
})
.attr("x2", function(d, i) {
return xScale(i[2]);
})
.attr("y2", function(d, i) {
return yScale(i[3]);
})
.attr("stroke-width", 2)
.attr("stroke", "blue")
.attr("fill", "none");
I´m very thankful for every hint on where I´m wrong. I´m on this stuff now for some days and just don´t get it :(
Everything works fine if i just draw one polyline with many lines and use the .data attribute just once. I cannot merge the lines to one path also, because they are not always connected and must be drawn seperately.
The complete (and now thanks to Christopher and Lars also working) code example looks like that:
var datasetLines = [];
var datasetPolylines = [];
/**Add a line to the dataset for lines*/
function addLineToDataset(x1, y1, x2, y2){
var newNumber1 = x1;
var newNumber2 = y1;
var newNumber3 = x2;
var newNumber4 = y2;
datasetLines.push([newNumber1, newNumber2, newNumber3, newNumber4]);
}
/**Add polyline to the dataset for polylines*/
function addPolyline(){
var polyline = [];
for (i in datasetLines) {
polyline[i] = datasetLines[i];
}
datasetPolylines.push(polyline);
}
/**Draw all polylines from the polylinearray to the svg*/
function showPolylineArray(){
//Create scale functions for x and y axis
var xScale = d3.scale.linear()
.domain([d3.min(outputRange, function(d) { return d[0]; }), d3.max(outputRange, function(d) { return d[0]; })])//get minimum and maximum of the first entry of the pointarray
.range([padding, w - padding]); //w, the SVGs width. without padding 0,w
var yScale = d3.scale.linear()
.domain([d3.min(outputRange, function(d) { return d[1]; }), d3.max(outputRange, function(d) { return d[1]; })])
.range([h - padding, padding]); //without padding h,0
d3.select("#mainsvg").selectAll("g")
.data(datasetPolylines)
.enter()
.append("g")
.selectAll("line")
.data(function (d) {return d;})
.enter()
.append("line")
.attr("x1", function(d) {
return xScale(d[0]);
})
.attr("y1", function(d) {
return yScale(d[1]);
})
.attr("x2", function(d) {
return xScale(d[2]);
})
.attr("y2", function(d) {
return yScale(d[3]);
})
.attr("stroke-width", 2)
.attr("stroke", "blue")
.attr("fill", "none")
.on('mouseover', function(d){ d3.select(this).style({stroke: 'red'}); })
.on('mouseout', function(d){ d3.select(this).style({stroke: 'blue'}); })
.append("title")
.text("Polyline");
}
Your code is almost working, except for the referencing of the data (as pointed out by Christopher). You didn't show us the complete code, but the bit you have should work fine with these modifications.
Complete example here.
How do I make my line x-axis based on date in d3.js?
I am attempting to teach myself how to use d3.js. I've been looking at the examples that come with it and have been attempting to recreate the line graph using json delivered data. I'm able to feed the data into the line graph, but the x-axis is supposed to be a date instead of a number. The date format that I'm using is MM/DD/YY, but the graph plots everything at 0. My json data is coming across fine, but I'm having trouble figuring out how to plot the x coordinates. This was taken straight from the line.js that comes in the d3.js examples folder when downloaded. The date portion doesn't do the trick. I'm hoping someone can point me to an example or be able to explain how I can make it work.
d3.json('jsonChartData.action',
function (data) {
console.log(data);
var w = 450,
h = 275,
p = 30,
x = d3.scale.linear().domain([0, 100]).range([0, w]),
y = d3.scale.linear().domain([0, 100]).range([h, 0]);
var vis = d3.select("body")
.data([data])
.append("svg:svg")
.attr("width", w + p * 2)
.attr("height", h + p * 2)
.append("svg:g")
.attr("transform", "translate(" + p + "," + p + ")");
var rules = vis.selectAll("g.rule")
.data(x.ticks(5))
.enter().append("svg:g")
.attr("class", "rule");
rules.append("svg:line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2", h - 1);
rules.append("svg:line")
.attr("class", function(d) { return d ? null : "axis"; })
.attr("y1", y)
.attr("y2", y)
.attr("x1", 0)
.attr("x2", w + 1);
rules.append("svg:text")
.attr("x", x)
.attr("y", h + 3)
.attr("dy", ".71em")
.attr("text-anchor", "middle")
.text(x.tickFormat(10));
rules.append("svg:text")
.attr("y", y)
.attr("x", -3)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(y.tickFormat(10));
vis.append("svg:path")
.attr("class", "line")
.attr("d", d3.svg.line()
.x(function(d) { return x(d3.time.days(new Date(d.jsonDate))); })
.y(function(d) { return y(d.jsonHitCount); }));
vis.selectAll("circle.line")
.data(data)
.enter().append("svg:circle")
.attr("class", "line")
.attr("cx", function(d) { return x(d3.time.days(new Date(d.jsonDate))); })
.attr("cy", function(d) { return y(d.jsonHitCount); })
.attr("r", 3.5);
});
JSON as printed out by my action:
[{"jsonDate":"09\/22\/11","jsonHitCount":2,"seriesKey":"Website Usage"},`{"jsonDate":"09\/26\/11","jsonHitCount":9,"seriesKey":"Website Usage"},{"jsonDate":"09\/27\/11","jsonHitCount":9,"seriesKey":"Website Usage"},{"jsonDate":"09\/29\/11","jsonHitCount":26,"seriesKey":"Website Usage"},{"jsonDate":"09\/30\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/03\/11","jsonHitCount":3,"seriesKey":"Website Usage"},{"jsonDate":"10\/06\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/11\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/12\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/13\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"10\/14\/11","jsonHitCount":5,"seriesKey":"Website Usage"},{"jsonDate":"10\/17\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/18\/11","jsonHitCount":6,"seriesKey":"Website Usage"},{"jsonDate":"10\/19\/11","jsonHitCount":8,"seriesKey":"Website Usage"},{"jsonDate":"10\/20\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"10\/21\/11","jsonHitCount":4,"seriesKey":"Website Usage"},{"jsonDate":"10\/24\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"10\/25\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"10\/27\/11","jsonHitCount":3,"seriesKey":"Website Usage"},{"jsonDate":"11\/01\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"11\/02\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"11\/03\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"11\/04\/11","jsonHitCount":37,"seriesKey":"Website Usage"},{"jsonDate":"11\/08\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"11\/10\/11","jsonHitCount":39,"seriesKey":"Website Usage"},{"jsonDate":"11\/11\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"11\/14\/11","jsonHitCount":15,"seriesKey":"Website Usage"},{"jsonDate":"11\/15\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"11\/16\/11","jsonHitCount":5,"seriesKey":"Website Usage"},{"jsonDate":"11\/17\/11","jsonHitCount":4,"seriesKey":"Website Usage"},{"jsonDate":"11\/21\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"11\/22\/11","jsonHitCount":3,"seriesKey":"Website Usage"},{"jsonDate":"11\/23\/11","jsonHitCount":11,"seriesKey":"Website Usage"},{"jsonDate":"11\/24\/11","jsonHitCount":2,"seriesKey":"Website Usage"},{"jsonDate":"11\/25\/11","jsonHitCount":1,"seriesKey":"Website Usage"},{"jsonDate":"11\/28\/11","jsonHitCount":10,"seriesKey":"Website Usage"},{"jsonDate":"11\/29\/11","jsonHitCount":3,"seriesKey":"Website Usage"}]`
You're trying to use d3.scale.linear() for dates, and that won't work. You need to use d3.time.scale() instead (docs):
// helper function
function getDate(d) {
return new Date(d.jsonDate);
}
// get max and min dates - this assumes data is sorted
var minDate = getDate(data[0]),
maxDate = getDate(data[data.length-1]);
var x = d3.time.scale().domain([minDate, maxDate]).range([0, w]);
Then you don't need to deal with the time interval functions, you can just pass x a date:
.attr("d", d3.svg.line()
.x(function(d) { return x(getDate(d)) })
.y(function(d) { return y(d.jsonHitCount) })
);
Working fiddle here: http://jsfiddle.net/nrabinowitz/JTrnC/