I'm rendering a line chart via d3, which is bound to an array of objects of the following format:
{ name: "somename",
pointStart: 90210,
pointInterval: 187,
data: [1,2,3,4,5]
}
The Y values are the values in data, and the X values is a sequence of Date values, calculated by adding pointStart to the product of pointInterval and the index of data
In addition to plotting the line path, I'm trying to overlay "circles" at each x,y coordinate. The line renders properly, and all but the first circle shows up.
Check this plunkr for the live example.
Since the line path already has the x,y coordinates, I was hoping to use that, and draw the circle on each pair, however the first circle coordinate isn't found and I'm not sure why.
This is the code that selects the line array, gets the x,y pairs, and then draws the circle.
The data is bound to a 9 element array, but only 8 circles are added to the dom...
lines.selectAll('path')
.data(function(d) { console.log(getDataArray(d)); return getDataArray(d); })
.enter()
.append('circle')
.attr({
class:'dot',
cx:line.x(),
cy:line.y(),
r:circleR,
fill: function(d,i,e) { return colors(data[e].name);}
})
It's because you're selection for "path" but adding "circles". When you do lines.selectAll('path') it returns a selection that contains 1 element because there is already a <path> element under lines. So when you do the data bind with 9 elements, the first element get bound to the existing path leaving the remaining 8 elements for the enter selection.
If you change it to this it should work:
lines.selectAll('circle')
.data(function(d) { console.log(getDataArray(d)); return getDataArray(d); })
.enter()
.append('circle')
Related
I am working on a d3 plot, where I have multiple elements which might overlap when drawn.
Each element renders a timeline and has multiple graphical units (start circle, line and end circle), something like as below:
O----------O O
O--------------------O
O-------O-----O-------O
For example the third line has two timeline plot elements which are overlapping as start time of the 2nd timeline is before end time of the first timeline. Note that 2nd timeline in the first line has only start time (as end time and start time are same).
Now, the following code brings an element of the timeline to front on mouseover by moving the DOM node to be the last child of its parent.
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
But the problem is that this is not altering the order of the bound data and is breaking the overall plot.
Each of the plot element has specific order in the dom which is bound to the d3 data in the same order. When the code above changes the order to bring any element to the front it is breaking the order, it still thinks that the order of the children are the same, which is wrong.
Here is a sample JSFiddle to describe the issue:
https://jsfiddle.net/pixelord/g2gt1f03/57/
How can I retain the data order once I have altered the dom elements?
Thanks.
Instead of doing the html update by yourself let d3 do it, remember that d3 stands for data driven documents so rewrite your problem as
On mouseover move the selection's datum to the last position and then rerender the graph
Imagine that your data is [0,1,2,3], when you mouseover on any element that represents the second datum you move the second datum to the last position i.e. [0,2,3,1] and that's pretty much it
.on("mouseover", function() {
var selection = d3.select(this);
var d = selection.datum()
// find d in data, extract it and push it
var index = data.indexOf(d)
var extract = data.splice(index, 1)
data = data.concat(extract)
draw()
});
Next when you bind your data make sure you add a way to distinguish from both states which is done with the second parameter sent to the .data() function which might be an id
var data = [
[5, 8, 6],
[10, 10, 6],
[20, 25, 6],
[23, 27, 6]
].map(function (d, i) {
return {
id: i,
x1: d[0],
y1: d[2],
x2: d[1],
y2: d[2]
}
});
// ...
var itemGroup = maingroup.selectAll(".itemGroup")
.data(data, function (d) { return d.id })
Finally you'll need to tell d3 that we have modified the order of the elements and that it needs to do what you were doing by hand which is reorder the elements
// update
itemGroup.order()
Demo
I like the way Mauricio solved the issue.
However, after some investigation I came to know that I can specify key value while binding the data. So here is my solution without re-ordering the data itself:
I added a key value property for each data object which is unique.
I specify the key value while binding the data like,
data(data_list, funcion(d){return d.keyValue})
the problem was fixed.
I'm trying to only display the line intersections within graticule and not the whole lines within a D3 map.
Is there any support to do that with d3.geo.graticule?
Thanks very much for your help!
Thierry
The short answer to your question is: no, there is no way to only render the intersections with d3.geo.graticule.
But the desired result could be achieved with little effort by putting the intersections you want to be drawn into a helper array which is passed to a path generator for rendering. I forked one of my Plunks to demonstrate the solution:
Set up a helper array containing geometry objects to be passed to the path generator.
// Generate two-dimensional array containing the coordinates of all
// intersections we are interested in.
var lonLatIntersections = d3.range(-180, 180, 10).map(function(lat) {
return d3.range(0, 360, 10).map(function(lon) {
// Return geometry objects that can be handled by the path generator.
return {type: "Point", coordinates: [lon,lat]};
});
});
Use a nested selection to bind the two-dimensional (lat/lon) array and use the enter selections to append the intersection points. The actual drawing is done by supplying the geometry objects to the path generator which will take into account the projection.
// Do a nested selection to bind the two-dimensional data.
map.selectAll("g.lat")
.data(lonLatIntersections)
.enter().append("g") // Create groups per latitude.
.classed("lat", true)
.selectAll("circle")
.data(function(d) { return d; }) // Data binding for longitudes
.enter().append("path")
.attr({
"d": path, // Use the path generator to draw points
"class": "confluence"
});
I have some geoJson data that I am charting using d3.geo.
When I write something like
d3.select("svg")
...
.attr("d", function(d) {
return path({
type:"MultiPoint",
coordinates: get_activity_coords_(d.activities)
});
})
I always get a circle for each coordinate. The coordinates represent locations of various stopping points of a journey. What I would prefer is a different shape for the first and the last coordinate.
Is it possible to do this using MultiPoint, is there an example that I can follow? I could draw the points one by one, but I recall reading that MultiPoint is far faster. Plus, the code would be much clearer to read.
Thanks a lot.
You can't do different shapes for MultiPoint geoJSON with d3.geo.path. You can change the radius based on a function, but it looks like you can only set it per feature and not per point, so you'd have to break your set of points into multiple features and lose any performance benefit from using the single element.
However, there are other ways to go about doing this.
One option, as you mentioned, is to create a nested selection with a separate <path> element for each point, and draw each path using a d3.svg.symbol() function. You can then customize the symbol function to be based on data or index.
var trips = d3.select("svg").selectAll("g.trips")
.data(/*The data you were currently using for each path,
now gets to a group of paths */)
.attr("class", "trips");
//also set any other properties for the each trip as a whole
var pointSymbol = d3.svg.symbol().type(function(d,i){
if (i === 0)
//this is the first point within its groups
return "cross";
if ( this === this.parentNode.querySelector("path:last-of-type") )
//this is the last point within its group
return "square";
//else:
return "circle";
});
var points = trips.selectAll("path")
.data(function(d) {
return get_activity_coords_(d.activities);
//return the array of point objects
})
.attr("transform", function(d){
/* calculate the position of the point using
your projection function directly */
})
.attr("d", pointSymbol);
Another option, which allows you to set custom shapes for the first and last point (but all intermediary points would be the same) is to connect the points as the vertices of a single, invisible <path> element and use line markers to draw the point symbols.
Your approach would be:
Create a <defs> element within your SVG (either hard-coded or dynamically with d3), and define the start, middle and end marker points within them. (You can use d3.svg.symbol() functions to draw the paths, or make your own, or use images, it's up to you.)
Use a d3.svg.line() function to create the path's "d" attribute based on your array of point coordinates; the x and y accessor functions for the line should use the projection function that you're using for the map to get the x/y position from the coordinates of that point. To avoid calculating the projection twice, you can save the projected coordinates in the data object:
var multipointLine = d3.svg.line()
.x(function(d,i) {
d.projectedCoords = projection(d);
return d.projectedCoords[0];
})
.y(function(d){ return d.projectedCoords[1];});
(You can't use your d3.geo.path() function to draw the lines as a map feature, because it will break the line into curves to match the curves of longitude and latitude lines in your map projection; to get the line markers to work, the path needs to be just a simple straight-line connection between points.)
Set the style on that path to be no stroke and no fill, so the line itself doesn't show up, but then set the marker-start, marker-mid and marker-end properties on the line to reference the id values of the correct marker element.
To get you started, here's an example using d3 to dynamically-generate line markers:
Is it possible to use d3.svg.symbol along with svg.marker
the pie chart update example on the bl.ocks site doesn't update the elements 'in place':
http://bl.ocks.org/j0hnsmith/5591116
function change() {
clearTimeout(timeout);
path = path.data(pie(dataset[this.value])); // update the data
// set the start and end angles to Math.PI * 2 so we can transition
// anticlockwise to the actual values later
path.enter().append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc(enterAntiClockwise))
.each(function (d) {
this._current = {
data: d.data,
value: d.value,
startAngle: enterAntiClockwise.startAngle,
endAngle: enterAntiClockwise.endAngle
};
}); // store the initial values
path.exit()
.transition()
.duration(750)
.attrTween('d', arcTweenOut)
.remove() // now remove the exiting arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Instead, it just treats the new array of value as brand new data and resizes the chart accordingly.
I've created a fiddle demonstrating the issue very simply:
http://jsfiddle.net/u9GBq/23/
If you press 'add', it add a random int to the array: this works as intended.
If you press 'remove', the only element getting transitioned out is always the last element to have entered the pie. In short, it behaves like a LIFO stack.
The expected behaviour is for the relevant pie arc to get transitioned out instead.
Is it possible to apply object consistency to pies? I've also tried adding a key function (not demonstrated on the fiddle) but that just breaks (oddly enough it works fine with my stacked graphs).
Thank you.
The easiest solution to this problem is to set missing values to zero, rather than removing them entirely, as in Part III of the Pie Chart Update series of examples. Then you get object constancy for free: you have the same number of elements, in the same order, across updates.
Alternatively, if you want a data join as in Part IV, you have to tell D3 where the entering arcs should enter from, and where the exiting arcs should exit to. A reasonable strategy is to find the closest neighboring arc from the opposite data: for a given entering arc, find the closest neighboring arc in the old data (pre-transition); likewise for a given exiting arc, find the closest neighboring arc in the new data (post-transition).
To continue the example, say you’re showing sales of apples in different regions, and want to switch to show oranges. You could use the following key function to maintain object constancy:
function key(d) {
return d.data.region;
}
(This assumes you’re using d3.layout.pie, which wraps your original data and exposes it as d.data.)
Now say when you transition to oranges, you have the following old data and new data:
var data0 = path.data(), // retrieve the old data
data1 = pie(region.values); // compute the new data
For each entering arc at index i (where d is data1[i]), you can step sequentially through preceding data in data1, and see if you can find a match in data0:
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j]; // a match!
}
}
If you find a match, your entering arcs can start from the matching arc’s end angle. If you don’t find a preceding match, you can then look for a following matching arc instead. If there are no matches, then there’s no overlap between the two datasets, so you might enter the arcs from angle 0°, or do a crossfade. You can likewise apply this technique to exiting arcs.
Putting it all together, here’s Part V:
Ok, found the solution.
The trick was to pass the key this way:
path = path.data(pie(dataset), function (d) {return d.data}); // this is good
as opposed to not passing it, or passing it the wrong way:
path = path.data(pie(dataset, function (d) {return d.data})); // this is bad
And here's an updated fiddle with a working transition on the right arc! :)
http://jsfiddle.net/StephanTual/PA7WD/1/
I have created a multi line chart with a simple animation. In the beginning there are no data and after clicking a button new values are emulated and the line "move" left. The move is animated using a "shift".
The problem occurs when the lines "fill" the whole graph area (that means there are y values for all x values) and then the lines are animated in a different way. It looks like the y values are animated on a curve, not slided to the left.
The animation works good for both axes:
svg.selectAll("g .x.axis")
.transition()
.duration(500)
.ease("linear")
.call(xAxis);
svg.selectAll("g .y.axis")
.transition()
.duration(500)
.ease("linear")
.call(yAxis);
And not for lines (this code helped me a lot)
svg.selectAll("g .city path")
.data(processedData).transition().duration(500)
.ease("linear")
.attr("d", function(d, i) { return line(d.values); })
.attr("transform", null);
The Fiddle is accessible here.
Thanks for help.
The problem is that you're deleting data when there is too much. The way d3 matches data to existing data (when you call the .data() function) is by index. That is, the first element in the array that you pass to .data() matches the first bound data element, regardless of what the data actually looks like.
What happens in your case is that as soon as you start deleting data, the individual data points are updated instead of shifted. That's why you're seeing the "squiggle" -- it's updating each data point to its new value, which is the value the data point to the right had before.
With the code you currently have, this is hard to fix because you are not matching the data for individual lines explicitly. I would recommend that you have a look at nested selections which allow you to draw multiple lines and still explicitly match the data for individual ones. The key is to use the optional second argument to .data() to supply a function that tells it how to match the data (see the documentation). This way you can tell it that some data points disappeared and the other ones should be shifted.
you can get around this problem in 2 step.
in function update() : redraw you .data() with the new point at the end but without remove the first old point (with animation), like that each key is the same before and after transition.
at the end of function update() : you can remove the old value and redraw .data() without animation.