scaling geo coordinates in D3.js - 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]);

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).

d3 choropleth map is extremely small

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);

How to keep polyline and svg shape object connection when svg shape object moves

I want to connect two svg shape objects using polyline.I draw two svg shape objects one by one and after that I draw that polyline to connect those shape objects.If i move one svg shape object that polyline should be come with that shape object.Polyline and svg object connection should fixed after drawing polyline.
I draw shapes like this
var sampleSVG = svg;
sampleSVG.append('circle')
.attr('id', 'startcricle'+idstartelement++)
.style("stroke", "black")
.style("stroke-width", "1.5")
.style("fill", "white")
.attr('transform', 'translate(' + '100' + ',' + '100' + ')')
.attr('r', '20')
.on("mouseover",function(d){
var t = d3.select(this).attr("id");
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x*ctm.a + y*ctm.c;
var yn = ctm.f + x*ctm.b + y*ctm.d;
return { x: xn, y: yn };
}
var circle = document.getElementById(t),
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
ctm = circle.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
if(arrowbuttonclick==1){
startx=coords.x;
starty=coords.y;
arrowbuttonclick=2;
}else if (arrowbuttonclick==2) {
endx=coords.x;
endy=coords.y;
arrowpath(startx,starty,endx,endy);
midx = startx+((endx-startx)/2);
console.log("ok2")
console.log(startx+","+starty+","+midx+","+starty+","+midx+","+endy+","+endx+","+endy)
sampleSVG.append("polyline") // attach a polyline
.style("stroke", "black") // colour the line
.style("fill", "none") // remove any fill colour
.attr("points", startx+","+starty+","+midx+","+starty+","+midx+","+endy+","+endx+","+endy);
startx=0;
starty=0;
endx=0;
endy=0;
arrowbuttonclick=0;
}
})
.call(drag)
});
When I click line button I change arrowbuttonclick=1.After select svg shapes it take coordinates and draw polyline.
Do i need to remove current two svg objects and create new svg objects with polyline to do this task or is there any easy way to do this?

D3 scatter plot - visible points outside the chart after zoom

I'm new to D3 and I've a question related to adding zoom into a chart.
I built a scatter plot with zoom/pan and everything is working except for the fact that when I use the zoom on the chart, I can see some points outside the chart "area" and I really want to avoid that.
The zoom behavior looks like this:
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed);
And the zoomed function like this:
function zoomed() {
var panX = d3.event.translate[0];
var panY = d3.event.translate[1];
var scale = d3.event.scale;
panX = panX > 10 ? 10 : panX;
var maxX = -(scale-1)*width-10;
panX = panX < maxX ? maxX : panX;
panY = panY > 10 ? 10 : panY;
var maxY = -(scale-1)*height-10;
panY = panY < maxY ? maxY : panY;
zoom.translate([panX, panY]);
main.select(".x.axis").call(xAxis);
main.select(".y.axis").call(yAxis);
main.selectAll("circle")
.attr("cx", function (d,i) { return xScale(d[0]); } )
.attr("cy", function (d) { return yScale(d[1]); } )
.attr("r", 5);
}
You can see a working example (and all the code) here
Is there a way I can define the "area" of scope for the zoom, so any point outside this area is not visible? or can I add something into the zoomed function to fix this?
Thank you very much.
One option would be to restrict the g region of the element in which your points are drawn using a clip path
See http://jsfiddle.net/henbox/duwyay3y/1/ for the code.
First define your clip path:
var clip = main.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', width)
.attr('height', height);
then add to the g element
main.append("g").attr("clip-path", "url(#clip)")
.selectAll("circle")
...

Resources