how to sort a shape selection in d3 - javascript

I need to change the order of all the shapes created by d3 in a svg container. But I'm unable to do it. The following script in JSFiddle, when executed locally, gives "a and b not defined". How should it be corrected?
var svg = d3.select("body")
.append("svg")
.attr("width", 100)
.attr("height", 100);
svg.append("rect")
.attr({x:0,y:0,height:10,width:10,fill:'yellow'})
.attr("sort",3);
svg.append("rect")
.attr({x:0,y:20,height:10,width:10,fill:'red'})
.attr("sort",2);
svg.append("rect")
.attr({x:0,y:40,height:10,width:10,fill:'blue'})
.attr("sort",1);
d3.selectAll("rect").sort(function(a,b){return a.attr("sort")-b.attr("sort")});
d3.selectAll("rect")
.each(function(d,i){alert(d3.select(this).attr("sort"))});

You need to sort like this:
var rects = d3.selectAll("rect")[0].sort(function(a, b) {
return d3.select(a).attr("sort") - d3.select(b).attr("sort")
}); //here rects will store the correct sorted order rectangle DOMs.
instead of doing like this:
d3.selectAll("rect").sort(function(a,b){return a.attr("sort")-b.attr("sort")});
Now to change the rectangle as per the sort do like this:
rects.forEach(function(rect, i){
d3.select(rect).attr("y", (i*20));//change the attribute of y
})
working code here
NOTE: I am just answering this as per the info you have provided but the correct approach would be to do it the data way suggested by #meetamit in the comment.

Related

Datum-Data difference in map behavior in d3

