D3 get attributes from element - javascript

I am trying some basic d3 and i have been trying to get the attributes of each of the rect using d3 but I am not able to get anything.
When i try d3.selectAll("rect"), I get
How do can i access attributes of rect by using something like d3.selectAll("rect").select("part1").attr(...) or something similar? I want to access different attributes of all rect.

You can get any attribute of an element using a getter:
d3.select(foo).attr("bar")
Which is basically the attr() function with just one argument.
Here is a demo. There are two classes of rectangles, part1 and part2. I'm selecting all part1 rectangles and getting their x positions:
var svg = d3.select("svg");
var rects = svg.selectAll(null)
.data(d3.range(14))
.enter()
.append("rect")
.attr("fill", "teal")
.attr("y", 20)
.attr("x", d => 10 + 12 * d)
.attr("height", 40)
.attr("width", 10)
.attr("class", d => d % 2 === 0 ? "part1" : "part2");
d3.selectAll(".part1").each(function(d,i) {
console.log("The x position of the rect #" + i + " is " + d3.select(this).attr("x"))
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Related

D3js legend color does not match to the map color javascript

I have a map already drawed. I would like to add a legend using d3.js. For example when filering by length, the map should show differents colors. Since a week, I couldn't achieve this task. My map color seem to be good but the legend does not match.
Could anybody help me with my draw link function ?
https://jsfiddle.net/aba2s/xbn9euh0/12/)
I think it's the error is about the legend function.
Here is the function that change my map color Roads.eachLayer(function (layer) {layer.setStyle({fillColor: colorscale(layer.feature.properties.length)})});
function drawLinkLegend(dataset, colorscale, min, max) {
// Show label
linkLabel.style.display = 'block'
var legendWidth = 100
legendMargin = 10
legendLength = document.getElementById('legend-links-container').offsetHeight - 2*legendMargin
legendIntervals = Object.keys(colorscale).length
legendScale = legendLength/legendIntervals
// Add legend
var legendSvg = d3.select('#legend-links-svg')
.append('g')
.attr("id", "linkLegendSvg");
var bars = legendSvg.selectAll(".bars")
//.data(d3.range(legendIntervals), function(d) { return d})
.data(dataset)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return legendMargin + legendScale * (legendIntervals - i-1); })
.attr("height", legendScale)
.attr("width", legendWidth-50)
.style("fill", function(d) { return colorscale(d) })
// create a scale and axis for the legend
var legendAxis = d3.scaleLinear()
.domain([min, max])
.range([legendLength, 0]);
legendSvg.append("g")
.attr("class", "legend axis")
.attr("transform", "translate(" + (legendWidth - 50) + ", " + legendMargin + ")")
.call(d3.axisRight().scale(legendAxis).ticks(10))
}
D3 expects your data array to represent the elements you are creating. It appears you are passing an array of all your features: but you want your scale to represent intervals. It looks like you have attempted this approach, but you haven't quite got it.
We want to access the minimum and maximum values that will be provided to the scale. To do so we can use scale.domain() which returns an array containing the extent of the domain, the min and max values.
We can then create a dataset that contains values between (and including) these two endpoints.
Lastly, we can calculate their required height based on how high the visual scale is supposed to be by dividing the height of the visual scale by the number of values/intervals.
Then we can supply this information to the enter/update/exit cycle. The enter/update/exit cycle expects one item in the data array for every element in the selection - hence why need to create a new dataset.
Something like the following shold work:
var dif = colorscale.domain()[1] - colorscale.domain()[0];
var intervals = d3.range(20).map(function(d,i) {
return dif * i / 20 + colorscale.domain()[0]
})
intervals.push(colorscale.domain()[1]);
var intervalHeight = legendLength / intervals.length;
var bars = legendSvg.selectAll(".bars")
.data(intervals)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return Math.round((intervals.length - 1 - i) * intervalHeight) + legendMargin; })
.attr("height", intervalHeight)
.attr("width", legendWidth-50)
.style("fill", function(d, i) { return colorscale(d) })
In troubleshooting your existing code, you can see you have too many elements in the DOM when representing the scale. Also, Object.keys(colorscale).length won't produce information useful for generating intervals - the keys of the scale are not dependent on the data.
eg

Extra legend present

