D3 albersUsa projection funtion return null - javascript

I'm pretty new to D3 and I'm trying to set point on a map.
I'm confused as I created a projection with this code:
var projection = d3.geo
.albersUsa()
.scale(500)
.translate([el.clientWidth / 2, el.clientHeight / 2]);
I use this projection to draw a map and it works fine.
But then whenever I call projection([10, 20]) it returns null whichever values I'm passing in.
What is my error?

From the documentation
# projection(location)
[…]
May return null if the specified location has no defined projected position, such as when the location is outside the clipping bounds of the projection.
The Albers USA projection is defined only within its borders and will yield null for coordinates outside the well-defined area.
See this comparison of calls to projection(location) using [10,20], which is not valid, and [-110,40], which is a valid point:
var projection = d3.geo
.albersUsa()
.scale(500)
.translate([960, 500]);
d3.selectAll("p")
.data([[10,20],[-110,40]])
.enter().append("p")
.text(function(d) { return d + ": " + projection(d); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

How to filter a featurecollection to an object that can be used with path.bounds()

I’ve been trying to make a map with a zoom to bounding box functionality, based on this example: https://bl.ocks.org/mbostock/9656675.
But for municipalities with islands, the zoom goes to the bounding box of the selected landmass instead of the bounding box of the municipality.
I figured out that in my data, municipalities with several areas separated by water consist of multiple polygons with the same nametag instead of a single multipolygon as in Mike Bostocks example above.
I managed to fix the issue for filling in the areas, so the error becomes even more obvious if you click on one of the small islands, but I cannot figure out how to properly zoom to the bounding box of the municipality instead of the land area.
I tried looking for different ways to filter or subset a featurecollection based on the areanames but my solutions all end up giving me a wrong data type, or a bounding box from -infinity to infinity.
To sum up, the intended behaviour is for the zoom to go to the bounding box of the highlighted area instead of the selected landmass.
Here is my map so far: http://plnkr.co/edit/iywWsM9RLs7UzI40q66M?p=preview
I slowed down the zoom a bit so the it’s easier to spot the error, I hope it’s not too annoying.
And here is the code piece where i suspect things are going wrong.
function clicked(d) {
if (d.properties.KOMNAVN == kommune) return reset();
d3.selectAll("path")
.attr("fill", "teal");
kommune = d.properties.KOMNAVN;
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = Math.max(1, Math.min(zoomExtent, 0.95 / Math.max(dx / w, dy / h))),
translate = [w / 2 - scale * x, h / 2 - scale * y];
svg.transition()
.duration(4000)
.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
d3.selectAll("path").filter(function(d) {
return d.properties.KOMNAVN == kommune
})
.attr("fill", "darkred");
}
Thanks in advance!
path.bounds (or projection.fitSize and projection.fitExtent) for that matter, requires a a geojson object, which can be a feature collection. Feeding this function an array will cause issues.
A feature collection looks like:
{
"type":"FeatureCollection",
"features": features
}
where features is an array of feature types.
You have a a feature collection for your dataset, and you can filter the features:
var filteredFeatures = data.features.filter(function(feature) {
return feature.properties.property == criteria
})
Then you can create a new feature collection with these filtered features. In your case this might look like:
var filteredFeatures = json.features.filter(function(feature) {
return feature.properties.KOMNAVN == d.properties.KOMNAVN;
})
var filteredFeatureCollection = {
"type":"FeatureCollection",
"features":filteredFeatures
}
No you can send this new feature collection to path.bounds.
Note that for your example I've moved the click function into the call back function for d3.json so that the scope of the json variable covers the click function.
Here's an updated plunker.

D3 vs Scipy (Voronoi diagram implementation)

Background
I'm working with a set of 8000 geographical points contained in csv file. On one hand I create a visualisation of Voronoi diagrams built using these points - it's done using D3 library. On the other hand I calculate these Voronoi diagrams in Python using Scipy.
My work logic is simple - I mess with my data on Python's side, making heatmaps, analysis and so on and then I visualise effects using D3. But today I accidentally found that Voronoi diagrams made by Scipy and D3 are different. I noticed that after using geojson.io to plot GeoJsons of Voronois made in Python just to see if I can visualise everything there.
As I said, the Voronois were different - some of them had different angles and some even had additional vertices.
Question:
Why is that happening? Why Voronoi diagrams calculated by these 2 libraries (D3 and Scipy) differ?
Further description
How it is done on D3 side: Based on Chris Zetter example http://chriszetter.com/blog/2014/06/15/building-a-voronoi-map-with-d3-and-leaflet/ I translate latitude and longitude into custom projection to visualise it on the mapbox map.
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[N_W.x , N_W.y],[S_E.x, S_E.y]])
I create Voronoi based on points that are visible within map border + some padding (filteredPoints)
filteredPoints = points.filter(function(d) {
var latlng = new L.LatLng(d.latitude, d.longitude);
if (!drawLimit.contains(latlng)) { return false };
// this translates points from coordinates to pixels
var point = map.latLngToLayerPoint(latlng);
key = point.toString();
if (existing.has(key)) { return false };
existing.add(key);
d.x = point.x;
d.y = point.y;
return true;
});
voronoi(filteredPoints).forEach(function(d) { d.point.cell = d});
How it is done on Python side: I use scipy.spatial.Voronoi.
from scipy.spatial import Voronoi
def create_voronois():
points = numpy.array(points_list)
vor = Voronoi(points)
Where "points_list" is a list of my 8000 geographical points.
EDIT:
Screenshot from my visualisation - black borders are Voronois made with D3, white ones are made by scipy.spatial.Voronoi. As we can see scipy is wrong. Did anyone compare these 2 libraries before?
http://imgur.com/b1ndx0F
Code to run. It prints GeoJson with badly calculated Voronois.
import numpy
from scipy.spatial import Voronoi
from geojson import FeatureCollection, Feature, Polygon
points = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000 ,53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000 ,53.6447222222000]]
def create_voronois(points_list):
points = numpy.array(points_list)
vor = Voronoi(points)
point_voronoi_list = []
feature_list = []
for region in range(len(vor.regions) - 1):
vertice_list = []
for x in vor.regions[region]:
vertice = vor.vertices[x]
vertice = (vertice[1], vertice[0])
vertice_list.append(vertice)
polygon = Polygon([vertice_list])
feature = Feature(geometry=polygon, properties={})
feature_list.append(feature)
feature_collection = FeatureCollection(feature_list)
print feature_collection
create_voronois(points)
Apparently your javascript code is applying a transformation to the data before computing the Voronoi diagram. This transformation does not preserve the relative distances of the points, so it does not generate the same result as your scipy code. Note that I'm not saying that your d3 version is incorrect. Given that the data are latitude and longitude, what you are doing in the javascript code might be correct. But to compare it to the scipy code, you have to do the same transformations if you expect to get the same Voronoi diagram.
The scripts below show that, if you preserve the relative distance of the input points, scipy's Voronoi function and d3.geom.voronoi generate the same diagram.
Here's a script that uses scipy's Voronoi code:
import numpy
from scipy.spatial import Voronoi, voronoi_plot_2d
import matplotlib.pyplot as plt
points = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000, 53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000, 53.6447222222000]]
vor = Voronoi(points)
voronoi_plot_2d(vor)
plt.axis('equal')
plt.xlim(22.65, 23.50)
plt.ylim(53.35, 53.85)
plt.show()
It generates this plot:
Now here's a javascript program that uses d3.geom.voronoi:
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js"></script>
</head>
<body>
<div id="chart">
</div>
<script type="text/javascript">
// This code is a hacked up version of http://bl.ocks.org/njvack/1405439
var w = 800,
h = 400;
var data = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000, 53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000, 53.6447222222000]
];
// Translate and scale the points. The same scaling factor (2*h) must be used
// on x and y to preserve the relative distances among the points.
// The y coordinates are also flipped.
var vertices = data.map(function(point) {return [2*h*(point[0]-22.5), h - 2*h*(point[1]-53.4)]})
var svg = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var paths, points;
points = svg.append("svg:g").attr("id", "points");
paths = svg.append("svg:g").attr("id", "point-paths");
paths.selectAll("path")
.data(d3.geom.voronoi(vertices))
.enter().append("svg:path")
.attr("d", function(d) { return "M" + d.join(",") + "Z"; })
.attr("id", function(d,i) {
return "path-"+i; })
.attr("clip-path", function(d,i) { return "url(#clip-"+i+")"; })
.style("fill", d3.rgb(230, 230, 230))
.style('fill-opacity', 0.4)
.style("stroke", d3.rgb(50,50,50));
points.selectAll("circle")
.data(vertices)
.enter().append("svg:circle")
.attr("id", function(d, i) {
return "point-"+i; })
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2)
.attr('stroke', d3.rgb(0, 50, 200));
</script>
</body>
</html>
It generates:
Based on a visual inspection of the results, I'd say they are generating the same Voronoi diagram.

