Could anyone please help me with the update pattern? For now this is what I get when I give my graph React component new data... the old one stays...
This is where I have my code https://github.com/danieltkach/react-d3-integration
I've been reading about it an I understand I have to do "stuff" in here... replace enter with join() ? And then do the 3 update functions inside? ...
// SVGs creation ---
const linkSvg = svgRef.current
.selectAll('path')
.data(LINKS, d=>d.agentName)
// .enter()
.join(
enter => enter.append('path')
.attr('stroke', d => !d.weight ? "gray" : linkStyles.onlineColor)
.attr('stroke-width', d => linkWeightScale(d.weight))
.attr('stroke-dasharray', d => {
if (!d.weight) return linkDashedScale(d.weight)
}),
update => update,
exit => exit.call(exit=>exit).remove()
)
I think your problem is here on GraphGenerator.jsx:
svgRef.current = d3.select('#viz')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g') // a new group is appended
You're appending a new g element each time. That new element has no data, so nothing is removed or updated in the join. If you remove the .append('g') it should work.
EDIT:
Furthur down in your node creation you use:
const circles = svgRef.current
.selectAll()
.data(NODES)
.join("g");
You aren't selecting anything, so nothing can be updated. Use this there:
const circles = svgRef.current
.selectAll('.node')
.data(NODES)
.join("g")
.classed('node',true)
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'm trying to
import { transition } from 'd3-transition'
and add an update transition to this code, but I can't figure it out how to use the transition library separately.
import { select } from "d3-selection";
//...
const binding = select(host.svg)
.select("#points")
.selectAll("circle.point")
.data(data);
const enter = binding.enter();
enter
.append("circle")
.attr("class", "point")
.attr("cx", (d) => projection(d)[0])
.attr("cy", (d) => projection(d)[1])
.attr("r", (d) => radius(d.count));
I realize I can just import the whole d3 library, but I'm doing it this way intentionally.
There are two questions here:
how to import transition with ES6?
how to add an update transition?
To import d3-transition, your import is correct:
import { transition } from 'd3-transition'
To add an update transition, you can use selection.join(). selection.join() takes a function per selection: one for the enter, update and exit selection.
To track update changes, note that you need to provide a key function to D3, otherwise it won't be able to tell what changed. A key function will identify uniquely each of your data.
Finally, use selection.call() to avoid breaking the method chain.
You can see the full example at the link.
svg
.selectAll("rect")
// Key function to track updates
.data(data, d => d)
// https://observablehq.com/#d3/selection-join
.join(
// Enter selection handler
enter =>
enter
.append("rect")
.attr("width", 100)
.attr("height", d => d.value)
.attr("x", (d, i) => 150 * i) // spacing
.attr("fill", "red"),
// Update selection handler
update =>
update.attr("fill", "green").call(update =>
update
.transition()
.duration(3000)
.attr("height", d => d.value)
)
);
Example on CodeSandbox. Make sure to refresh the browser to see the transition.
I am following the video tutorial (https://channel9.msdn.com/Blogs/Azure/Building-Power-BI-custom-visuals-that-meet-your-app-needs) to create a bar chart (custom visual) within Power BI.
Code snippet (in the video at 13.40):
let bars = this.barContainer.selectAll('.bar').data(viewModel.dataPoints);
bars.enter().append('rect').classed('bar', true);
bars
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.exit().remove();
The above code is written in update method of visual.ts file.
As per my understanding, the enter() method will create rect elements with class bar based on the viewModel.dataPoints data.
The problem is I am not able to get it to work upon initial load, when the visual is loaded, this works only when the visual is resized. I cannot see any attributes getting applied on the rect elements upon load, but its getting applied when a resize is made on the visual.
If I changed the code to:
bars.enter().append('rect').classed('bar', true)
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.transition()
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category));
bars.exit().remove();
The above code works fine, The attributes are getting applied for both initial load and resize.
Additional Info (Constructor code)
let svg = this.svg = d3.select(options.element).append('svg').classed('barChart', true);
this.barContainer = svg.append('g').classed('barContainer', true);
this.xAxis = svg.append('g').classed('xAxis', true);
Is this the way its supposed to work in the latest version (D3 Js 5.0)? or I am doing anything wrong?
The .merge() function does the trick.
The solution is:
let bars = this.barContainer.selectAll('.bar').data(viewModel.dataPoints);
const a = bars.enter().append('rect').merge(<any>bars); // merge before attr()
a.classed('bar', true);
a
.transition().duration(50)
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.value))
.attr('y', d => yScale(d.value))
.attr('x', d => xScale(d.category)); // This code now works for initial
// load and update.
bars.exit().remove();
This should help talk you through merge. There is a link at the bottom if you would prefer to use the new join method in d3.js v5.
https://www.codementor.io/#milesbryony/d3-js-merge-in-depth-jm1pwhurw
Or for more complex use see Mike Bostock's
https://observablehq.com/#d3/selection-join
I'm just learning d3.js and want to visualize my data in a 2d grid of colored rectangles. The d3 code below gets called whenever my data changes. It properly builds the initial grid, updates existing cells correctly, and reacts to removal of existing items as intended (cells get removed + others updated).
However, when I add a (new) data item, the grid data gets updated correctly but no new cells will be added - which means that the last element in the grid won't be displayed.
From my very basic d3.js understanding, the difference in size between the existing data and the new data should result in a non-empty "squares.enter()" selection in the enter block such that a new rect element should get added to the last row.
Could anyone tell me how my assumption is wrong? or give me a hint what I have to change to add new data?
//format 1d to 2d array
let d = this.gridData(this.props.data);
//Enter rows
let rows = d3
.select(this.svgRef.current)
.selectAll(".row")
.data(d)
.enter()
.append("g")
.attr("class", "row");
//Enter cells for each row
let squares = rows.selectAll(".square").data(d => d);
squares.enter().append("rect")
.attr("class", "square")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.width)
.attr("height", d => d.height)
.style("fill", d => d.c)
.style("stroke", d => {
if (d.id == this.props.focus) {
return "#FF0000";
} else {
return "#FFFFFF";
}
});
//Update cells
squares = d3.select(this.svgRef.current).selectAll(".row")
squares.selectAll(".square")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.width)
.attr("height", d => d.height)
.style("fill", d => d.c)
.style("stroke", d => {
if (d.id == this.props.focus) {
return "#FF0000";
} else {
return "#FFFFFF";
}
});
//exit => remove rows/cells
squares.exit().remove();
squares.selectAll(".square").data(d => d).exit().remove();
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 ?