d3 arc transition anticlockwise not clockwise - javascript

I am looking at having two animated donut graphs and need to animate them a certain way.
If you look at the jsfiddle:
https://jsfiddle.net/5uc1xfxm/
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<script type="text/javascript" src="/js/lib/dummy.js"></script>
<link rel="stylesheet" type="text/css" href="/css/result-light.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
.animated-ring {
margin-left: auto;
margin-right: auto;
//margin-top: 50px;
width: 200px;
background-color: #fff;
}
</style>
<script type='text/javascript'>//<![CDATA[
window.onload=function(){
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
// An arc function with all values bound except the endAngle. So, to compute an
// SVG path string for a given angle, we pass an object with an endAngle
// property to the `arc` function, and it will return the corresponding string.
var arc1 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.75 * tau);
var arc2 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.25 * tau);
// Get the SVG container, and apply a transform such that the origin is the
// center of the canvas. This way, we don’t need to position arcs individually.
var svg1 = d3.select("#anim1"),
width = +svg1.attr("width"),
height = +svg1.attr("height"),
g = svg1.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var svg2 = d3.select("#anim2"),
width = +svg2.attr("width"),
height = +svg2.attr("height"),
h = svg2.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add the background arc, from 0 to 100% (tau).
var background1 = g.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc1);
var background2 = h.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc2);
// Add the foreground arc in orange, currently showing 12.7%.
var foreground1 = g.append("path")
.datum({
endAngle: 0.75 * tau
})
.style("fill", "#EF4939")
.attr("d", arc1);
var foreground2 = h.append("path")
.datum({
endAngle: 0.25 * tau
})
.style("fill", "blue")
.attr("d", arc2);
// Every so often, start a transition to a new random angle. The attrTween
// definition is encapsulated in a separate function (a closure) below.
d3.timer(function() {
foreground1.transition()
.duration(75)
.attrTween("d", arcTween(0 * tau));
foreground2.transition()
.duration(75)
.attrTween("d", arcTween2(0.5 * tau));
});
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
// The function passed to attrTween is invoked for each selected element when
// the transition starts, and for each element returns the interpolator to use
// over the course of transition. This function is thus responsible for
// determining the starting angle of the transition (which is pulled from the
// element’s bound datum, d.endAngle), and the ending angle (simply the
// newAngle argument to the enclosing function).
return function(d) {
// To interpolate between the two angles, we use the default d3.interpolate.
// (Internally, this maps to d3.interpolateNumber, since both of the
// arguments to d3.interpolate are numbers.) The returned function takes a
// single argument t and returns a number between the starting angle and the
// ending angle. When t = 0, it returns d.endAngle; when t = 1, it returns
// newAngle; and for 0 < t < 1 it returns an angle in-between.
var interpolate = d3.interpolate(d.endAngle, newAngle);
// The return value of the attrTween is also a function: the function that
// we want to run for each tick of the transition. Because we used
// attrTween("d"), the return value of this last function will be set to the
// "d" attribute at every tick. (It’s also possible to use transition.tween
// to run arbitrary code for every tick, say if you want to set multiple
// attributes from a single function.) The argument t ranges from 0, at the
// start of the transition, to 1, at the end.
return function(t) {
// Calculate the current arc angle based on the transition time, t. Since
// the t for the transition and the t for the interpolate both range from
// 0 to 1, we can pass t directly to the interpolator.
//
// Note that the interpolated angle is written into the element’s bound
// data object! This is important: it means that if the transition were
// interrupted, the data bound to the element would still be consistent
// with its appearance. Whenever we start a new arc transition, the
// correct starting angle can be inferred from the data.
d.endAngle = interpolate(t);
// Lastly, compute the arc path given the updated data! In effect, this
// transition uses data-space interpolation: the data is interpolated
// (that is, the end angle) rather than the path string itself.
// Interpolating the angles in polar coordinates, rather than the raw path
// string, produces valid intermediate arcs during the transition.
return arc1(d);
};
};
}
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween2(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc2(d);
};
};
}
}//]]>
</script>
</head>
<body>
<div class="animated-ring">
<svg width="200" height="200" id="anim1"></svg>
<svg width="200" height="200" id="anim2"></svg>
</div>
<script>
// tell the embed parent frame the height of the content
if (window.parent && window.parent.parent){
window.parent.parent.postMessage(["resultsFrame", {
height: document.body.getBoundingClientRect().height,
slug: "5uc1xfxm"
}], "*")
}
</script>
</body>
</html>
You will see the two animations. The orange graph is working as required but the blue graph isn't.
The start and end positions are correct but instead of it moving clockwise and filling only a quarter of the circle. I need it to move anti-clockwise and fill three quarters of the circle.
Any ideas?

