Change style on points, d3 - javascript

I have this d3 polygons based on a dataset.
let svg = d3.select($element[0]).append('svg')
.attr("width", 800)
.attr("height", 800);
let myGroups = svg.selectAll('g').data(rooms);
let myGroupsEnter = myGroups.enter().append("g");
myGroupsEnter.append("polygon")
.style("fill", "none")
.style("stroke", "black")
.style("strokeWidth", "10px")
.attr("points", function (d) {
let q = d.footprint.coordinates.map(function (point, idx) {
let _d = [];
_d.push(scale(point[0]));
_d.push(scale(point[1]));
return _d;
});
return q.join(" ");
});
});
I want to change the style fill attribute on the items that meets a requirment when user selects a value from a radiobutton.
Can I somehow update this attribute? Or do I need to redraw everything?

I think this is what you're looking for.
.style('fill', function(d) {
/* match item (d) with some condition */
})
On value change as well, you can just restyle/refill the polygons:
svg.selectAll('polygon').style('fill', function(d) {
/* match item (d) with value selected */
});

Related

d3.js add second label to bar chart

2 part question:
I have a bar chart with created using multiple arrays. These arrays contain the % wins of baseball teams; the relevant team colours; and their names.
I can create one set of labels on the chart, either the names or the win %. However I can't get both on at the same time. See below.
The code I am using is:
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
WinsLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 685 - d[1];
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "black")
.text(function(d){
return d[1]/10 + "%";
});
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
TeamLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 700 - d[1]/2;
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "white")
.text(function(d){
return d[0];
});
When I run the code with both scripts, only the win % shows up, but the names don't. In order to get the names to show up I have to remove the first label.
The 2 parts to my question are:
How would I get both sets of labels to show up at the same time?
How can I get the names to be arranged vertically in the rectangles/bars?
D3 stands for data driven something; and it's core principle is based on linking elements / selection, with data. When you set data, (var selection = selectAll(...).data(...)), you get 3 cases to think about:
Some existing elements can be linked to certain item in new data. You access them using selection
Some elements cannot be linked to any item in new data. You access them using selection.exit()
Some items in new data cannot be linked to any element from selection. You access them by using selection.enter()
In its simplest case, the linking between data and elements is made by index -- ie first element in selection is linked with first item in data array, second with second, and so on. The d3 cannot find element for the data item (= gets put into .enter() selection) if and only if (in this by-index context) the index of that data item is bigger than the size of the selection.
On your initial select
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is empty, since there are no text tags yet. And since its empty, all of the to-be-created placeholders are inside .enter() selection. However, on your next select for the other label type
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is of the size of the passed data, and thus .enter() selection is empty; it's the TeamLabel selection that contains all of the old elements (percentage label text tags), but they got their data values reassigned.
Andrew proposed one solution to assign classes, but personally I'd take all elements that relate to same team and put it under one group.
var TeamArray = ["Yankees", "Rays", "RedSox", "Jays","Orioles", "Twin", "Indians", "WhiteSox", "Detroit", "Royals", "Astros", "Rangers", "A's", "Angels","Mariners"];
var WinPercArray = [653, 609, 540, 400, 300, 667, 521, 458, 383, 347, 660, 511, 500, 458, 442];
var Colours = ["#003087", "#092C5C", "#BD3039", "#134A8E", "#DF4601", "#002B5C", "#0C2340", "#C4CED4", "#FA4616", "#BD9B60", "#EB6E1F", "#C0111F", "#003831", "#003263", "#005C5C"];
var data = d3.zip(TeamArray, WinPercArray, Colours);
var svg = d3.select('body').append('svg').attr('height', 300).attr('width', 800);
var teams = svg.selectAll('g.teams')
.data(data);
var scale = d3.scaleLinear()
.domain([0, 1000])
.range([200, 0]);
var teamsEnter = teams.enter()
.append('g')
.classed('team', true)
.attr('transform', function(d, i){
return 'translate(' + (i*50) + ',0)';
})
teamsEnter.append('rect')
.attr('width', 50)
.attr('y', function(d) { return scale(d[1]); })
.attr('height', function(d) { return scale(0) - scale(d[1]); })
.style('fill', function(d) { return d[2]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 30; })
.text(function(d){ return d[0]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 15; })
.text(function(d){ return d[1]; });
text {
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Groups in some way act as encapsulation of inner items, so you can mentally separate data binding to groups (ie when to create / update / delete it), from actual logic that takes place when working with its children

D3 update pattern drawing the same element twice

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>

JavaScript/d3 - Assign Attribute within Another Attribute [duplicate]

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.

maintaining the layering of elements after adding new elements

I'm drawing a little clickable graph data browser.
Example:
First, I load a few movies, and I see this:
Then, after I click on one of the nodes (Hellraiser, in this case), I use ajax to load additional related information properties and values, and end up with this:
The lines and circles of the newly added nodes are obviously drawn after the originally clicked node was.
Here is the draw method that gets called every time new data is ready to be added to the graph:
function draw() {
force.start();
//Create edges as lines
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 2)
.on("mouseover", lineMouseover)
.on("mouseout", lineMouseout);
//create the nodes
var node = svg.selectAll(".node")
.data(dataset.nodes)
.enter()
.append("g")
.attr("class", "node")
.on("click", callback)
.attr("r", function(d, i) { //custom sizes based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].size;
} else {
return _design["other"].size;
}
})
.call(force.drag);
//create fancy outlines on the nodes
node.append("circle")
.attr("r", function(d,i) { //custom sizes based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].size * r;
} else {
return _design["other"].size * r;
}
})
.style("stroke", "black")
.style("stroke-width", 3)
.style("fill", function(d, i) { //custom color based on datatype
if(d.datatype && (d.datatype in _design) ) {
return _design[d.datatype].color;
} else {
return _design["other"].color;
}
})
.attr("class","circle");
//Add text to each node.
node.append("text")
.attr("dx", 0)
.attr("dy", ".25em")
//.attr("class", "outline")
.attr("fill", "black")
.text(function(d, i) {
return d.name;//d.name
});
};
How do I go about drawing those lines underneath the clicked node?
You can group the different kinds of elements below g elements that you can create at the beginning in the required order. This way, anything you append to them later will be ordered correctly:
var links = svg.append("g"),
nodes = svg.append("g"),
labels = svg.append("g");
// ...
var edges = links.selectAll("line")
.data(dataset.edges)
.enter()
.append("line");
var node = nodes.selectAll(".node")
.data(dataset.nodes)
.enter()
.append("g")
// etc.

