For example, I need to calculate a Math.sqrt of my data for each attr, how can I calculate only one time the Math.sqrt(d)?
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
return Math.sqrt(d) + 1
})
.attr("cy", function(d) {
return Math.sqrt(d) + 2
})
.attr("rx", function(d) {
return Math.sqrt(d) + 3
})
.attr("ry", function(d) {
return Math.sqrt(d) + 4
});
Has any elegant/performative mode? I'm thinking this way:
var aux;
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
aux = Math.sqrt(d);
return aux + 1
})
.attr("cy", function(d) {
return aux + 2
})
.attr("rx", function(d) {
return aux + 3
})
.attr("ry", function(d) {
return aux + 4
});
An underestimated feature of D3 is the concept of local variables which were introduced with version 4. These variables allow you to store information on a node (that is the reason why it is called local) independent of the data which might have been bound to that node. You don't have to bloat your data to store additional information.
D3 locals allow you to define local state independent of data.
Probably the major advantage of using local variables over other approaches is the fact that it smoothly fits into the classic D3 approach; there is no need to introduce another loop whereby keeping the code clean.
Using local variables to just store a pre-calculated value is probably the simplest use case one can imagine. On the other hand, it perfectly illustrates what D3's local variables are all about: Store some complex information, which might require heavy lifting to create, locally on a node, and retrieve it for later use further on in your code.
Shamelessly copying over and adapting the code from Gerardo's answer the solution can be implemented like this:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var roots = d3.local(); // This is the instance where our square roots will be stored
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
return roots.set(this, Math.sqrt(d)) * 3; // Calculate and store the square root
})
.attr("cy", function(d) {
return roots.get(this) * 3; // Retrieve the previously stored root
})
.attr("rx", function(d) {
return roots.get(this) + 3; // Retrieve the previously stored root
})
.attr("ry", function(d) {
return roots.get(this) + 4; // Retrieve the previously stored root
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably, the most idiomatic way for doing this in D3 is using selection.each, which:
Invokes the specified function for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]).
So, in your case:
circle.each(function(d){
//calculates the value just once for each datum:
var squareRoot = Math.sqrt(d)
//now use that value in the DOM element, which is 'this':
d3.select(this).attr("cx", squareRoot)
.attr("cy", squareRoot)
//etc...
});
Here is a demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.each(function(d) {
var squareRoot = Math.sqrt(d);
d3.select(this)
.attr("cx", function(d) {
return squareRoot * 3
})
.attr("cy", function(d) {
return squareRoot * 3
})
.attr("rx", function(d) {
return squareRoot + 3
})
.attr("ry", function(d) {
return squareRoot + 4
});
})
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Another common approach in D3 codes is setting a new data property in the first attr method, and retrieving it latter:
.attr("cx", function(d) {
//set a new property here
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
//retrieve it here
return d.squareRoot * 3
})
//etc...
That way you also perform the calculation only once per element.
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100).map(function(d) {
return {
value: d
}
});
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
return d.squareRoot * 3
})
.attr("rx", function(d) {
return d.squareRoot + 3
})
.attr("ry", function(d) {
return d.squareRoot + 4
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: by the way, your solution with var aux will not work. Try it and you'll see.
Related
so I'm trying to create a visual representations of a couple of vlans and the connections of switches in each of them. I tried implementing it with this example I found online https://bl.ocks.org/mbostock/3037015 , the problem is that when i created a loop to go through all of the vlans, only the last vlan is drawn, there's really no reason I can see of why this is happening since all elements are calling the function.
If I remove the last element from the array with delete data['80'] then the one before the last starts working, so the only one working it the last one of the dictionary object, don't why though
code:
var data = {{ graph_vlans | safe }};
console.log(data);
$(document).ready(() => {
//-----------------------------------------------------------------
// TREE DISPLAY ---------------------------------------------------
//-----------------------------------------------------------------
var toggler = document.getElementsByClassName("caret");
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function () {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
//-----------------------------------------------------------------
// NETWORK DIAGRAM ------------------------------------------------
//-----------------------------------------------------------------
var width = 960, height = 500;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
console.log(key);
console.log(key["4"]);
var svg = d3.select("#graph_" + key).append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(function (d) {
return radius(d.source.size) + radius(d.target.size) + 20;
});
var graph = data[key];
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", function (d) {
return (d.bond * 2 - 1) * 2 + "px";
});
link.filter(function (d) {
return d.bond > 1;
}).append("line")
.attr("class", "separator");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return radius(d.size);
})
.style("fill", function (d) {
return color(d.atom);
});
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.atom;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
}
function tick() {
link.selectAll("line")
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
});
Problem
I made some fake data for your plot and got this:
Your other force layouts are drawing, they're just not positioned. They're at [0,0] - barely visible here, in the top left corner of the SVG. So why is this?
Each for loop iteration you redefine any existing link and node variables - their scope extends beyond the for statement so you overwrite the previous defintion. var restricts a variables scope by function, the for statement doesn't limit scope if using var.
Because of this, when you call the tick function for each force layout, only the last layout is updated because node and link refer to the last layouts nodes and links.
So only your last force layout does anything.
Solution
There are a few solutions, I'm proposing one that adds two simple changes from your current code.
We need to get each force layout's nodes and links to the tick function. Currently we have all the force layout tick functions using the same node and link references. Ultimately, this is a variable scoping issue.
We can start by placing the tick function into the for loop. But, this still runs into the same problem by itself: node and link have a scope that isn't limited to the for loop (or the current iteration of the for loop) - each tick function will still use the same node and link references.
To fix this, we also need to use let when defining link and node (instead of var), now these variables have a block level scope, meaning each iteration's definitions of link and node won't overwrite the previous iterations.
By moving the tick function into the for loop and using let to define node and link, each time we call the tick function it will use the appropriate nodes and links.
Here's an example using a slightly modified example of the above code (removing some of the styling that relies on data properties and re-sizing the layouts for snippet view, but with the changes proposed above):
var data = {
"a":{
nodes:[{name:1},{name:2},{name:3}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
},
"b":{
nodes:[{name:"a"},{name:"b"},{name:"c"}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
}
}
// TREE DISPLAY
var width = 500, height = 100;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(20);
var graph = data[key];
let link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", 1)
.style("stroke","#ccc")
let node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node");
node.append("circle")
.attr("r", 5)
.attr("fill","#eee");
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.name;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
function tick() {
link.selectAll("line")
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
I’ve created a line graph in D3. To ensure the line doesn’t overlap the y-axis, I have altered the range of the x-axis. As a result of this, there is a gap between the x-axis and the y-axis which I am trying to fill with another line.
The rest of the graph uses the D3 update pattern. However, when I try to use the pattern on this simple line, two path elements are drawn (one on top of the other). I have tried numerous solutions to correct this issue but I’m not having any luck. Does anyone have any suggestions?
The code below is what is drawing two of the same path elements
var xAxisLineData = [
{ x: margins.left , y: height - margins.bottom + 0.5 },
{ x: margins.left + 40, y: height - margins.bottom + 0.5 }];
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
var enter = update.enter()
.append("path")
.attr("d", xAxisLine(xAxisLineData))
.attr("class", "xAxisLine")
.attr("stroke", "black");
Your problem is here:
var update = vis.selectAll(".xAxisLine")
.data(xAxisLineData);
this is a null selection, assuming there are no elements with the class xAxisLine, which means that using .enter().append() will append one element for each item in the xAxisLineData array.
You want to append one path per set of points representing a line, not one path for each in a set of points representing a line.
You really just want one line to be drawn, so you could do:
.data([xAxisLineData]);
or, place all the points in an array when defining xAxisLineData
Now you are passing a data array to the selection that contains one item: an array of points - as opposed to many items representing individual points. As the data array has one item, and your selection is empty, using .enter().append() will append one element:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data([lineData])
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Compare without using an array to hold all the data points:
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [{x:100,y:100},{x:200,y:200}]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", xAxisLine(lineData))
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, we can make one last change. Since each item in the data array is bound to the element, we can reference the datum, not the data array xAxisLineData, which would make adding multiple lines much easier:
.attr("d", function(d) { return xAxisLine(d) })
Note in the demo below that the variable xAxisLineData is defined as an array of arrays of points, or an array of multiple lines.
var svg = d3.select("body").append("svg").attr("width",500).attr("height",400);
var lineData = [[{x:100,y:100},{x:200,y:200}],[{x:150,y:150},{x:260,y:150}]]
var xAxisLine = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var colors = ["steelblue","orange"];
var line = svg.selectAll(null)
.data(lineData)
.enter()
.append("path")
.attr("d", function(d) { return xAxisLine(d) }) // use the element's datum
.attr("class", "xAxisLine")
.attr("stroke-width", function(d,i) { return (1-i) * 10 + 10; })
.attr("stroke", function(d,i) { return colors[i]; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
I've created a stacked chart animation/update app. However there appears to be NaN values being passed into the y and height variables. I am unsure as to what is wrong. If you toggle the data the charts eventually fill up.
jsFiddle
but the problem may occur first in setting the yaxis
svg.select("g.y")
.transition()
.duration(500)
.call(methods.yAxis);
It looks like something goes wrong in the bar rect enter/exit code.
//_morph bars
var bar = stacks.selectAll("rect")
.data(function(d) {
return d.blocks;
});
// Enter
bar.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) { return methods.y(d.y1); })
.attr("width", methods.x.rangeBand())
.style("fill", function(d) { return methods.color(d.name); });
// Update
bar
.attr("y", methods.height)
.attr("height", initialHeight)
.attr("width", methods.x.rangeBand())
.transition()
.duration(500)
.attr("x", function(d) { return methods.x(d.Label); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { return methods.y(d.y0) - methods.y(d.y1); })
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { methods.y(d.y0) - methods.y(d.y1); })
.remove();
//__morph bars
I've managed to narrow down the problem to the setDBlock function.
It appears if another chart has the same set of data, it takes on additional object parameters inside the dblock obj.
http://jsfiddle.net/XnngU/44/
I'm not sure at this stage as to how to clean it up. But I have isolated this via a legend and a function.
setDBlocks: function(incomingdata){
var data = incomingdata.slice(0);
methods.color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Label"; }));
data.forEach(function(d) {
console.log("D", d);
var y0 = 0;
if(d["blocks"] == undefined){
d.blocks = methods.color.domain().map(function(name) {
var val = d[name];
if(isNaN(val)){
val = 0;
}
return {name: name, values: val, y0: y0, y1: y0 += +val};
});
}
d.total = d.blocks[d.blocks.length - 1].y1;
});
}
I've fixed the anomaly by deleting data in the update function. I'm not sure why though the data is not unique - it looks like if the data is the same - as the last chart - it gets modified accordingly and used again for its next chart. Is there a better way of cleaning this up - I've tried to keep objects unique and clean by cloning/splicing but maybe that is contributing towards the problem.
delete d.blocks;
delete d.total;
http://jsfiddle.net/XnngU/53/
update: function(data){
methods.el = this;
var selector = methods.el["selector"];
data.forEach(function(d) {
delete d.blocks;
delete d.total;
});
methods.animateBars(selector, data);
}
I'm not sure if I've grouped my elements properly, but my layout in d3 is like so:
var circleGroup = svg.selectAll("g")
.data(nodeList)
.enter()
.append("g")
This creates a bunch a groups, I need a circle in each group:
circleGroup.append("circle")
.attr("cx", function(d,i){
return coordinates[i][0];
})
.attr("cy", function(d,i){
return coordinates[i][1];
})
.attr("r", function(d){
return 10;
})
.attr("fill", "white");
The data itself doesn't actually have any coordinate data so I dynamically arrange them in a circle and just position them based on index. I also add some labels. I repeat coordinates[i][0] here but is there a way to access the "cx" and "cy" attributes of the circles? I tried a few forms of d3.select(this) but I'm getting nothing.
circleGroup.append("text")
.attr("x", function(d,i){
return coordinates[i][0];
})
.attr("y", function(d,i){
return coordinates[i][1];
})
.style("text-anchor","middle")
.text(function(d,i){
return d;
});
Don't mess with indices, this is hard to maintain and error prone. Instead of that, given your specific tree structure, use node.previousSibling:
circleGroup.append("text")
.attr("x", function() {
return d3.select(this.previousSibling).attr("cx");
})
.attr("y", function() {
return d3.select(this.previousSibling).attr("cy");
})
Here is a demo using (most of) your code:
var svg = d3.select("svg")
var circleGroup = svg.selectAll("g")
.data(d3.range(5))
.enter()
.append("g");
circleGroup.append("circle")
.attr("cx", function(d, i) {
return 20 + Math.random() * 280;
})
.attr("cy", function(d, i) {
return 20 + Math.random() * 130;
})
.attr("r", function(d) {
return 10;
})
.style("opacity", 0.2);
circleGroup.append("text")
.attr("x", function() {
return d3.select(this.previousSibling).attr("cx");
})
.attr("y", function() {
return d3.select(this.previousSibling).attr("cy");
})
.style("text-anchor", "middle")
.text("Foo");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I have a treemap I put together with d3.js. I populate the data via getJSON. It works great. However, I have this functionality in a setInterval method and it doesnt seem to be refreshing itself.
var treemap = d3.layout.treemap()
.padding(4)
.size([w, h])
.value(function(d) { return d.size; });
var svg = d3.select("#chart").append("svg")
.style("position", "relative")
.style("width", w + "px")
.style("height", h + "px");
function redraw3(json) {
var cell = svg.data([json]).selectAll("g")
.data(treemap)
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
cell.append("rect")
.attr("width", function(d) { return d.dx; })
.attr("height", function(d) { return d.dy; })
.style("fill", function(d) { return d.children ? color(d.data.name) : null; });
cell.append("text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.children ? null : d.data.name; });
}
setInterval(function() {
d3.json("http://localhost:8080/dev_tests/d3/examples/data/flare2.json", function(json) {
redraw3(json);
});
}, 3000);
My question specifically, is why when I change data in the json file doesn't it show up 3 seconds later in the treemap?
Thank you in advance.
What's in the data? Because if the data array has the same length, the enter() selection (which corresponds to previously unbound data) will have a length of zero. Mike Bostock wrote a great tutorial called Thinking with Joins, which I would recommend reading before you go any further.
The svg.data() call seems redundant, and for clarity's sake I'd recommend doing this instead:
var leaves = treemap(json);
console.log("leaves:", leaves); // so you can see what's happening
// cell here is the bound selection, which has 3 parts
var cell = svg.selectAll("g")
.data(leaves);
// you might want to console.log(cell) here too so you can take a look
// 1. the entering selection is new stuff
var entering = cell.enter()
.append("g")
entering.append("rect")
// [update rectangles]
entering.append("text")
// [update text]
// 2. the exiting selection is old stuff
cell.exit().remove();
// 3. everything else is the "updating" selection
cell.select("rect")
// [update rectangles]
cell.select("text")
// [update text]
You can also encapsulate the updating of cells in a function and "call" it on both the entering and updating selections, so you don't have to write the same code twice:
function update() {
cell.select("rect")
// [update rectangles]
cell.select("text")
// [update text]
}
entering.append("rect");
entering.append("text");
entering.call(update);
cell.call(update);