Assuming I understand the problem,
Instead of having the arc transition from 0.25 tau to 0.5 tau, try -0.5 tau:
.attrTween("d", arcTween2(-0.5 * tau));
This will move the arc backwards and cause it to fill 3/4 of the complete donut. This fits the pattern from the first donut which behaves properly, where the ultimate end angle is less than the start angle.
Updated fiddle here
And a snippet using your code for good measure (I've changed the order of the donuts so you don't need to quickly scroll):
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<script type="text/javascript" src="/js/lib/dummy.js"></script>
<link rel="stylesheet" type="text/css" href="/css/result-light.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
.animated-ring {
margin-left: auto;
margin-right: auto;
//margin-top: 50px;
width: 200px;
background-color: #fff;
}
</style>
<script type='text/javascript'>//<![CDATA[
window.onload=function(){
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
// An arc function with all values bound except the endAngle. So, to compute an
// SVG path string for a given angle, we pass an object with an endAngle
// property to the `arc` function, and it will return the corresponding string.
var arc1 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.75 * tau);
var arc2 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.25 * tau);
// Get the SVG container, and apply a transform such that the origin is the
// center of the canvas. This way, we don’t need to position arcs individually.
var svg1 = d3.select("#anim1"),
width = +svg1.attr("width"),
height = +svg1.attr("height"),
g = svg1.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var svg2 = d3.select("#anim2"),
width = +svg2.attr("width"),
height = +svg2.attr("height"),
h = svg2.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add the background arc, from 0 to 100% (tau).
var background1 = g.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc1);
var background2 = h.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc2);
// Add the foreground arc in orange, currently showing 12.7%.
var foreground1 = g.append("path")
.datum({
endAngle: 0.75 * tau
})
.style("fill", "#EF4939")
.attr("d", arc1);
var foreground2 = h.append("path")
.datum({
endAngle: 0.25 * tau
})
.style("fill", "blue")
.attr("d", arc2);
// Every so often, start a transition to a new random angle. The attrTween
// definition is encapsulated in a separate function (a closure) below.
d3.timer(function() {
foreground1.transition()
.duration(75)
.attrTween("d", arcTween(0 * tau));
foreground2.transition()
.duration(75)
.attrTween("d", arcTween2(-0.5 * tau));
});
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
// The function passed to attrTween is invoked for each selected element when
// the transition starts, and for each element returns the interpolator to use
// over the course of transition. This function is thus responsible for
// determining the starting angle of the transition (which is pulled from the
// element’s bound datum, d.endAngle), and the ending angle (simply the
// newAngle argument to the enclosing function).
return function(d) {
// To interpolate between the two angles, we use the default d3.interpolate.
// (Internally, this maps to d3.interpolateNumber, since both of the
// arguments to d3.interpolate are numbers.) The returned function takes a
// single argument t and returns a number between the starting angle and the
// ending angle. When t = 0, it returns d.endAngle; when t = 1, it returns
// newAngle; and for 0 < t < 1 it returns an angle in-between.
var interpolate = d3.interpolate(d.endAngle, newAngle);
// The return value of the attrTween is also a function: the function that
// we want to run for each tick of the transition. Because we used
// attrTween("d"), the return value of this last function will be set to the
// "d" attribute at every tick. (It’s also possible to use transition.tween
// to run arbitrary code for every tick, say if you want to set multiple
// attributes from a single function.) The argument t ranges from 0, at the
// start of the transition, to 1, at the end.
return function(t) {
// Calculate the current arc angle based on the transition time, t. Since
// the t for the transition and the t for the interpolate both range from
// 0 to 1, we can pass t directly to the interpolator.
//
// Note that the interpolated angle is written into the element’s bound
// data object! This is important: it means that if the transition were
// interrupted, the data bound to the element would still be consistent
// with its appearance. Whenever we start a new arc transition, the
// correct starting angle can be inferred from the data.
d.endAngle = interpolate(t);
// Lastly, compute the arc path given the updated data! In effect, this
// transition uses data-space interpolation: the data is interpolated
// (that is, the end angle) rather than the path string itself.
// Interpolating the angles in polar coordinates, rather than the raw path
// string, produces valid intermediate arcs during the transition.
return arc1(d);
};
};
}
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween2(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc2(d);
};
};
}
}//]]>
</script>
</head>
<body>
<div class="animated-ring">
<svg width="200" height="200" id="anim2"></svg>
<svg width="200" height="200" id="anim1"></svg>
</div>
<script>
// tell the embed parent frame the height of the content
if (window.parent && window.parent.parent){
window.parent.parent.postMessage(["resultsFrame", {
height: document.body.getBoundingClientRect().height,
slug: "5uc1xfxm"
}], "*")
}
</script>
</body>
</html>

