I have a project where i want to draw points on the San Fransico map in D3, the problem is that the point is only shown in the topleft corner.
The scale and the translate of the projection are calculated based on the coordinates.
// Set up size
var width = 1000,
height = width;
//coordinates
var lattop = 37.8423216;
var lonleft = -122.540474;
var lonright = -122.343407;
//calculate scale
var scale = 360*width/(lonright-lonleft);
//var scale = Math.min(width/ Math.PI, height/ Math.PI)
// Set up projection that map is using
var projection = d3.geo.mercator()
.scale(scale)
.translate([0,0]);
var trans = projection([lonleft, lattop]);
projection.translate([-1*trans[0],-1*trans[1]]);
var path = d3.geo.path().projection(projection);
var imagebox = d3.select("#map").append("svg").attr("width",width).attr("height",height);
How the points are drawed (all the coordinates of the points are between lattop, longleft and longright)
points = data
allDataLayer = imagebox.append("g");
var category = {}
allDataLayer.selectAll("circle")
.data(points).enter()
.append("circle")
.attr("class", "circles")
.attr("cx", function (d) { console.log(d.X); return projection(d.X); })
.attr("cy", function (d) { return projection(d.Y); })
.attr("r", "20px")
.attr("fill", "#C300C3")
.style("opacity", 0.5)
Why are these points not drawn on their coordinates on the map?
Allready viewed this answer: Stackoverflow question d3js-scale-transform and translate
You are not using your projection correctly:
return projection(d.X);
In most instances, a plain unadultered Mercator such as yours excepted, the x or y value of a projected point is dependent on both latitude and longitude. A d3 geoProjection is designed for this majority of times, and thus you must provide both latitude and longitude:
return projection([d.X,d.Y])[0] // cx
return projection([d.X,d.Y])[1] // cy
Providing one value will return a NaN value, and the SVG renderer will typically render these values as zero, meaning all your points get [0,0] centering values and will be located in the top left corner.
Related
So in short I have a spiral timeline and currently map single dates on to it. This works fine. However I know want to map date ranges on this spiral. So currently the data has two columns with dates in: 'vstart' and 'vend' and these are the two data points I'd like to connect.
I know how to get the angles of these two columns based upon their x and y position when mapped onto the spiral. But I'm unsure how to draw a radial line between these two points.
This is the code currently used to draw the initial spiral:
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "grey")
.style("stroke", ("6, 5"))
.style("opacity",0.5);
so radius is defined as:
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
and angle:
var theta = function(r) {
return numSpirals * Math.PI * r;
};
console.log(theta);
points:
var points = d3.range(start, end + 0.001, (end - start) / 779)
which produces an array that looks like this:
This successfully produces a spiral, and now I want to map some individual radial lines on this same path but related to dates.
So for example for 'vstart':
var linePerS = timeScale(d.vstart)
angleOnLineS = path.node().getPointAtLength(linePerS);
d.linePerS = linePerS;
d.x = angleOnLineS.x;
d.y = angleOnLineS.y;
d.a = (Math.atan2(angleOnLineS.y, angleOnLineS.x) * 360 / Math.PI) - 90;
And then the same but configured to 'vend'
So I can get the angle of the dates as mapped on the spiral but then how do I draw a radial line between the angle of vstart and vend?
I was wondering if I had to do something like this:
var arcUn = function(d,i) { [
[{"x": d.vstart, "y": d.vstart},
{"x": d.vend, "y": d.vend}]
]}
In order to have two points to connect. I don't want it to draw a continuous path but treat each line as a new path and loop through drawing a path between vstart and vend and repeating.
So I imagine if I was using one column and drawing a continuous line the code might look like this?
var arcs = d3.radialLine()
.curve(d3.curveCardinal)
.angle(function(d,i){var linePerS = timeScale(d.vstart)
angleOnLineS = path.node().getPointAtLength(linePerS);
d.linePer = linePer;
d.x = angleOnLineS.x;
d.y = angleOnLineS.y;
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 360 / Math.PI) - 90;
return d.a;
})
.radius(radius);
Radius here is taken from the radius that generates the original radius for the spiral.
Then:
svg.append("path")
.datum(spiralData)
.attr("id", "arcs")
.attr("d", arcs)
.style("fill", "none")
.style("stroke", "blue")
.style("stroke", ("6, 5"))
.style("opacity",0.5);
Though when I try this I get an error: "Error: attribute d: Expected number, "MNaN,NaNCNaN,NaN,…"."
So any help here is appreciated...
I'm using d3.js to try to map coordinates for a csv data file that contains an order id, latitude & longitude of the address the order was shipped to, and the amount that was spent on the order. I've tried mapping them out linearly and tried using a log scale, but the plot points still seem skewed. I was trying to get it to look like a map of the US which it slightly resembles, but the map seems warped/skewed. I made sure that the longitude was set to the x-axis and that latitude was set for the y-axis. The radius of the circles are related to the amount spent on orders. I'm wondering if it has something to do with the scale used, but this is my first time trying to mess with d3, so any help/advice would be appreciated!
var outerWidth = 500;
var outerHeight = 250;
var margin = { left: -50, top: 0, right: -50, bottom: 0 };
var xColumn = "longitude";
var yColumn = "latitude";
var rColumn = "total";
var dollarPerPixel = 10;
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scale.log().range([0, innerWidth]);
var yScale = d3.scale.log().range([innerHeight, 0]);
var rScale = d3.scale.sqrt();
function render(data){
xScale.domain( d3.extent(data, function (d){ return d[xColumn]; }));
yScale.domain( d3.extent(data, function (d){ return d[yColumn]; }));
rScale.domain([0, d3.max(data, function (d){ return d[rColumn]; })]);
// Compute the size of the biggest circle as a function of dollarPerPixel.
var dollarMax = rScale.domain()[1];
var rMin = 0;
var rMax = Math.sqrt(dollarMax / (dollarPerPixel * Math.PI));
rScale.range([rMin, rMax]);
var circles = g.selectAll("circle").data(data);
circles.enter().append("circle");
circles
.attr("cx", function (d){ return xScale(d[xColumn]); })
.attr("cy", function (d){ return yScale(d[yColumn]); })
.attr("r", function (d){ return rScale(d[rColumn]); });
circles.exit().remove();
}
function type(d){
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.total = +d.total;
return d;
}
d3.csv("data.csv", type, render);
While scales may seem to be an appropriate method for plotting geographic points: don't use this approach.
You lose control over rotation of a projection and you cannot use a non cylindrical projection (only unrotated cylindrical projections can plot lat and long independently). But it also makes it very hard to align features positioned by scales with other map elements if they don't use the same approach.
Instead, D3 has a wide range of built in projections.
The projections take a [longitude,latitude] pair and return a [x,y] coordinate. Latitudes and longitudes must be in degrees, x and y are in pixels.
To create a projection you can use:
var projection = d3.geoMercator() // or geoAlbers, geoSupportedProjection, etc.
To use it, just pass it a coordinate:
projection([long,lat]) // [x,y]
In your case this might look like (for the cx, cy looks similar)
.attr("cx", function(d) { return projection([d.long,d.lat])[0] })
Now this projection is centered at 0,0 degrees by default and set up for a 960x500 pixel map. But you can modify scale, center and rotation, for example:
var projection = d3.geoMercator().center([-100,35]).scale(1000)
For a more complete run down of projection methods you should look at the documentation for d3-geo.
In your case there is a special composite projection that covers the US, d3.geoAlbersUsa, which has room for Hawaii and Alaska. But, because of its composite nature is less flexible, though you can still scale it. The default scale anticipates 960x600 pixel map (setting larger map scales spreads the map over a larger area).
I want to rotate and zoom graphic around its center with D3.js. When I zoom graphic I want to zoom it with current aspect ratio and vice versa when I rotate graphic I want to zoom it to the current point that my mouse points. For zooming I use wheel of the mouse and for rotation I use the button of the mouse.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
var g = svg.append("g");
g.append("line")
.attr("x1", "20")
.attr("y1", "20")
.attr("x2", "60")
.attr("y2", "60")
.attr("stroke", "black")
.attr("stroke-width", "10");
svg.call(d3.drag()
.on("drag",onDrag)
)
// ##########################
var boxCenter = [100, 100];
// #############################
function onDrag(){
var x = d3.event.sourceEvent.pageX,
y = d3.event.sourceEvent.pageY;
var angle = Math.atan2(x - boxCenter[0],
- (y - boxCenter[1]) )*(180/Math.PI);
g.attr("transform", "rotate("+angle+")");
}
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a)
};
};
}
Here is my example:
https://jsfiddle.net/6Lyjz35L/
For the rotation around center to be correct at the initial zoom you need to add a 'transform-origin' attribute to 'g'.
g.attr("transform-origin", "50% 50%");
The other problems you're having stem from assigning the 'transform' attribute in two separate places. An element ('g') can only have one 'transform' attribute applied at a time, so you're overwriting one or the other each time you rotate or zoom. To fix this you can create a helper method which will append both of the transforms you want in a single string.
var currentAngle = 0;
var currentZoom = '';
function getTransform(p_angle, p_zoom) {
return `${p_zoom} rotate(${p_angle})`;
// return p_zoom + " rotate(" + p_angle + ")";
}
// In the rotate:
currentAngle = angle;
g.attr("transform", getTransform(currentAngle, currentZoom));
// In the zoom:
currentZoom = d3.event.transform;
g.attr("transform", getTransform(currentAngle, currentZoom));
There is one more issue which is introduced by the zoom, and that is that you'll have to calculate a new transform-origin at different zoom levels.
The issue I said was introduced by the zoom was actually the result of applying the operations in the incorrect order. Originally I applied the rotation and THEN then translation. It actually needs to be reversed, translation and THEN rotation. This will keep the correct transform-origin.
Here's a fiddle with those changes: https://jsfiddle.net/scmxcszz/1/
I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
I have a chart with some circles on it. When the user hovers over a circle, I want to create a mouseover event and pass the x and y coordinates of the center of that circle. How do I do that?
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.number); })
.attr("cy", function(d) { return y(d.area); })
.call(d3.my_helper.tooltip(function(d, i){return "Area: "+ d.area;}));
d3.my_helper.tooltip = function(accessor){
return function(selection){
var circle_x = ???; // the center of the circle
var circle_y = ???; // the center of the circle
selection.on("mouseover", function(d, i){
// do stuff here with circle_x and circle_y
});
};
};
.on('mouseover', function(d) {
var target_x = d3.event.target.cx.animVal.value*scale + k_x;
var target_y = d3.event.target.cx.animVal.value*scale + k_y;
}
you might need to +/- some constant k_x, k_y to correct for static offsets as well as access to the scale factor if you are using the scale method on the graph, otherwise you can ignore these
*note you probably don't want to try and mix jQuery and D3 if you can use D3 since the event properties likely contain references to the data that can be used, for example, in rendering tooltips
You will need to find the offset of the svg elem itself and then add the "cy" attribute (center y) to the y coordinate and the "cx" attribute (center x) to the x coordinate accordingly:
$('circle').hover(function (ev) {
var svgPos = $('svg').offset(),
x = svgPos.left + $(ev.target).attr('cx'),
y = svgPos.top + $(ev.target).attr('cy'),
coords = [x, y];
// coords now has your coordinates
});
If you are not using jQuery, consider using a usual hover event listener as well as .offsetTop and .offsetLeft on the element.