The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle
Related
I just mimic the code d3 update pattern trying to render some rect with updated data
here is my code.
function update(data){
var r = g.selectAll("rect").data(data,function(d){return (d)});
r.exit().attr("class","exit").remove()
r
.attr("class","update")
.attr("x",(d, i) =>{return i* (50+interval)})
.attr("y", (d)=>{ return y(d)})
.attr("width", "20px")
.transition(t)
.attr("height",( d => { return height-padding.top-padding.bottom-y(d);}))
r.enter()
.append("rect")
.attr("width", "20px")
.attr("class","new")
.attr("x",(d, i) =>{ return i * (50+interval)})
.attr("y", (d)=>{return y(d)})
.attr("height",( d => { return height-padding.top-padding.bottom-y(d);}))
}
then I call the update function twice
update([3,2,1,5,4,10,9,7,8,6])
setTimeout(()=>{update([2,3,1,5,4,10,9,7,8,6])},1000)
Expected: only the first and second rect will be rerendered and set class "new", but in fact, all the rect will be set class "new" .
Codepen
The enter/exit pattern works when the data is an array of identified objects.
Replace this code:
var r = g.selectAll("rect").data(data,function(d){return (d)});
with:
const _data = data.map((v,i) => ({id: i, value: v}));
const r = g.selectAll("rect").data(_data,d => d.id);
The D3 will identify each object and update it accordingly instead of replacing with a new one.
See it's working in a pen
UPD:
If you want to highlight the items whose values have been changed, you can save the current value in an attribute of a newly added item:
r.enter()
.append("rect")
.attr('cur-value', d => d.value)
...
then, on update, query the value and compare with the one in datum:
r.attr("class","update")
...
.each(function(d) {
const rect = d3.select(this);
const prevValue = parseInt(rect.attr('cur-value'));
rect.attr('cur-value', d.value);
rect.style('fill', prevValue === d.value ? 'black' : 'red')
});
You can see it's working in the updated pen.
I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR
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/
I'm trying to create a reusable pie chart with dynamic transitions as a learning task. I'm working off of the d3.js resuable components e-book by Chris Viau.
The problem I'm having is basically its not updating, but creating multiple pie charts. I'm wondering if I'm not understanding how d3.dispatch works or whether I've messed something up in the way the pie char should work. It creates multiple circles instead of dynamically updating a single pie chart with random values.
here is my jsfiddle.
http://jsfiddle.net/seoulbrother/Upcr5/
thanks!
js code below:
d3.edge = {};
d3.edge.donut = function module() {
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var dispatch = d3.dispatch("customHover");
function graph(_selection) {
_selection.each(function(_data) {
var pie = d3.layout.pie()
.value(function(_data) { return _data; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
if (!svg){
var svg = d3.select(this).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
var path = svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.ease("elastic")
.duration(750)
.attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
}
d3.rebind(graph, dispatch, "on");
return graph;
}
donut = d3.edge.donut();
var data = [1, 2, 3, 4];
var container = d3.select("#viz").datum(data).call(donut);
function update(_data) {
data = d3.range(~~(Math.random() * 20)).map(function(d, i) {
return ~~(Math.random() * 100);
});
container.datum(data).transition().ease("linear").call(donut);
}
update();
setTimeout( update, 1000);
The main reason for multiple SVGs appearing is that you're not checking if there is one already correctly. You're relying on the variable svg being defined, but define it only after checking whether it is defined.
The better way is to select the element you're looking for and check whether that selection is empty:
var svg = d3.select(this).select("svg > g");
if (svg.empty()){ // etc
In addition, you need to handle the update and exit selections in your code in addition to the enter selection. Complete jsfiddle here.
I'm just starting out with D3.js. I've created a simple enough donut chart using this example. My problem is, if I have an array of objects as my data source - data points for ex. would be a1.foo or a1.bar - and I want to switch between them, how would i go about doing this? My current solution looks ugly and it can't be the proper way of doing it - code below.
//Call on window change event
//Based on some parameter, change the data for the document
//vary d.foo to d.bar and so on
var donut = d3.layout.pie().value(function(d){ return d.foo})
arcs = arcs.data(donut(data)); // update the data
Is there a way I can set the value accessor at run time other than defining a new pie function?
Generally to switch the data that is being displayed you would create a redraw() function that would then update the data for the chart. In the redraw you'll need to make sure to handle the three cases - what should be done when data elements are modified, what should be done when new data elements are added, and what should be done when data elements are removed.
It usually looks something like this (this example changes the data set through panning, but it doesn't really matter). See the full code at http://bl.ocks.org/1962173.
function redraw () {
var rects, labels
, minExtent = d3.time.day(brush.extent()[0])
, maxExtent = d3.time.day(brush.extent()[1])
, visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});
...
// upate the item rects
rects = itemRects.selectAll('rect')
.data(visItems, function (d) { return d.id; }) // update the data
.attr('x', function(d) { return x1(d.start); })
.attr('width', function(d) { return x1(d.end) - x1(d.start); });
rects.enter().append('rect') // draw the new elements
.attr('x', function(d) { return x1(d.start); })
.attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
.attr('width', function(d) { return x1(d.end) - x1(d.start); })
.attr('height', function(d) { return .8 * y1(1); })
.attr('class', function(d) { return 'mainItem ' + d.class; });
rects.exit().remove(); // remove the old elements
}