I adding two legends to my bar chart but I don't know why there is a extra legend appear. I don't know which part of my code is wrong since I only define two legend in my code.
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var legend = svg.append("g")
.attr("class", "legend")
.attr("x", width - 65)
.attr("y", 25)
.attr("height", 100)
.attr("width", 100);
legend.selectAll('g').data(data)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25)
.attr("width", 10)
.attr("height", 10)
.style("fill", color_hash[String(i)][1]);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 8)
.attr("height", 30)
.attr("width", 100)
.style("fill", color_hash[String(i)[1]])
.text(color_hash[String(i)][0]);
});
the black rectangle is the extra one:
With the enter/update/exit cycle in D3, you generally want to have a data array that contains one item for every element you want drawn. You have:
a color has object color_hash, this is what you really want to use to draw the legend, and
some data array data, though we don't know what is inside of this.
We are using data to visualize color_hash, this is not ideal.
For one, you only want to plot 2 elements, I can tell you that the length of data is at least 3:
You create an empty g with:
var legend = svg.append("g")
Then you select child g elements of that:
legend.selectAll('g')
Since there are none, this is an empty selection. Then you assign data to this selection and enter new HTML/SVG elements:
legend.selectAll('g')
.data(data)
.enter()
.append('g')
Since legend is an empty selection, the enter selection will create one HTML/SVG element for each item in the data array. After entering (and/or exiting), the number of HTML/SVG elements should be equal to the number of items in the data array. So, data must have at least 3 items in it (it could have more if additional elements are created, but they fall outside of the SVG/container bounds. This also explains why the third box has no color or text: the color hash has no values with key 2 or greater).
D3 is creates elements from data, generally in a one to one relationship between elements and items. To create our legend, the data array should be what we want to plot. As a consequence, we need to convert the color hash to an array:
var legendData = [
{name: "A", color:"crimson"},
{name: "B", color:"steelblue"}
];
Now we just supply that to selection.data()
And, since we are now binding the data we want to draw to the legend entries, we can also simplify the code, instead of:
.style("fill", color_hash[String(i)][1]);
and
.text(color_hash[String(i)][0]);
We can just use:
.style("fill",d.color);
and
.text(d.name);
This gives us:
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var width = 300;
var height = 200;
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
var legendData = [
{name:"A",color:"crimson"},
{name:"B",color:"steelblue"}
]
var legend = svg.append("g")
legend.selectAll('g')
.data(legendData)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25+25)
.attr("width", 10)
.attr("height", 10)
.style("fill", d.color);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 33)
.attr("height", 30)
.attr("width", 100)
.style("fill", d.color)
.text(d.name);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I'm just focusing on the enter cycle here: there could be further revisions to placement and a different approach to nested appends can offer benefits compared to appending children with .each

d3 how to tie text to top right corner of view port while zooming and panning

