I'm able to plot some weather data onto a map using the following code. However the points are rectangles and i'd like to get them smoother.
,
I'd like to plot them smoother like something similar to
I believe I need to look into interpolating, spatial analysis, and/or Choropleth maps. I think they are different algorithms in doing this. I feel like i need to fill in more points in between the existing ones? And with that is it possible to make gradient like points? Is this doable in D3? Or should i consider using three.js or WebGL stuff?
var width = 960,
height = 960;
var map = {};
var projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-50m.json", function(error, world) {
svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
svg.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
map.plot_points = [];
map.max = 30;
map.min = -1;
var opacity = d3.scale.linear()
.domain([map.min, map.max])
.range([0,1]);
var rainbow = ["#CE0C82", "#800CCE", "#1F0CCE", "#0C5BCE", "#0C99CE", "#2ECE0C", "#BAE806", "#FEFF00", "#FFCD00", "#FF9A00", "#FF6000", "#FF0000"];
zs.forEach(function(zv,zi){
zv.forEach(function(zzv, zzi){
if(zzv != 999)
{
map.plot_points.push({lat: ys[zi], long:xs[zzi],value:zzv});
}
})
});
console.log(map);
var points = svg.selectAll("rects.points")
.data(map.plot_points)
.enter()
.append("rect")
.attr("class", "points")
.style("fill", function(d) {
var scale = d3.scale.linear().domain([map.min, map.max]).range([1, rainbow.length]);
return rainbow[Math.round(scale(d.value))];
}).attr("width", 8)
.attr("height", 8)
.style("fill-opacity", 1)
.attr("transform", function(d) {
return "translate(" + projection([d.long, d.lat]) + ")";
})
It sounds like the problem in your case is the data. What you would need to do is take the original data and interpolate it to a smoother form. For this, you can use a GIS program such as QGIS. How exactly to do that depends on what format your original data is in.
Once you have the smoother data, you can plot it again in D3. My guess is that the end result would be somewhat similar to what I've done here, where contour lines are drawn to much the same effect as what you're aiming for.
Maybe you could take a look into heatmap js.
http://www.patrick-wied.at/static/heatmapjs/
Although is point based it may give you a hint.
It uses canvas instead of svg.
Jason Davies wrote an implementation of the conrec algorithm that does exactly what you need:
https://github.com/jasondavies/conrec.js
It's got a working example inside
I believe that the white stripes are happening because of the projection you are using;
In fact the height of each rectangle should adjust accordingly going north and south from the Equator, because the Mercator projection alters the distance going north and south.
To have a fixed height of the rectangles you could try with this projection instead:
http://bl.ocks.org/mbostock/3757119
which preserves the dimension going north and south
Related
I've created a globe which has circles and a drag. The problem is that the circles appear on the far side of the globe. I would like those circles to be hidden.
My bl.ock can be found here:
http://bl.ocks.org/anonymous/dc2d4fc810550586d40d4b1ce9088422/40c6e199a5be4e152c0bd94a13ea94eba41f004b
For example, I would like my globe to function like this one: https://bl.ocks.org/larsvers/f8efeabf480244d59001310f70815b4e
I've seen solutions such as this one: How to move points in an orthogonal map? but it doesn't quite work for me. The points simply disappear, as d[0] and d[1] seem to be undefined.
I've also tried using methods such as this: http://blockbuilder.org/tlfrd/df1f1f705c7940a6a7c0dca47041fec8 but that also doesn't seem to work. The problem here seems to be that he is using the json as his data, while my circles data are independent of the json.
Only similar example I've found is the one: https://bl.ocks.org/curran/115407b42ef85b0758595d05c825b346 from Curran but I don't really understand his code. His method is quite different than mine.
Here is my JavaScript code:
(function(){
var h = 600;
var w = 900;
var i = 0;
var map = void 0;
var world = void 0;
var US = void 0;
var margin = {
top: 10,
bottom: 40,
left: 0,
right: 30
};
var circleScale = d3.scaleSqrt()
.domain([0, 4445])
.range([0.5, 10])
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var dragging = function(d){
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx/6, c[1] - d3.event.dy/6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
}
var drag = d3.drag()
.on("drag", dragging)
var projection = d3.geoOrthographic().clipAngle(90);
var path = d3.geoPath().projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", w)
.attr("height", h)
d3.json("world.json", function(json){
d3.csv("arms_transfer_2012_2016_top - arms_transfer_2012_2016_top.csv", function(error, data){
var countries = topojson.feature(json, json.objects.countries).features
var US = countries[168]
map = svg.append('g').attr('class', 'boundary');
world = map.selectAll('path').data(countries);
US = map.selectAll('.US').data([US]);
Circles = map.selectAll(".circles").data(data)
console.log(countries[168])
world.enter()
.append("path")
.attr("class", "boundary")
.attr("d", path)
US.enter()
.append("path")
.attr("class", "US")
.attr("d", path)
.style("fill", "lightyellow")
.style("stroke", "orange")
Circles.enter()
.append("circle")
.attr("class", "circles")
.attr("r", function(d){
return circleScale(d.Millions)
})
.attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
.style("fill", "#cd0d0e")
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h)
.call(drag)
})
})
})();
There are a few different methods to achieve this, but one of the easier methods would be to calculate the angular distance between the projection centroid (as determined by the rotation) and the circle center on the drag event:
map.selectAll("circle")
.style("display", function(d) {
var circle = [d.Longitude_imp, d.Latitude_imp];
var rotate = projection.rotate(); // antipode of actual rotational center.
var center = [-rotate[0], -rotate[1]]
var distance = d3.geoDistance(circle,center);
return (distance > Math.PI/2 ) ? 'none' : 'inline';
})
Take the center of each point and get the rotational center with projection.rotate() - note that the rotation values are inverse of the centering point. A rotation of [10,-20] centers the map at [-10,20], you move the map under you. With these two points we can use d3.geoDistance() which calculates the distance between two points in radians, hence the use of Math.PI/2 - which gives us points outside of 90 degrees, for these we hide, for the rest we show.
This can be incorporated a little nicer into your code, but I keep it separate here to show what is happening clearer.
Here's an example block - drag to trigger, I haven't applied the logic to the initial load.
An alternative approach, as noted by Gerardo Furtado, would be to use a path to display the circles - using path.pointRadius to set the size of the circle for each point. Instead of appending a circle, you could append path with the following format:
Circles.enter()
.append("path")
.attr("class", "circles")
.attr("d",createGeojsonPoint)
The, on update/drag:
map.selectAll('.circles').attr('d',createGeojsonPoint);
This method uses the clip angle of the orthographic to hide features when they are more than 90 degrees from the center of the projection (as determined by rotation). Your createGeojsonPoint function needs to set the radius and return a valid geojson object:
var createGeojsonPoint = function(d) {
console.log(d);
path.pointRadius(circleScale(d.Millions)); // set point radius
return path({"type":"Point","coordinates":[d.Longitude_imp,d.Latitude_imp]}) // create geojson point, return path data
}
All together, with the necessary modifications, your code might look like this.
US map with d3.v3 using Mike Bostock's example:
I want the map to zoom into the marked locations initially when the page loads but the entire map should be rendered so that a user can zoom out if he wants to.
var w = 300;
var h = 280;
//Define map projection
var projection = d3.geo.albersUsa()
.translate([w/2, h/2])
.scale([300]);
//Define path generator
var path = d3.geo.path()
.projection(projection);
//Create SVG element
var svg = d3.select("#map1").append("svg")
.attr("width", w)
.attr("height", h)
var g = svg.append("g");
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "1000")
.style('opacity', 0)
.style("font-family", "sans-serif")
.style("background-color", "white")
.style("border-radius", "5px")
.style("padding", "10px")
.style('color', '#000')
.style("font-size", "12px");
//Load in GeoJSON data
d3.json("us-states.json", function(json) {
d3.csv("cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.longi, d.lati])[0];
})
.attr("cy", function(d) {
return projection([d.longi, d.lati])[1];
})
.attr("r", 4)
.style("fill", "#4F6D88")
.on("mouseover", function(d){
tooltip.transition().style("opacity", 0.9)
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY) + 'px')
.text(d.city)
})
.on("mousemove", function(event){
tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");
})
.on("mouseout", function(){
tooltip.transition().delay(500).style("opacity", 0);
});
});
//Bind data and create one path per GeoJSON feature
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path);
});
var zoom = d3.behavior.zoom()
.scaleExtent([1, 50])
.on("zoom", function() {
var e = d3.event,
tx = Math.min(0, Math.max(e.translate[0], w - w * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], h - h * e.scale));
zoom.translate([tx, ty]);
g.attr("transform", [
"translate(" + [tx, ty] + ")",
"scale(" + e.scale + ")"
].join(" "));
});
svg.call(zoom)
I have the code to zoom in with scroll which i have pasted above but i want it to zoom on load to those specific locations. How i want it to be:
There are two primary ways to zoom a map in d3:
modify the projection which will re-draw the paths, or
modify the drawn paths with scale and transform.
Modifying the projection is easiest in d3v4 with fitSize or fitExtent - though you would need to turn your points into geojson. You can also manually calculate the translate and scale values to update a projection (see this answer by Mike Bostock which explains this common d3v3 approach).
Alternatively, you can modify the drawn paths by calling the zoom function - this question asked yesterday has an excellent example of doing so (in d3v4). Or you can calculate and apply the zoom manually and then update the zoom to indicate the current scale and translate. I'll use the common method of modifying a d3v3 projection mentioned above (with Mike's answer) and apply it to the transform on the paths - rather than modifying the projection. Though it should not be difficult to see how my answer could be changed to modify the projection instead.
First you need to determine the maximum difference between the x and y coordinates of your points. If dealing with two points, this will be fairly easy:
var data = [[-100,45],[-110,45]];
var p1 = projection(data[0]);
var p2 = projection(data[1]);
var dx = Math.abs(p1[0] - p2[0]);
var dy = Math.abs(p1[1] - p2[1]);
I'm assuming a simple data format for the sake of a shorter answer. Also, if dealing with many points, this would be a bit more complex. One potential option would be to place your points in geojson and get the bounding box of the points.
Now we need to find out the centroid of the points - in the case of two points this is just the average of the x and y values:
var x = (p1[0] + p2[0])/2;
var y = (p1[1] + p2[1])/2;
Next we need to calculate a new scale, while also determining if the scale is restricted by the difference in x values of the coordinates or the difference in y values of the coordinates:
var scale = 0.9 / Math.max( dx/w , dy/h );
The 0.9 reduces the scale slightly, it is the same as 0.9 * scale and allows a variable amount of margin. The value returned by dx/w is one over the scale value we need to stretch the difference across the width of the svg container.
(it would probably make more sense written like: var scale = 0.9 * Math.min(w/dx,h/dy); - we want to limit the zoom by the lowest scale value and multiply it by some percentage to give margins. But the other representation is ubiquitous in online examples)
Now we have a scale, we only need to determine a translate. To do so we find out how far we need to re-position the values held in the x and y variables so that those values would be centered:
var translate = [w/2 - scale * x, h/2-scale*y];
Now you can set the initial scale and translate of the map:
g.attr("transform", "translate("+translate+")scale("+scale+")");
But, you probably want to update the zoom parameters on page load to reflect the initial zoom and translate:
zoom.translate(translate);
zoom.scale(scale);
This way when you zoom in or out from the initial view, the change is relative to your initial zoom.
Now all you have to do is include the above code when you add the points. Note that this technique might require some modification if you want to return to the initial position.
I'm trying to build something with d3.js and GeoJSON for the first time. I have managed to get a country - Estonia to be displayed but it is so small you can barely see it. I tried to play with projection geoMercator().scale but it doesn't work - no increase in size.
Please see a picture attached below(under bar chart):
Here is my js:
var projection = d3.geoMercator()
.translate([w/2, h/2])
.scale([100]);
var path = d3.geoPath()
.projection(projection)
var w3 = 2000;
var h3 = 1500;
var svg = d3.select("body")
.append("svg")
.attr("width", w3)
.attr("height", h3)
d3.json("estonia.json", function (json){
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", "#2294AA");
})
What am I doing wrong?
I took your code and saw that when I increased the scale, Estonia disappeared. Had to center the projection like this:
var projection = d3.geoMercator()
.center([24.312863, 57.793424])
.scale([500])
.translate([w/2, h/2])
I took the coordinates [24.312863, 57.793424] from the .json file.
I need to draw draw many filled rectangles inside a graph and would like to achieve this using svg paths.
(I'm not using the rect tag because perfomance will suffer over time.)
My current approach uses the d3.svg.area to generate the path for each area, but the rectangles are not being drawn properly.
As far as I understand the rendered path attribute, it seems that path is missing a moveTo per rectangle.
The following is my simplified code of the problem.
var data = [
{x0:0,x1:50,y0:0,y1:10},
{x10:0,x1:60,y0:20,y1:30},
];
var width = 500;
var barHeight = 20;
var areaFunc = d3.svg.area()
//.interpolate('step')
.x0(function(d){return d.x0;})
.x1(function(d){return d.x1;})
.y0(function(d){return d.y0;})
.y1(function(d){return d.y1;});
var chart = d3.select('#chart')
.attr('width', width)
.attr('height', barHeight * data.length);
chart.append('path')
//.data(data)
.attr('d', areaFunc(data))
.attr('class', 'absences area')
.attr('style', 'fill:blue;stroke:black;stroke-width:1');
JSFiddle: http://jsfiddle.net/3kLdkgz8/
If you want several rectangles, you need to define your data like that
var data = [
[{x0:10,x1:60,y0:0,y1:0},{x0:10,x1:60,y0:20,y1:20}], // rect 1
[{x0:100,x1:600,y0:20,y1:20},{x0:100,x1:600,y0:200,y1:200}]// rect 2
];
And call it like that
chart.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', areaFunc)
.attr('class', 'absences area')
.attr('style', 'fill:blue;stroke:black;stroke-width:1');
See http://jsfiddle.net/zh1vqfos/2/
I am new to 3Djs and today I've built a geographic map of England from a JSON (TopoJSON) to be used as a chart.
What I would need now is to color fill the resulting SVG dynamically based on a percentage (it's an achievement, like how much of the country you have traveled) but I can't find which technique should I use to accomplish that.
EDIT: just a color is used to fill the map. What changes is what percentage of the map area is color filled (eg. 30% (achieved) red 70% white (not achieved), 50% red 50% white and so on). (My reputation doesn't allow me to post images.)
This is the code I've used to build the map
var width = 120,
height = 145;
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(1200 * .6)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
var svg = d3.select(".target-distribution").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("uk.json", function(error, uk) {
svg.selectAll(".subunit")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary IRL");
svg.append("path")
.datum(topojson.feature(uk, uk.objects.places))
.attr("d", path)
.attr("class", "place");
svg.selectAll(".place-label")
.data(topojson.feature(uk, uk.objects.places).features)
});
What I did to accomplish my goal:
Clone the SVG map (with the jQuery clone() method)
Overlaid the clone on the original (via position: absolute)
Used the CSS clip property on the clone to only show the area I needed.
Try checking out this useful example from d3's creator:
This shows a map with geo data from a TopoJSON file and population data loaded separately from a .csv file, in which the population data determines the fill color:
http://bl.ocks.org/mbostock/4060606