d3 choropleth map is extremely small - d3.js

I am trying to draw an svg map from a topojson file located here. When I run the code below, I see a small red collection of g elements that is that map, but I'm not sure how to make it larger. I've tried doing projection.scale(100) but that does not work.
Here is a fiddle.
<svg width=500 height=500></svg>
async function run() {
const res = await fetch(
"https://rawcdn.githack.com/jasonicarter/toronto-geojson/0fb40bd54333bc3d397a26cf4f68abb1b6d94188/toronto_topo.json"
);
const jsondata = await res.json();
const width = 500;
const height = 500;
const neighbourhoods = topojson.feature(jsondata, jsondata.objects.toronto);
const projection = d3.geoAlbers().translate([width / 2, height / 2])
const svg = d3.select("svg")
svg
.append("g")
.selectAll("path")
.data(neighbourhoods.features)
.enter()
.append("path")
.attr("d", d3.geoPath().projection(projection))
.attr("fill", "red")
.attr("stroke", "white");
console.log("done")
}
run();

Indeed, you have to use the scale and the translate properties to scale / center your map.
But d3.geoProjection also provides some convenience functions such as fitExtent and fitSize in order to fit the projection on one specific GeoJSON feature object.
As your dataset is containing many features, I propose to use topojson.mesh to obtain a unique object representing your whole dataset (as a mesh) to use its extent with the fitSize method of the projection to scale your map :
const neighbourhoods = topojson.feature(jsondata, jsondata.objects.toronto);
const mesh = topojson.mesh(jsondata, jsondata.objects.toronto);
const projection = d3.geoAlbers()
.fitSize([width, height], mesh);
const svg = d3.select("svg")
svg
.append('g')
.selectAll("path")
.data(neighbourhoods.features)
.enter()
.append("path")
.attr("d", d3.geoPath().projection(projection))
.attr("fill", "red")
.attr("stroke", "white");
Which (after adding a border on the svg element) gives the following :
If you wanted to fit the extent using a some padding (lets say 20px) you could have use the following :
const projection = d3.geoAlbers()
.fitExtent([[20, 20], [width - 20, height - 20]], mesh);

Related

How do I use event listeners to swtich my d3 bar chart where I only see one and not both?

I have 2 buttons that i want to use to control what data set I am using for my bar chart. Right now I can click on one and it shows my d3 graph without problems. But when I want to switch to the other graph, I click on the button and it shows me that graph on top of my previous graph. How do I make it so that when I switch between graphs, it only shows me one graph.
var djockey = 'top5jockey.csv'
var dtrainer = 'top5trainer.csv'
// Define SVG area dimensions
var svgWidth = 1500;
var svgHeight = 1000;
// Define the chart's margins as an object
var chartMargin = {
top: 30,
right: 30,
bottom: 130,
left: 30
};
// Define dimensions of the chart area
var chartWidth = svgWidth - chartMargin.left - chartMargin.right;
var chartHeight = svgHeight - chartMargin.top - chartMargin.bottom;
// Select body, append SVG area to it, and set the dimensions
var svg = d3.select("body")
.append("svg")
.attr("height", svgHeight)
.attr("width", svgWidth);
// Append a group to the SVG area and shift ('translate') it to the right and to the bottom
var chartGroup = svg.append("g")
.attr("transform", `translate(${chartMargin.left}, ${chartMargin.top})`);
var btnj = document.getElementById("Jockey")
btnj.addEventListener('click', function(e){
change(e.target.id)
})
var btnt = document.getElementById("Trainer")
btnt.addEventListener('click', function(e){
change(e.target.id)
})
function change(value){
if(value === 'Jockey'){
update(djockey);
}else if(value === 'Trainer'){
update(dtrainer);
}
}
function update(data){
d3.csv(data).then(function(data) {
console.log(data);
// Cast the hours value to a number for each piece of tvData
data.forEach(function(d) {
d.Count = +d.Count;
});
// Configure a band scale for the horizontal axis with a padding of 0.1 (10%)
var xScale = d3.scaleBand()
.domain(data.map(d => d.Name))
.range([0, chartWidth])
.padding(0.1);
// Create a linear scale for the vertical axis.
var yLinearScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.Count)])
.range([chartHeight, 0]);
// Create two new functions passing our scales in as arguments
// These will be used to create the chart's axes
var bottomAxis = d3.axisBottom(xScale);
var leftAxis = d3.axisLeft(yLinearScale).ticks(10);
// Append two SVG group elements to the chartGroup area,
// and create the bottom and left axes inside of them
chartGroup.append("g")
.call(leftAxis);
chartGroup.append("g")
.attr("class", "x_axis")
.attr("transform", `translate(0, ${chartHeight})`)
.call(bottomAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
// Create one SVG rectangle per piece of tvData
// Use the linear and band scales to position each rectangle within the chart
chartGroup.selectAll("#bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.Name))
.attr("y", d => yLinearScale(d.Count))
.attr("width", xScale.bandwidth())
.attr("height", d => chartHeight - yLinearScale(d.Count));
}).catch(function(error) {
console.log(error);
})
};
D3 has a function allowing you to remove all svg elements. Basically, you select the svg, then run .remove() at the top of your event listener. It will clear out all svg elements.

