I'm getting JSON data do D3 line graph. Next requesting new data after last point and concat() the new data, updating the line and moving to the left on each transition()
I need to recalculate xScale each time to put the new time values and remove the old ones on x-axis. And the x-axis is scrolling to the left smoothly and correctly. But when I start to recalculation the xScale my line path stop smooth translation to the left and just jumping immediately on each update_path(). If I remove xScale.domain(d3.extent(... the path is transitioning smoothly to the left, but don't have the new times on x-axis.
function update_path(svg,path,dataset,xScale) {
var last=dataset[dataset.length-1];
var last_point=last[Object.keys(last)[0]];
// check new data and put it
d3.json("data.php?chartID=1&last_point="+last_point).then(function(data) {
dataset=dataset.concat(data);
var yScale=d3.scaleLinear()
.domain([d3.max(dataset, function(d) { return d[Object.keys(d)[1]]; }), 0])
.range([0, range]);
xScale.domain(d3.extent(dataset, function(d) { return new Date(d[Object.keys(d)[0]]); }))
var translate=dataset[0];
var translate_point=translate[Object.keys(translate)[0]];
var prelast=dataset[dataset.length-2];
var prelast_point=prelast[Object.keys(prelast)[0]];
var last=dataset[dataset.length-1];
var last_point=last[Object.keys(last)[0]];
var prelast_date=new Date(prelast_point);
var last_date=new Date(last_point);
var seconds = (last_date.getTime() - prelast_date.getTime());
var line=prepare_line(xScale,yScale);
// update x-axis
var xaxis_call=d3.axisBottom(xScale).ticks().tickSize(-height);
svg.selectAll("g.main_g").selectAll("g.x-axis")
.transition()
.duration(seconds)
.ease(d3.easeLinear)
.attr("transform", "translate(-"+ xScale(new Date(translate_point))+",100)")
.call(xaxis_call)
// update line
path
.attr("d", line(dataset))
.transition()
.duration(seconds)
.ease(d3.easeLinear)
.attr("transform", "translate(-"+ xScale(new Date(translate_point))+")")
.on('end', function() {
update_path(svg,path,dataset,xScale);
});
dataset.shift();
});
}
So how to fix the line path to be updated and to move smoothly to the left at the same time while coming the new data from the right?
I have reordered my code a little bit and also the key in my case for smooth translate was .attr('transform', null) on path object.
function update_path(svg,path,dataset,xScale) {
// get last date
var last=dataset[dataset.length-1];
var last_point=last[Object.keys(last)[0]];
// check new data and put it
d3.json("data.php?chartID=1&last_point="+last_point).then(function(data) {
data=make_dataset(data,data_index); // add some my stuff to dataset object
dataset=dataset.concat(data);
for (i=0; i<data.length; i++) {
dataset.shift();
}
var translate=dataset[0];
var translate_point=translate[Object.keys(translate)[0]];
var prelast=dataset[dataset.length-2];
var prelast_point=prelast[Object.keys(prelast)[0]];
var last=dataset[dataset.length-1];
var last_point=last[Object.keys(last)[0]];
var prelast_date=new Date(prelast_point);
var last_date=new Date(last_point);
var seconds = (last_date.getTime() - prelast_date.getTime());
var line=prepare_line(xScale,yScale);
// update x-axis
var xaxis_call=d3.axisBottom(xScale)
svg.selectAll("g.main_g").selectAll("g.x-axis")
.transition()
.duration(seconds)
.ease(d3.easeLinear)
.attr("transform", "translate(-"+ xScale(new Date(translate_point))+","+height+")")
.call(xaxis_call)
// update line
path.attr('transform', null)
.attr("d", line(dataset))
.transition()
.duration(seconds)
.ease(d3.easeLinear)
.attr("transform", "translate(-"+ xScale(new Date(translate_point))+")")
.on('end', function() {
// my custom functions for preparing Scales
var xScale=axis_scaleTime(dataset, width);
var yScale=axis_scaleLinear_values1(dataset, height);
update_path(svg,path,dataset,xScale);
});
});
}
Related
I have a D3 line chart with one line being added at load time and two lines being added dynamically after a user presses a button. The second line should transition from the original line and the third line should transition from the position of the second line's finished transition. I have got it partially working but the final line is painted in position before the transition occurs and then is removed when the transition starts , which looks weird. How do I amend the code so that this final transition line does not paint before the transition takes place. The original line:
function drawAD(){
//setup original line
var aggregateDemandLine = d3.line()
.x(function(d,i){return xScale(ADX[i])})
.y(function(d,i){return yScale(ADY[i])})
g.append("path")
.attr("class","AD1")
.attr("fill","none")
.attr("stroke","green")
.attr("stroke-width","3px")
.attr("d",aggregateDemandLine(ADX))
}
Below is the function to add the second and third lines with transitions.
function accelerate(){
var initial, accelerated;
var initialAD = d3.line()
.x(function(d,i){return xScale(ADX[i])})
.y(function(d,i){return yScale(initialADMoveY[i])})
g.append("path").attr("class","first").attr("d", function(){
initial = initialAD(ADX);
return initial;
}).attr("stroke","green")
.attr("stroke-width",3)
.attr("pointer-events","none")
.attr("class","first")
var acceleratedAD = d3.line()
.x(function(d,i){return xScale(ADX[i])})
.y(function(d,i){return yScale(acceleratedADY[i])})
g.append("path").attr("class","second").attr("d", function(){
accelerated = acceleratedAD(ADX)
return accelerated;
})
.attr("stroke","green")
.attr("stroke- width",3).attr("pointer-events","none")
.attr("class","second");
d3.select('.first')
.transition()
.duration(2000)
.attrTween('d', function (d) {
return d3.interpolateString(ADPath, initial);
}).on("end",secondTransition);
function secondTransition(){
d3.select('.second')
.transition()
.duration(2000)
.attrTween('d', function (d) {
return d3.interpolateString(initial, accelerated);
});
}
function accelerate(){
var initial, accelerated;
var initialAD = d3.line()
.x(function(d,i){return xScale(ADX[i])})
.y(function(d,i){return yScale(initialADMoveY[i])})
//create the initial path
var initial = initialAD(ADX);
var acceleratedAD = d3.line()
.x(function(d,i){return xScale(ADX[i])})
.y(function(d,i){return yScale(acceleratedADY[i])})
//create accelerated
var accelerated = acceleratedAD(ADX);
g.append("path").attr("class","first").attr("d",ADPath)
.attr("stroke-width",3)
.attr("stroke","green")
.transition()
.duration(2000)
.attrTween("d" ,function(){
return d3.interpolateString(ADPath,initial)
}).on("end",secondTransition)
function secondTransition(){
g.append("path").attr("class","second").attr("d",initial)
.attr("stroke-width",3)
.attr("stroke","green")
.transition()
.duration(2000)
.attrTween('d', function (d) {
return d3.interpolateString(initial, accelerated);
});
}
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
I got the following D3 v4 pie chart, every time I try updating it the data doesn't update correctly. I have been reading around tried following some other example, but just can't seem to get it to work. Current update function looks like this:
function PieGenUpdater(data, colourRangeIn) {
var dataset = data;
var width = 400;
var height = 400;
var radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(radius/1.5)
.outerRadius(radius);
var pie = d3.pie()
.value(function(d) { return d.percent; })
.sort(null);
var svg = d3.select('#c-pie');
var path = svg.selectAll('path').data(pie(dataset));
path.enter()
.append("path")
.attr('fill', function(d, i) {
return d.data.color;
})
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTweenCoverage);
path.exit().remove();
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
function arcTweenCoverage(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
}
https://jsfiddle.net/mahsan/zup6kafk/
Any help is greatly appreciated.
Here is the 4 years too late answer...
In PieGenUpdater change
var path = svg.selectAll('path').data(pie(dataset));
to
var path = svg.select('g').selectAll('path').data(pie(dataset));
In your update function you were adding the additional paths from dataset 1 directly under element instead of the element
I like dcjs, http://bl.ocks.org/d3noob/6584483 but the problem is I see no labels anywhere for the line chart (Events Per Hour). Is it possible to add a label that shows up just above the data point, or even better, within a circular dot at the tip of each data point?
I attempted to apply the concepts in the pull request and came up with:
function getLayers(chart){
var chartBody = chart.chartBodyG();
var layersList = chartBody.selectAll('g.label-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'label-list');
}
var layers = layersList.data(chart.data());
return layers;
}
function addDataLabelToLineChart(chart){
var LABEL_FONTSIZE = 50;
var LABEL_PADDING = -19;
var layers = getLayers(chart);
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
labels.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.attr('x', function (d) {
return dc.utils.safeNumber(chart.x()(d.x));
})
.attr('y', function (d) {
var y = chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.attr('fill', 'white')
.style('font-size', LABEL_FONTSIZE + "px")
.text(function (d) {
return chart.label()(d);
});
dc.transition(labels.exit(), chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
I changed the "layers" to be a new group rather than using the existing "stack-list" group so that it would be added after the data points and therefore render on top of them.
Here is a fiddle of this hack: https://jsfiddle.net/bsx0vmok/
There is probably a simple answer to this question... . I'm using d3 to create a globe, showing all countries. I also have a div with the name of all the countries in it. When I click on a country name, I want the globe to spin to that country. But I'm having trouble getting the syntax right. Can anyone help, please?
var feature;
var projection = d3.geo.azimuthal()
.scale(zoom)
.origin([-71.03,42.37])
.mode("orthographic")
.translate([380, 450]);
var circle = d3.geo.greatCircle()
.origin(projection.origin());
var scale = {
orthographic: 380,
stereographic: 380,
gnomonic: 380,
equidistant: 380 / Math.PI * 2,
equalarea: 380 / Math.SQRT2
};
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#globe").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("dblclick", dblclick)
.on("mousedown", mousedown);
var g = svg.append("g");
d3.json("simplified.geojson", function(collection) {
g.append("g")
.attr("id", "countries")
g.append("g")
.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.ISO3; })
.attr("fill", function(d) { return d.properties.FILL; }) //change color and make clickable if data on this country exists
.on("mouseover", pathOver)
.on("mouseout", pathOut)
.on( "dblclick", dblclick)
.on("mousewheel.zoom", null)
.on("click", click);
feature = svg.selectAll("path");
feature.append("svg:title")
.text(function(d) { return d.properties.NAME; });
//here is where I want to be able to click a country name in the div and have the globe rotate to that country:
$('.represented').click(function(){
var countryabbrev = $(this).attr('id');
projection.origin(projection.invert(#path.centroid(#CAN))); //this line is wrong
refresh(1500);
showPerson(countryabbrev)
});
I've gotten it to find the country and rotate. Now the rotate is sketchy, but at least there's progress:
$('.represented').click(function(){
var countryabbrev = $(this).attr('id');
getCentroid(d3.select("#" + countryabbrev));
//projection.origin(projection.invert(#path.centroid(#CAN)));
projection.origin(getCentroid(d3.select("#" + countryabbrev)));
refresh(1500);
//showPerson(countryabbrev)
});
function getCentroid(selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node(),
// use the native SVG interface to get the bounding box
bbox = element.getBBox();
// return the center of the bounding box
return [bbox.x + bbox.width/2, bbox.y + bbox.height/2];
}