Related

Prevent panning outside of map bounds in d3v5

in d3 v3, I used this example to prevent panning outside of an SVG. Here's the relevant code:
.on("zoom", function() {
// the "zoom" event populates d3.event with an object that has
// a "translate" property (a 2-element Array in the form [x, y])
// and a numeric "scale" property
var e = d3.event,
// now, constrain the x and y components of the translation by the
// dimensions of the viewport
tx = Math.min(0, Math.max(e.translate[0], width - width * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], height - height * e.scale));
// then, update the zoom behavior's internal translation, so that
// it knows how to properly manipulate it on the next movement
zoom.translate([tx, ty]);
// and finally, update the <g> element's transform attribute with the
// correct translation and scale (in reverse order)
g.attr("transform", ["translate(" + [tx, ty] + ")","scale(" + e.scale + ")"].join(" "));
}
In d3 v5, it doesn't work anymore. All the examples allow the map to pan ridiculous amounts off screen. My goal is that the rightmost edge of the map never goes further left than the rightmost edge of the div, etc. How can I accomplish this? Are there any more recent examples?
For the example you linked, these are the necessary changes inside the zoom function for that to work with D3 v5:
var e = d3.event.transform,
tx = Math.min(0, Math.max(e.x, width - width * e.k)),
ty = Math.min(0, Math.max(e.y, height - height * e.k));
Besides that, change the group translate function and remove zoom.translate([tx, ty]);.
Here is the original code with those changes:
<html>
<head>
<title>Restricted zoom behavior in d3</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
</style>
</head>
<body>
<script>
// first, define your viewport dimensions
var width = 960,
height = 500;
// then, create your svg element and a <g> container
// for all of the transformed content
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", randomColor),
g = svg.append("g");
// then, create the zoom behvavior
var zoom = d3.zoom()
// only scale up, e.g. between 1x and 50x
.scaleExtent([1, 50])
.on("zoom", function() {
// the "zoom" event populates d3.event with an object that has
// a "translate" property (a 2-element Array in the form [x, y])
// and a numeric "scale" property
var e = d3.event.transform,
// now, constrain the x and y components of the translation by the
// dimensions of the viewport
tx = Math.min(0, Math.max(e.x, width - width * e.k)),
ty = Math.min(0, Math.max(e.y, height - height * e.k));
// then, update the zoom behavior's internal translation, so that
// it knows how to properly manipulate it on the next movement
// and finally, update the <g> element's transform attribute with the
// correct translation and scale (in reverse order)
g.attr("transform", [
"translate(" + [tx, ty] + ")",
"scale(" + e.k + ")"
].join(" "));
});
// then, call the zoom behavior on the svg element, which will add
// all of the necessary mouse and touch event handlers.
// remember that if you call this on the <g> element, the even handlers
// will only trigger when the mouse or touch cursor intersects with the
// <g> elements' children!
svg.call(zoom);
// then, let's add some circles
var circle = g.selectAll("circle")
.data(d3.range(300).map(function(i) {
return {
x: Math.random() * width,
y: Math.random() * height,
r: .01 + Math.random() * 50,
color: randomColor()
};
}).sort(function(a, b) {
return d3.descending(a.r, b.r);
}))
.enter()
.append("circle")
.attr("fill", function(d) {
return d.color;
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
});
function randomColor() {
return "hsl(" + ~~(60 + Math.random() * 180) + ",80%,60%)";
}
</script>
</body>
</html>