How to adjust the size of a d3.js globe?

I'm trying to set the size of this globe to 200 x 200px.
I've learned that the projection is currently sized 960 x 500px.
Changing the size of the SVG doesn't shrink the globe. I'm having trouble understanding why.
Without luck I have tried to add the following to the code:
var width = 200;
var height = 200;
And
const width = 200;
const height = 200;
And
const svg = d3.select('svg')
.attr('width', 200).attr('height', 200);
How would I best approach this, and what am I doing wrong?
My code:
<!DOCTYPE html>
<html>
<body>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script>
const width = 960;
const height = 500;
const config = {
speed: 0.005,
verticalTilt: -20,
horizontalTilt: 0
}
let locations = [];
const svg = d3.select('svg')
.attr('width', width).attr('height', height);
const markerGroup = svg.append('g');
const projection = d3.geoOrthographic();
const initialScale = projection.scale();
const path = d3.geoPath().projection(projection);
const center = [width/2, height/2];
drawGlobe();
drawGraticule();
enableRotation();
function drawGlobe() {
d3.queue()
.defer(d3.json, 'world-110m.json')
.defer(d3.json, 'locations.json')
.await((error, worldData, locationData) => {
svg.selectAll(".segment")
.data(topojson.feature(worldData, worldData.objects.countries).features)
.enter().append("path")
.attr("class", "segment")
.attr("d", path)
.style("stroke", "silver")
.style("stroke-width", "1px")
.style("fill", (d, i) => 'silver')
.style("opacity", ".5");
locations = locationData;
drawMarkers();
});
}
function drawGraticule() {
const graticule = d3.geoGraticule()
.step([10, 10]);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", "#fff")
.style("stroke", "#ececec");
}
function enableRotation() {
d3.timer(function (elapsed) {
projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
svg.selectAll("path").attr("d", path);
drawMarkers();
});
}
function drawMarkers() {
const markers = markerGroup.selectAll('circle')
.data(locations);
markers
.enter()
.append('circle')
.merge(markers)
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.attr('fill', d => {
const coordinate = [d.longitude, d.latitude];
gdistance = d3.geoDistance(coordinate, projection.invert(center));
return gdistance > 1.57 ? 'none' : 'tomato';
})
.attr('r', 7);
markerGroup.each(function () {
this.parentNode.appendChild(this);
});
}
</script>
</body>
</html>
Projection.scale()
The scale of the projection determines the size of the projected world. Generally speaking d3 projections have a default scale value that will fill a 960x500 SVG/Canvas. A map produced with d3.geoOrthographic doesn't have a long edge, so this is 500x500 pixels. The default scale value is: 249.5 - half the width/height (allowing for stroke width). This scale factor is linear on both width and height: double it and double both (quadruple projected size of world). So if you want a 200x200 px world you'll want: 99.5 to be your scale value.
This is the default for d3.geoOrthographic, other scales have other scale defaults. For a Mercator, for example, it is 480/π: 2π of longitude across 960 pixels of width.
Projection.translate()
However, if you change the scale for a 200x200 pixel world, you'll have an issue with the default projection translate. By default this is set to [250,480] - half of [500,960], the default D3 anticipated size of the SVG/Canvas. This coordinate is where the geographic center of the projection (by default 0°N,0°W) is projected to. You'll want to change this to a value of [100,100]: the center of your SVG/Canvas.
Solution
const projection = d3.geoOrthographic()
.scale(99.5)
.translate([100,100]);
Automagic Solution
There is an easier way, but understanding the mechanics can be useful.
projection.fitSize()/.fitExtent() both set scale and translate automatically based on a specified width/height / extent. In your case this is easy to solve manually, but you could also use:
d3.geoOrthographic()
.fitSize([width,height],geoJsonObject)
or
d3.geoOrthographic()
.fitExtent([[left,top],[right,bottom]],geojsonObject)
As you're using topojson: topojson.feature returns a geojson object (with a features property containing individual features - the array of features can't be passed to fitSize or fitExtent).

