Datum-Data difference in map behavior in d3 - javascript

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>

Related

Programmatically control brush and pass relevant information

this is an extension of this question from yesterday. I am attempting to draw and create multiple brushes. The bruteforce method i am using does ok, but i need additional functionality to programmatically control each of these newly created brushes,
createBrush(){
//Brushes
var brushGroups = []
for(var i=0; i<this.uniqueCats.length; i++){
//BRUSH CONTAINER
brushGroups[i] = d3.select('#brushContainer')
.append('g')
.attr('id',`${this.uniqueCats[i]}brush`)
.attr('transform', `translate(${this.margin.left},0)`)
.attr('class','brush');
//BRUSH
var brushID = this.uniqueCats[i]
this[`${this.uniqueCats[i]}Brush`] = d3.brushX()
.extent([[0, (i*135)+38], [this.width,(i*135)+170]])
.on('end',(i)=>{
console.log('hi')
this.updateBubbles(i)
})
//CALL BRUSH from the brush container
brushGroups[i].call(this[`${this.uniqueCats[i]}Brush`])
}
}
As you can see above I 1) create a brush container 2) create a brush, 3) call that brush from the container. The main issue i am having it passing relevant information to the on for this.updateBubbles(). The hope was for each brush to pass in specific information to updatebubbles() that would allow me to understand which brush was being activated.
From there I decided to use the more elegant d3 approach with enter() but, this method fails to invoke a brush at all,
createBrushv2(){var that =this
var brushContainer = d3.select('#brushContainer')
.selectAll('g')
.data(this.uniqueCats)
var brushContainerEnter = brushContainer
.enter()
.append('g')
.attr('id', d=> `${d}brush`)
brushContainer = brushContainerEnter.merge(brushContainer)
.attr('transform', `translate(${this.margin.left},0)`)
.attr('class','brush')
.each( function(d,i){
console.log(i)
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end',that.updateBubbles(d))
})
}
The updateBubbles() function is whats evoked from each brush, so this function needs to be able to understand which brush is selected.
There are several issues in your code, which makes your question too broad. Therefore, I'll only deal with the issue of the brush not being invoked.
Before anything else, congratulations for moving from approach #1 (for loop) to approach #2 (D3 selections). In a D3 code, it's almost a very bad idea using loops to append anything.
That being said, unlike your first snippet, you're not actually calling the brush in the second one.
Therefore, this...
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end',that.updateBubbles(d))
Should be:
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end', that.updateBubbles)(d3.select(this))
Even better, you should use a selection.call, just like in your first approach. Also, have in mind that that.updateBubbles(d) will invoke the function immediately, and that's not what you want.
Here is a very basic snippet as a demo:
const data = d3.range(5);
const svg = d3.select("svg");
createBrushv2();
function createBrushv2() {
var brushContainer = svg.selectAll(".brush")
.data(data)
var brushContainerEnter = brushContainer.enter()
.append('g');
brushContainer = brushContainerEnter.merge(brushContainer)
.attr('transform', function(d) {
return "translate(0," + (d * 25) + ")"
})
.attr('class', 'brush')
.each(function(d) {
d3.brushX()
.extent([
[0, 0],
[300, 20]
])
.on('end', updateBubbles)(d3.select(this))
})
.each(function() {
d3.brushX().move(d3.select(this), [0, 300])
})
};
function updateBubbles(brush) {
console.log(brush)
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Can I append a literal SVG element with d3?

I'd like to append a literal SVG element with d3.
So instead of writing
svg.selectAll("circle")
.data(data)
.enter()
.append("circle") // etc etc
I'd like to do:
svg.selectAll("circle")
.data(data)
.enter()
.append('<circle cx="158.9344262295082" cy="200" r="16" fill="red"></circle>')
so that I could create a complex template elsewhere (for example with handlebars), and then compile it with data and append it.
You can do this, although not via the selection.append() function. Instead you'd need to use the selection.html() function.
This would make it quite difficult to use in the context of data-joins, but not impossible. This is probably the best you could do, which involves adding an additional svg group to the DOM which may not be a bad thing anyway:
var svg = d3.selectAll("svg");
svg.selectAll("circle")
.data([50, 100, 150])
.enter()
.append("g")
.attr("transform", function(d) { return "translate(" + [d, d] + ")"; })
.html('<circle r="16" fill="red"></circle>');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="500"></svg>
I guess taking this answer a bit further, you could actually embed the result that you wish to render directly into your data object. So you've add some code that looked like:
.html(function(d) { return d.templatedHTML; });
At this point however stop and ask yourself the question: "What am I using D3 for?". D3 is described as
Data Driven Documents
If you're using something like handlebars, then you're taking away one of the core responsibilities that D3 was designed for (building some DOM from some data) and giving it to some other library.
I'm not stating you shouldn't do that (as you did mention complex templates) but do just ask yourself the question to make sure that it's a path you wish to go down.
No, you can't. Don't believe me? Check their docs HERE
What you must do is call .append(), followed by several calls of .attr(attr_name, attr_value) to set each attribute's value. D3 does not work like jQuery.
D3 doesn't provide this functionality, and it does not make much sense (when you think about manipulating elements based on data).
But, as a side note, you can implement your own function to append a literal SVG element.
This is a function created by Chris Viau, named appendSVG:
d3.selection.prototype.appendSVG =
d3.selection.enter.prototype.appendSVG = function(SVGString) {
return this.select(function() {
return this.appendChild(document.importNode(new DOMParser()
.parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString +
'</svg>', 'application/xml').documentElement.firstChild, true));
});
};
After extending the prototype, you can use it:
selection.appendSVG("<some literal SVG element>");
Here is a demo. First, we set the data:
var data = [{x:30,y:50},{x:420,y:100},{x:160,y:150},{x:260,y:30}];
Then, we append our literal SVG element in the usual way:
var myLiteral = svg.selectAll(".literal")
.data(data)
.enter()
.appendSVG("<literal SVG here>");
And finally we set the positions using translate:
.attr("transform", function(d){
return "translate(" + d.x + "," + d.y + ")";
});
Check the snippet:
d3.selection.prototype.appendSVG = d3.selection.enter.prototype.appendSVG = function(SVGString) {return this.select(function() { return this.appendChild(document.importNode(new DOMParser().parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString + '</svg>', 'application/xml').documentElement.firstChild, true));});};
var svg = d3.select("body").append("svg").attr("width", 500).attr("height", 300);
var data = [{x:30,y:50},{x:420,y:100},{x:160,y:150},{x:260,y:30}];
var myLiteral = svg.selectAll(".literal")
.data(data)
.enter()
.appendSVG("<path d='M 22.889471,25.607172 C 22.589713,24.605127 24.092318,24.708731 24.554936,25.108955 C 25.808602,26.193538 25.053398,28.14136 23.885905,28.938102 C 21.797533,30.363287 19.018523,29.16303 17.893076,27.101823 C 16.241437,24.076919 17.936475,20.36976 20.896603,18.945312 C 24.841988,17.046747 29.504523,19.25402 31.216796,23.116087 C 33.371517,27.976105 30.644503,33.605344 25.878773,35.599962 C 20.106834,38.015712 13.505062,34.765112 11.231216,29.094691 C 8.551568,22.412295 12.327973,14.834577 18.903736,12.283452 C 26.495714,9.3380778 35.051552,13.641683 37.878656,21.12322 C 41.09099,29.624218 36.259254,39.159651 27.87164,42.261821 C 18.462006,45.741988 7.9459296,40.381466 4.5693566,31.087558 C 0.82072068,20.769559 6.7105029,9.2720694 16.910868,5.6215926' style='fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'/>")
.attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")"});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

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];
})

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