I built a visualization on top of http://bl.ocks.org/patricksurry/6621971.
Basically I added d3.geo.circle and d3.svg.arc on the map.
What I observed is when I pan/zoom the map, the circle remains intact, but the arc disappears.
When I inspected the elements in chrome, I saw that the attribute 'd' of arc path vanished, but for circle path, it got updated appropriately.
Can anyone help me understand why the updated projection got applied to circle path element but not in arc. Is there a way to force re-projection of arcs without have to remove and re-create them?
UPDATE 1: Since this question seemed difficult to recreate, and jsfiddle won't not allow uploading a geo-data file , I am posting the source here:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
background-color: lavender;
border: 1px solid black;
}
path {
fill: oldlace;
stroke: #666;
stroke-width: .5px;
}
path.circle {
fill: red;
stroke: #666;
stroke-width: .5px;
}
path.arc1 {
fill: green;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 1600,
height = 400,
rotate = 60, // so that [-60, 0] becomes initial center of projection
maxlat = 83; // clip northern and southern poles (infinite in mercator)
var projection = d3.geo.mercator()
.rotate([rotate,0])
.scale(1) // we'll scale up to match viewport shortly.
.translate([width/2, height/2]);
// find the top left and bottom right of current projection
function mercatorBounds(projection, maxlat) {
var yaw = projection.rotate()[0],
xymax = projection([-yaw+180-1e-6,-maxlat]),
xymin = projection([-yaw-180+1e-6, maxlat]);
return [xymin,xymax];
}
// set up the scale extent and initial scale for the projection
var b = mercatorBounds(projection, maxlat),
s = width/(b[1][0]-b[0][0]),
scaleExtent = [s, 10*s];
projection.scale(scaleExtent[0]);
var zoom = d3.behavior.zoom()
.scaleExtent(scaleExtent)
.scale(projection.scale())
.translate([0,0]) // not linked directly to projection
.on("zoom", redraw);
var path = d3.geo.path()
.projection(projection);
var svg = d3.selectAll('body')
.append('svg')
.attr('width',width)
.attr('height',height)
.attr('id', 'svg')
.call(zoom);
d3.json("js/data/world-110m2.json", function ready(error, world) {
// adding geo paths
svg.selectAll('path')
.data(topojson.feature(world, world.objects.countries).features)
.enter().append('path')
// adding a circle
svg.append("path")
.datum(d3.geo.circle().angle(2).origin([-10, 0]))
.attr("d", path)
.attr("class", "circle");
redraw();
// adding a pie arc
var r = 10;
var p = Math.PI * 2;
var arc1 = d3.svg.arc()
.innerRadius(r - 5)
.outerRadius(r)
.startAngle(0);
var arcData = JSON.parse('[{ "lon" : "0", "lat":"0", "endAngle":"6.4" }]');
var arcs1 = svg.selectAll("path.arc1");
arcs1 = arcs1.data(arcData)
.enter()
.append("path")
.attr("class", "arc1")
.attr("fill", "green")
.attr("transform", function(d, i) { return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")"; })
.attr("d", arc1);
});
// track last translation and scale event we processed
var tlast = [0,0],
slast = null;
function redraw() {
if (d3.event) {
var scale = d3.event.scale,
t = d3.event.translate;
console.log(d3.event.scale + " [" +d3.event.translate + "]");
// if scaling changes, ignore translation (otherwise touch zooms are weird)
if (scale != slast) {
projection.scale(scale);
} else {
var dx = t[0]-tlast[0],
dy = t[1]-tlast[1],
yaw = projection.rotate()[0],
tp = projection.translate();
// use x translation to rotate based on current scale
projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
// use y translation to translate projection, clamped by min/max
var b = mercatorBounds(projection, maxlat);
if (b[0][1] + dy > 0) dy = -b[0][1];
else if (b[1][1] + dy < height) dy = height-b[1][1];
projection.translate([tp[0],tp[1]+dy]);
}
// save last values. resetting zoom.translate() and scale() would
// seem equivalent but doesn't seem to work reliably?
slast = scale;
tlast = t;
}
svg.selectAll('path').attr('d', path);
}
</script>
I finally figured out what was going wrong. I was supposed to apply transformation on arc elements. So basically, in the redraw() method I did:
var scaleRatio = 1;
function redraw() {
if (d3.event) {
var scale = d3.event.scale,
t = d3.event.translate;
//console.log(d3.event.scale + " [" +d3.event.translate + "]");
// if scaling changes, ignore translation (otherwise touch zooms are weird)
if (scale != slast) {
projection.scale(scale);
} else {
var dx = t[0]-tlast[0],
dy = t[1]-tlast[1],
yaw = projection.rotate()[0],
tp = projection.translate();
// use x translation to rotate based on current scale
projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
// use y translation to translate projection, clamped by min/max
var b = mercatorBounds(projection, maxlat);
if (b[0][1] + dy > 0) dy = -b[0][1];
else if (b[1][1] + dy < height) dy = height-b[1][1];
projection.translate([tp[0],tp[1]+dy]);
}
// save last values. resetting zoom.translate() and scale() would
// seem equivalent but doesn't seem to work reliably?
if(slast==null)
scaleRatio=1;
else
scaleRatio = scaleRatio * (scale/slast);
console.log(slast+'-' + scaleRatio);
slast = scale;
tlast = t;
}
svg.selectAll('path').attr('d', path);
svg.selectAll("path.arc1")
.attr("transform", function(d, i) { return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")scale(" + scaleRatio + ")" })
.attr("d", arc1);
}
But, I still have a knowledge gap, as to why svg.path elements require explicit re-projection, unlike d3.geo.path elements . Hope someone helps me on this.
Related
I have a question about D3 cartography.
I am working on a little project and I am new to D3.
I have started out from this example: http://bl.ocks.org/mbostock/5914438
Instead of the showing the state-mesh, I would like to show circles on the map in certain locations (lon/lat). I am currently facing a problem that the circles are not on the correct spots on the map. I suspect the problem lies in the special projection that Mike uses. He uses a 1x1 square projection. Probably this is necessary for displaying the tiles. When I project the coordinates, the values are all between -1 and 1. I thought I could fix it by multiplying it width the height and width but it didn't work. Below is my code (snippet does not run because it is missing a file). Thanks for the assistance!
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: red;
stroke-linejoin: round;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: 0.4;
stroke: #111;
}
</style>
<svg>
</svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/d3-tile.v0.0.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 9, 1 << 23])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("path");
var circle = svg.append("g")
d3.json("/data/flyingsites/AD.json", function(error, flyingsites) {
if (error) console.log(error);
// Compute the projected initial center.
var center = projection([6.2, 45.8]);//45,809718, 6,252314
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
//add flying sites
circle.selectAll("circle")
.data(flyingsites.features)
.enter().append("circle")
.attr('r',5)
.attr('cx',function(d) { return projection(d.geometry.coordinates)[0]*width})
.attr('cy',function(d) { return projection(d.geometry.coordinates)[1]*height})
.style('fill','red')
//console.log(flyingsites.features);
//console.log(circle);
});
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
circle
.attr("transform", "translate(" + transform.x + "," + transform.y + ")");
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
</script>
The approach you are taking won't work, for one, it doesn't consider the scale (just translate). This is critical as d3-tile uses geometric zooming - it applies a zoom transform (scale and translate) to all the vector elements (not the tiles), this is why the projection projects everything to a one pixel square area and never changes with the zoom.
To solve this, place your circles the same as the example places (and sizes) the polygons:
vector
.attr("d", path(topojson.mesh(us, us.objects.counties)));
circle
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
Both of these position features the same way: with the projection only, projecting to a one pixel square. The zoom applies the transform to cover the whole svg. Also, since we are scaling that one pixel to fit the svg, we want the radius to be scaled appropriately too.
Now we can apply a transform to the circles the same as the polygons:
circle
.attr("transform", transform);
Of course, we could scale the radius down each zoom too, using the zoom k to modify the size of the circle:
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("circle");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
var coord = [-100,40]
vector
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.attr("r", 5/transform.k);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
Ultimately d3-tile can be a bit confusing to start with because you are using quite a few coordinate systems (tile, zoom, pixel, projected, geographic), and normally aren't projecting the entire map to a 1 pixel square.
I cannot work out how to drag a path around the svg object using d3.js
Specifically, I have a normal distribution shape rendered as a path to the svg and I want to be able to click on it and drag it around the svg space (but there is nothing unique about this particular shape etc) .
I have seen examples for points, straight lines and shapes but not a path.
My simplified code is below. Unless I am way off the mark, I suspect the error is with the dragged function right at the bottom.
Javascript:
// width and height for svg object
var w = 500;
var h = 500;
/// setting up svg object
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scale.linear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scale.linear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scale.linear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.svg.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
// plots filled normal distribution to svg
g1 = svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75);
// Problem is probably with the below line and related function dragged
d3.select("path.area1").on("drag", dragged);
function dragged() {
var dx = d3.event.dx,
dy = d3.event.dy;
d3.select(this)
.attr("transform", path => "translate(" + dx + "," + dy + ")");
}
Here is a version of your code which implements the drag:
var w = 500;
var h = 250;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scale.linear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scale.linear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scale.linear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.svg.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75)
.call(d3.behavior.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
<body></body>
<script src="https://d3js.org/d3.v3.min.js"></script>
It's actually the exact same way of doing as any other drags on other types of shapes. You just apply the drag behavior on the selected node.
Here is the part in charge of the drag implementation:
svg.append("path")
.datum(dataset)
.attr("d", area1)
...
.call(d3.behavior.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
The main thing you missed is the fact that the dx and dy you receive from the event are the movements of the mouse (the "delta" of movement). These movements can't become the new position of the shape. They have to be added to the existing x and y current position of the shape.
And here is the same code but for the version 4 of d3:
var w = 500;
var h = 250;
var svg = d3.select("body").append("svg").attr("width", w).attr("height", h)
// Values for calculating pdf of normal distribution
var sigma = 4;
var mu = 0;
var N = 10;
var step = 0.1;
var dataset = [];
var x;
// creating the pdf of the normal distribution and plotting it for -N to N
var C = 1/(sigma*Math.sqrt(2*Math.PI));
for (x=-N; x < N; x += step) {
var E = (x-mu)/sigma;
E = -(E*E)/2;
var d = C*Math.exp(E);
dataset.push(d);
}
// Scales slightly over fancy, required for features stripped out
var overlap = w*0.1;
var xscale1 = d3.scaleLinear().range([0, w/2+overlap]).domain([0, dataset.length-1]).clamp(true);
var xscale2 = d3.scaleLinear().range([w/2-overlap, w]).domain([0, dataset.length-1]).clamp(true);
// So specifies the height as max in dataset and it takes up 1/2 the svg
var yscale = d3.scaleLinear().domain([0, d3.max(dataset)]).range([h,h/2]);
var area1 = d3.area()
.x(function(d,i) { return xscale1(i); })
.y0(h)
.y1(function(d,i) { return yscale(d); });
// plots filled normal distribution to svg
g1 = svg.append("path")
.datum(dataset)
.attr("class", "area1")
.attr("d", area1)
.attr("opacity",0.75)
.call(d3.drag().on("drag", dragged));
function dragged(d) {
// Current position:
this.x = this.x || 0;
this.y = this.y || 0;
// Update thee position with the delta x and y applied by the drag:
this.x += d3.event.dx;
this.y += d3.event.dy;
// Apply the translation to the shape:
d3.select(this)
.attr("transform", "translate(" + this.x + "," + this.y + ")");
}
<body></body>
<script src="https://d3js.org/d3.v4.min.js"></script>
I've modified the y.domain of my D3 bar chart so it starts at a value above zero. However, I want to add a little "zig zag line" to indicate this, as in the picture below.
How could I do this in D3? Many thanks!
I'd just hack this on by adding another path to the y axis:
// define how much space you'd like to create the axis "break" in
var axisBreakSpace = 50;
// Add the X Axis, with the space
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + axisBreakSpace) + ")")
.call(xAxis);
// Add the Y Axis, normally
var yG = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// add the zigzags path
yG.append("path")
.attr("d", function(){
var numZags = 10, // number of zigzags
zagDist = (axisBreakSpace - 5) / numZags; // y distance on each zig or zag, -5 is a bit of space to finish it off
// build the path at
var curZig = height,
d = "M0," + curZig;
for (var i = 0; i < numZags; i++){
curZig += zagDist;
d += (i % 2 === 0) ? " L10," + curZig : " L-10," + curZig;
}
// finish it off to the x-axis
d += " L0," + (height + axisBreakSpace);
return d;
});
Full working code sample:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// Set the dimensions of the canvas / graph
var margin = {
top: 30,
right: 20,
bottom: 100,
left: 50
},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom,
axisBreakSpace = 50;
// Set the ranges
var x = d3.scale.linear().range([0, width])
.domain([0, 10]);
var y = d3.scale.linear().range([height, 0])
.domain([200,1000]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
var yAxis = d3.svg.axis().scale(y)
.orient("left");
// Define the line
var line = d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
});
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var data = d3.range(10).map(function(d){
return {
x: d,
y: (Math.random() * 800) + 200
}
});
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", line(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + axisBreakSpace) + ")")
.call(xAxis);
// Add the Y Axis
var yG = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
yG.append("path")
.attr("d", function(){
var numZags = 10,
zagDist = (axisBreakSpace - 5) / numZags;
var curZig = height,
d = "M0," + curZig;
for (var i = 0; i < numZags; i++){
curZig += zagDist;
d += (i % 2 === 0) ? " L10," + curZig : " L-10," + curZig;
}
d += " L0," + (height + axisBreakSpace);
return d;
});
</script>
</body>
I would create some data and pass it to the D3 library. Something similar to this :
var data = [{
x1: xAxisSTARTPOINTX, //start
y1; xAxisSTARTPOINTY,
x2: firstXPointOnZigZag,
y2; firstYPointOnZigZag},{
.
.
. },{
x1: lastXPointOnZigZag, //end
y1; lastYPointOnZigZag,
x2: yAxisSTARTPOINTX,
y2; yAxisSTARTPOINTY}
}]
The values you put between will be the points on the zig zag which you can make up/generate.
Then pass this to this :
d3.select(container).data(data).enter().append('path')
.attr('x1', function(d){ return d.x1})
.attr('y1', function(d){ return d.y1})
.attr('x2', function(d){ return d.x2})
.attr('y2', function(d){ return d.y2})
.style('stroke','black');
You could generate the points yourself so you can change how many 'zigzags' you want by changing 'i' in the for loop.
A function to create points, something similar to this :
function createPoints(xAxisStartPoint, yAxisStartPoint){ //pass two arrays
var xAxisStartX = xAxisStartPoint[0], //xAxisStartPointX
xAxisStartY = xAxisStartPoint[1], //xAxisStartPointY
yAxisStartX = yAxisStartPoint[0], //xAxisStartPointX
yAxisStartY = yAxisStartPoint[1]; //yAxisStartPointY
var difference = xAxisStartY-yAxisStartY; //gets the difference between xAxis and yAxis to make sure the points are equal distance apart.
var allPoints = []; //array to populate with points
var numberOfPoints = 4; //number of zigzags
var movement = 20; //movement left and right
for(var i=0;i<=numberOfPoints;i++){
var thisPoint = [];
if(i===0){ //push xAxisStartPoint
thisPoint.push({
x:xAxisStartX,
y:xAxisStartY
})
} else if(i===4){ //push yAxisStartPoint
thisPoint.push({
x:yAxisStartX,
y:yAxisStartY
})
} else {
thisCalcPointX;
if(i%2 > 0){ //if i is odd move left
thisCalcPointX = xAxisStartX-movement; //move point to the left
} else { //if it's even move right
thisCalcPointX = xAxisStartX+movement; //move point to the right
}
thisCalcPointY = xAxisStartY + difference/i; //move point up from xAxis start point at equal distance between xAxis and yAxis
thisPoint.push({
x: xAxisStartX,
y: thisCalcPointY
})
}
allPoints.push(thisPoint); //push this point to array of points
}
return allPoints; //return the points
}
//then pass this to create the path
var xAxisStart = [ xAxisStartX, xAxisStartY];
var yAxisStart= [ yAxisStartX, yAxisStartY];
var dataPoints = createPoints([xAxisStart, yAxisStart])
d3.select(container).data(dataPoints).enter().append('path')
.attr('x1', function(d){ return d.x1})
.attr('y1', function(d){ return d.y1})
.attr('x2', function(d){ return d.x2})
.attr('y2', function(d){ return d.y2})
.style('stroke','black');
Above code is not tested and just done on the fly, may need playing with, but the logic should work to create random points either side between both axis.
I started working with this d3.js Donut Chart: JSFiddleI am trying to change it into a Pie Chart without the circle in the middle. I am new to d3.js. I have tried several different ideas but have been unable to get this to remove the circle in the middle of the chart. Any and all help is appreciated
Here is my code:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<style>
.label-text {
alignment-baseline : middle;
font-size: 12px;
font-family: arial,helvetica,"sans-serif";
fill: #393939;
}
.label-line {
stroke-width: 1;
stroke: #393939;
}
.label-circle {
fill: #393939;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg>
<g id="canvas">
<g id="art" />
<g id="labels" /></g>
</svg>
<script>
var data = [{
label: 'Star Wars',
instances: 207
}, {
label: 'Lost In Space',
instances: 3
}, {
label: 'the Boston Pops',
instances: 20
}, {
label: 'Indiana Jones',
instances: 150
}, {
label: 'Harry Potter',
instances: 75
}, {
label: 'Jaws',
instances: 5
}, {
label: 'Lincoln',
instances: 1
}];
svg = d3.select("svg");
canvas = d3.select("#canvas");
art = d3.select("#art");
labels = d3.select("#labels");
// Create the pie layout function.
// This function will add convenience
// data to our existing data, like
// the start angle and end angle
// for each data element.
jhw_pie = d3.layout.pie();
jhw_pie.sort(null);
jhw_pie.value(function (d) {
// Tells the layout function what
// property of our data object to
// use as the value.
return d.instances;
});
// Store our chart dimensions
cDim = {
height: 500,
width: 500,
innerRadius: 50,
outerRadius: 150,
labelRadius: 175
}
// Set the size of our SVG element
svg.attr({
height: cDim.height,
width: cDim.width
});
// This translate property moves the origin of the group's coordinate
// space to the center of the SVG element, saving us translating every
// coordinate individually.
canvas.attr("transform", "translate(" + (cDim.width / 2) + "," + (cDim.height / 2) + ")");
pied_data = jhw_pie(data);
// The pied_arc function we make here will calculate the path
// information for each wedge based on the data set. This is
// used in the "d" attribute.
pied_arc = d3.svg.arc()
.innerRadius(50)
.outerRadius(150);
// This is an ordinal scale that returns 10 predefined colors.
// It is part of d3 core.
pied_colors = d3.scale.ordinal()
.range(["#04B486", "#F2F2F2", "#F5F6CE", "#00BFFF","orange","purple","pink"]);
// Let's start drawing the arcs.
enteringArcs = art.selectAll(".wedge").data(pied_data)
.enter();
enteringArcs
.append("g")
.attr("class", "wedge")
.append("path")
.attr("d", pied_arc)
.style("fill", function (d, i) {
return pied_colors(i);
});
// Now we'll draw our label lines, etc.
enteringLabels = labels.selectAll(".label").data(pied_data).enter();
labelGroups = enteringLabels.append("g").attr("class", "label");
labelGroups.append("circle").attr({
x: 0,
y: 0,
r: 2,
fill: "#000",
transform: function (d, i) {
centroid = pied_arc.centroid(d);
return "translate(" + pied_arc.centroid(d) + ")";
},
'class': "label-circle"
});
// "When am I ever going to use this?" I said in
// 10th grade trig.
textLines = labelGroups.append("line").attr({
x1: function (d, i) {
return pied_arc.centroid(d)[0];
},
y1: function (d, i) {
return pied_arc.centroid(d)[1];
},
x2: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
return x;
},
y2: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
y = Math.sin(midAngle) * cDim.labelRadius;
return y;
},
'class': "label-line"
});
textLabels = labelGroups.append("text").attr({
x: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
sign = (x > 0) ? 1 : -1
labelX = x + (5 * sign)
return labelX;
},
y: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
y = Math.sin(midAngle) * cDim.labelRadius;
return y;
},
'text-anchor': function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
return (x > 0) ? "start" : "end";
},
'class': 'label-text'
}).text(function (d) {
return d.data.label
});
alpha = 0.5;
spacing = 12;
function relax() {
again = false;
textLabels.each(function (d, i) {
a = this;
da = d3.select(a);
y1 = da.attr("y");
textLabels.each(function (d, j) {
b = this;
// a & b are the same element and don't collide.
if (a == b) return;
db = d3.select(b);
// a & b are on opposite sides of the chart and
// don't collide
if (da.attr("text-anchor") != db.attr("text-anchor")) return;
// Now let's calculate the distance between
// these elements.
y2 = db.attr("y");
deltaY = y1 - y2;
// Our spacing is greater than our specified spacing,
// so they don't collide.
if (Math.abs(deltaY) > spacing) return;
// If the labels collide, we'll push each
// of the two labels up and down a little bit.
again = true;
sign = deltaY > 0 ? 1 : -1;
adjust = sign * alpha;
da.attr("y", +y1 + adjust);
db.attr("y", +y2 - adjust);
});
});
// Adjust our line leaders here
// so that they follow the labels.
if (again) {
labelElements = textLabels[0];
textLines.attr("y2", function (d, i) {
labelForLine = d3.select(labelElements[i]);
return labelForLine.attr("y");
});
setTimeout(relax, 20)
}
}
relax();
</script>
</body>
</html>
Thanks
See this updated fiddle.
The code contained the following lines, of which the innerRadious was changed to 0.
pied_arc = d3.svg.arc()
.innerRadius(00) // <- this
.outerRadius(150);
It's a bit misleading, as there's an innerRadius variable somewhere before that, but it's not used at this point. While you're at it, you might want to align all of that stuff.
I'm trying to implement zooming on a d3 graphic with a bunch of data as circles. The data is made up of 1 large circle and many smaller circles plotted inside it. I want to click on the smaller circles and zoom to them. I'm using a variation of the zoom from the zoomable circle packing demo. I don't want to use the demo code directly but I've got it mostly working.
Initially all the circles are in their correct positions. However when I click on the smaller circles, they shift position right before the zoom. When I click on the white circle to zoom back, you can see they are now permanently shifted. And when it does zoom, the circles don't zoom into the center of the viewport, like they do in the demo.
I've noticed that when I comment out the transform line
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
the circles now remain in their correct positions. Their sizes scale up as they should, but now they just merge into one another as they get bigger, because I'm no longer translating them. So the problem must be something in my transform attribute, but I can't figure out what it is. Or maybe it's something with my initial view? When I uncomment the zoomTo(view) the circles immediately move to the incorrect positions.
How do I get their positions to remain in the right positions? And how do I get the circles to zoom to the center of the viewpoint? I thought I followed the demo code pretty closely, but it's not quite working right. Any ideas?
I'd also like the axes to zoom as well but I haven't gotten that far into my problem yet.
Here's my jsfiddle.
And my full javascript code
function loadPlateDesign(){
var width = 400;
var height = 400;
var padding = 55;
var plateid = 7443;
var plateCen = {'ra': 230.99167, 'dec': 42.68736 };
var data = [{'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200},
{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': '1901', 'r': 10.0,
'children':[{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': 'a', 'r': 2.0}]},
{'color': 'blue', 'cx': -167.524, 'cy': -90.009, 'name': '711', 'r': 5.0}];
var xscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
var yscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
// initial focus and view
var focus = {'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200};
var view = [xscale(0.0),yscale(0.0),200*2];
// make the main svg element
var svg = d3.select('#platedesign').append('svg')
.attr('width',width+padding)
.attr('height',height+padding);
// add the plate and ifu data
var ifus=svg.selectAll('circle').data(data).enter().append('circle')
.attr('id',function(d){return d.name;})
.attr('cx',function(d,i){return xscale(d.cx);})
.attr('cy',function(d,i){return yscale(d.cy);})
.attr('r',function(d,i){return d.r;})
.style('fill',function(d,i){return d.color;})
.style('stroke','black')
.on('click',function(d){
if (focus != d) zoom(d), d3.event.stopPropagation();
});
// add the axes
var rascale = d3.scale.linear().domain([plateCen.ra+1.5,plateCen.ra-1.5]).range([0,400]);
var decscale = d3.scale.linear().domain([plateCen.dec+1.5,plateCen.dec-1.5]).range([0,400]);
xaxis = d3.svg.axis().scale(rascale).orient('bottom');
yaxis = d3.svg.axis().scale(decscale).orient('right').ticks(5);
svg.append('g').attr('class','x axis')
.attr('transform','translate(0,'+(height+5)+')')
.call(xaxis)
.append('text')
.attr('x',width/2)
.attr('y',35)
.style('text-anchor','middle')
.text('RA');
svg.append('g').attr('class','y axis')
.attr('transform','translate('+(width+5)+',0)')
.call(yaxis)
.append('text')
.attr('transform','rotate(90)')
.attr('x',height/2)
.attr('y',-35)
.style('text-anchor','middle')
.text('Dec');
var node = svg.selectAll("circle");
//zoomTo(view);
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [xscale(d.cx), yscale(d.cy), d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
}
Looks like you are mixing positioning methods. You set an initial cx and cy but then you zoom on a transform. Re-factoring a bit to get all the positioning done with transform fixes it up. I also found that you should initiate the view and do the zoom calculations on your d.cx and d.cy instead of on the xscale(d.cx).
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [d.cx, d.cy, d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + xscale((d.cx - v[0]) * k) + "," + yscale((d.cy - v[1]) * k) + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
Updated fiddle.