I am creating a mapping application in d3 and want to tie some text to the top right corner of my view port. Additionally, I want the text to remain in the top right corner while I zoom and pan across the application.I think I can solve my problem by figuring out how to get the coordinates of the top right corner of my view. Knowing this information would allow me to then set the coordinates of my text element. I've tried manually setting the dimensions of the containing svg element and then moving the text to that location but interestingly this didn't work. I was hoping to be able to find the coordinates programatically rather than setting coordinates manually. How can I do this in d3/javascript?
EDIT:
My code is a modification of this code by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM
My own zooming and panning code has essentially remained the same as the above example:
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
I'm trying to append the text at the very bottom of the code:
countriesGroup.append("text")
.attr("transform", "translate(" How do I get top right coordinates? ")")
.style("fill", "#ff0000")
.attr("font-size", "50px")
.text("This is a test");
My idea is to be able to get the top right coordinates of the view port through the code rather than setting it manually and then have the coordinates of the text update as the user zooms or pans.
To keep something in place while zooming and panning you could invert the zoom:
point == invertZoom(applyZoom(point))
This isn't particularly efficient, as we are using two operations to get to the original number. The zoom is applied here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")");
While the inversion would need to look something like:
text.attr("x", d3.zoom.transform.invert(point)[0])
.attr("y", d3.zoom.transform.invert(point)[1])
.attr("font-size", baseFontSize / d3.zoom.transform.k);
Where point and base font size are the original anchor point and font size. This means storing that data somewhere. In the example below I assign it as a datum to the text element:
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g")
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
var text = g.append("text")
.datum({x: width-10, y: 20, fontSize: 12})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("text-anchor","end")
.attr("font-size",function(d) { return d.fontSize; })
.text("This is a test");
function zoomed() {
g.attr("transform", d3.event.transform);
var d = text.datum();
var p = d3.event.transform.invert([d.x,d.y]);
var x1 = p[0];
var y1 = p[1];
text.attr("x",x1)
.attr("y",y1)
.attr("font-size", d.fontSize / d3.event.transform.k)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Better Solution
The above is the solution to the approach you seem to be looking for. But the end result is best achieved by a different method. As I mention in my comment, the above approach goes through extra steps that can be avoided. There can also be some size/clarity changes in the text when zooming (quickly) using the above method
As noted above, you are applying the zoom here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
The zoom transform is applied only to countriesGroup, if your label happens to be in a different g (and not a child of countriesGroup), it won't be scaled or panned.
We wouldn't need to apply and invert the zoom, and we wouldn't need to update the position or font size of the text at all.
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g");
var g2 = svg.append("g"); // order does matter in layering
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
// position once and leave it alone:
var text = g2.append("text")
.attr("x", width - 10)
.attr("y", 20 )
.style("text-anchor","end")
.attr("font-size", 12)
.text("This is a test");
function zoomed() {
// apply the zoom to the g that has zoomable content:
g.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

d3 chord: center text on circle

I use the d3 chord diagram example of Andrew and want to center all text labels within the curved slice. I tried many things but was never able to center the texts. Do you know what wizzard trick there is needed?
var width = 720,
height = 720,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(".1%");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("ex_csv.csv", function(cities) {
d3.json("ex_json.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; });
// Remove the labels that don't fit. :(
groupText.filter(function(d, i) { return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength(); })
.remove();
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
}
});
});
</script>
As an aside, I would suggest looking to upgrade to v4, documentation for v2 is nearly non-existent and is very hard to help with.
You can set both the text-anchor and the startOffset property to achieve what you are looking for.
First, you'll want to set text-anchor to middle as it is easier to specify the middle point than to find the middle point and work back to find where the text should start.
Second you'll need to set a startOffset. Note that if you use 50%, the text will not appear where you want, as the total length of the text path is all sides of the closed loop (chord anchor) you are appending to. Setting it to 25 % would work if you did not have a different outer and inner radius. But, as you have an outer radius that is 24 pixels greater than the inner radius you can try something like this to calculate the number of pixels you need to offset the center of the text:
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; })
.attr("startOffset",function(d,i) { return (groupPath[0][i].getTotalLength() - 48) / 4 })
.style("text-anchor","middle");
I subtract 48 because the sides of the anchor are 24 pixels each (the difference in the radii). I divide by four because the path doubles back on itself. If it was a general line I would just divide by two.
This approach is a little simplistic as the outer circumference is not the same as the inner circumference of each chord anchor, so I am off by a little bit, but it should be workable.
For labels that are on the cusp of being displayed, this will be awkward: the inner radius is shorter, so the formula for deteriming if a string is short enough to be displayed may be wrong - which may lead to some characters climbing up the side of the anchor (your example also 16 pixels as the difference in radii to calculate if text is too long, rather than 24).
This is the end result:
Here is a demonstration.

d3 rect not showing up

I want to display the legend (caption?) of some data.
For each part of the legend, I append a rect and a p to a parent division.
The problem is that rect are not showing up.
Here is my code:
var groups = {{ groups|safe }};
groups.forEach(function(group, i) {
var div = d3.select("#collapse-legend");
div.append("rect")
.attr("width", 17)
.attr("height", 17)
.style("fill-opacity", 1)
.style("fill", function (d) { return c(i); });
div.append("p").text(group)
});
Now, when I check the content of my web page, I get both rect and p, but rect:
is not showing up
seems to have a width of 0 (showing its area with firefox)
Is there some mistake in my code? Are there better ways to achieve this? I am very new to javascript and d3.js so please be indulgent ^^
Update
So this is what I ended with.
HTML:
<div ...>
<svg id="legend-svg"></svg>
</div>
JavaScript:
// set height of svg
d3.select("#legend-svg").attr("height", 18*(groups.length+1));
// for each group, append rect then text
groups.forEach(function(group, i) {
d3.select("#legend-svg").append("rect")
.attr("y", i*20)
.attr("width", 18)
.attr("height", 18)
.style("fill-opacity", 1)
.style("fill", function (d) { return c(i); });
d3.select("#legend-svg").append("text")
.attr("x", 25)
.attr("y", i*20+15)
.text(group);
});
SVG elements like <rect> cannot be direct children of html <div> elements. You must put them inside an <svg> container element.
SET X + Y values for rect :
.attr("x", 50)
.attr("y", 50)

Categories

Resources