How to properly apply a color gradient to an arc?

I am working on building a visual that looks something like this: .
So far I've managed to create this:
The idea is to map a value to an angle so that I know where to point the arrow and then I will color the arrow the same color as the point on the arc that its pointing to.
I essentially have two questions:
First what can I do in order to make the colors line up better. I've used a linear gradient like so:
let defs = this.gaugeEl
.append("defs")
.classed("definitions",true);
let gradient = defs
.append("linearGradient")
.classed("linearGradient",true);
gradient
.attr({
id: 'gradient',
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%',
spreadMethod: "pad"
});
gradient
.append("stop")
.classed('start',true)
.attr({
offset: '0%',
'stop-color': 'lawngreen',
'stop-opacity': 1
});
gradient.append("stop")
.classed('end',true)
.attr({
offset: '100%',
'stop-color': 'red',
'stop-opacity': 1
});
The effect is not what I was hoping for, what can be done?
The next question about how the gradient works, I need to be able to associate an angle with a color so that I can color the arrow and the tick marks properly and in my current setup I don't know how to do that. Is it even possible?
I don't know how much useful this will be for you. But I followed the below implementation
Split the arc into tiny arcs
Used scaleLinear for associating color and angle and divided the arc into four segments
Ignore bad math and code !
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#chart {
width: 960px;
height: 350px;
}
</style>
<body>
<svg id="chart">
</svg>
<script src="http://d3js.org/d3.v5.min.js"></script>
<script>
var vis = d3.select("#chart").append("g")
var pi = Math.PI;
var line = d3.line()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
var lines = []
var breakPoints = 100;
var angleArr = [];
var arcArr = [];
//angleArr[0] = -pi/2;
var colorScale = d3.scaleLinear()
.domain([-pi/2, -pi/3,30*pi/180,pi/2])
.range(['lightgreen', 'lightgreen', 'yellow','red']);
var angleScale = d3.scaleLinear()
.range([-pi/2,pi/2])
.domain([0,breakPoints - 1]);
var prevAngle = -pi/2;
for(var i = 0; i < breakPoints; i++) {
angleArr[i] = angleScale(i);
var singleArrow = [{"x":(150*Math.sin(angleArr[i])), "y":-(150*Math.cos(angleArr[i]))},{ "y":-(170*Math.cos(angleArr[i])), "x":(170*Math.sin(angleArr[i]))}];
//var subArc = {"start": prev, "end":0};
var subArc = {};
lines.push(singleArrow);
subArc["start"] = prevAngle;
subArc["end"] = angleArr[i];
prevAngle = angleArr[i];
arcArr.push(subArc);
}
var arc = d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(-(pi/2)) //converting from degs to radians
.endAngle(pi/2) //just radians
vis.attr("width", "400").attr("height", "400") // Added height and width so arc is visible
.append("g")
.attr("transform", "translate(200,200)");
vis.selectAll("line")
.data(lines)
.enter()
.append("path").attr("class","arrow").attr("d", line).attr("stroke",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
vis.selectAll("arcs")
.data(arcArr)
.enter()
.append("path").attr("class","arc").attr("d", function(d,i) {
return d3.arc()
.innerRadius(160)
.outerRadius(170)
.startAngle(d.end)
.endAngle(d.start)()}).attr("fill",function(d,i) {
return colorScale(angleArr[i])}).attr("transform", "translate(200,200)");
</script>
</body>
There is an easier way to do this now with a css property called conic-gradient
https://css-tricks.com/snippets/css/css-conic-gradient/
It sets the color according to the angle, given a center point. Maybe you could get the angle to the point with a click event, calculate the angel from the center, and set the colour that way.
There's some more info on conic gradients here, including how to calculate it: https://wiki.inkscape.org/wiki/index.php/Advanced_Gradients#Conical_gradient

Split an SVG path lengthwise into multiple colours

I have a tree visualisation in which I am trying to display paths between nodes that represent a distribution with multiple classes. I want to split the path lengthwise into multiple colours to represent the frequency of each distribution.
For example: say we have Class A (red) and Class B (black), that each have a frequency of 50. Then I would like a path that is half red and half black between the nodes. The idea is to represent the relative frequencies of the classes, so the frequencies would be normalised.
My current (naive) attempt is to create a separate path for each class and then use an x-offset. It looks like this.
However, as shown in the image, the lines do not maintain an equal distance for the duration of the path.
The relevant segment of code:
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "red")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + -2.5 + "," + 0.0 + ")"; });
linkGroup.append("path").attr("class", "link")
.attr("d", diagonal)
.style("stroke", "black")
.style("stroke-width", 5)
.attr("transform", function(d) {
return "translate(" + 2.5 + "," + 0.0 + ")"; });
It would be great if anyone has some advice.
Thanks!
A possible solution is to calculate the individual paths and fill with the required color.
Using the library svg-path-properties from geoexamples.com you can calculate properties (x,y,tangent) of a path without creating it first like it is done in this SO answer (this does not calculate the tangent).
The code snippet does it for 2 colors but it can be easy generalized for more.
You specify the colors, percentage and width of the stroke with a dictionary
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
percent is the amount color[0] takes from the stroke width.
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
The pathPoints parameters
path that needs to be stroked, can be generated by d3.line path example from SO answer
var lineGenerator = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveNatural);
var curvePoints = [[0,0],[0,10],[20,30]];
var duoPath = pathPoints(lineGenerator(curvePoints), 10, duoProp);
path length interval at which to sample (unit pixels). Every 10 pixels gives a good approximation
dictionary with the percent and width of the stroke
It returns an array with the paths to be filled, 1 for each color.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/svg-path-properties#0.4.4/build/path-properties.min.js"></script>
</head>
<body>
<svg id="chart" width="350" height="350"></svg>
<script>
var svg = d3.select("#chart");
function pathPoints(path, stepLength, duoProp) {
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(d => props.getPropertiesAtLength(d));
var pFactor = percent => (percent - 0.5) * duoProp.width;
tProps.forEach(p => {
p.x0 = p.x - pFactor(0) * p.tangentY;
p.y0 = p.y + pFactor(0) * p.tangentX;
p.xP = p.x - pFactor(duoProp.percent) * p.tangentY;
p.yP = p.y + pFactor(duoProp.percent) * p.tangentX;
p.x1 = p.x - pFactor(1) * p.tangentY;
p.y1 = p.y + pFactor(1) * p.tangentX;
});
var format1d = d3.format(".1f");
var createPath = (forward, backward) => {
var fp = tProps.map(p => forward(p));
var bp = tProps.map(p => backward(p));
bp.reverse();
return 'M' + fp.concat(bp).map(p => `${format1d(p[0])},${format1d(p[1])}`).join(' ') + 'z';
}
return [createPath(p => [p.x0, p.y0], p => [p.xP, p.yP]), createPath(p => [p.xP, p.yP], p => [p.x1, p.y1])]
}
var duoProp = { color: ["red", "black"], percent: 0.30, width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
</script>
</body>
</html>
As a quick follow-up to rioV8's excellent answer, I was able to get their code working but needed to generalise it to work with more than two colours. In case someone else has a similar requirement, here is the code:
function pathPoints(path, stepLength, duoProp) {
// get the properties of the path
var props = spp.svgPathProperties(path);
var length = props.getTotalLength();
// build a list of segments to use as approximation points
var tList = d3.range(0, length, stepLength);
tList.push(length);
var tProps = tList.map(function (d) {
return props.getPropertiesAtLength(d);
});
// incorporate the percentage
var pFactor = function pFactor(percent) {
return (percent - 0.5) * duoProp.width;
};
// for each path segment, calculate offset points
tProps.forEach(function (p) {
// create array to store modified points
p.x_arr = [];
p.y_arr = [];
// calculate offset at 0%
p.x_arr.push(p.x - pFactor(0) * p.tangentY);
p.y_arr.push(p.y + pFactor(0) * p.tangentX);
// calculate offset at each specified percent
duoProp.percents.forEach(function(perc) {
p.x_arr.push(p.x - pFactor(perc) * p.tangentY);
p.y_arr.push(p.y + pFactor(perc) * p.tangentX);
});
// calculate offset at 100%
p.x_arr.push(p.x - pFactor(1) * p.tangentY);
p.y_arr.push(p.y + pFactor(1) * p.tangentX);
});
var format1d = d3.format(".1f");
var createPath = function createPath(forward, backward) {
var fp = tProps.map(function (p) {
return forward(p);
});
var bp = tProps.map(function (p) {
return backward(p);
});
bp.reverse();
return 'M' + fp.concat(bp).map(function (p) {
return format1d(p[0]) + "," + format1d(p[1]);
}).join(' ') + 'z';
};
// create a path for each projected point
var paths = [];
for(var i=0; i <= duoProp.percents.length; i++) {
paths.push(createPath(function (p) { return [p.x_arr[i], p.y_arr[i]]; }, function (p) { return [p.x_arr[i+1], p.y_arr[i+1]]; }));
}
return paths;
}
// generate the line
var duoProp = { color: ["red", "blue", "green"], percents: [0.5, 0.7], width: 15 };
var duoPath = pathPoints("M30,30C160,30 150,90 250,90S350,210 250,210", 10, duoProp);
duoPath.forEach( (d, i) => {
svg.append("path")
.attr("d", d)
.attr("fill", duoProp.color[i])
.attr("stroke", "none");
});
Note that the percents array specifies the cumulative percentage of the stroke, not the individual percentages of the width. E.g. in the example above, the red stroke will span 0% to 50% width, the blue stroke 50% to 70% width and the green stroke 70% to 100% width.

How to get my D3.js pie charts to transition properly

I'm missing something here and I can't seem to figure it out. I have another pie chart on my page that transitions smooth, but it deals with different data. I keep getting some error of " attribute d: Expected arc flag ('0' or '1'), etc" on the transition. I've console.logged the data I'm using to draw it (the d.(x)Pct) and none of the values are negative. Basically, I'm creating the arc, then setting the endAngle on startup dynamically. Then 10 seconds later, when my update comes in, it should reset the endAngle with the new data. My d.(x)Pct data is all under 1 since 1 is a full circle. What's weird is there are 3 circles, the middle one transitions just fine, but the other 2 disappear and re-appear after the transition is over with. They are updated with the new data, but you don't see the transition considering they disappear. Code is below:
// Creates the arc for the pie chart
var pieArc = d3.svg.arc()
.startAngle(0)
.innerRadius(0)
.outerRadius(100);
// Draw the circles (carrierServices portion)
var pieChartsCarrierServices = pieChartsSvg.selectAll(".CarrierServicesPies").data(data);
pieChartsCarrierServices.enter()
.append("path")
.attr({
"class": "CarrierServicesPies",
"d": function (d) {
pieArc.endAngle((2 * Math.PI) * d.carrierServicesPct)
return pieArc();
},
"transform": function (d) {
var w = $(this).parent().width();
return "translate(" + (w/2) + "," + (yScale(d.id) + 50) + ")" // Radius divided by 2 to center with bar graphs
},
"fill": "white"
});
pieChartsCarrierServices.exit().remove();
pieChartsCarrierServices.transition().duration(750)
.attr({
"d": function (d) {
pieArc.endAngle((2 * Math.PI) * d.carrierServicesPct)
return pieArc();
}
});

Issues with preserveAspectRatio, viewBox and layout.size()

I have written the following code designed to display a d3js tree layout but encountered some difficulty when trying to get the generated svg to resize according to its aspect ratio. I was able (in the attached demo) to get the svg to scale the way I had desired but the code I have written is constrained by const ASPECT_RATIO, as seen here:
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
and again, further down, here:
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
Is there a way to circumvent this? I would prefer not having to change this value by hand every time the aspect ratio of the svg changes (that is, any time new content is added).
Regards,
Brian
tl;dr: Help me eliminate const ASPECT_RATIO.
Code:
/// <reference path="d3.d.ts" />
"use strict";
/* (c) brianjenkins94 | brianjenkins94.me | MIT licensed */
// Get JSON, impose synchronicity
d3.json("js/data.json", function(error, treeData) {
if (!error) {
// Instantiate canvas
var canvas = d3.select("#canvas");
// Aspect ratio nonsense
const ASPECT_RATIO = 1.89260808926;
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
// Update
update();
function update() {
// Add an SVG group element
canvas.append("g");
// Instantiate group
var group = canvas.select("g");
// Translate group right
group.attr("transform", "translate(64, 0)");
// Instantiate layout tree
var layout = d3.layout.tree();
// Initialize layout dimensions
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
// Instantiate rotation diagonal
var diagonal = d3.svg.diagonal();
// Rotate projection 90 degrees about the diagonal
diagonal.projection(function(d) { return [d.y, d.x]; });
// Initialize node array
var nodes = layout.nodes(treeData);
// Initialize link array
var links = layout.links(nodes);
// Select all paths in group
group.selectAll("path")
// For each link, create a path
.data(links).enter().append("path")
// Provide the specific diagonal
.attr("d", diagonal);
// Select all groups in group
var node = group.selectAll("g")
// For each node, create a group
.data(nodes).enter().append("g")
// Translate accordingly
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// Add a circle at every node
node.append("circle")
.attr("r", 3);
// Add label
node.append("text")
// To the left if the node has children, otherwise right
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 0)
// Branch if the node has children
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
}
} else {
console.log("There was a connection error of some sort.");
}
});
Demo:
https://rawgit.com/brianjenkins94/html5tree/master/index.html
Here's what I learned:
While an svg can be "dimensionless" it can't be "aspect-ratio-less." There must be some core aspect ratio to govern the resize.
The tree.size() and tree.nodeSize() functions create an "arbitrary coordinate system" that "is not limited screen coordinates."
What the aspect ratio is, is up to the developer and can be expressed as "arbitrary coordinates" or, in accordance with responsive design, can be expressed as relativistic measurements.
My solution is as follows:
// Viewbox & preserveAspectRatio
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (2 * window.innerWidth));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
...
// Initialize layout dimensions
layout.size([(2 * window.innerWidth), (window.innerWidth - 128)]);
Thus eliminating the dependence on const ASPECT_RATIO in favor of relative measurements based off of browser dimensions.
This can potentially (and almost certainly will) cause rendering inconsistency across multiple viewports, but can be handled accordingly by querying the viewport prior to rendering and employing cosmetic adjustments.

Categories

Resources