Related
I know there are other solutions to this problem, but honestly they are very hard to parse out and implement into my own code. If someone can point me in the right direction in the code snippets below, I would greatly appreciate it.
ISSUE: Overlapping lines in d3 drilldown pie chart:
GOAL: To show d3 drilldown pie chart without overlapping labels.
VisualizationScopeModel.prototype.renderDrilldownPieGraph = function (data) {
let width = 950,
height = 500,
margin = 75,
radius = Math.min(width - margin, height - margin) / 2,
pieChart = d3.layout.pie().sort(null).value(function (d) {
return d.hasOwnProperty('genres') ? d.genres.length : d.count;
}),
arc = d3.svg.arc().outerRadius(radius),
outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9),
polylineArc = d3.svg.arc()
.innerRadius(radius * 0.7)
.outerRadius(radius * 0.9);
// clear existing svg element
d3.select("svg").text("");
let svg = d3.select("#my_dataviz")
.append("svg"),
defs = svg.append("svg:defs"),
// Declare a main gradient with the dimensions for all gradient entries to refer
mainGrad = defs.append("svg:radialGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0).attr("cy", 0).attr("r", radius).attr("fx", 0).attr("fy", 0)
.attr("id", "master"),
// The pie sectors container
arcGroup = svg.append("svg:g")
.attr("class", "arcGroup")
.attr("filter", "url(#shadow)")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// The sector text labels
labels = svg.append("g")
.attr("class", "labels")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// The text lines point a sector
lines = svg.append("g")
.attr("class", "lines")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
// Declare shadow filter
let shadow = defs.append("filter").attr("id", "shadow")
.attr("filterUnits", "userSpaceOnUse")
.attr("x", -1 * (width / 2)).attr("y", -1 * (height / 2))
.attr("width", width).attr("height", height);
shadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "4")
.attr("result", "blur");
shadow.append("feOffset")
.attr("in", "blur")
.attr("dx", "4").attr("dy", "4")
.attr("result", "offsetBlur");
shadow.append("feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "offsetBlur")
.attr("mode", "normal");
// "Fold" pie sectors by tweening its current start/end angles
// into 2*PI
function tweenOut(data) {
data.startAngle = data.endAngle = (2 * Math.PI);
var interpolation = d3.interpolate(this._current, data);
this._current = interpolation(0);
return function (t) {
return arc(interpolation(t));
};
}
// "Unfold" pie sectors by tweening its start/end angles
// from 0 into their final calculated values
function tweenIn(data) {
var interpolation = d3.interpolate({ startAngle: 0, endAngle: 0 }, data);
this._current = interpolation(0);
return function (t) {
return arc(interpolation(t));
};
}
// Helper function to extract color from data object
function getColor(data, index) {
return data.color;
}
// Helper function to extract a darker version of the color
function getDarkerColor(data, index) {
return d3.rgb(getColor(data, index)).darker();
}
function findChildenByName(name) {
for (i = 0; i < data.length; i++) {
if (data[i].name == name) {
return data[i].genres;
}
}
return data;
}
// Redraw the graph given a certain level of data
function updateGraph(name) {
var currData = data;
if (name != undefined) currData = findChildenByName(name);
// Create a gradient for each entry (each entry identified by its unique category)
var gradients = defs.selectAll(".gradient").data(currData, function (d) { return d.name; });
gradients.enter().append("svg:radialGradient")
.attr("id", function (d, i) { return "gradient" + d.name; })
.attr("class", "gradient")
.attr("xlink:href", "#master");
gradients.text("").append("svg:stop").attr("offset", "0%").attr("stop-color", getColor);
gradients.append("svg:stop").attr("offset", "90%").attr("stop-color", getColor);
gradients.append("svg:stop").attr("offset", "100%").attr("stop-color", getDarkerColor);
// Create a sector for each entry in the enter selection
var paths = arcGroup.selectAll("path")
.data(pieChart(currData), function (d) { return d.data.name; });
paths.enter().append("svg:path").attr("class", "sector");
// Each sector will refer to its gradient fill
paths.attr("fill", function (d, i) { return "url(#gradient" + d.data.name + ")"; })
.transition().duration(1000)
.attrTween("d", tweenIn).each("end", function () {
this._listenToEvents = true;
});
// Mouse interaction handling
paths.on("click", function (d) {
if (this._listenToEvents) {
// Reset inmediatelly
d3.select(this).attr("transform", "translate(0,0)")
// Change level on click if no transition has started
paths.each(function () {
this._listenToEvents = false;
});
if (d.data.genres) updateGraph(d.data.name);
else updateGraph()
}
}).on("mouseover", function (d) {
// Mouseover effect if no transition has started
if (this._listenToEvents) {
// Calculate angle bisector
var ang = d.startAngle + (d.endAngle - d.startAngle) / 2;
// Transformate to SVG space
ang = (ang - (Math.PI / 2)) * -1;
// Calculate a 10% radius displacement
var x = Math.cos(ang) * radius * 0.1;
var y = Math.sin(ang) * radius * -0.1;
d3.select(this).transition()
.duration(250).attr("transform", "translate(" + x + "," + y + ")");
}
}).on("mouseout", function (d) {
// Mouseout effect if no transition has started
if (this._listenToEvents) {
d3.select(this).transition()
.duration(150).attr("transform", "translate(0,0)");
}
});
// Collapse sectors for the exit selection
paths.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
// Create sector texts
var texts = labels.text("").selectAll('text')
.data(pieChart(currData), function (d) { return d.data.org_name; });
texts.enter().append("text").attr("dy", ".75em")
.text(function (d) {
return `${d.data.org_name} (${d.data.hasOwnProperty('genres') ? d.data.genres.length : d.data.count})`;
});
function midAngle(d) {
return d.startAngle + (d.endAngle - d.startAngle) / 2;
}
texts.transition().duration(1000)
.attrTween("transform", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = (radius + margin / 2) * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate(" + pos + ")";
};
})
.styleTween("text-anchor", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
return midAngle(d2) < Math.PI ? "start" : "end";
};
});
texts.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
// Create text lines point sectors
var polylines = lines.text("").selectAll("polyline")
.data(pieChart(currData), function (d) { return d.data.name; });
polylines.enter().append("polyline");
polylines.transition().duration(1000)
.attrTween("points", function (d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function (t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = (radius + margin / 2) * (midAngle(d2) < Math.PI ? 1 : -1);
return [polylineArc.centroid(d2), outerArc.centroid(d2), pos];
};
});
polylines.exit().transition().duration(1000)
.attrTween("d", tweenOut).remove();
}
updateGraph();
}
I am working on a d3 application - which features a hooped chart. I have a version which is a radial 360 type of chart - but I am unsure how you'd configure the path to arc in this fashion.
//old js fiddle of a crescent chart
http://jsfiddle.net/2wfktc3g/
var $this = $('.crescentchart');
var data = [{
label: 'Fudge Brownie',
value: 5,
},
{
label: 'Cherry Vanilla',
value: 60,
},
{
label: 'Pistachio',
value: 5,
},
{
label: 'Caramel',
value: 10,
}
];
var oldData = "";
var width = $this.data('width'),
height = $this.data('height'),
radius = $this.data('r'),
thickness = $this.data("thickness"),
spacing = $this.data("spacing");
var color = d3.scaleOrdinal()
.range(["#bc658d", "#82c4c3", "#f9d89c", "#f5a7a7"]);
var svg = d3.select($this[0])
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('class', 'crescentchart')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var segments = svg.append('g').attr('class', 'segments');
var data = setData(data, radius);
//append previous value to it.
$.each(data, function(index, value) {
if (oldData[index] != undefined) {
data[index]["previousEndAngle"] = oldData[index].endAngle;
} else {
data[index]["previousEndAngle"] = 0;
}
});
var arcpaths = segments.selectAll("path")
.data(data);
arcpaths.enter().append("path")
.style("fill", function(d, i) {
return color(i);
})
.transition()
.ease(d3.easeElastic)
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.transition()
.ease(d3.easeElastic)
.style("fill", function(d, i) {
return color(i);
})
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.exit().transition()
.ease(d3.easeBounce)
.duration(750)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
})
.remove();
function arcTween(b, thickness, ir) {
var prev = JSON.parse(JSON.stringify(b));
prev.endAngle = b.previousEndAngle;
var i = d3.interpolate(prev, b);
return function(t) {
return getArc(thickness, ir)(i(t));
};
}
function getRadiusRing(ir, i) {
return ir - (i * (thickness + spacing));
}
function getArc(thickness, ir) {
var arc = d3.arc()
.innerRadius(function(d) {
return getRadiusRing(ir, d.index);
})
.outerRadius(function(d) {
return getRadiusRing(ir + thickness, d.index);
})
.startAngle(function(d, i) {
return d.startAngle;
})
.endAngle(function(d, i) {
return d.endAngle;
});
return arc;
}
function setData(data, r) {
var diameter = (2 * Math.PI) * r;
var segmentValueSum = 0;
$.each(data, function(ri, va) {
segmentValueSum += va.value;
});
$.each(data, function(ri, va) {
var segmentValue = va.value;
var fraction = segmentValue / segmentValueSum;
var arcBatchLength = fraction * (2 * Math.PI);
var arcPartition = arcBatchLength;
var startAngle = Math.PI;
var endAngle = startAngle + arcPartition;
data[ri]["startAngle"] = startAngle;
data[ri]["endAngle"] = endAngle;
data[ri]["index"] = ri;
});
return data;
}
//legend
var legendsvgw = 150;
var legendsvgh = 100;
var ringRadius = 5;
var vertical = 20;
var legendsvg = d3.select($this[0])
.append("svg")
.attr("width", legendsvgw)
.attr("height", legendsvgh)
.append("g")
.attr('class', 'legendsvg')
.attr("transform", "translate(" + 10 + "," + 10 + ")");
var legend = legendsvg.append("g")
.attr("class", "legend");
var labels = legend.selectAll("text.labels")
.data(data);
labels.enter().append("text")
.attr("class", "labels")
.attr("dx", 15)
.attr("dy", function(d, i) {
return (vertical * i) + ringRadius * 2;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
labels.exit().remove();
var ring = legend.selectAll("circle")
.data(data);
ring.enter().append("circle")
.attr("cy", function(d, i) {
return (vertical * i) + ringRadius;
})
.attr("r", ringRadius)
.attr("width", ringRadius * 2)
.attr("height", ringRadius * 2)
.style("fill", function(d, i) {
return color(i);
});
ring.exit().remove();
body {
background: #eeeeee;
}
.arc path {
stroke: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>CrescentChart I</h1>
<div class="crescentchart" data-width="300" data-height="300" data-r="90" data-thickness="6" data-spacing="10" />
Trying to keep it in line with your current implementation, I'd consider two different cases. One where d.fraction <= 0.25, so the arc part is the only thing required to draw the bar. In that case, just use the arc. The other, when d.fraction > 0.25, you can draw yourself, because you can assume already that the arc goes exactly from 180 to 270 degrees, and all you need it to draw the bar on top before closing it. I used d3.path() extensively for this example, I recommend you keep it next to you when reading the code.
var $this = $('.crescentchart');
var data = [{
label: 'Fudge Brownie',
value: 45,
},
{
label: 'Cherry Vanilla',
value: 60,
},
{
label: 'Pistachio',
value: 5,
},
{
label: 'Caramel',
value: 10,
}
];
var oldData = "";
var width = $this.data('width'),
height = $this.data('height'),
radius = $this.data('r'),
thickness = $this.data("thickness"),
spacing = $this.data("spacing");
var color = d3.scaleOrdinal()
.range(["#bc658d", "#82c4c3", "#f9d89c", "#f5a7a7"]);
var svg = d3.select($this[0])
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr('class', 'crescentchart')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var segments = svg.append('g').attr('class', 'segments');
var data = setData(data, radius);
//append previous value to it.
$.each(data, function(index, value) {
if (oldData[index] != undefined) {
data[index]["previousFraction"] = oldData[index].fraction;
} else {
data[index]["previousFraction"] = 0;
}
});
var arcpaths = segments.selectAll("path")
.data(data);
arcpaths.enter().append("path")
.style("fill", function(d, i) {
return color(i);
})
.transition()
.ease(d3.easeElastic)
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.transition()
.ease(d3.easeElastic)
.style("fill", function(d, i) {
return color(i);
})
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
});
arcpaths.exit().transition()
.ease(d3.easeBounce)
.duration(3000)
.attrTween("d", function(d) {
return arcTween(d, thickness, radius);
})
.remove();
function arcTween(b, thickness, ir) {
var prev = JSON.parse(JSON.stringify(b));
prev.endAngle = b.previousEndAngle;
prev.fraction = b.previousFraction;
var i = d3.interpolate(prev, b);
const arc = getArc(thickness, ir);
return function(t) {
return arc(i(t));
};
}
function getRadiusRing(ir, i) {
return ir - (i * (thickness + spacing));
}
// The portion of the fraction that is drawn as an arc,
// instead of as a straight part
const arcPortion = 3 / 8;
function getArc(thickness, ir) {
var arc = d3.arc()
.innerRadius(function(d) {
return getRadiusRing(ir, d.index);
})
.outerRadius(function(d) {
return getRadiusRing(ir + thickness, d.index);
})
.startAngle(- arcPortion * 2 * Math.PI)
.endAngle(function(d, i) {
return -(d.fraction + arcPortion) * 2 * Math.PI;
});
// This function is only called when endAngle is greater than
// 3 * Math.PI, otherwise we simply draw an arc, because it makes
// no difference.
function jShape(d, i) {
const context = d3.path();
const innerRadius = getRadiusRing(ir, d.index);
const outerRadius = getRadiusRing(ir + thickness, d.index);
const startAngle = -arcPortion * 2 * Math.PI - Math.PI / 2;
const endAngle = 0;
// Start at the correct position on the inside
context.moveTo(
innerRadius * Math.cos(startAngle),
innerRadius * Math.sin(startAngle)
);
context.arc(0, 0, innerRadius, startAngle, endAngle, true);
// Now draw the straight part
const fullLength = innerRadius * 2 * Math.PI;
// The first 0.25 corresponds to the curved part drawn earlier.
const straightLength = (d.fraction - arcPortion) * fullLength
context.lineTo(
innerRadius * Math.cos(endAngle),
innerRadius * Math.sin(endAngle) - straightLength
);
// Move to the outside
context.lineTo(
outerRadius * Math.cos(endAngle),
innerRadius * Math.sin(endAngle) - straightLength
);
context.lineTo(
outerRadius * Math.cos(endAngle),
outerRadius * Math.sin(endAngle)
);
// And curve back
context.arc(0, 0, outerRadius, endAngle, startAngle, false);
context.closePath();
return context + "";
}
return function(d, i) {
return d.fraction <= arcPortion ? arc(d, i) : jShape(d, i);
}
}
function setData(data, r) {
var segmentValueSum = 0;
$.each(data, function(ri, va) {
segmentValueSum += va.value;
});
$.each(data, function(ri, va) {
var segmentValue = va.value;
var fraction = segmentValue / segmentValueSum;
data[ri]["index"] = ri;
data[ri]["fraction"] = fraction;
});
return data;
}
body {
background: #eeeeee;
}
.arc path {
stroke: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<h1>CrescentChart I</h1>
<div class="crescentchart" data-width="300" data-height="300" data-r="90" data-thickness="6" data-spacing="10" />
I am trying to figure out how to arrange the labels so that they do not overlap. Here is a picture of the chart
As you can see, with really small values, the text labels overlap. I tried to iterate over each text element and modify it's position, but that doesn't seem to be working. You can see at the bottom of this function that I tried to get the position of each text element and then modify it. What am I doing wrong? I've been at it for hours.
_renderDonutChart() {
let self = this;
// console.log("Donut Chart is beginning render")
let textOffset = 14;
self.graph.data[0].forEach(function (d) {
d.value = +d.value;
})
console.log(self.graph.data[0])
let boxSize = (self.options.radius + self.options.padding) * 2;
let parent = d3.select(self.ui.parent);
//let color = d3.scaleOrdinal(['#dc8710', '#9e3400', '#f19b12']);
let color = d3.scaleOrdinal(d3.schemeCategory20c);
let svg = parent.append('svg')
.attr('width', boxSize * 2)
.attr('height', boxSize)
.attr('transform', 'translate(-111,0)')
.append('g')
.attr('transform', 'translate(' + boxSize + ',' + boxSize / 2 + ')');
svg.append('g')
.attr('class', 'slices')
svg.append("g")
.attr("class", "labelName")
svg.append("g")
.attr("class", "labelValue")
svg.append("g")
.attr("class", "lines")
svg.append("div")
.attr("class", "progress-circle__box progress-circle__box--victorytype")
let arc = d3.arc()
.innerRadius(self.options.radius - self.options.border)
.outerRadius(self.options.radius);
let outerArc = d3.arc()
.innerRadius((self.options.radius - self.options.border) * 1.2)
.outerRadius((self.options.radius) * 1.2);
let legendRectSize = self.options.radius * 0.05;
let legendSpacing = self.options.radius * 0.02;
let pie = d3.pie()
.value(function(d) { return d.value; })
.sort(null);
let slice = svg.select('.slices')
.selectAll('path.slice')
.data(pie(self.graph.data[0]))
.enter()
.append('path')
.attr("class", "slice")
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
})
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || 0;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
function midAngle(d){
return d.startAngle + (d.endAngle - d.startAngle)/2;
}
let text = svg.select(".labelName").selectAll("text")
.data(pie(self.graph.data[0]))
.enter()
.append("text")
.attr('class', 'label')
.attr("dy", ".35em")
.attr('transform', function(d) {
// effectively computes the centre of the slice.
// see https://github.com/d3/d3-shape/blob/master/README.md#arc_centroid
var pos = outerArc.centroid(d);
// changes the point to be on left or right depending on where label is.
pos[0] = self.options.radius * 0.97 * (midAngle(d) < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
// if slice centre is on the left, anchor text to start, otherwise anchor to end
return (midAngle(d)) < Math.PI ? 'start' : 'end';
})
.style("fill", "white")
.text(function(d) {
return (" " + d.data.label+": " +d.value+"");
})
.transition().duration(1000)
.attrTween("transform", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = self.options.radius * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate("+ pos +")";
};
})
.styleTween("text-anchor", function(d){
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
return midAngle(d2) < Math.PI ? "start":"end";
};
})
.text(function(d) {
return (d.data.label+": "+d.value+"%");
})
let polyline = svg.select(".lines").selectAll("polyline")
.data(pie(self.graph.data[0]))
.enter()
.append("polyline")
.attr('points', function(d) {
var pos = outerArc.centroid(d);
pos[0] = self.options.radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
return [arc.centroid(d), outerArc.centroid(d), pos]
})
.style("fill", "none")
.style("stroke", "white")
.style("stroke-width", "1px");
let prev;
text.each(function(d, i) {
if(i > 0) {
let thisbb = this.getBoundingClientRect(),
prevbb = prev.getBoundingClientRect();
// move if they overlap
console.log(thisbb.left);
console.log(prevbb.right);
if(!(thisbb.right < prevbb.left ||
thisbb.left > prevbb.right ||
thisbb.bottom < prevbb.top ||
thisbb.top > prevbb.bottom)) {
var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
d3.select(this).attr("transform",
"translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
(self.options.radius + textOffset + off) + "," +
Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
(self.options.radius + textOffset + off) + ")");
}
}
prev = this;
});
// console.log("Donut Chart is ending render")
}
I had the same issue. The best solution for me was to increase the size of the Svg area (based on current window) and add some padding to the legends based on the radius of the donut.
ie:
var margin = {top: 20, right: 100, bottom: 30, left: 40};
var svgWidth = window.innerWidth - (window.innerWidth/4);
var width = svgWidth,
height = (Math.min(width) / 2) + 100,
radius = Math.min(width, height) / 3;
var legendRectSize = (radius * 0.08);
var legendSpacing = (radius * 0.05);
This worked for me until the item count became too high. I amended from labels around the edge to display in the center, on slice/arc hover events. (There is an animation on the data value, which is why they are slanted in this snapshot)
I thought this made the unreadable data labels less confusing.
I thing you had same problem with this, you need to add more margin to your pie chart segment, assume your pie return 3% value, and you draw a text with line, it will stack on another, you need to devine margin if that value < 3 draw text with y+margin
if(percent<3){
o[1]
pos[1] += i*15
}
//return [label.centroid(d),[o[0],0[1]] , pos];
return [label.centroid(d),[o[0],pos[1]] , pos];
})
try to append this code structure to your chart
Can someone help me implementing a spiral chart similar to the one below using d3.js?
I've just got the basic spiral plot (a simple one) as of now but not been able to append bars to the plot based on the timeline as shown in the image. I'm trying out a few things (if you see the commented code).
Here's my fiddle, and my code:
var width = 400,
height = 430,
axes = 12,
tick_axis = 9,
start = 0,
end = 2.25;
var theta = function(r) {
return 2 * Math.PI * r;
};
var angle = d3.scale.linear()
.domain([0, axes]).range([0, 360])
var r = d3.min([width, height]) / 2 - 40;
var r2 = r;
var radius = d3.scale.linear()
.domain([start, end])
.range([0, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
var path = svg.selectAll(".spiral")
.data([points])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
var z = d3.scale.category20();
var circles = svg.selectAll('.circle')
.data(points);
/* circles.enter().append('circle')
.attr('r', 5)
.attr('transform', function(d) { return 'translate(' + d + ')'})
.style('fill', function(d) { return z(d); });
*/
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
var movingCircle = circle.transition().duration(4000)
.attrTween('transform', translateAlongPath(path.node()))
// .attr('cx', function(d) { return radius(d) * Math.cos(theta(d))})
// .attr('cy', function(d) { return radius(d) * Math.sin(theta(d))})
function translateAlongPath(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
//console.log(p)
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function pathXY(path) {
var l = path.getTotalLength();
var start = 0;
/* for(i=start; i<l; i++) {
var point = path.getPointAtLength(i);
svg.append('rect').transition().duration(400).attr('transform', 'translate(' + point.x +','+point.y+')')
.attr('width', 10).attr('height', 30).style('fill', z);
}*/
}
pathXY(path.node());
/*var test = translateAlongPath(path.node())()();
//console.log(test)
var bars = svg.selectAll('.bar')
.data(points).enter().append('rect').transition().duration(2000)
// .attrTween('transform', translateAlongPath(path.node()))
.attr('class', 'bar')
.attr('width', 10)
.attr('height', 20)
.style('fill', function(d) { return z(d)});
*/
var rect = svg.append('rect').attr('width', 10).attr('height', 10);
rect.transition().duration(3400)
.attrTween('transform', translateAlongPath(path.node()));
It'd be great to have a few similar examples (i.e. spiral timeline plot).
Thanks.
Glad you came back and updated your question, because this is an interesting one. Here's a running minimal implementation. I've commented it ok, so let me know if you have any questions...
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end - start) / 1000);
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", "steelblue");
// fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
var someData = [];
for (var i = 0; i < N; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
someData.push({
date: currentDate,
value: Math.random()
});
}
// here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime()
.domain(d3.extent(someData, function(d){
return d.date;
}))
.range([0, spiralLength]);
// yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(someData, function(d){
return d.value;
})])
.range([0, (r / numSpirals) - 30]);
// append our rects
svg.selectAll("rect")
.data(someData)
.enter()
.append("rect")
.attr("x", function(d,i){
// placement calculations
var linePer = timeScale(d.date),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % distance are on the spiral
d.x = posOnLine.x; // x postion on the spiral
d.y = posOnLine.y; // y position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.value);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")"; // rotate the bar
});
// add date labels
var tF = d3.timeFormat("%b %Y"),
firstInMonth = {};
svg.selectAll("text")
.data(someData)
.enter()
.append("text")
.attr("dy", 10)
.style("text-anchor", "start")
.style("font", "10px arial")
.append("textPath")
// only add for the first of each month
.filter(function(d){
var sd = tF(d.date);
if (!firstInMonth[sd]){
firstInMonth[sd] = 1;
return true;
}
return false;
})
.text(function(d){
return tF(d.date);
})
// place text along spiral
.attr("xlink:href", "#spiral")
.style("fill", "grey")
.attr("startOffset", function(d){
return ((d.linePer / spiralLength) * 100) + "%";
})
</script>
</body>
</html>
I want to create rotate animation in a d3.js chart. With help of Gilsha I was able to create a chart like this :
Now on click of any of the outer circles i want to rotate all circles and align the one which was clicked in the place of "hllooo" circle.
I have copied one angletween which is rotating the circles but the problem is that text inside that circle also gets rotated and creates something like this:
As you can see the text is also rotated which i dont want. The code for transform function is:
function angleTween(d, i) {
var angle = 360 - ((i + 1) * 70);
var i = d3.interpolate(0, 90);
return function (t) {
return "rotate(" + i(t) + ")";
};
}
So how to keep text unrotated and just rotate the shape ?
Calulcate the x and y attributes of text elements by using SVGPoint matrixTransform.
earth.on('click', function() {
texts.style("opacity", 0);
earthLayer
.transition()
.duration(2000)
.attrTween("transform", angleTween)
.each("end", function() {
var svgEl = this.ownerSVGElement;
var angle = d3.select(this).attr("transform").match(/\d+/g)[0];
var matrix = svgEl.createSVGMatrix().rotate(angle);
texts.each(function(d, i) {
var point = this.ownerSVGElement.createSVGPoint();
point.x = +d.cx;
point.y = +d.cy;
point = point.matrixTransform(matrix);
d3.select(this).attr("x", point.x).attr("y", point.y);
});
texts.style("opacity", 1);
});
});
var now = d3.time.year.floor(new Date());
var spacetime = d3.select('body');
var width = 960,
height = 700,
radius = Math.min(width, height);
var radii = {
"sun": radius / 6,
"earthOrbit": radius / 2.5,
"earth": radius / 15,
"moonOrbit": radius / 16,
"moon": radius / 96
};
// Space
var svg = spacetime.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Sun
var sun = svg.append("circle")
.attr("class", "sun")
.attr("r", radii.sun)
//.style("fill", "rgba(255, 204, 0, 1.0)");
.style("stroke", "#f58c2e")
.style("stroke-width", "10")
.style("fill", "none");
// Earth's orbit
var orbit = svg.append("circle")
.attr("class", "earthOrbit")
.attr("r", radii.earthOrbit)
.style("fill", "none")
.style("stroke", "#bababa")
.style("stroke-width", "30");
// Current position of Earth in its orbit
var earthOrbitPosition = d3.svg.arc()
.outerRadius(radii.earthOrbit + 1)
.innerRadius(radii.earthOrbit - 1)
.startAngle(0)
.endAngle(0);
svg.append("path")
.attr("class", "earthOrbitPosition")
.attr("d", earthOrbitPosition)
.style("fill", "rgba(255, 204, 0, 0.75)");
// Time of day
var day = d3.svg.arc()
.outerRadius(radii.earth)
.innerRadius(0)
.startAngle(0)
.endAngle(0);
svg.append("path")
.attr("class", "day")
.attr("d", day)
.attr("transform", "translate(0," + -radii.earthOrbit + ")")
.style("fill", "rgba(53, 110, 195, 1.0)");
// Current position of the Moon in its orbit
var moonOrbitPosition = d3.svg.arc()
.outerRadius(radii.moonOrbit + 1)
.innerRadius(radii.moonOrbit - 1)
.startAngle(0)
.endAngle(0);
svg.append("path")
.attr("class", "moonOrbitPosition")
.attr("d", moonOrbitPosition)
.attr("transform", "translate(0," + -radii.earthOrbit + ")")
.style("fill", "rgba(113, 170, 255, 0.75)");
function getCirclePoints(points, radius, center) {
var circlePositions = [];
var slice = 2 * Math.PI / points;
for (var i = 0; i < points; i++) {
var angle = slice * i;
var newX = (center.X + radius * Math.cos(angle));
var newY = (center.Y + radius * Math.sin(angle));
circlePositions.push({
cx: newX,
cy: newY
});
}
return circlePositions;
}
var circlePositions = getCirclePoints(10, radii.earthOrbit, {
X: 0,
Y: 0
});
var earthLayer = svg.append("g").classed("earthLayer", true);
var textLayer = svg.append("g").classed("textLayer", true);
var earth = earthLayer.selectAll(".earth").data(circlePositions)
.enter()
.append("circle")
.attr("cx", function(d) {
return d.cx;
})
.attr("cy", function(d) {
return d.cy;
})
.attr("class", "earth")
.style("fill", "white")
.attr("r", radii.earth)
.style("stroke", "#bababa")
.style("stroke-width", "10");
texts = textLayer.selectAll("text").data(circlePositions).enter().append("text").attr("x", function(d) {
return d.cx
}).attr("dx", -radii.earth / 2).attr("y", function(d) {
return d.cy
}).text(function(d, i) {
if (i == 0) return "hllooo";
else return "hllooo" + i;
});
earth.on('click', function() {
texts.style("opacity", 0);
earthLayer
.transition()
.duration(2000)
.attrTween("transform", angleTween)
.each("end", function() {
var svgEl = this.ownerSVGElement;
var angle = d3.select(this).attr("transform").match(/\d+/g)[0];
var matrix = svgEl.createSVGMatrix().rotate(angle);
texts.each(function(d, i) {
var point = this.ownerSVGElement.createSVGPoint();
point.x = +d.cx;
point.y = +d.cy;
point = point.matrixTransform(matrix);
d3.select(this).attr("x", point.x).attr("y", point.y);
});
texts.style("opacity", 1);
});
});
function angleTween(d, i) {
var angle = 360 - ((i + 1) * 70);
var i = d3.interpolate(0, angle);
return function(t) {
return "rotate(" + i(t) + ")";
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Hope this helps.