I'm pretty new to d3js and trying to understand the difference between using data and datum to attach data to elements. I've done a fair bit of reading the material online and I think I theoretically understand what's going on but I still lack an intuitive understanding. Specifically, I have a case where I'm creating a map using topojson. I'm using d3js v7.
In the first instance, I have the following code to create the map within a div (assume height, width, projection etc. setup correctly):
var svg = d3.select("div#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath()
.projection(projection);
var mapGroup = svg.append("g");
d3.json("json/world-110m.json").then(function(world){
console.log(topojson.feature(world, world.objects.land))
mapGroup.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
The console log for the topojson feature looks like this:
And the map comes out fine (with styling specified in a css file):
But if I change datum to data, the map disappears. I'm trying to improve my understanding of how this is working and I'm struggling a little bit after having read what I can find online. Can someone explain the difference between data and datum as used in this case and why one works and the other doesn't?
Thanks for your help!
There are several differences between data() and datum(), but for the scope of your question the main difference is that data() accepts only 3 things:
An array;
A function;
Nothing (in that case, it's a getter);
As you can see, topojson.feature(world, world.objects.land) is an object. Thus, all you'd need to use data() here (again, not the idiomatic D3, I'm just addressing your specific question) is wrapping it with an array:
.data([topojson.feature(world, world.objects.land)])
Here is your code using data():
var svg = d3.select("div#map").append("svg")
.attr("width", 500)
.attr("height", 300)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath();
var mapGroup = svg.append("g");
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json").then(function(world) {
const projection = d3.geoEqualEarth()
.fitExtent([
[0, 0],
[500, 300]
], topojson.feature(world, world.objects.land));
path.projection(projection);
mapGroup.append("path")
.data([topojson.feature(world, world.objects.land)])
.attr("class", "land")
.attr("d", path);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="map"></div>

D3 projection not setting cx property

I have a map of the USA that I'm trying to display lat/lon points over. I've mashed together a few examples to get this far, but I've hit a wall. My points are in a csv file, which I'm not sure how to upload here, but it's just 65,000 rows of number pairs. For instance 31.4671154,-84.9486771.
I'm mostly following the example from Scott Murray's book here.
I'm using the Albers USA projection.
var projection = d3.geo.albersUsa()
.scale(1200)
.translate([w / 2, h / 2]);
And setting up the landmarks as an svg group appended to the map container.
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.on("click", stopped, true);
svg.append("rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h)
.on("click", reset);
var g = svg.append("g");
var landmarks = svg.append("g")
I read the data and try to set circles at each lat/lon point.
d3.csv("./public/assets/data/landmark_latlon_edited.csv", function(error, latlon){
console.log(latlon);
landmarks.selectAll("circle")
.data(latlon)
.enter()
.append("circle")
.attr({
'fill': '#F00',
'r': 3
})
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
.attr('cy', function(d){
return projection([d.lon, d.lat])[1];
})
.style({
'opacity': .75
});
});
Now, the problem is that the cx property is not receiving a value. When viewed in the inspector the circles don't show a cx, and, indeed, appear in the svg at the appropriate y values, but in a stacked column at x=0.
<circle fill="#F00" r="3" cy="520.8602676002965" style="opacity: 0.75;"></circle>
I found an old issue I thought might be related here which states that the projection method will return null if you try to feed it values outside of its normal bounding box. I opened the csv in Tableau and saw a couple values that were in Canada or some U.S. territory in the middle of the Pacific (not Hawaii), and I removed those, but that didn't solve the problem.
I'm decidedly novice here, and I'm sure I'm missing something obvious, but if anyone can help me figure out where to look I would greatly appreciate it. Lots of positive vibes for you. If I can add anything to clarify the problem please let me know.
Thanks,
Brian
I had the same problem when I updated to d3 v3.5.6. Here is what I did to check for null values, so that you don't try to access the [0] position of null:
.attr("cx", function(d) {
var coords = projection([d.lon, d.lat]);
if (coords) {
return coords[0];
}
})
I'm sure there is a cleaner way to do this, but it worked for me.
You have a little error in your function generating cx values which messes it all up. It's just one parenthesis in the wrong place:
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
By coding [d.lon, d.lat][0] you are just passing the first value of the array, which is d.lon, to the projection and are returning the result of projection() which is an array. Instead, you have to place the [0] outside the call of projection() because you want to access the value it returned. Check your function for cy where you got things right. Adjusting it as follows should yield the correct values for cx:
.attr('cx', function(d){
return projection([d.lon, d.lat])[0];
})

Making a d3 chart update with new data

I have an example of a chart on jsFiddle which has multiple groups of multiple lines. It draws successfully, but I would like to be able to transition to new sets of data.
The example should update with new data after 4 seconds. Although the chart gets called (and outputs something in the console), the lines aren't updated.
I've tried lots of variations based on existing, simpler examples, with no luck. I suspect its the nested data based on this SO answer that's confusing me even more than usual.
SO insists I have some code in the answer, so here's where I assume I need to add/change something:
svg.transition().attr({ width: width, height: height });
g.attr('transform', 'translate(' + margin.left +','+ margin.right + ')');
var lines = g.selectAll("g.lines").data(function(d) { return d; });
lines.enter().append("g").attr("class", "lines");
lines.selectAll("path.line.imports")
.data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports');
lines.selectAll('path.line.imports')
.data(function(d) { return [d.values]; })
.transition()
.attr("d", function(d) { return imports_line(d); });
// repeated for path.line.exports with exports_line(d).
The problem is the way you're determining the top-level g element that everything is appended to:
var g = svg.enter().append('svg').append('g');
This will only be set if there is no SVG already, as you're only handling the enter selection. If it exists, g will be empty and therefore nothing will happen. To fix, select the g explicitly afterwards:
var g = svg.enter().append('svg').append('g').attr("class", "main");
g = svg.select("g.main");
Complete demo here.

d3.js dynamic data reload possibly related to exit().remove()

I have implemented this d3 visualization http://bl.ocks.org/4745936 , to be loaded with dynamic data instead of a .tsv
in my case, once my server passes new information to the selector, a second chart gets rendered under the first one, instead of modifying the contents of the existing graph.
I believe it has to do with this append method.
var svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
so I tried adding other exit().remove() methods to legend and cities variables right after they append('g'); but my javascript console says the exit() method does not exist at that point.
I feel I have the completely wrong approach, how do I update an existing graph like this? Having a second and third graph get generated alongside the previous ones is not the outcome I wanted at all
You're right the append method is adding a new svg element every time. To prevent the duplicate charts you need to check if the svg element exists already. So try something like this at the begining:
var svg = d3.select("#mycontainer > svg")
if (svg.empty())
svg = d3.select(selector).append("svg");
...
As stated in the exit() docs, This method is only defined on a selection returned by the data operator. So make sure that you're calling exit on a selection returned from .data(..).
scott's answer is one way of ensuring that the initialization happens only once.
However, I prefer a more d3-ic way of handling this:
var svg = d3.select(selector)
.selectAll('svg')
.data( [ dataFromTSV ] ); // 1 element array -> 1 svg element
// This will be empty if the `svg` element already exists.
var gEnter = svg.enter()
.append('svg')
.append('g');
gEnter.append( ... ); // Other elements to be appended only once like axis
svg.attr('width', ...)
.attr('height', ...);
// Finally, working with the elements which are surely in the DOM.
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.selectAll(...).attr(...);
This approach is exemplified in the reusable charts example's source code.
I prefer this approach because it keeps the code very declarative and true to the visualisation by hiding away the logic of initialisation and updates.
I would modify the original example: http://jsfiddle.net/8Axn7/5/ to http://jsfiddle.net/3Ztt8/
Both the legend and the graph are defined from svgElem with one single element of data:
var svgElem = d3.select("#multiLinegraph").selectAll('svg')
.data([cities]);
// ...
var svg = svgElem.select('g');
// ...
var city = svg.selectAll(".city")
.data(
function (d) { return d; },
function (d) { return d.name; } // Object consistency
);
// ...
var legend = svg.selectAll('g.legend')
.data(
function(d) { return d; },
function (d) { return d.name; } // Object consistency
);
Also, the static properties are set only once when the element is entered (or exited), while the update properties are set (transitioned) with each update:
gEnter.append("g")
.attr("class", "y multiLineaxis")
.append('text')
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Requests (#)");
svg.select('g.y.multiLineaxis').transition().call(yAxis);
The code, in my opinion, follows the cycle of enter-update-exit cleanly.
I was able to solve this problem with some jQuery and CSS voodoo
basically since my d3 graph adds an svg element to an existing selector (a div in my case), I was able to check for the name of this dynamically
var svgtest = d3.select(selector+" > svg"); getting the svg subchild element of that div. then I could use jquery to remove that element from the dom completely, and then let d3 continue running and append svg's all it wants!
var svgtest = d3.select(selector+" > svg");
if(!svgtest.empty())
{
$(selector+" > svg").remove();
}
First of all you should remove old svg, after then you can add updated charts.
For that you should add only one line before you append svg.
And its working.
var flag=d3.select("selector svg").remove();
//----your old code would be start here-------
var svg = d3.select(selector).append("svg")

Proper format for drawing polygon data in D3

I've tried this different ways, but nothing seems to be working. Here is what I currently have:
var vis = d3.select("#chart").append("svg")
.attr("width", 1000)
.attr("height", 667),
scaleX = d3.scale.linear()
.domain([-30,30])
.range([0,600]),
scaleY = d3.scale.linear()
.domain([0,50])
.range([500,0]),
poly = [{"x":0, "y":25},
{"x":8.5,"y":23.4},
{"x":13.0,"y":21.0},
{"x":19.0,"y":15.5}];
vis.selectAll("polygon")
.data(poly)
.enter()
.append("polygon")
.attr("points",function(d) {
return [scaleX(d.x),scaleY(d.y)].join(",")})
.attr("stroke","black")
.attr("stroke-width",2);
I assume the problem here is either with the way I am defining the points data as an array of individual point objects, or something to do with how I'm writing the function for the .attr("points",...
I've been looking all over the web for a tutorial or example of how to draw a simple polygon, but I just can't seem to find it.
A polygon looks something like: <polygon points="200,10 250,190 160,210" />
So, your full poly array should produce one long string that will create one polygon. Because we are talking about one polygon the data join should also be an array with only one element. That is why the code below shows: data([poly]) if you wanted two identical polygons you would change this to data([poly, poly]).
The goal is to create one string from your array with 4 objects. I used a map and another join for this:
poly = [{"x":0.0, "y":25.0},
{"x":8.5,"y":23.4},
{"x":13.0,"y":21.0},
{"x":19.0,"y":15.5}];
vis.selectAll("polygon")
.data([poly])
.enter().append("polygon")
.attr("points",function(d) {
return d.map(function(d) {
return [scaleX(d.x),scaleY(d.y)].join(",");
}).join(" ");
})
.attr("stroke","black")
.attr("stroke-width",2);
See working fiddle: http://jsfiddle.net/4xXQT/
The above answers are needlessly complicated.
All you have to do is specify the points as a string and everything works fine. Something like this below will work.
var canvas = d3.select("body")
.append("svg")
.attr("height", 600)
.attr("width", 600);
canvas.append("polygon")
.attr("points", "200,10 250,190 160,210")
.style("fill", "green")
.style("stroke", "black")
.style("strokeWidth", "10px");
are you trying to draw polygon shapes? - like this. http://bl.ocks.org/2271944 The start of your code looks like a typical chart - which would usually conclude something like this.
chart.selectAll("line")
.data(x.ticks(10))
.enter().append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2", 120)
.style("stroke", "#ccc");

Categories