I am creating an area graph using d3.
This code works, but I am not happy with the data structure in the update method (toggle), indeed I don't understand what I am doing wrong.
When creating the graph I pass in the data via:
data([nsw,qld])
// then draw the chart
When I update the graph I pass in the data via:
chart.data([nsw]);
// draw the nsw series in the chart
chart.data([qld]);
// draw the qld series in the chart
Surely I can update the data by passing it in in the correct format of [nsw,qld]
<script type="text/javascript" src="lib/d3.min.js" charset="utf-8"></script>
<svg id="chart"></svg>
<script>
var NSW = "NSW";
var QLD = "QLD";
var width = 400;
var height = 400;
var years = [1,2,3,4,5];
var data = years.map(function(){ return [Math.random(),Math.random()]; }); // generate bogus data
var nsw = data.map(function(d) { return d[0];}); // extract new south wales data
var qld = data.map(function(d) { return d[1];}); // extract queensland data
var chart = d3.select("#chart").attr("width", width).attr("height", height).append("g");
var x = d3.scale.linear().domain([0, years.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(d){ return Math.max(d[0], d[1]); })]).range([height,0]);
var area = d3.svg.area().x(function(d,i) { return x(i); }).y0(height).y1(function(d, i) { return y(d); });
console.log([nsw,qld])
chart
.selectAll("path.area")
.data([nsw,qld]) // !!! here i can pass both arrays in.
.enter()
.append("path")
.attr("fill", "rgba(0,0,0,0.5)")
.attr("class", function(d,i) { return [NSW,QLD][i]; })
.attr("d", area);
chart.on("click", function() {
data = years.map(function(){return [ Math.random(),Math.random()];}); // switch in some new random data
var nsw = data.map(function(d) { return d[0];})
var qld = data.map(function(d) { return d[1];})
y.domain([0, d3.max(data, function(d){ return Math.max(d[0], d[1]); })]).range([height, 0]);
var svg = chart.transition();
/*
svg
.selectAll("path")
.data([nsw,qld]) // !!! this doesn't work.
.duration(750)
.attr("d", function(d) { return area(d); });
*/
chart.data([nsw]); // !!! here i'm only passing in one!
svg.select("path.NSW")
.duration(750)
.attr("d", function(d) {return area(d); });
chart.data([qld]); // !!! ...and then another
svg.select("path.QLD")
.duration(750)
.attr("d", function(d) { return area(d); });
});
</script>
Link to fiddle:
http://jsfiddle.net/MLA3x/
The problem is that you're using a transition in the update part and not a selection. That is, you're calling .data() on a transition, which you can't do. It works fine if you add the transition after updating the data:
chart
.selectAll("path")
.data([nsw,qld])
.transition()
.duration(750)
.attr("d", function(d) { return area(d); });
Complete demo here.
Related
I created this chart using D3 V5. Also, I have attached the sample data on the fiddle you can view by clicking here.
I've included the tick function code block which appends new domains for x and y scales and line/data on the path to slide left:
When the tick function executes, the line sort of rebuilds which makes it look like it bounces.
How can it be smooth, without a bounce at all when it rebuilds the line?
var tr = d3
.transition()
.duration(obj.tick.duration)
.ease(d3.easeLinear);
function tick() {
return setInterval(function() {
var newData = [];
var tickFunction = obj.tick.fnTickData;
if (tickFunction !== undefined && typeof tickFunction === "function") {
newData = tickFunction();
for (var i = 0; i < newData.length; i++) {
obj.data.push(newData[i]);
}
}
if (newData.length > 0) {
var newMaxDate, newMinDate, newDomainX;
if (isKeyXDate) {
newMaxDate = new Date(
Math.max.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newMinDate = new Date(
Math.min.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newDomainX = [newMinDate, newMaxDate];
} else {
newDomainX = [
d3.min(obj.data, function(d) {
return d[obj.dataKeys.keyX];
}),
d3.max(obj.data, function(d) {
return d[obj.dataKeys.keyX];
})
];
}
// update the domains
//x.domain([newMinDate, newMaxDate]);
if (obj.tick.updateXDomain) {
newDomainX = obj.tick.updateXDomain;
}
x.domain(newDomainX);
if (obj.tick.updateYDomain) {
y.domain(obj.tick.updateYDomain);
}
path.attr("transform", null);
// slide the line left
if (obj.area.allowArea) {
areaPath.attr("transform", null);
areaPath
.transition()
.transition(tr)
.attr("d", area);
}
path
.transition()
.transition(tr)
.attr("d", line);
svg
.selectAll(".x")
.transition()
.transition(tr)
.call(x.axis);
svg
.selectAll(".y")
.transition()
.transition(tr)
.call(y.axis);
// pop the old data point off the front
obj.data.shift();
}
}, obj.tick.tickDelay);
}
this.interval = tick();
That bounce is actually the expected result when you transition the d attribute, which is just a string.
There are several solutions here. Without refactoring your code too much, a simple one is using the pathTween function written by Mike Bostock in this bl.ocks: https://bl.ocks.org/mbostock/3916621. Here, I'm changing it a little bit so you can pass the datum, like this:
path.transition()
.transition(tr)
.attrTween("d", function(d) {
var self = this;
var thisd = line(d);
return pathTween(thisd, 1, self)()
})
Here is the forked plunker: https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview
As Gerardo notes, transitioning the d attribute of the path won't work very well unless you modfiy the approach. Here's a simple example of the sort of wiggle/bouncing that will arise if simply updating the d attribute of the path:
Pᴏɪɴᴛs ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ᴀᴄʀᴏss sᴄʀᴇᴇɴ, ᴡɪᴛʜ ᴘᴀᴛʜ ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ғʀᴏᴍ ᴏɴᴇ ᴅᴀᴛᴀ sᴇᴛ ᴛᴏ ᴛʜᴇ ɴᴇxᴛ.
The above behavior is noted by Mike Bostock in a short piece here, and here's a snippet reproducing the above animation:
var n = 10;
var data = d3.range(n).map(function(d) {
return {x: d, y:Math.random() }
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.x; }))
.range([10,490])
var y = d3.scaleLinear()
.range([290,10]);
var line = d3.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height", 400)
.append("g");
var path = svg.append("path")
.datum(data)
.attr("d", line);
var points = svg.selectAll("circle")
.data(data, function(d) { return d.x; })
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 5);
function tick() {
var transition = d3.transition()
.duration(1000);
var newPoint = {x:n++, y: Math.random() };
data.shift()
data.push(newPoint);
x.domain(d3.extent(data,function(d) { return d.x; }))
points = svg.selectAll("circle").data(data, function(d) { return d.x; })
points.exit()
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.remove();
points.enter().append("circle")
.attr("cx", function(d) { return x(d.x)+30; })
.attr("cy", function(d) { return y(d.y); })
.merge(points)
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("r", 5);
path.datum(data)
.transition(transition)
.attr("d", line)
.on("end", tick);
}
tick();
path {
fill: none;
stroke: black;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
One solution to this wiggle/bounce is:
add an additional point(s) to the data,
redraw the line with the recently added to data array
find out the next extent of the data
transition the line to the left
update the scale and transition the axis
remove the first data point(s)
This is also proposed in Mike's article that I've linked to. Here would be a basic implementation with your code:
I've avoided a setInterval function by recursively calling the function at the end of the last transition:
function slide() {
// Stop any ongoing transitions:
d3.selectAll().interrupt();
// A transition:
var transition = d3.transition()
.duration(2000)
.ease(d3.easeLinear)
// 1. add an additional point(s) to the data
var newData = obj.tick.fnTickData();
obj.data.push(...newData);
// 2. redraw the line with the recently added to data array
path.datum(obj.data)
areaPath.datum(obj.data)
// Redraw the graph, without the translate, with less data:
path.attr("transform","translate(0,0)")
.attr("d", line)
areaPath.attr("transform","translate(0,0)")
.attr("d", area)
// 3. find out the next extent of the data
// Assuming data is in chronological order:
var min = obj.data[newData.length][obj.dataKeys.keyX];
var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];
// 4. transition the line to the left
path.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
areaPath.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
// 5. update the scale and transition the axis
x.domain([new Date(min),new Date(max)])
// Update the xAxis:
svg.selectAll('.x')
.transition(transition)
.call(x.axis)
.on("end",slide); // Trigger a new transition at the end.
// 6. remove the first data point(s)
obj.data.splice(0,newData.length)
}
slide();
Here's an updated plunkr.
I am trying to change the x-scale of already plotted path in d3.js
Also, i am able to change the x-scale of circles using following code.
var s = d3.event.selection || xTimeScale.range();
xTimeScale.domain(s.map(xDateScale.invert, xDateScale));
d3.selectAll(".dot").transition().duration(10).attr("cx",function(d){ return xTimeScale(d.date); })
As you can see i have changed the domain of xTimeScale with new values. and then select all the circles with class(.dot) and replot the x and y with changed xTimeScale.
Now i also want to change the path with this new scale.
Here is the code when my path first plotted.
var lineFunction = d3.line().x(function(d) { return x(d.date); }).y(function(d) { return y(d.value); })
var lineGraph = svg.append("path").attr("d",lineFunction(data)).attr("stroke", "blue")
.attr("stroke-width", 2).attr("fill", "none").attr("class","dotsLine");
Since you're just changing the scale used by the line generator you don't need to rebind the data (by the way, I see that you are not binding any data), you just need to pass the new scale to the line generator and, then, update the d attribute of the path.
Look at this demo. There is a x scale, named xScale1, used by the line generator:
var line = d3.line()
.x(function(d) {
return xScale1(d)
})
When you click the button, the line generator uses another scale...
line.x(function(d) {
return xScale2(d)
})
... and the path d attribute is updated:
path.transition()
.duration(1000)
.attr("d", line(data));
Here is the demo:
var svg = d3.select("svg");
var data = [12, 130, 45, 60, 110, 21];
var yScale = d3.scaleLinear()
.domain(d3.extent(data))
.range([140, 10]);
var xScale1 = d3.scalePoint()
.domain(data)
.range([10, 290]);
var xScale2 = d3.scalePoint()
.domain(data.concat(d3.range(6)))
.range([10, 290]);
var line = d3.line()
.x(function(d) {
return xScale1(d)
})
.y(function(d) {
return yScale(d)
})
.curve(d3.curveBasis);
var path = svg.append("path")
.attr("d", line(data))
.attr("fill", "none")
.attr("stroke", "black");
d3.select("button").on("click", function() {
line.x(function(d) {
return xScale2(d)
})
path.transition()
.duration(1000)
.attr("d", line(data));
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Click me</button>
<br>
<svg></svg>
I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.
I've got an array of objects called graphData (size varies). Each element contains all the information required to create a d3 graph, and I am able to successfully draw the graphs if I access graphData elements by hardcoding (i.e. graphdata[0], graphdata[1] etc).
The problem comes when I attempt to use a for loop to generate one graph for each of the elements. Looked around stackoverflow and the web, but the solutions are all about generating a fixed number of multiple graphs, not generating multiple graphs dynamically.
Below is my working code for generating one graph. What is the recommended way to generate x number of graphs automatically?
var graphData = data.graph;
var RADIUS = 15;
var edgeData = graphData[0].edges;
var nodeData = graphData[0].nodes;
var stageNum = graphData[0].stage;
var xScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[0];
}),
d3.max(edgeData, function (d) {
return d.start[0];
})])
.range([50, w - 100]);
var yScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[1];
}),
d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([50, h - 100]);
var rScale = d3.scale.linear()
.domain([0, d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([14, 17]);
// already have divs with classes stage1, stage2... created.
var svg = d3.select(".stage" + stageNum).append("svg")
.attr({"width": w, "height": h})
.style("border", "1px solid black");
var elemEdge = svg.selectAll("line")
.data(edgeData)
.enter();
var edges = elemEdge.append("line")
.attr("x1", function (d) {
return xScale(d.start[0]);
})
.attr("y1", function (d) {
return yScale(d.start[1]);
})
.attr("x2", function (d) {
return xScale(d.end[0]);
})
.attr("y2", function (d) {
return yScale(d.end[1]);
})
.attr("stroke-width", 2)
.attr("stroke", "black");
var elemNode = svg.selectAll("circle")
.data(nodeData)
.enter();
var nodes = elemNode.append("circle")
.attr("cx", function (d) {
return xScale(parseInt(d.x));
})
.attr("cy", function (d) {
return yScale(parseInt(d.y));
})
.attr({"r": rScale(RADIUS)})
.style("fill", "yellow")
.style("stroke", "black");
Mike Bostock recommends implementing charts as reusable closures with methods. This would be an ideal implementation in your case as you want to have
multiple graphs with different data
potential reloading with new data (hopefully this is what you mean by dynamic?)
In broad strokes, what you want to do is wrap your code above into a function in very much the same way Mike describes in the post above, and then have data be an attribute of your closure. So here is some badly hacked code:
// your implementation here
var chart = function(){...}
var graphData = d3.json('my/graphdata.json', function(error, data){
// now you have your data
});
// let's say you have a div called graphs
var myGraphs = d3.select('.graphs')
.data(graphData)
.enter()
.append('g')
//now you have g elements for each of your datums in the graphData array
//we use the saved selection above and call the chart function on each of the elements in the selection
myGraphs.call(chart);
//note that internally in your `chart` closure, you have to take in a selection
//object and process it(data is already bound to each of your selections from above):
function chart(selection) {
selection.each(function(data) {
//...
Here is some more good reading on the topic.
Well you can try the following approach.
var graphData = data.graph;
//forEach will return each element for the callback, you can then make use of the e1 to draw the graph.
graphData.forEach(function(e1){
//graph code goes here.
});
providing this as your source array
//it's just a single circle in 3, 4
var stuff = [3, 4];
var source = [ [stuff, stuff], [stuff] ];
a bit of Array stuff
Array.prototype.max = function() {
return Math.max.apply(null, this);
};
Array.prototype.min = function() {
return Math.min.apply(null, this);
};
setup:
var dim = [];
source.forEach(function(elem){
elem.forEach(function(circle){
dim.push(circle.min());
dim.push(circle.max());
});
});
var min = dim.min();
var max = dim.max();
var x = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
var y = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
d3.select('body').selectAll('div')
.data(source) //first step: a div with an svg foreach array in your array
.enter()
.append('div')
.append('svg')
.selectAll('circle') //second step: a circle in the svg for each item in your array
.data(function(d){
return d; //returns one of the [stuff] arrays
}).enter()
.append('circle')
.attr('r', 5)
.attr('cx', function(d){
return x(d[0]);
})
.attr('cy', function(d){
return y(d[1]);
})
.style('fill','blue');
I've got a horizontal bar graph with transition on the x-axis. It looks exactly how I want, almost.
Sadly, the red gridlines (tickSize(-h)) are in the back. I need to bring them to the front.
Code is here: http://bl.ocks.org/greencracker/1cb506e7375a2d825e24
I'm new to transitions and I suspect I'm calling something in the wrong order.
Any suggestions on gridlines to front and/or suggestions how to DRY this code? It is fairly not DRY, but I'm starting with easy baby steps. Key parts:
d3.csv("georgia_counties_vmt.csv", function(input) {
data = input;
data.forEach(function(d) { d.value = +d.value; });
data.forEach(function (d) {d.n1 = +d.n1; })
data.sort(function(a, b) { return b.value - a.value; });
x.domain([0, d3.max(data, function(d) { return d.value; })]);
y.domain(data.map(function(d) { return d.name; }));
initAxes(); // draws tiny axes that transition into proper size
change(); // calls redraw()
// skip some, then:
function redraw() {
// unrelated bar drawing stuff here:
//calls regular-size axes
svg.selectAll("g.y_axis").call(yAxis)
svg.selectAll("g.x_axis").call(xAxis)
}
function initAxes() // initializes axes with range at [0,1]
{
var initXscale = d3.scale.linear()
.range([0, 1])
.domain([0, d3.max(data, function(d) { return d.value; })]);
var initXaxis = d3.svg.axis()
.scale(initXscale)
.tickSize(-h)
.orient("top");
svg.append("g")
.attr("class", "x_axis")
.call(initXaxis);
var initYscale = d3.scale.ordinal()
.rangeBands([0, 1], 0.1)
.domain(data.map(function(d) { return d.name; }));
var initYaxis = d3.svg.axis()
.scale(initYscale)
.orient("left")
.tickSize(0);
svg.append("g")
.attr("class", "y_axis")
.call(initYaxis);
}
This is because you missed initializing the bars before initializing the axis, you can add init bars code before initAxes() and keep all your other codes no change.
d3.csv("georgia_counties_vmt.csv", function(input) {
...
y.domain(data.map(function(d) { return d.name; }));
initBars(); // init bars before init axes
initAxes();
change();
}); // close d3.csv ...
...
// new added function to init bars
function initBars() {
var bar = svg.selectAll("g.bar")
.data(data)
.attr("class", "bar");
var barEnter = bar.enter().append("g")
.attr("class", "bar")
.attr("transform", function (d) {return "translate(0," + (d.y0 = (y(d.name))) +")" ;});
barEnter.append("rect")
.attr("width", 0)
.attr("height", y.rangeBand()/2);
}