I am trying get a few html elements to follow each other along a SVG path. I would like them to stay the same distance apart as they go around the path. I would also like the SVG image to scale to the container that holds it.
I have created a codepen that demonstrates what I have so far:
http://codepen.io/mikes000/pen/GIJab
The problem I am having is that when the elements move along the X axis they seem to get further apart than they do on the Y axis.
Is there a way to make them stay the same distance as they travel along the line?
Thanks!
Update**
After some further fiddling I have discovered that the distance variation seems to be caused by the aspect ratio of the SVG viewbox being increased for X greater than it is for Y. When it is stretched along the X axis 1px down the line may become 3px on the screen.
The position of the red squares is being set by moving them in front and behind by half the width of the black box. When traveling along the line if the viewbox aspect ratio is changed the distance between each point on the line increase or decreases based off of this.
I have tried creating a similar SVG with the exact viewbox of the size of the container div and the red dots are exactly on the ends of the black box all the way down the line. This doesn't solve problem because I would like the SVG with the line to scale to any size container it is placed inside.
I think if there is a way to calculate how many pixels the size of the black box is in relation to how many pixels down the line it covers the red dots would line up exactly.
Any ideas how to accomplish this or any ideas on a better way to approach this problem?
Take a look at http://jsfiddle.net/4LzK4/
var svg = d3.select("#line").append("svg:svg").attr("width", "100%").attr("height", "100%");
var data = d3.range(50).map(function(){return Math.random()*10})
var x = d3.scale.linear().domain([0, 10]).range([0, 700]);
var y = d3.scale.linear().domain([0, 10]).range([10, 290]);
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d,i) {return x(i);})
.y(function(d) {return y(d);})
var path = svg.append("svg:path").attr("d", line(data));
var circle =
svg.append("circle")
.attr("cx", 100)
.attr("cy", 350)
.attr("r", 3)
.attr("fill", "red");
var circleBehind =
svg.append("circle")
.attr("cx", 50)
.attr("cy", 300)
.attr("r", 3)
.attr("fill", "blue");
var circleAhead =
svg.append("circle")
.attr("cx", 125)
.attr("cy", 375)
.attr("r", 3)
.attr("fill", "green");
var pathEl = path.node();
var pathLength = pathEl.getTotalLength();
var BBox = pathEl.getBBox();
var scale = pathLength/BBox.width;
var offsetLeft = document.getElementById("line").offsetLeft;
var randomizeButton = d3.select("button");
svg.on("mousemove", function() {
var x = d3.event.pageX - offsetLeft;
var beginning = x, end = pathLength, target;
while (true) {
target = Math.floor((beginning + end) / 2);
pos = pathEl.getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== x) {
break;
}
if (pos.x > x) end = target;
else if (pos.x < x) beginning = target;
else break; //position found
}
circle
.attr("opacity", 1)
.attr("cx", x)
.attr("cy", pos.y);
posBehind = pathEl.getPointAtLength(target-10);
circleBehind
.attr("opacity", 1)
.attr("cx", posBehind.x)
.attr("cy", posBehind.y);
posAhead = pathEl.getPointAtLength(target+10);
circleAhead
.attr("opacity", 1)
.attr("cx", posAhead.x)
.attr("cy", posAhead.y);
});
randomizeButton.on("click", function(){
data = d3.range(50).map(function(){return Math.random()*10});
circle.attr("opacity", 0)
path
.transition()
.duration(300)
.attr("d", line(data));
});
Instead of calculating the positions of the circles behind and ahead on your own, use getPointAtLength relative to the centre of object that has to stay in the middle.
Inspired by: http://bl.ocks.org/duopixel/3824661
Related
I've created a globe which has circles and a drag. The problem is that the circles appear on the far side of the globe. I would like those circles to be hidden.
My bl.ock can be found here:
http://bl.ocks.org/anonymous/dc2d4fc810550586d40d4b1ce9088422/40c6e199a5be4e152c0bd94a13ea94eba41f004b
For example, I would like my globe to function like this one: https://bl.ocks.org/larsvers/f8efeabf480244d59001310f70815b4e
I've seen solutions such as this one: How to move points in an orthogonal map? but it doesn't quite work for me. The points simply disappear, as d[0] and d[1] seem to be undefined.
I've also tried using methods such as this: http://blockbuilder.org/tlfrd/df1f1f705c7940a6a7c0dca47041fec8 but that also doesn't seem to work. The problem here seems to be that he is using the json as his data, while my circles data are independent of the json.
Only similar example I've found is the one: https://bl.ocks.org/curran/115407b42ef85b0758595d05c825b346 from Curran but I don't really understand his code. His method is quite different than mine.
Here is my JavaScript code:
(function(){
var h = 600;
var w = 900;
var i = 0;
var map = void 0;
var world = void 0;
var US = void 0;
var margin = {
top: 10,
bottom: 40,
left: 0,
right: 30
};
var circleScale = d3.scaleSqrt()
.domain([0, 4445])
.range([0.5, 10])
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var dragging = function(d){
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx/6, c[1] - d3.event.dy/6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
}
var drag = d3.drag()
.on("drag", dragging)
var projection = d3.geoOrthographic().clipAngle(90);
var path = d3.geoPath().projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", w)
.attr("height", h)
d3.json("world.json", function(json){
d3.csv("arms_transfer_2012_2016_top - arms_transfer_2012_2016_top.csv", function(error, data){
var countries = topojson.feature(json, json.objects.countries).features
var US = countries[168]
map = svg.append('g').attr('class', 'boundary');
world = map.selectAll('path').data(countries);
US = map.selectAll('.US').data([US]);
Circles = map.selectAll(".circles").data(data)
console.log(countries[168])
world.enter()
.append("path")
.attr("class", "boundary")
.attr("d", path)
US.enter()
.append("path")
.attr("class", "US")
.attr("d", path)
.style("fill", "lightyellow")
.style("stroke", "orange")
Circles.enter()
.append("circle")
.attr("class", "circles")
.attr("r", function(d){
return circleScale(d.Millions)
})
.attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
.style("fill", "#cd0d0e")
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h)
.call(drag)
})
})
})();
There are a few different methods to achieve this, but one of the easier methods would be to calculate the angular distance between the projection centroid (as determined by the rotation) and the circle center on the drag event:
map.selectAll("circle")
.style("display", function(d) {
var circle = [d.Longitude_imp, d.Latitude_imp];
var rotate = projection.rotate(); // antipode of actual rotational center.
var center = [-rotate[0], -rotate[1]]
var distance = d3.geoDistance(circle,center);
return (distance > Math.PI/2 ) ? 'none' : 'inline';
})
Take the center of each point and get the rotational center with projection.rotate() - note that the rotation values are inverse of the centering point. A rotation of [10,-20] centers the map at [-10,20], you move the map under you. With these two points we can use d3.geoDistance() which calculates the distance between two points in radians, hence the use of Math.PI/2 - which gives us points outside of 90 degrees, for these we hide, for the rest we show.
This can be incorporated a little nicer into your code, but I keep it separate here to show what is happening clearer.
Here's an example block - drag to trigger, I haven't applied the logic to the initial load.
An alternative approach, as noted by Gerardo Furtado, would be to use a path to display the circles - using path.pointRadius to set the size of the circle for each point. Instead of appending a circle, you could append path with the following format:
Circles.enter()
.append("path")
.attr("class", "circles")
.attr("d",createGeojsonPoint)
The, on update/drag:
map.selectAll('.circles').attr('d',createGeojsonPoint);
This method uses the clip angle of the orthographic to hide features when they are more than 90 degrees from the center of the projection (as determined by rotation). Your createGeojsonPoint function needs to set the radius and return a valid geojson object:
var createGeojsonPoint = function(d) {
console.log(d);
path.pointRadius(circleScale(d.Millions)); // set point radius
return path({"type":"Point","coordinates":[d.Longitude_imp,d.Latitude_imp]}) // create geojson point, return path data
}
All together, with the necessary modifications, your code might look like this.
I use the d3 chord diagram example of Andrew and want to center all text labels within the curved slice. I tried many things but was never able to center the texts. Do you know what wizzard trick there is needed?
var width = 720,
height = 720,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(".1%");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("ex_csv.csv", function(cities) {
d3.json("ex_json.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; });
// Remove the labels that don't fit. :(
groupText.filter(function(d, i) { return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength(); })
.remove();
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
}
});
});
</script>
As an aside, I would suggest looking to upgrade to v4, documentation for v2 is nearly non-existent and is very hard to help with.
You can set both the text-anchor and the startOffset property to achieve what you are looking for.
First, you'll want to set text-anchor to middle as it is easier to specify the middle point than to find the middle point and work back to find where the text should start.
Second you'll need to set a startOffset. Note that if you use 50%, the text will not appear where you want, as the total length of the text path is all sides of the closed loop (chord anchor) you are appending to. Setting it to 25 % would work if you did not have a different outer and inner radius. But, as you have an outer radius that is 24 pixels greater than the inner radius you can try something like this to calculate the number of pixels you need to offset the center of the text:
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; })
.attr("startOffset",function(d,i) { return (groupPath[0][i].getTotalLength() - 48) / 4 })
.style("text-anchor","middle");
I subtract 48 because the sides of the anchor are 24 pixels each (the difference in the radii). I divide by four because the path doubles back on itself. If it was a general line I would just divide by two.
This approach is a little simplistic as the outer circumference is not the same as the inner circumference of each chord anchor, so I am off by a little bit, but it should be workable.
For labels that are on the cusp of being displayed, this will be awkward: the inner radius is shorter, so the formula for deteriming if a string is short enough to be displayed may be wrong - which may lead to some characters climbing up the side of the anchor (your example also 16 pixels as the difference in radii to calculate if text is too long, rather than 24).
This is the end result:
Here is a demonstration.
I am using D3.js v4.
I have a minimum example working with zooming in and out on a single axis, with the following code:
// Create dummy data
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
// Set window parameters
var width = 330
var height = 200
// Append div, svg
d3.select('body').append('div')
.attr('id', 'div1')
d3.select('#div1')
.append("svg").attr("width", width).attr("height",height)
.attr('id','chart')
// Create scaling factors
var x = d3.scaleLinear()
.domain([0,1])
.range([0, (width - 30)])
var y = d3.scaleLinear()
.domain([0,1])
.range([0,height])
// Create group, then append circles
d3.select('#chart').append('g')
.attr('id','circlesplot')
d3.select('#circlesplot')
.selectAll('circles')
.data(data)
.enter().append('circle')
.attr('cx', function(d,i){ return x(d[0]); })
.attr('cy', function(d,i){ return y(d[1]); })
.attr('r', 4)
// Create y axis, append to chart
var yaxis = d3.axisRight(y)
.ticks(10)
var yaxis_g = d3.select('#chart').append('g')
.attr('id', 'yaxis_g')
.attr('transform','translate(' + (width - 30) +',0)')
.call(yaxis)
// Create zoom svg to the right
var svg = d3.select('#div1')
.append('svg')
.attr('width', 30)
.attr('height', height)
.attr('transform', 'translate('+ width + ',0)')
.call(d3.zoom()
.on('zoom', zoom))
function zoom() {
// Rescale axis during zoom
yaxis_g.transition()
.duration(50)
.call(yaxis.scale(d3.event.transform.rescaleY(y)))
// re-draw circles using new y-axis scale
var new_y = d3.event.transform.rescaleY(y);
d3.selectAll('circle').attr('cy', function(d) { return new_y(d[1])})
}
fiddle here: https://jsfiddle.net/v0aw9Ler/#&togetherjs=2wg7s8xfhC
Putting the mouse just to the right of the yaxis and scrolling gives the zooming function on the y axis.
What I'd like to happen is for the y axis maximum (in this case 1.0) to stay fixed, while zooming only in the other direction. You can kind of see what I mean by placing the mouse at the very bottom and just to the right of the y axis, and see the points cluster at the bottom of the graph.
I think it has to do with using zoom.extent(), but I'm just really not sure where to go from here; advice is greatly appreciated.
Source for this min working example:
http://bl.ocks.org/feyderm/03602b83146d69b1b6993e5f98123175
I have a graph, and I want the line representing the graph to show a circle at the x coordinate when hovering on top of the svg area. This circle should follow the path of the line that represents the curve. The problem is that I don't know how to do this.
The code below shows how far I have succeded, and it indeed adds a circle to the document at the right x coordinate. Now, what do I replace the question mark with?
svg.on("mousemove", function() {
d3.select("path.line")
.style("stroke-width", "2.5px");
svg.append("svg:circle")
.attr("cx", Math.floor(event.offsetX-m[1]))
.attr("cy", ?)
.attr("r", "10")
.attr("fill", "red");
});
SVG provides a native function called .getPointAtLength() which returns a the x and y values of a path at any length you pass at it.
You would need to iterate through the length of the line until you find the corresponding y position. Here is how you would do it in D3:
var svg = d3.select("#line").append("svg")
var path =
svg.append("path")
.attr("d", "M0,168L28,95.99999999999997L56,192L84,71.99999999999997L112,120L140,192L168,240L196,168L224,48L252,24L280,192L308,120L336,24L364,168L392,95.99999999999997L420,168L448,95.99999999999997L476,192L504,71.99999999999997L532,120L560,192L588,216L616,168L644,48L672,24L700,192L728,120L756,24L784,192L812,71.99999999999997")
.attr("fill", "none")
.attr("stroke", "black");
var circle =
svg.append("circle")
.attr("cx", 100)
.attr("cy", 350)
.attr("r", 3)
.attr("fill", "red");
var pathEl = path.node();
var pathLength = pathEl.getTotalLength();
var BBox = pathEl.getBBox();
var scale = pathLength/BBox.width;
var offsetLeft = document.getElementById("line").offsetLeft;
var randomizeButton = d3.select("button");
svg.on("mousemove", function() {
var x = d3.event.pageX - offsetLeft;
var beginning = x, end = pathLength, target;
while (true) {
target = Math.floor((beginning + end) / 2);
pos = pathEl.getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== x) {
break;
}
if (pos.x > x) end = target;
else if (pos.x < x) beginning = target;
else break; //position found
}
circle
.attr("opacity", 1)
.attr("cx", x)
.attr("cy", pos.y);
});
You can see a demo here: http://bl.ocks.org/3824661
As the title states, I have created a D3 line/area graph, and I am finding it difficult to get the graph's width to remain constant, depending on the amount of data I have given it to render, it scales the width of the graph accordingly, but I am unsure of how I can get it to remain at a constant width, regardless of the amount of data given, which is what I would like to achieve.
I imagine it has something to do with the scaling of the x and y coordinates, but I am stuck at the moment and can't seem to figure out why it is doing this.
Here is the code I have thus far,
//dimensions and margins
var width = 625,
height = 350,
margin = 5,
// get the svg and set it's width/height
svg = d3.select("#main")
.attr("width", width)
.attr("height", height);
//initialize the graph
init([
[12345,42345,32345,22345,72345,62345,32345,92345,52345,22345],
[1234,4234,3234,2234,7234,6234,3234,9234,5234,2234]
]);
$("button").live('click', function(){
var id = $(this).attr("id");
if(id == "one"){
updateGraph([
[52345,32345,12345,22345,62345,72345,92345,32345,22345,22345,52345,32345,12345,22345,62345,72345,92345,32345,22345,22345,52345,32345,12345,22345,62345,72345,92345,32345,22345,22345],
[4234,12345,2234,32345,6234,7234,9234,3234,2234,2234,4234,1234,2234,3234,6234,7234,9234,3234,2234,2234,4234,1234,2234,3234,6234,7234,9234,3234,2234,2234]
]);
}else if(id == "two"){
updateGraph([
[12345,42345,32345,22345,72345,62345,32345,92345,52345,22345,12345,42345,32345,22345,72345,62345,32345,92345,52345,22345,12345,42345,32345,22345,72345],
[1234,2345,3234,2234,7234,6234,3234,9234,5234,2234,1234,4234,3234,2234,7234,6234,3234,9234,5234,2234,1234,4234,3234,2234,7234]
]);
}
});
function init(data){
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]),
y = d3.scale.linear()
.domain([0,d3.max(data[0])])
.range([height-margin, margin]),
/* line path generator */
line = d3.svg.line().interpolate('monotone')
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); }),
/* area path generator */
area = d3.svg.area().interpolate('monotone')
.x(line.x())
.y1(line.y())
.y0(y(0)),
groups = svg.selectAll("g")
.data(data)
.enter()
.append("g");
svg.select("g")
.selectAll("circle")
.data(data[0])
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 4);
/* add the areas */
groups.append("path")
.attr("class", "area")
.attr("d",area)
.style("fill", function(d,i) { return (i == 0 ? "steelblue" : "red" ); });
/* add the lines */
groups.append("path")
.attr("class", "line")
.attr("d", line);
}
function updateGraph(data){
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]),
y = d3.scale.linear()
.domain([0,d3.max(data[0])])
.range([height-margin, margin]),
/* line path generator */
line = d3.svg.line().interpolate('monotone')
.x(function(d,i) { return x(i); })
.y(function(d) { return y(d); }),
/* area path generator */
area = d3.svg.area().interpolate('monotone')
.x(line.x())
.y1(line.y())
.y0(y(0));
groups = svg.selectAll("g")
.data(data),
circles = svg.select("g")
.selectAll("circle");
circles.data(data[0])
.exit().remove();
circles.data(data[0])
.enter().append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 4);
/* animate circles */
circles.data(data[0])
.transition()
.duration(1000)
.attr("cx", line.x())
.attr("cy", line.y());
/* animate the lines */
groups.select('.line')
.transition()
.duration(1000)
.attr("d",line);
/* animate the areas */
groups.select('.area')
.transition()
.duration(1000)
.attr("d",area);
}
As well as a fiddle http://jsfiddle.net/JL33M/
Thank you!
The width of the graph depends on the range() you give it. range([0,100]) will always "stretch" the domain() values to take up 100 units.
That's what your code is currently doing:
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, width-margin]);// <-- always a fixed width
You want the width to depend on the number of data entries. Say you've decided you want each data point to take up 5 units, then range() needs to depend on the size of the dataset:
var x = d3.scale.linear()
.domain([0,data[0].length])
.range([margin, 5 * data[0].length]);// <-- 5 units per data point
Of course, under these conditions, your graph width grows with the dataset; if you give it a really long data array of, say, 500 points, the graph would be 2500 units wide and likely run off screen. But if your data is such that you know the maximum length of it, then you'll be fine.
On an unrelated note, I think your code could use a refactoring to be less repetitive. You should be able to achieve what you're doing with a single update() function, without the need for the init() function.
This tutorial by mbostock describe the "general update pattern" I'm referring to. Parts II and III then go on to explaining how to work transitions into this pattern.