svg - Aligning text outside circular arc d3js

Can someone help me to create below image using d3js. I able to create pie chart as required but stuck to render outer text with arrows and all.
Wheel with outer text
As of know I have achieved circle creation using below code.
var svg = d3.select("svg");
var margin = {top: 40, right: 45, bottom: 30, left: 40};
console.log(svg);
var width = svg.attr('width');
var height = svg.attr('height');
var radius = Math.min(width, height)/2;
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var hoverStyle = {
zindex: '2px'
};
var hoverExitStyle = {
zindex: "0px"
}
var animateSpeed = 500;
// Define a Pie
var pie = d3.pie()
.sort(null)
.value(function(d) {return d.number});
// define pie section
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
//
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
// Get pie sections based on the data.
var pieSections = pie(data);
var arc = g.selectAll('.arc')
.data(pieSections)
.enter().append("g")
.attr("class", "arc")
.append('a')
.attr("href", function(d) { return d.data.url; });
arc.append("path")
.attr("d", path).transition()
.attr("fill", function(d) { return d.data.color; });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.title; });
The text and the arrows are two separate concerns that probably merit their own questions.
Curved text
To do text on a path with d3, you might want to look at the textpath documentation. It's going to be a little tricky; basically, you'll want to create a second d3.arc() generator with a slightly longer outer radius. Use the longer one to set the d attribute of path elements (that you need to create) inside the SVG's defs object, and reference those path elements' ids from textpath elements (that you also need to create).
Curved arrows
To accomplish this exactly like the image, you're probably going to need to some manual construction (including figuring out the math!) of the d attribute yourself to add appropriate arrowheads (see the SVG path syntax). If you're doing a static image, it might be faster to just create the lines (again, using a longer-radius d3.arc() generator), and export the SVG with something like SVG crowbar to a drawing program like Illustrator or Inkscape, and add the arrowheads there.

D3 js map overflowing SVG Canvas - Part of the map is completely hidden

Am terribly irritated in making this simple (very very simple map) to get it work
Here is the JSON
https://raw.githubusercontent.com/datasets/geo-boundaries-world-110m/master/countries.geojson
This is the code
var w = 800;
var h = 800;
var svg = d3.select("#map")
.append("svg")
.attr("width", w)
.attr("height", h);
//Load in GeoJSON data
d3.json('https://raw.githubusercontent.com/datasets/geo-boundaries-world-110m/master/countries.geojson', function(error,geoJSON){
svg.selectAll("path")
.data(geoJSON.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", "steelblue");
});
And all is see is , one huge Africa map and rest of the map is eaten by SVG width. How to resize the map to fit SVG size.d3
You are missing the projection:
I am assuming you need geoEquirectangular projection so your code will become:
var projection = d3.geoEquirectangular()
.scale(h / (2*Math.PI))//scale it down to h / (2*Math.PI)
.translate([w/2, h/4]);//translate as per your choice
var path = d3.geoPath()
.projection(projection);
Other projection are here
Working code here

scaling geo coordinates in D3.js

I like to draw a circle in each coordinate. Using only width, height, and coordinates, how do I scale the coordinates? I'm very new to D3.js and I guess I'm doing something wrong with the projection part.
var width = 200,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var coordinates = [ [43.08803611,-79.06312222],
[43.09453889,-79.05636667] ];
var group = svg.append('g');
var projection = d3.geo.mercator()
.translate([width,height]);
var projectedCoordinates = [];
for (var i=0; i<coordinates.length; i++) {
projectedCoordinates[i] = projection(coordinates[i]);
}
group.selectAll("circle")
.data(projectedCoordinates)
.enter()
.append("circle")
.attr("r",4)
.attr("cx", function(d){ return d[0]; })
.attr("cy", function(d){ return d[1]; });
You are almost there, the only problem is that your projection is projecting the coordinates outside your drawing area. You can use the .center() function to tell the projection where the center is and .scale() to "zoom in". You should also only translate the projection by half the width and height of the container, otherwise the center will be in the bottom right corner.
The following example values should enable you to see the points:
var projection = d3.geo.mercator()
.center([43.09, -79.06])
.scale(50000)
.translate([width/2,height/2]);

Resources