I want to build this kind of chart with d3js (no need in any North, East etc.)
just this kind of data
[{year:2017, data:[10,20,30]}, {year:2016, data:[20,20,30]}]
Is there any example pie with sectors?
Thanks.
I think what might be similar to what you want is:
Sequential Sunburst Diagrams
In the main gallery for d3js you can find some examples for sunburst graphs
D3 has a method for generating arc segments of varying widths specified by inner and outer radius, start and end angles d3.arc
var arc = d3.arc();
arc({
innerRadius: 0,
outerRadius: 100,
startAngle: 0,
endAngle: Math.PI / 2
});
in order to create the kind of diagram you have in mind you'll need to work out what those properties are for each piece of data you have. This is typically achieved using a layout function; you pass an array of your data to the function and it returns an array of values required to create the arc segments.
The process by which you convert the data values to arc properties is where the difficult work lies. I've created a bl.ock giving an example of how you might do this though if you're using this layout you should make sure that the areas rather than arc thickness are proportional to the data values for expedience I haven't.
Related
I made the following TopoJSON file: https://gofile.io/d/CKBGhF
I want to view it in my browser with a basic D3.js script. From https://bost.ocks.org/mike/map/ I found a small tutorial about the script. But because my map is made from a vector converted into TopoJSON it has no real coordinates. With QGIS I saved it first as a GeoJSON and with mapshaper I saved it as TopoJSON.
How should I view my map? Are the coordinates or scaling completely wrong? Or is my TopoJSON not good?
My HTML/Javascript code (I only changed the filename):
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* CSS goes here. */
</style>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 1160;
var projection = d3.geo.mercator()
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("topojson_mapshaper.json", function(error, uk) {
svg.append("path")
.datum(topojson.feature(uk, uk.objects.subunits))
.attr("d", path);
});
</script>
In my response I'm upgrading all code to d3v5 due to the inclusion of new features since d3v4 that aid with drawing this data (such as d3.geoIdentity, projection.fitSize). Since D3 v4 there are some changes to D3 namespace (d3.geo.path and d3.geo.projectionName are now d3.geoPath and d3.geoProjectionName). And in D3v5, d3.json now returns promise.
There are a few things at play here.
Topojson.feature
First, D3 only draws geojson objects with d3.geoPath, D3 does not draw topojson directly. So your data, while stored as topojson, is converted to geojson here:
topojson.feature(uk, uk.objects.subunits)
But, your topojson data uk, doesn't have a subunits property that contains features. You instead have a sub unit property called test:
...463908,4.10162]},"objects":{"test":{"type":"GeometryCollection"...
Normally platforms such as mapshaper apply the filename of the source file as the sub property name, so I'm guessing your source file, exported from QGIS, was test.geojson or something to that effect.
If we log:
console.log(topojson.feature(uk, uk.objects.test))
We see valid geojson. Now that we have geojson, we can draw it.
Coordinates
As for coordinate systems, topojson preserves the original coordinate system by default. When converting back to geojson, your coordinates will be the same as originally. So this statement "it has no real coordinates" isn't true unless your source data has no real coordinates.
However, you can project or reproject points from the command line, or with mapshaper, while producing topojson. It appears you have applied a projection on your points because when I convert your topojson to geojson, I see coordinates that look like pixel values (units that are unlikely to be the original coordinates exported from QGIS).
If using projected coordinates (Cartesian points, not lat long pairs, whether units are meters or pixels), we cannot use a D3 geoProjection: these take 3D points and project them to a plane.
Since your values look like pixel values, we can pass your data directly to a null projection:
var path = d3.geoPath()
Or, more explicitly:
var path = d3.geoPath(null);
var path = d3.geoPath().projection(null);
In D3v3 and earlier, this needs to be explicitly set
This applies no transform to the coordinates in the geojson. It treats each geojson coordinate as a pixel coordinate and draws your features accordingly (example)
However, that option isn't useful if the projected extent of our features doesn't match the SVG/Canvas extent. Instead, we could use a geoTransform or geoIdentity to apply an appropriate transform.
The geoIdentity option is the easiest as it provides the convenient fitSize method that lets us automagically size the geojson to our SVG/Canvas dimensions (fitExtent allows specification of a margin, while fitSize assumes no margin). All of these options can be passed to d3.geoPath as a projection (example, full screen).
The last example should show you how to draw the data. You mostly have line strings, which will make filling features difficult, unless you only wish to show borders.
Further Reading
Based on previous experience, there are a few related questions that come to mind:
Preprojected Geometry vs Projecting on the Fly
This question addresses possible quesions on pre-projected geometry as it appears you have preprojected your geometry for a screen size. This introduces alignment challenges, the trade off is quicker rendering time.
Scaling a Map to Fit SVG
This question deals with preprojected geometry and fitting the features to the screen. It speaks more in depth to fitSize, fitExtent, and geoTransform.
I am a newbie, attempting to copy someone else's choropleth code and use my data.
My data matches their data structure and I can get the correct data to console.log() and my tooltips display correct data.
But my map is just a big block of colour with no paths.
Here is a codepen, but this is the code that renders the map features:
var path = d3.geo.path();
svg.append("g")
.attr("class", "county")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.style("fill", function (d) {
return color(pairFipsWithId[d.id]);
})
The result looks like this:
⚠️This question and answer both use d3v3 - d3v4+ geo paths are slightly different, see this question/answer for d3v4+.
If you are making a D3 choropleth, you should be looking for a recent version of D3 - d3v3 is a bit dated now. However, as v3 has some differences, this isn't quite a duplicate of this question, but the problem is the same:
The example map you use has unprojected geographic data
Your geographic data is projected.
In other words, your example's geographic data uses a 3 dimensional coordinate system measured in degrees latitude and longitude, while your geographic data uses a coordinate system where the units are pixels. But, you don't specify this change and D3 does not know to correct for it.
In D3v3, the default projection for a d3.geo.path() is d3.geo.albersUsa(), all D3 projections assume your data is recorded in latitudes and longitudes, otherwise we would need to specify an additional parameter to projections to indicate what coordinate space the input data uses.
A sign that your data is already projected can come from mapshaper.org, if you drag your topojson into the window your map is upside down. Why? Geographic coordinates increase as one moves north (typically up) while pixel coordinates increase as one moves down. This is your data in mapshaper.org:
We cannot "unproject" the data as we don't know what projection was used to create it. But we could assign a null projection to the geo.path:
var path = d3.geo.path().projection(null)
The null projection simply takes each coordinate in the data and converts it to a pixel coordinate with no transform - input values are treated as pixel values. As this map was designed for a web map it doesn't need any scaling or centering (depending on the size of your SVG).
Here's an updated codepen.
In d3v4+, the default projection of a geoPath is a null projection off the bat, it also comes with selection.raise() and selection.lower() methods that can simplify your code a bit, as well as better options for fitting any dataset to a SVG/Canvas of a given size. Here's a d3v4 codePen with a few modifications, d3v5 requires a bit more in updating d3.json
In this jsfiddle I have a D3 map that I took from here, but I'm trying to fit it in an svg that is half the original size. For that, I changed:
var width = 480;
var height = 300;
.....
var path = d3.geoPath(d3.geoIdentity().translate([width/2, height/2]).scale(height*.5));
But it's not working. How to make the map fit the svg?
D3's geoIdentity exposes almost all the standard methods of a d3 projection (off the top of my head, only rotation is not possible as the identity assumes cartesian data). Most importantly here, it exposes the fitSize and fitExtent methods. These methods set translate and scale based on the coordinate extent of displayed geojson data and the pixel extent of the svg/canvas.
To scale your features with a geo identity you can use:
d3.geoIdentity().fitSize([width,height],geojsonObject)
Note that an array of geojson features won't work, but a geojson feature collection or any individual feature/geometry object works too. The width and height are of the svg/canvas.
If you want to apply a margin, you can use:
d3.geoIdentity().fitExtent([[margin,margin],[width-margin,height-margin]],geojsonObject)
The margin doesn't need to be uniform, the format is [[left,top],[right,bottom]],geojsonObject
If using fitSize or fitExtent there is no need to set center, translate or scale manually and setting these afterwards will recenter or rescale the map.
I'm using a d3.geoOrthographic projection to draw great circle arcs between cities using a d3.geoPath:
path({
type: "LineString",
coordinates: [origin, destination],
precision: 1
})
Following Mike Bostock's example here I can capture the projected x,y values for the resampled points, but I'd like to capture the corresponding sequence of lat/lon values, prior to projection. I've tried wrapping my original projection to intercept the point method, but it only gets called for the start and end of the line, not the intermediate points. I think the resampling is buried somewhere deeper in the stream. Is there some clever way I can capture the intermediate points getting generated in D3's resampleLine?
Actually the thing I'm really looking for is a sequence of 'interpolator' offsets along the (untransformed) great arc, ranging from 0 at the start to 1 at the end, so that I can vary the stroke color uniformly, as a geo version of this example. As a fallback I could use geoInterpolate directly and draw "too many" points at regular intervals, but it seemed nicer to go use the adaptive machinery.
In one application I have both d3 map and leaflet map, I follow Mike's code to sync them(http://bost.ocks.org/mike/leaflet/) and draw svg points on leaflet map. The problem is that some points being cutoff at the edge of SVG container, and other SVG elements added later on (an animated pulse in this case) will only partially shown. I wonder if there's anyway to expand the SVG container based on the features (points) bound.
a working demo is here: http://xqin1.github.io/usschools/usac_school_stat.html, to reproduce the issue:
1. select the 'Number of Students' slider bar to 5300-6545, the two points on Leaftlet map only half shown.
2. click the first table row, map will zoom to the point, but the animated pulse being cut off.
Any suggestions are highly appreciated.
thanks
xiaoming
I had this problem as well, and found a solution. Referencing mbostock's sample code you just need to add a little bit of padding to the projected bounding box that is calculated each time reset is called:
var bottomLeft = project(bounds[0]),
topRight = project(bounds[1]);
Insert something like this (just after the above declarations):
var padding = 25; // In pixels, choose large enough to prevent edge clipping
// of your largest element
bottomLeft = [bottomLeft[0]-padding, bottomLeft[1]+padding]
topRight = [topRight[0]+padding, topRight[1]-padding]