I would like to use Datamaps.js in a similar way as Google Charts in order to select some regions around the world.
Firstly, I would like to access the default "setProjection" function, but I do not know how.
Then, I would like to create projections in several regions around the world.
However, I have seen only the Africa example in the Datamaps web
Any help would be appreciated.
EDIT:
What I am currently testing is this code. It surely could be improved, but it works:
function getProjection() {
//Projection por defecto
var currentProjection;
var region = $("#mapRegions")[0].value;
if (region === 0) {
// Zoom in on World
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([0, 20])
.rotate([-10, 0])
.scale(65)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
} else if (region == 1) {
// Zoom in on Africa
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([23, 5])
.rotate([4.4, 0])
.scale(150)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
} else if (region == 2) {
// Zoom in on Europa
currentProjection = function(element) {
var projection = d3.geo.mercator()
.center([45, 60])
.rotate([0, 0])
.scale(150)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
var path = d3.geo.path()
.projection(projection);
return {
path: path,
projection: projection
};
}
}
return currentProjection;
}
The key was to change the d3.geo projection, as I am currently working with Mercator.
Related
I'm trying to build a d3js chart that zooms only on the X-axis but allows panning on both axes. The example below has the effect I desire:
https://jsfiddle.net/xpr364uo/
However, I'm having trouble translating this into my own code. For one, I'm rendering to canvas so I don't have the ability to set the "transform" attribute on some element. Also my zooming uses rescaleX/rescaleY on copies of the scales, as is the "new way" to do zooming via d3-zoom, from what I understand:
const zoomBehavior = zoom().on('zoom', () => {
const xDomain = event.transform.rescaleX(x2).domain();
const yDomain = event.transform.rescaleY(y2).domain();
xScale.domain(xDomain);
yScale.domain(yDomain);
render();
});
This works to zoom/pan on both axes. How can I modify it to get the same affect as in the fiddle? What am I supposed to do with deltaPanY (from the fiddle), in my code?
You could keep track of a second zoom transform (I'll call this yTransform) and use this to rescale the y axis. As you want the x to zoom normally, you can still use d3.event.transform.rescaleX() to rescale on the X axis, while the yTransform can be used to rescale on the Y axis.
When panning, the y translate value of yTransform should be updated with the current zoom state. Conversely, when zooming, yTransform should be used to override the change in the zoom state's y translate.
Perhaps something like:
var yTransform = d3.zoomIdentity; // initial state for the y transform
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform; // zoom state
x2 = t.rescaleX(x); // rescale x as normal (t.y is irrelevant)
// for a pan event, update the y translate
if (d3.event.sourceEvent.type != "wheel") yTransform.y = t.y;
// for a scroll, use the current y translate
else t.y = yTransform.y;
y2 = yTransform.rescaleY(y); // rescale y.
render();
})
The k and x values for yTranslate don't matter: the scale is always 1 as we aren't zooming in, and the x translate is irrelevant to rescale on the y axis. The above doesn't account for double click events, but I'll add that below.
var dots = d3.range(100)
.map(function() {
return {x: Math.random(), y: Math.random()}
})
var x = d3.scaleLinear().range([0,500])
var x2 = x.copy();
var y = d3.scaleLinear().range([0,300])
var y2 = y.copy();
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
// Just for reference:
var axis = d3.axisRight(y);
var g = d3.select("svg").append("g");
g.call(d3.axisRight(y2))
render();
var yTransform = d3.zoomIdentity;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") yTransform.y = t.y;
else t.y = yTransform.y;
y2 = yTransform.rescaleY(y);
render();
})
canvas.call(zoom);
function render() {
context.clearRect(0,0,500,300);
dots.forEach(function(d) {
context.beginPath();
context.arc(x2(d.x), y2(d.y), 5, 0, 2 * Math.PI);
context.stroke();
})
g.call(d3.axisRight(y2));
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
svg {
pointer-events:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="500" height="300"></canvas>
<svg width="500" height="300"></svg>
As I'm modifying properties directly - which is not the most ideal.
Alternatively, we could track a translate offset on the y (the difference between a y translate with dbl clicks/wheel events and without those events). Both y offset and y translate could be used to create an appropriate zoom transform:
var yOffset = 0;
var lastY = 0;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") {
lastY = t.y - yOffset;
y2 = d3.zoomIdentity.translate(0,t.y-yOffset).rescaleY(y);
}
else {
yOffset = t.y - lastY; // ignore change in y for dbl click and wheel events
}
render();
})
var dots = d3.range(100)
.map(function() {
return {x: Math.random(), y: Math.random()}
})
var x = d3.scaleLinear().range([0,500])
var x2 = x.copy();
var y = d3.scaleLinear().range([0,300])
var y2 = y.copy();
var canvas = d3.select("canvas")
var context = canvas.node().getContext("2d");
// Just for reference:
var axis = d3.axisRight(y);
var g = d3.select("svg").append("g");
g.call(d3.axisRight(y2))
render();
var yOffset = 0;
var lastY = 0;
var zoom = d3.zoom()
.on("zoom", function() {
var t = d3.event.transform;
x2 = t.rescaleX(x);
// For dbl clicks, d3.event.sourceEvent is null.
if (d3.event.sourceEvent && d3.event.sourceEvent.type != "wheel") {
lastY = t.y - yOffset;
y2 = d3.zoomIdentity.translate(0,t.y-yOffset).rescaleY(y);
}
else {
yOffset = t.y - lastY; // ignore change in y for dbl click and wheel events
}
render();
})
canvas.call(zoom);
function render() {
context.clearRect(0,0,500,300);
dots.forEach(function(d) {
context.beginPath();
context.arc(x2(d.x), y2(d.y), 5, 0, 2 * Math.PI);
context.stroke();
})
g.call(d3.axisRight(y2));
}
canvas, svg {
position: absolute;
top: 0;
left: 0;
}
svg {
pointer-events:none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="500" height="300"></canvas>
<svg width="500" height="300"></svg>
I would like to create D3 bubble charts by placing the creation in an object and calling instances of the object.
Outside of an object the script works well, showing scarttered bubbles, distributed among the canvas.
However, when I attempt to place the script in an object constructor, the forceSimulation function doesn't seem to scattered the bubbles on about the y axis. They simply gather together at the same cy and cx
this.forceStrength = 0.03;
this.width = 940;
this.height = 600;
this.center = { x: this.width / 2, y: this.height / 2 };
this.svg = null;
this.bubbles = null;
this.nodes = [];
this.charge = function(d) {
return -Math.pow(d.radius, 2.0) * this.forceStrength;
}
this.simulation = d3.forceSimulation()
.velocityDecay(0.2) //controls the friction at each "tick"
.force('x', d3.forceX().strength(this.forceStrength).x(this.center.x))
.force('y', d3.forceY().strength(this.forceStrength).y(this.center.y))
.force('charge', d3.forceManyBody().strength(this.charge))
.on('tick',this.ticked)
.on('end', function() {
// layout is done
console.log("end")
});
UPDATE Here is a jsfiddle of the full example code
I've been keen on developing a choropleth map for Namibia.But found two interesting tools. leaflet and D3, though leaflet has clear instructions to implement which i did Its not so functionally in line with what i want to do. And that is where D3Geo came in. I've everything set but this function below to set my projection.
var projection = d3.geo.conicConformal()
.rotate([, 0])
.center([0, 0])
.parallels([ , ])
.scale(1000)
Is there just no function to just simply add the co-ordinates as how its done in the leaflet function below. for us who are not so geocentric.
var map = L.map('mapid').setView([-22.26,16.52], 5);
And if there isn't, can someone please guide me on how to convert the coordinates (-22.26,16.52 )to show Namibia using the d3.geo.conicConformal().
Correct me if it didn't address your issue (maybe you can provide a minimal example showing where you are stuck, using JSFiddle for example), but if I understand well you want to move/zoom/center the displayed image on the extend of your country. Here is an example doing this (I also added some code on how the layer was added for consistency):
// Define the projection you want to use,
// setting scale and translate to some starting values :
var projection = d3.geoConicConformal()
.translate([0, 0])
.scale(1)
var layer_name = "your_layer_name";
var geo_features = topojson.feature(topoObj, topoObj.objects[layer_name]).features;
// Define the path generator :
var path = d3.geoPath().projection(projection);
var width = 800,
height = 600;
// This is the main svg object on which you are drawing :
var map = d3.select("body").append("div")
.style("width", width + "px")
.style("height", height + "px")
.append("svg")
.attr("id", "svg_map")
.attr("width", width)
.attr("height", height);
// Add you layer to the map
map.append("g").attr("id", layer_name)
.attr("class", "layers")
.selectAll("path")
.data(geo_features)
.enter().append("path")
.attr("d", path)
.attr("id", (d,i)=> "feature_" + i)
.styles({"stroke": "rgb(0, 0, 0)", "fill": "beige")
// Where the job is done :
scale_to_layer(layer_name)
function scale_to_layer(name){
var bbox_layer = undefined;
// use all the paths of the layer (if there is many features)
// to compute the layer bbox :
map.select("#"+name).selectAll('path').each(function(d, i){
var bbox_path = path.bounds(d);
if(bbox_layer === undefined){
bbox_layer = bbox_path;
}
else {
bbox_layer[0][0] = bbox_path[0][0] < bbox_layer[0][0]
? bbox_path[0][0] : bbox_layer[0][0];
bbox_layer[0][1] = bbox_path[0][1] < bbox_layer[0][1]
? bbox_path[0][1] : bbox_layer[0][1];
bbox_layer[1][0] = bbox_path[1][0] > bbox_layer[1][0]
? bbox_path[1][0] : bbox_layer[1][0];
bbox_layer[1][1] = bbox_path[1][1] > bbox_layer[1][1]
? bbox_path[1][1] : bbox_layer[1][1];
}
});
// Compute the new scale param, with a little space (5%) around the outer border :
var s = 0.95 / Math.max((bbox_layer[1][0] - bbox_layer[0][0]) / width,
(bbox_layer[1][1] - bbox_layer[0][1]) / height);
// Compute the according translation :
var t = [(width - s * (bbox_layer[1][0] + bbox_layer[0][0])) / 2,
(height - s * (bbox_layer[1][1] + bbox_layer[0][1])) / 2];
// Apply the new projections parameters :
projection.scale(s)
.translate(t);
// And redraw your paths :
map.selectAll("g.layer").selectAll("path").attr("d", path);
};
Also, note that this example use d3 v4 (but in this case it doesn't change a lot apart from the naming of geoPath and geoConicConformal)
On a dc.geoChoroplethChart, I'm setting the radius of geojson points using the pointRadius method of the path:
.geoPath().pointRadius(function(feature, index) {
var v = placeGroup.all().filter(function(item) { return item.key === feature.id; })[0].value;
return (v == 0)? 0 : pointScale(v);
});
I'm finding that it works well, but on redraw() the sizes of the points are not adjusted. They are adjusted on a render(). How do I get them to be adjusted with a redraw() as well?
Here's the full chunk of code for the geo chart, in case it's relevant
var projection = d3.geo.mercator()
.scale(1)
.translate([0, 0]);
var path = d3.geo.path()
.projection(projection);
var width = 280,
height = 200,
b = path.bounds(places), // [[left, top], [right, bottom]]
x_extent = Math.abs(b[1][0] - b[0][0]),
y_extent = Math.abs(b[1][1] - b[0][1]),
s = (.95 / Math.max(x_extent / width, y_extent / height)),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
// Update projection with our actual data
projection
.scale(s)
.translate(t)
;
var mapchart = dc.geoChoroplethChart("#map-chart");
var valueDomain = [0, placeGroup.top(1)[0].value];
var maxPointRadius = Math.min(width, height) / 40,
minPointRadius = maxPointRadius / 2;
var pointScale = d3.scale.linear()
.domain(valueDomain)
.range([minPointRadius, maxPointRadius]);
mapchart.width(width)
.height(height)
.projection(projection)
.dimension(placeDim)
.group(placeGroup)
.colors(d3.scale.quantize().range(['#feb24c','#fd8d3c','#fc4e2a','#e31a1c','#bd0026'])) //first three '#ffffcc','#ffeda0', '#fed976', last one,'#800026'
.colorDomain(valueDomain)
.colorCalculator(function (d) { return d ? mapchart.colors()(d) : '#ccc'; })
.overlayGeoJson(places.features, "placeLayer", function (d) {
return d.id;
}).geoPath().pointRadius(function(feature, index) {
var v = placeGroup.all().filter(function(item) { return item.key === feature.id; })[0].value;
return (v == 0)? 0 : pointScale(v);
});
It looks like the geoChoroplethChart won't redraw the geojson unless the projection has changed. (It isn't expecting you to change the geoPath - as stated in the documentation, that's mostly a convenience method for reading and determining the center.)
https://github.com/dc-js/dc.js/blob/develop/src/geo-choropleth-chart.js#L169-L171
As a workaround, I'd suggest forcing a redraw by resetting the projection each time the chart redraws. Something like:
mapchart.on('preRedraw', function() {
mapchart.projection(projection);
});
https://github.com/dc-js/dc.js/blob/develop/web/docs/api-latest.md#basemixinon--basemixin
I have been struggling for a few days trying to use dc.js with d3.js projection to draw a map of South Africa and the provinces. I have exhausted my search as most results incorporate the path used for SVG when not using dc.js and I can't seem to find a suitable example for correcting a projection in dc.js.
I can't seem to find the map thats being drawing and I don't know how to correct my projection.
I really really don't know what i'm missing, and anyone that can assist will be appreciated.
UPDATE: I have geoJson that ive tested in mapshaper and it works so the geojson is fine. I am just struggling with the projection.
zaMap = zaMapString
//new array
var zaMapData = [];
for(var p in zaMap["features"])
{
console.log("ndx2 province data " + zaMap["features"][p]["properties"]["name"]);
zaMapData.push({
province: zaMap["features"][p]["properties"]["name"],
donation: 1000
})
};
//crossfilter instance
var ndx2 = crossfilter(zaMapData);
//dimensions and group for dc/d3
var provinceDim = ndx2.dimension(function(d) {console.log("province d " + d["province"]); return d["province"];});
var donationsByProvince = provinceDim.group().reduceSum(function(d) {
return d["donation"];
});
//geoChoroplethChart
var zaChart = dc.geoChoroplethChart("#map");
//start of chart code using d3 and dc
zaChart.dimension(provinceDim)
.group(donationsByProvince)
.width(1000)
.height(330)
.colors(["#E2F2FF", "#C4E4FF", "#9ED2FF", "#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"])
.projection(d3.geo.mercator()
.scale(26778)
.translate([8227, 3207]))
.overlayGeoJson(zaMap["features"], "name", function (d) {
return d.properties.name;
});
dc.renderAll();
$("#progress").css({"display": "none"});
})
UPDATE 2: I switched from fiddle to codepen so I could upload the geoJson file as a asset. The geoJson takes a while to load but using code from an existing stackoverflow question, I have gotten the map to draw and projection to correct itself automatically. The d3.js function is not wrapping the dc.js to tie in with crossfilter.js as yet but I am working on that. But this is progress :)
In http://jsfiddle.net/Jimmypoo/f67xo5ry/1/, you are trying to use JSON.parse to parse an zaMapString, which is already a JS object. You don't need to parse it, it's not a string.
Secondly, d3.json is meant for passing in a remote URL, which d3 grabs for you. You are trying to pass in a JS object, which already exists. So you can remove that function, and simply use .overlayGeoJson(zaMap["features"], "name", function (d) { inside.
You also forgot to include jQuery, yet you use it in $("#progress").css({"display": "none"});. You'll need to wrap the entire JS section in a $(document).ready as well.
Also, you are including the scripts multiple times, in both minified and unminified forms.You only need one instance of each library.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.min.js"></script>
You are also trying to include dc's CSS as JavaScript.
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.css"></script>
It should be added in JsFiddle's left-hand side resource panel instead.
I also don't think assigning #map directly to the body of your document is going to make things easier for you either..would recommend including something interior of that like <div id="map" style="width:100%;height:300px"></div>
These suggestions don't solve all your problems but get you most of the way along.You still have projection issues. Here is an forked fiddle to move from - http://jsfiddle.net/uggtjem6/
I have gotten the geoJson to work with d3.js, dc.js and crossfiler.
var width = 300;
var height = 400;
var zaMapData = [];
//geoChoroplethChart
var zaChart = dc.geoChoroplethChart("#map");
d3.json("https://s3-us-west-2.amazonaws.com/s.cdpn.io/384835/layer1.json", function(json) {
var zaMap = JSON.stringify(json);
console.log(zaMap);
for (var i = 0; i < json.features.length; i++) {
console.log("ndx2 province data " + json["features"][i]["properties"]["PROVINCE"]);
zaMapData.push({
province: json["features"][i]["properties"]["PROVINCE"],
donation: i*1000
})
};
//crossfilter instance
var ndx2 = crossfilter(zaMapData);
//dimensions and group for dc/d3
var provinceDim = ndx2.dimension(function(d) {console.log("province d " + d["province"]); return d["province"];});
var donationsByProvince = provinceDim.group().reduceSum(function(d) {
return d["donation"];
});
var max_province = donationsByProvince.top(1)[0].value;
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
//create dc.js chart
zaChart.dimension(provinceDim)
.group(donationsByProvince)
.width(width)
.height(height)
.colors(["#E2F2FF", "#C4E4FF", "#9ED2FF", "#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"])
.colorDomain([0, max_province])
.projection(d3.geo.mercator()
.center(center)
.scale(scale)
.translate(offset))
.overlayGeoJson(json["features"], "PROVINCE", function (d) {
return d.properties.PROVINCE;
})
.title(function (p) {
return "Province: " + p["key"]
+ "\n"
+ "Total Donations: R " + Math.round(p["value"])
});
dc.renderAll();
});
My codepen here.