Get width of d3.js SVG text element after it's created

I'm trying to get the widths of a bunch of text elements I have created with d3.js
This is how I'm creating them:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
});
I'm then using the width to create rectangles the same size as the text's boxes
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 25;
})
.attr("width", function(d, i) {
//To Do: find width of each text element, after it has been generated
var textWidth = svg.selectAll("text")
.each(function () {
return d3.select(this.getComputedTextLength());
});
console.log(textWidth);
return textWidth;
})
.attr("height", function(d) {
return 30;
})
I tried using the Bbox method from here but I don't really understand it. I think selecting the actual element is where I'm going wrong really.
I would make the length part of the original data:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
})
.each(function(d) {
d.width = this.getBBox().width;
});
and then later
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", function(d) { return d.width; });
You can use getBoundingClientRect()
Example:
.style('top', function (d) {
var currElemHeight = this.getBoundingClientRect().height;
}
edit: seems like its more appropriate for HTML elements. for SVG elements you can use getBBbox() instead.
d3.selectAll returns a selection. You can get each of the elements by navigating through the array in the _groups property. When you are determining the width of a rectangle, you can use its index to get the corresponding text element:
.attr('width', function (d, i) {
var textSelection = d3.selectAll('text');
return textSelection._groups[0][i].getComputedTextLength();
});
The _groups property of d3's selection has a list of nodes at [0]. This list contains all of the selected elements, which you can access by index. It's important that you get the SVG element so that you can use the getComputedTextLength method.
You may also want to consider creating the rect elements first, then the text elements, and then going back to the rectangles to edit the width attribute, so that the text elements are on top of the rectangles (in case you want to fill the rectangles with color).
Update:
It's typically preferred that you don't access _groups, though, so a safer way to get the matching text element's width would be:
.attr('width', function (d, i) {
return d3.selectAll('text').filter(function (d, j) { return i === j; })
.node().getComputedTextLength();
});
Using node safely retrieves the element, and filter will find the text element which matches index.

Categories