i want to create map of Africa using d3.js and topoJSON. I have that datasource https://gist.githubusercontent.com/bricedev/3905007f1794b0cb0bcd/raw/ad5c995f6990f7c3c7fad5c6206bc6fd5462f1fb/africa.json
That is my code. How i can get properties and create correct map? Please help me where is the error?
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="D3byEX 12.15" />
<meta charset="utf-8">
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 1000, height = 728;
var svg = d3.select("body").append("svg")
.attr({ width: width, height: height });
var mainGroup = svg.append("g");
mainGroup.style({ stroke: "white", "stroke-width": "2px", "stroke-opacity": 0.0 });
var projection = d3.geo.mercator();
var path = d3.geo.path().projection(projection);
var url = 'https://gist.githubusercontent.com/bricedev/3905007f1794b0cb0bcd/raw/ad5c995f6990f7c3c7fad5c6206bc6fd5462f1fb/africa.json';
d3.json(url, function (error, africa) {
var countries = topojson.feature(africa, africa.objects.countries).features;
var neighbors = topojson.neighbors(africa.objects.countries.geometries);
var color = d3.scale.category20();
mainGroup.selectAll("path", "countries")
.data(countries)
.enter().append("path")
.attr("d", path)
.style("fill", function (d, i) {
return color(d.color = d3.max(neighbors[i],
function (n) { return countries[n].color; }) + 1 | 0);
});
mainGroup.selectAll("path")
.on("mouseover", function () {
console.log("mouseover");
d3.select(this).style("stroke-opacity", 1.0);
});
mainGroup.selectAll("path")
.on("mouseout", function () {
d3.select(this).style("stroke-opacity", 0.0);
});
});
</script>
Your topojson is not in WGS84, that is to say lat/long coordinate space or unprojected data. D3.projection() requires WGS84.
Your topojson is already projected in, what I assume is, the Africa Lambert Conformal Conic projection. You do not need to use a projection to display this in d3.js. In order to display this data without a projection you can define the projection of your geoPath as:
path = d3.geoPath().projection(null);
This is how the topojson was projected in the block in which that data came from.
If you need to scale or translate the projection, then d3.geoTransform can help you, see this block.
Alternatively, you can reproject your topojson so that it uses lat/long pairs and will properly project using d3.projection().
Related
I'm trying to draw choropleth map of Seoul but it just gives me only a black rectangle.
I thought there might be a problem with projection.scale but I couldn't figure it out.
I used scale.fitSize() function to handle it. but my d3 is older version that not available of fitSize().
my code is here:
var width = 600, height = 700;
var svg = d3.select('#chart').append('svg')
.attr('width',width)
.attr('height',height);
var projection = d3.geo.mercator()
.center([128,36])
.scale(5000)
.translate([width/2, height/2]);
var path = d3.geo.path()
.projection(projection);
d3.json('Seoulmap.json',function(error,data) {
var features = topojson.feature(data, data.objects['Seoulmap']).features;
svg.selectAll('path')
.data(features)
.enter().append('path')
.attr('class','name')
.attr('d',path)
.attr('id',function(d) { return d.properties.ADM_DR_NM; });
});
How my code is rendering black rectangle:
Have a topoJSON file that I am importing - seems like this should be easy to flip, but I have no idea. Should I be transforming the object after it's created or adjusting the JSON? I tried using some projections, which flipped the object, but distorted it all over the place.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.counties {
fill: blue;
}
.states {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var path = d3.geoPath()
//first make projection
//var projection = d3.geoMercator();
//var path = d3.geoPath()
// .projection(projection);
d3.json("data.topojson", function(error, us) {
if (error) throw error;
var counties = topojson.feature(us, us.objects.counties),
counties = counties.features.filter(function(d) { return d.properties.AWATER === 0; });
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(counties)
.enter().append("path")
.attr("d", path)
.style("fill", 'blue')
.append('g')
attr('transform', 'rotate(180 0 0)');
});
</script>
You aren't using a projection - so the coordinates in the file are translated to pixels with no transformation. If your data has typical geographic coordinates, or lat longs that are both positive, high values are at the north end (the top in most maps), and low values are at the south end (the bottom in most maps).
In svg coordinates, low values are located at the top and increase as one moves towards the bottom - the opposite of most geographic coordinate conventions. You could use a geoIdentity as your projection to flip the json's y coordinates:
var projection = d3.geoIdentity()
.reflectY(true)
.fitSize([width,height],geojson)
Where geojson is your feature collection: topojson.feature(us, us.objects.counties)
And then use it with your path:
var path = d3.geoPath().projection(projection);
I am working on building a visual that looks something like this: .
So far I've managed to create this:
The idea is to map a value to an angle so that I know where to point the arrow and then I will color the arrow the same color as the point on the arc that its pointing to.
I essentially have two questions:
First what can I do in order to make the colors line up better. I've used a linear gradient like so:
let defs = this.gaugeEl
.append("defs")
.classed("definitions",true);
let gradient = defs
.append("linearGradient")
.classed("linearGradient",true);
gradient
.attr({
id: 'gradient',
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%',
spreadMethod: "pad"
});
gradient
.append("stop")
.classed('start',true)
.attr({
offset: '0%',
'stop-color': 'lawngreen',
'stop-opacity': 1
});
gradient.append("stop")
.classed('end',true)
.attr({
offset: '100%',
'stop-color': 'red',
'stop-opacity': 1
});
The effect is not what I was hoping for, what can be done?
The next question about how the gradient works, I need to be able to associate an angle with a color so that I can color the arrow and the tick marks properly and in my current setup I don't know how to do that. Is it even possible?
I don't know how much useful this will be for you. But I followed the below implementation
Split the arc into tiny arcs
Used scaleLinear for associating color and angle and divided the arc into four segments
Ignore bad math and code !
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#chart {
width: 960px;
height: 350px;
}
</style>
<body>
<svg id="chart">
</svg>
<script src="http://d3js.org/d3.v5.min.js"></script>
<script>
var vis = d3.select("#chart").append("g")
var pi = Math.PI;
var line = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var lines = []
var breakPoints = 100;
var angleArr = [];
var arcArr = [];
//angleArr[0] = -pi/2;
var colorScale = d3.scaleLinear()
.domain([-pi/2, -pi/3,30*pi/180,pi/2])
.range(['lightgreen', 'lightgreen', 'yellow','red']);
var angleScale = d3.scaleLinear()
.range([-pi/2,pi/2])
.domain([0,breakPoints - 1]);
var prevAngle = -pi/2;
for(var i = 0; i < breakPoints; i++) {
angleArr[i] = angleScale(i);
var singleArrow = [{"x":(150*Math.sin(angleArr[i])), "y":-(150*Math.cos(angleArr[i]))},{ "y":-(170*Math.cos(angleArr[i])), "x":(170*Math.sin(angleArr[i]))}];
//var subArc = {"start": prev, "end":0};
var subArc = {};
lines.push(singleArrow);
subArc["start"] = prevAngle;
subArc["end"] = angleArr[i];
prevAngle = angleArr[i];
arcArr.push(subArc);
}
var arc = d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(-(pi/2)) //converting from degs to radians
.endAngle(pi/2) //just radians
vis.attr("width", "400").attr("height", "400") // Added height and width so arc is visible
.append("g")
.attr("transform", "translate(200,200)");
vis.selectAll("line")
.data(lines)
.enter()
.append("path").attr("class","arrow").attr("d", line).attr("stroke",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
vis.selectAll("arcs")
.data(arcArr)
.enter()
.append("path").attr("class","arc").attr("d", function(d,i) {
return d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(d.end)
.endAngle(d.start)()}).attr("fill",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
</script>
</body>
There is an easier way to do this now with a css property called conic-gradient
https://css-tricks.com/snippets/css/css-conic-gradient/
It sets the color according to the angle, given a center point. Maybe you could get the angle to the point with a click event, calculate the angel from the center, and set the colour that way.
There's some more info on conic gradients here, including how to calculate it: https://wiki.inkscape.org/wiki/index.php/Advanced_Gradients#Conical_gradient
I am drawing a map of the United States,Costa Rica and Canada. And I would like the maps to adapt to the size of the div#statesvg
<div id="statesvg" style="width:100%; height:100%"></div>
the size of div#statesvg is dynamic.
I only want by default that the maps fits exactly to the div that contains it.
I'm trying to center each map, but it's not centered. I would like to know if there is any mathematical formula or something to scale the map until it fully occupies the svg container.
if(json=="usa.json"){
// D3 Projection
var projection = d3.geo.albersUsa()
.translate([width/2, height/2])
.scale((height*1.25));
}
if(json=="canada.json"){
//canada lat long 54.6965251,-113.7266353
var projection = d3.geo.mercator()
.center([-113.7266353,54.6965251 ])
.translate([width/2, height/2])
.scale((height*1.25));
}
if(json=="costarica.json"){
//costa rica lat long
var projection = d3.geo.mercator()
.center([-87.0531006,8.351569 ])
.translate([width/2, height/2])
.scale((height*1.25));
}
// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
this is my actual problem for each map
thanks!
this is my code:
http://plnkr.co/edit/lJgx0fbLcEh7e4W3j6XD?p=preview
There is this nice gist from nrabinowitz, which provides a function which scales and translate a projection to fit a given box.
It goes through each of the geodata points (data parameter), projects it (projection parameter), and incrementally update the necessary scale and translation to fit all points in the container (box parameter) while maximizing the scale:
function fitProjection(projection, data, box, center) {
...
return projection.scale(scale).translate([transX, transY])
}
I've adapted part of your code (the Canada map) to use it:
d3.json("canada.json", function(data) {
var projection =
fitProjection(d3.geo.mercator(), data, [[0, 0], [width, height]], true)
var path = d3.geo.path().projection(projection);
d3.select("#statesvg svg").remove();
var svg = d3.select("#statesvg").append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
svg.selectAll("path")
.data(data.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d) { return "rgb(213,222,217)"; });
});
As commented by the author, it seems to only work for Mercator projections.
I'm using Leaflet to create a map with an added D3 layer on top. I want to automatically scale and zoom to the overlay layer, similar to the way you can automatically fit geo objects within their container in pure D3 (see example).
In making Leaflet and D3 play nicely I have to use a custom geo transformation per this example:
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
var transform = d3.geo.transform({point: projectPoint}),
path = d3.geo.path().projection(transform);
This makes projecting D3 onto a Leaflet map effortless, but I'm left without any clue as to how to determine latitude/longitude for my layer map. I need these coordinates in order to set the center, then I'd also need to set the zoom level.
How can I set automatically setView and setZoom in Leaflet to fit a D3 overlay layer?
Here is my implementation:
var map = new L.Map("map", {
// Initialize map with arbitrary center/zoom
center: [37.8, -96.9],
zoom: 4
})
var layer = map
.addLayer(new L.TileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"));
var figure = d3.select('figure');
var width = figure.node().clientWidth;
var height = figure.node().clientHeight;
var svg = d3.select(map.getPanes().overlayPane)
.append("svg")
.style('width', width)
.style('height', height);
var g = svg.append("g").attr("class", "leaflet-zoom-hide");
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
var transform = d3.geo.transform({ point: projectPoint });
var path = d3.geo.path().projection(transform);
d3.json('conway-ar.json', function(error, collection) {
if (error) console.warn(error);
var city = g.append('path')
.datum(collection.city.geometry)
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('d', path);
// Update center/zoom based on location of "city"
// map.setView([someLat, someLng]);
// map.setZoom(someZoomLevel);
map.on('viewreset', update);
update();
function update() {
city.attr('d', path);
}
});
I was able to implement a solution using Leaflet.D3SvgOverlay, a library for using D3 with Leaflet that automates the geo transforms.
First I recorded the bounds of the rendered path with proj.pathFromGeojson.bounds(d). This library had a handy method that converted layer points to latitude/longitude, proj.layerPointToLatLng. I was then able to use D3's map.fitBounds to simultaneously adjust the center/zoom based on the recorded boundaries. See the following code:
var bounds = [];
var city = sel.append('path')
.datum(cityGeo)
.attr('d', function(d) {
bounds = proj.pathFromGeojson.bounds(d);
return proj.pathFromGeojson(d);
});
var b = [proj.layerPointToLatLng(bounds[0]),
proj.layerPointToLatLng(bounds[1])];
map.fitBounds(b);
The full implementation of this can be seen in my bl.ock.