d3.js fisheye distortion on map

I'm trying to distort a d3.geo.path() map with the fisheye.js plugin (https://github.com/d3/d3-plugins/tree/master/fisheye).
To distort an object the plugin needs x & y attributes.
In the d3.js wiki it says:
A projection function takes a two-element array of numbers representing the coordinates of a location, [longitude, latitude], and returns a similar two-element array of numbers representing the projected pixel position [x, y]. For example, a rudimentary spherical Mercator projection:
https://github.com/mbostock/d3/wiki/Geo-Paths
So the distortion should be possible, I just can't wrap my head around it.
I'm using the world-50m.json for my projection. Once loaded there is an the arcs array. I think those are the coordinates I need to manipulate. But this is guesswork...
Thanks,
Kim
I found your post looking for the answer, and it doesn't appear to be out there on the internets. But, like you say, it's possible!
Following the documentation from the fisheye.js (https://github.com/d3/d3-plugins/tree/master/fisheye), in the mousemove callback you need to use fisheye on the coordinates.
Since fisheye uses the .x and .y attributes, I modified the fisheye code to just use the two pair [x,y] to avoid making that intermediate data structure every time in the callback.
Then you can do it like this:
canvas.on("mousemove", function() {
// console.log("mouse:");
// console.log(d3.mouse(this));
var here = d3.mouse(this);
// console.log(here); // [1030, 125]
// console.log(projection.invert(here)); // [-72.4713375653601, 45.14035261565636]
var inverted = projection.invert([here[0],here[1]]); // [-72.4713375653601, 45.14035261565636]
// console.log(inverted); // [-72.4713375653601, 45.14035261565636]
// burlington is lat 44, lon -73
fisheye.focus(inverted);
// of course, the path function takes [longitude, latitude], so -72, 44 for burlington
// https://github.com/mbostock/d3/wiki/Geo-Paths
// (so that's what it gives back)
states.attr("d",null)
.attr("d", function(d) {
// console.log("original:");
// console.log(d.geometry);
if (d.geometry.type === "Polygon") {
var b = d.geometry.coordinates.map(function(d) { return d.map(function(f) { return fisheye(f);}); });
}
else {
var b = d.geometry.coordinates.map(function(d) { return d.map(function(f) { return f.map(function(g) { return fisheye(g); }); }); });
}
// console.log(b);
var c = {type: d.geometry.type, coordinates: b};
// console.log("new:");
// console.log(c);
return path(c);
});
You can view a live version here: http://panometer.org/instruments/teletherms/?window=25&var=maxT&year=1914&city=BURLINGTON%20WSO%20AP,%20VT

D3.js Brush Controls: getting extent width, coordinates

I'm using d3.js, and was wondering how I might get the sides, width, coordinates, etc; of the extent. In an example like this http://bl.ocks.org/mbostock/1667367
Brush.extent()
When using a brush control, you access information about the state of the brush using the .extent() method on the brush object.
The information returned by the .extent() method depends on what sort of scale(s) you have connected to the brush object.
If you have one scale linked (either an X-scale or a Y-scale, but not both), then the extent method returns a two-element array of the form [minimum, maximum].
If you have both X and Y scales attached to the brush object, then the extent method returns a nested array of the form [‍​[xMinimum, yMinimum], [xMaximum, yMaximum]​].
But what are these minimum and maximum values? That also depends on the scale. If the scale has a valid .invert(value) method, the minimum and maximum values will be converted into your data domain values. For ordinal, threshold and other scales which do not have a simple invert method, the brush function returns the values in the coordinate system in effect for the brush element.
1. One dimension brush
To answer your question for the specific example you linked to, we need to look at the brush object and scales objects. In that example, the brush is connected to the horizontal scale on the smaller, "context" chart (the x2 scale):
var x = d3.time.scale().range([0, width]),
x2 = d3.time.scale().range([0, width]),
y = d3.scale.linear().range([height, 0]),
y2 = d3.scale.linear().range([height2, 0]);
var brush = d3.svg.brush()
.x(x2)
.on("brush", brushed);
brush's initialisation
The brush object created above only exists in the Javascript, not in the document. However, the object is also a function which can be called (similar to the axis functions) in order to create a series of rectangles which will respond to mouse events (these are invisible) and the one "extent" rectangle (which in this example is coloured gray with a white border).
context.append("g")
.attr("class", "x brush")
.call(brush) //call the brush function, causing it to create the rectangles
.selectAll("rect") //select all the just-created rectangles
.attr("y", -6)
.attr("height", height2 + 7); //set their height
The default size of the invisible rectangles is based on the output range of the X and Y scales. Because this brush doesn't have a Y scale, the constant height and vertical position of the rectangles has to be set explicitly.
The initial size of the extent rectangle is based on the extent of the brush object (zero width and height by default). The height of that rectangle is also set in the above code.
brush interaction
When the user interacts with the brush on screen, the brush object captures those events and (1) updates the width of the "extent" rectangle, (2) calls the function which you associated with the "brush" event in the line .on("brush", brushed).
The brushed() function is:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
}
The purpose of this brush is to scale the main chart, and this is done by setting the domain of the main chart's X-scale. If the brush has zero-width, brush.empty() returns true and the main chart X-domain is set to the full domain shown in the small chart.
However, if the brush has a valid width, the empty test returns false, and the domain is set to the results of brush.extent(). Because the brush is attached to a linear X scale, and no Y scale, the extent is returned in the form [xMin, xMax] (in the data numbers), which is exactly what is needed for setting the domain.
Extracting values from brush
If you needed to know the width in the data values, it is a simple matter of subtraction:
var extent = brush.extent(); //returns [xMin, xMax]
var width = extent[1] - extent[0]; //data-width = max - min
However, if you are drawing other elements on screen, you want to know the actual coordinates in the SVG, not just the data values. To do the conversion, you use the same thing you always use to convert from data to SVG coordinates: your scale function. Remembering to use the x2 scale that controls the small chart, not the zoomed-in scale of the main chart, that would look like:
var extent = brush.extent(); //returns [xMin, xMax]
var rangeExtent = [x2( extent[0] ), x2( extent[1] ) ]; //convert
var rangeWidth = rangeExtent[1] - rangeExtent[0];
2. X and Y brush
To re-emphasize, this example is for a brush with one (horizontal / X) scale, which is a linear scale. If you were using both X and Y linear scales, you would need to separate out the X and Y extent values with code like this:
function brushed() {
if (brush.empty()) {
//either the height OR the width is empty
x.domain( x2.domain() ); //reset X scale
y.domain( y2.domain() ); //reset Y scale
}
var extent = brush.extent();
x.domain( [extent[0][0] , extent[1][0] ] ); //min and max data X values
y.domain( [extent[0][1] , extent[1][1] ] ); //min and max data Y values
var rangeExtent = [
[x2(extent[0][0]), y2(extent[0][1])],
[x2(extent[1][0]), y2(extent[1][1])]
];
var rangeWidth = rangeExtent[1][0] - rangeExtent[0][0];
var rangeHeight = rangeExtent[1][1] - rangeExtent[0][1];
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
focus.select(".y.axis").call(yAxis);
}
Extracting values from brush
If you want to know the coordinates of the top-left point of the rectangle, you'll also need to know whether your Y scale switches the minimum value from top to bottom.
Alternately, you could get the width, height, and top left coordinate from the on-screen "extent" rectangle, which the brush object modifies for you:
function brushed() {
//use the brush object's values to set the data domains:
if (brush.empty()) {
//either the height OR the width is empty
x.domain( x2.domain() ); //reset X scale
y.domain( y2.domain() ); //reset Y scale
}
var extent = brush.extent();
x.domain( [extent[0][0] , extent[1][0] ] ); //min and max data X values
y.domain( [extent[0][1] , extent[1][1] ] ); //min and max data Y values
//use the brush extent rectangle to get the SVG coordinates:
var extentRect = d3.select("g.x.brush rect.extent");
var rangeWidth = extentRect.attr("width");
var rangeHeight = extentRect.attr("height");
var rangeLeft = extentRect.attr("x");
var rangeTop = extentRect.attr("y");
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
focus.select(".y.axis").call(yAxis);
}
If you are using ordinal scales, it is more complicated for zooming, but less complicated for finding screen coordinates. This answer describes how to use a brush with an ordinal scale to zoom. However, since the values returned by .extent() are already in SVG coordinates, you would not have to use the scale itself to convert back if you want coordinates and width
References
The API page for the brush control has a set of thumbnail images at the top of the page; click on any of them to open up a working example. For more discussion, you might be interested in this tutorial with another good example of brushes in action.

bezier control point orientation d3

I'm hand cranking a network diagram from D3 (I didn't like the output of Force Directed). To do this, I've generated an array of nodes, each with an x/y co-ordinate.
{
"nodes" : [
{
"name" : "foo",
"to" : ["bar"]
},
{
"name" : "bar",
"to" : ["baz"]
},
{
"name" : "baz"
}
]
}
I then generate an svg, with a parent svg:g, and bind this data to a series of svg:g elements hanging off the parent.
addSvg = function () {
// add the parent svg element
return d3.select('#visualisation')
.append('svg')
.attr('width', width)
.attr('height', height);
};
addSvgGroup = function (p) {
// add a containing svg:g
return p.append('svg:g').
attr('transform', 'translate(0,0)');
};
addSvgNodes = function(p, nodes) {
// attach all nodes to the parent p data
// and render to svg:g
return p.selectAll('g')
.data(nodes)
.enter()
.append('svg:g')
.attr('class', 'node');
};
Then I manually position the nodes (this will be dynamic later, I'm just getting my feet)
transformNodes = function (nodes) {
// position nodes manually
// deprecate later for column concept
nodes.attr('transform', function (o, i) {
offset = (i + 1) * options.nodeOffset;
// options.nodeOffset = 150
o.x = offset;
o.y = offset / 2;
return 'translate(' + offset + ',' + offset / 2 + ')';
});
};
Then I attach these items to the parent svg:g, and hang some text off them.
This results in a staircase of text descending left to right within the svg. So far, so good.
Next, I want to generate some links, so I use a method to determine if the current node has a relationship, and then get that nodes location. Finally, I generate a series of links using d3.svg.diagonal and set their source/target to the appropriate nodes. Written longhand for clarity.
getLinkGenerator = function (o) {
return d3.svg.diagonal()
.source(o.source)
.target(o.target)
.projection(function (d) {
console.log('projection', d);
return [d.x, d.y]
});
};
Now, so far, so good - except the control handles for the bezier are not where I would like them to be. For example from node A to node B the path d attribute is thus:
<path d="M150,75C150,112.5 300,112.5 300,150" style="fill: none" stroke="#000"></path>
But I'd like it to alter the orientation of the control handles - i.e
<path d="M150,75C200,75 250,150 300,150" style="fill: none" stroke="#000"></path>
This would make it look more like a dendrograph from the page of examples. What I noticed in the collapsible dendrograph example is that it returns an inversion of the axes:
return [d.y, d.x]
But if I do this, while the control points are oriented as I would like, the location of the points is out of whack (i.e their x/y co-ordinates are also reversed, effectively translating them.
Has anyone else encountered an issue like this or have an idea of how to fix it?
OK, so I took a look at this and figured out a solution. It appears that some of the layouts (dendrogram, collapsed tree) are inverting the co-ordinates of source/target in the path links so that when they hit the projection call, they get reversed back into their correct location, but with the orientation of their bezier points rotated.
So, if you're hand cranking a custom layout and you want to orient the bezier controls horizontally (like the examples), that's what you need to do.

Categories