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.
Related
I am having trouble with my force directed graph. I had to make some changed to my nodes for design purposes and ever since, my forces stoped working.
Each node is now a "g" element with two "circle" elements inside. One being the background of the node and the other being the partially transparent foreground.
Unlike before where I would apply ".call(drag(simulation))" to my node that used to be a "circle", I now need to apply it the the "g" element.
As seen on the screenshot, the nodes are not where they are supposed to be. They are detached from their respective links, and are all in the center of the map , on the top of each other.
Any clue on what I am doing wrong?
ForceGraph(
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{src, target}, …])
){
var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
const nodeStrength = -450 // -1750
const linkDistance = 100
const linkStrokeOpacity = 1 // link stroke opacity
const linkStrokeWidth = 3 // given d in links, returns a stroke width in pixels
const linkStrokeLinecap = "round" // link stroke linecap
const linkStrength =1
var width = this.$refs.mapFrame.clientWidth // scale to parent container
var height = this.$refs.mapFrame.clientHeight // scale to parent container
const N = d3.map(nodes, nodeId);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = nodes.map(n => Object.assign({}, n));
links = links.map(l => ({
orig: l,
//Object.assign({}, l)
source: l.src,
target: l.target
}));
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(nodeStrength);
forceLink.strength(linkStrength);
forceLink.distance(linkDistance)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
const svg = d3.create("svg")
.attr("id", "svgId")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("viewBox", [-width/2,-height/2, width,height])
.classed("svg-content-responsive", true)
const defs = svg.append('svg:defs');
defs.selectAll("pattern")
.data(nodes)
.join(
enter => {
// For every new <pattern>, set the constants and append an <image> tag
const patterns = enter
.append("pattern")
.attr("preserveAspectRatio", "none")
.attr("viewBox", [0,0, 100,100])
.attr("width", 1)
.attr("height", 1);
patterns
.append("image")
.attr("width", 80)
.attr("height", 80)
.attr("x", 10)
.attr("y", 10);
return patterns;
}
)
// For every <pattern>, set it to point to the correct
// URL and have the correct (company) ID
.attr("id", d => d.id)
.select("image")
.datum(d => {
return d;
})
.attr("xlink:href", d => {
return d.image
})
const link = svg.append("g")
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", linkStrokeWidth)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line")
;
link.attr("stroke", "white")
var node = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group");
node.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
node.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
})
node.call(drag(simulation))
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
//.transform("translate", d => "translate("+[d.x,d.y]+")"); // triggers error
.attr("transform", d => "translate("+[d.x,d.y]+")");
//.attr("cx", d => d.x)
//.attr("cy", d => d.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
return Object.assign(svg.node() );
}//forcegraph
EDIT 1
I updated the ticked() function with what was suggested but
".transform("translate", d => "translate("+[d.x,d.y]+")");" triggered the following error :
Mapping.vue?d90b:417 Uncaught TypeError: node.transform is not a function
at Object.ticked (Mapping.vue?d90b:417:1)
at Dispatch.call (dispatch.js?c68f:57:1)
at step (simulation.js?5481:32:1)
at timerFlush (timer.js?74f4:61:1)
at wake (timer.js?74f4:71:1)
So I changed it for ".attr("transform", d => "translate("+[d.x,d.y]+")");"
I don't get any error anymore but my nodes are still all in the center of the map as per the initial screenshot.
I am not quite sure what I am doing wrong. Perhaps I need to call ".call(drag(simulation))" on each of the two circles instead of calling it on node?
g elements don't have cx or cy properties. Those are specific to circle elements (and ellipses). This is why your positioning does not work. However, both circle and g can use a transform for positioning. Instead of:
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
You can use:
node
.transform("translate", d => "translate("+[d.x,d.y]+")");
In regards to your question title, d3 does not apply a force to the elements but rather the data itself. The forces continue to work regardless of whether you render the changes - as seen in your case by the links which move as they should.
I have a plunker here - https://plnkr.co/edit/7Eg34HyhXY6M3UJccAFq?p=preview
Its a simple stacked bar chart
I wanted to be able to click on a color in the legend and update the chart to show just that bar only
Ive found a few examples online but they are a bit too complex - just needed a simple method if thats possible
let legendItems = legend.selectAll('li')
.data(legendKeys)
.enter()
.append('li');
legendItems.append('span')
.attr('class', 'rect')
.style('background-color', (d, i) =>{
return colors[i];
});
legendItems.append('span')
.attr('class', 'label')
.html((d) => {
return d
});
You should bind click event handler this way:
let legendItems = legend.selectAll('li')
.data(legendKeys)
.enter()
.append('li')
.on('click', handleLegendItemClick);
Handler-function should looks like this (pay attention on the comments):
function handleLegendItemClick(d, i) {
// change opacity to show active legend item
legendItems.filter(legendItem => legendItem !== d).style('opacity', 0.5);
legendItems.filter(legendItem => legendItem === d).style('opacity', 1);
// update domain of y scale and update tick on y axis
y.domain([0, stackedSeries[i][0].data[d]]);
yAxis.transition().call(d3.axisLeft(y));
// bind new data
let enter = rects.data([stackedSeries[i]])
.enter()
.append('rect');
// remove old rects
rects.exit();
// draw new rect
rects.merge(enter)
.attr('height', (d) => {
return height - y(d[0].data[d.key]);
})
.attr('y', (d) => {
return 0;
})
.style('fill', (d, i) => {
return colorScale(d);
});
}
Check this fork of your plnkr. (I also add color scale - colorScale for applying a color by legend key name, not by index as you did.)
const colorScale = d3.scaleOrdinal(colors);
colorScale.domain(legendKeys);
A D3 bubblechart. Group and position svg:circles and svg:text elements
the function render() creates an svg element, circle and text as usual. This function includes .exit.remove update patterns.
runSimulation() is executed after page opening and a createChart() function.
click on a circle executes runSimulation() again, removing the circle
with .exit().remove() etc.
Simplified code:
fundtion render (){
const nodeEnter = this.nodeElements
.enter()
.append('svg:svg');
nodeEnter
.append('circle')
.on('click',runSimulation);
const textEnter = this.textElements
.enter()
.append('svg:text');
}
this.runSimulation(){
this.render();
function ticked(){
this.nodeElements
.attr('cx', cxValue)
.attr('cy', cyValue):
}
this.simulation.nodes.on.('tick',ticked);
}
On the first run the cx and cy attributes are appended to the svg:svg while the circles do not have the attributes and everything is rendered in the top left corner ( also with using svg:g)
on the click action the runSimulation is executed a second time; now the circle gets the cx and cy attributes attached and all elements move into the expected position.
-I am looking for a way to get the cx cy attributes to the circle on the first rendering so that the parent elements do not cluster at x=0 y =0, or to get x and y to svg:svg; the shown pattern is not working and I appreciate your help.
this.nodeElements = this.nodeGroup.selectAll('g')
.data(this.data, node => node.nodeName);
this.nodeElements.exit().remove();
const nodeEnter = this.nodeElements
.enter()
.append('g');
nodeEnter
.append('circle')
.attr('fill', node => this.color(node.familyType))
.attr('r', function (node) {
return (node.nodeName.length > 10) ? "75" : node.nodeName.length*6;
})
.on('click', this.runSimulation);
let wrap = (text) => {
text.each(function() {
let text = d3.select(this),
const textnode = nodeEnter
.append('text')
.attr('id', 'text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('dy', '0.1em')
.attr('font-size', '1.2em');
this.settings.getValue('option1').then((option) => {
if (option === true) {
let type = node => node.nodeName;
textnode
.text(type);
}
if (option === false) {
let type = node => node.otherName;
textnode
.text(type);
}
})
Thank you for your answer, Steven. runSimulation executes a tick function that does add the proper d.x and d.y now. I am not sure why it did not work - ionic lifecycle events ?
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
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/