I have an svg element with data created this way:
var chart = d3.select("#my-div").append("svg");
var chartData = [];
chartData.push([{x: 1, y: 3}, {x: 2, y: 5}]);
chartData.push([{x: 1, y: 2}, {x: 2, y: 3}]);
.domain([1, 5]);
var lineFunc = d3.svg.line()
.x(function (d) {
return xRange(d.x);
})
.y(function (d) {
return yRange(d.y);
})
.interpolate('linear');
chart.append('g').classed('lines', true).selectAll('path').data(chartData).enter()
.append('path')
.attr('d', function(d) {
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
After that I am trying to update my data and update the chart:
chartData[1].push({x: 5, y: 5});
chart.selectAll('g.lines').selectAll('path').data(chartData)
.attr('d', function(d) {
console.log('updating:');
console.log(d);
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
but it prints 'updating' twice (for both of the chartData elements), but I've changed only one (chartData[1]
). How to prevent it to not update the ones that I didn't change? I will have many functions, so it will be ineffiecient to update all of them when only one has changed.
// EDIT to @mef's answer
I changed data to (I don't mind updating entire chartData[X] data, I just want to avoid updating entire chartData):
chartData.push({key: 'A', data: [{x: 1, y: 3}, {x: 2, y: 5}]});
chartData.push({key: 'B', data: [{x: 1, y: 2}, {x: 2, y: 3}]});
and then when adding data I've put .data(chartData, function(d) {return d.key})
and when updating I did the same, but it still updates both.
I tried also to put .data(chartData, function(d) {return 'A'})
or .data(chartData, function(d) {return 'B'})
when updating the data and it updates only one, but always the data with A
key (whether this function returns A
or B
).
So the whole code looks like this:
var chart = d3.select("#my-div").append("svg");
var chartData = [];
chartData.push({key: 'A', data: [{x: 1, y: 3}, {x: 2, y: 5}]});
chartData.push({key: 'B', data: [{x: 1, y: 2}, {x: 2, y: 3}]});
var xRange = d3.scale.linear().range([50, 780]).domain([1, 5]);
var yRange = d3.scale.linear().range([380, 20]).domain([2, 9]);
var lineFunc = d3.svg.line()
.x(function (d) {
return xRange(d.x);
})
.y(function (d) {
return yRange(d.y);
})
.interpolate('linear');
chart.append('g').classed('lines', true).selectAll('path')
.data(chartData, function(d) {return d.key}).enter()
.append('path')
.attr('d', function(d) {
return lineFunc(d.data);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
updating data
chartData[1].data.push({x: 5, y: 5});
chart.selectAll('g.lines').selectAll('path')
.data(chartData, function(d) {return d.key})
.attr('d', function(d) {
console.log('updating:');
console.log(d);
return lineFunc(d.data);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
OK, it can be done...
Here is a lazy way to do it...
Detect the phase of the d3 data binding process (key on nodes or key on data) and use different key for each:var k = Array.isArray(this) ? lineD(d, lineFunc) : d3.select(this).attr("d");
Align the formatting of the two key values by writing and reading back from a dummy node during the "data key" phase. (that's the lazy part!)
var chart = d3.select("#my-div").append("svg")
.attr("height", 600)
.attr("width", 900);
var chartData = [];
chartData.push([{x: 1, y: 3}, {x: 2, y: 5}]);
chartData.push([{x: 1, y: 2}, {x: 2, y: 3}]);
var xRange = d3.scale.linear().range([50, 780]).domain([1, 5]);
var yRange = d3.scale.linear().range([380, 20]).domain([2, 9]);
var lineFunc = d3.svg.line()
.x(function (d) {
return xRange(d.x);
})
.y(function (d) {
return yRange(d.y);
})
.interpolate('linear');
chart.append('g').classed('lines', true).selectAll('path')
.data(chartData, key)
.enter().append('path')
.attr('d', function(d) {
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
//updating data
chartData[1].push({x: 5, y: 5});
var update = chart.selectAll('g.lines').selectAll('path')
.data(chartData, key);
update.enter().append('path')
.attr('d', function (d) {
console.log('updating:');
console.log(d);
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
update.exit().remove();
function key(d, i, j) {
var k = Array.isArray(this) ? lineAttr(d, lineFunc, "d") : d3.select(this).attr("d");
console.log((Array.isArray(this) ? "data\t" : "node\t") + k)
return k;
function lineAttr(d, lineFunct, attribute) {
var l = d3.select("svg").selectAll("g")
.append("path").style("display", "none")
.attr(attribute, lineFunct(d))
d = l.attr(attribute);
l.remove();
return d;
}
}
Output
node M50,328.57142857142856L232.5,225.71428571428572
node M50,380L232.5,328.57142857142856
data M50,328.57142857142856L232.5,225.71428571428572
data M50,380L232.5,328.57142857142856L780,225.71428571428572
updating:
Array[3]0: Object1: Object2: Objectlength: 3__proto__: Array[0]
This is more efficient but only applies if you know that only the number of points on the lines will change and the number of lines is fixed.
//updating data
chartData[1].push({x: 5, y: 5});
chart.selectAll('g.lines').selectAll('path')
.data(chartData)
.filter(changed)
.attr('d', function (d) {
console.log('updating:');
console.log(d);
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none');
function changed(d) {
var s = d3.select(this);
console.log("data\t" + lineAttr(s.datum(), lineFunc, "d"));
console.log("node\t" + s.attr("d")); console.log("\n")
return lineAttr(s.datum(), lineFunc, "d") != s.attr("d");
function lineAttr(d, lineFunct, attribute) {
var l = d3.select("svg").selectAll("g")
.append("path").style("display", "none")
.attr(attribute, lineFunct(d))
d = l.attr(attribute);
l.remove();
return d;
}
}
Output
data M50,328.57142857142856L232.5,225.71428571428572
node M50,328.57142857142856L232.5,225.71428571428572
data M50,380L232.5,328.57142857142856L780,225.71428571428572
node M50,380L232.5,328.57142857142856
updating:
Array[3]
//updating data
alert("base");
chartData[1].push({ x: 5, y: 5 });
updateViz();
alert("change");
chartData.push([{x: 3, y: 1}, {x: 5, y: 2}])
updateViz();
alert("enter");
chartData.shift();
updateViz();
alert("exit");
function updateViz() {
var update = chart.selectAll('g.lines').selectAll('path')
.data(chartData),
enter = update.enter()
.append('path')
.attr('d', function (d) {
return lineFunc(d);
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none'),
changed = update.filter(changed)
.attr('d', function (d) {
console.log('updating:');
console.log(d);
return lineFunc(d);
});
update.exit().remove();
function changed(d) {
var s = d3.select(this);
console.log("data\t" + lineAttr(s.datum(), lineFunc, "d"));
console.log("node\t" + s.attr("d")); console.log("\n")
return lineAttr(s.datum(), lineFunc, "d") != s.attr("d");
function lineAttr(d, lineFunct, attribute) {
var l = d3.select("svg").selectAll("g")
.append("path").style("display", "none")
.attr(attribute, lineFunct(d))
d = l.attr(attribute);
l.remove();
return d;
}
}
}
Read this
You should use a key function, in order to allow to d3 to find out whether the records have changed, and match accordingly when you update the dataset.
At the moment, your data elements are javascript objects, and d3 does not check whether they have been changed compared to previous version (that would be tricky).
Ideally, you should find a unique identifier for your records, and include it in your dataset.
You would then replace .data(chartData)
by .data(chartData, function(d) { return d.id })
In case you have no property in your dataset you could use as unique record identifier, you still can do something like this:
.data(chartData, function(d) {
return d.map(function(coord) {
return coord.x + '-' + coord.y
}).join('-')
}
The key here would be a concatenation of all coordinates values of your object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With