I have been looking to create a moving gauge and struggled to find an off the shelf solution. I then stumbled across the gauge posted in the below link. Currently it runs random numbers in the chart. I would like it to change value based on an array of numerical values (not %ages) that I have, over a timeframe that I specify. The numbers are currently a simple Excel column.
So the gauge would go through the thousand or so numbers I have across, say, a minute.
I'll be frank, I'm a novice at coding and have limited experience in NetLogo and R. Hence why I'm here asking for pointers.
Any advice would be greatly appreciated. Thank you.
https://codepen.io/leomarquine/pen/xGzMjZ
var size = 150,
thickness = 60;
var color = d3.scale.linear()
.domain([0, 50, 100])
.range(['#db2828', '#fbbd08', '#21ba45']);
// .domain([0, 17, 33, 50, 67, 83, 100])
// .range(['#db4639', '#db7f29', '#d1bf1f', '#92c51b', '#48ba17', '#12ab24', '#0f9f59']);
var arc = d3.svg.arc()
.innerRadius(size - thickness)
.outerRadius(size)
.startAngle(-Math.PI / 2);
var svg = d3.select('#chart').append('svg')
.attr('width', size * 2)
.attr('height', size + 20)
.attr('class', 'gauge');
var chart = svg.append('g')
.attr('transform', 'translate(' + size + ',' + size + ')')
var background = chart.append('path')
.datum({
endAngle: Math.PI / 2
})
.attr('class', 'background')
.attr('d', arc);
var foreground = chart.append('path')
.datum({
endAngle: -Math.PI / 2
})
.style('fill', '#db2828')
.attr('d', arc);
var value = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size * .9) + ')')
.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('class', 'value');
var scale = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size + 15) + ')')
.attr('class', 'scale');
scale.append('text')
.text(100)
.attr('text-anchor', 'middle')
.attr('x', (size - thickness / 2));
scale.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('x', -(size - thickness / 2));
setInterval(function() {
update(Math.random() * 100);
}, 1500);
function update(v) {
v = d3.format('.1f')(v);
foreground.transition()
.duration(750)
.style('fill', function() {
return color(v);
})
.call(arcTween, v);
value.transition()
.duration(750)
.call(textTween, v);
}
function arcTween(transition, v) {
var newAngle = v / 100 * Math.PI - Math.PI / 2;
transition.attrTween('d', function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
function textTween(transition, v) {
transition.tween('text', function() {
var interpolate = d3.interpolate(this.innerHTML, v),
split = (v + '').split('.'),
round = (split.length > 1) ? Math.pow(10, split[1].length) : 1;
return function(t) {
this.innerHTML = d3.format('.1f')(Math.round(interpolate(t) * round) / round) + '<tspan>%</tspan>';
};
});
}
Related
I want to make a visual that shows ordinal data (ratings). There are 12 rating dimensions, and each rating will have its own dedicated line appended to a circle. The polar orientation of the line designates a category (i.e. lines pointing to 1 o'clock = category 1, 2 o'clock = category 2, and so forth). The length of the line indicates the ratings value (short = bad, long = good). The result should resemble a snow flake or a sun burst.
The name is stored in a string. The ratings for each company are stored in an array. Here are two slices of my data variable:
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
I have the grid-system placement for the companies functioning, but there seems to be an issue with the way I'm aligning the lines about the circle. Relevant code:
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
fmcG.append('line')
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
It would seem that I have the trig mapped out correctly, but in implementation I am proven wrong: the lines are not being appended about the circle like a snow flake / sun burst / clock.
Snippet:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
{'fmc':'fmc3', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d,i) => 'fmc' + i)
.attr('transform', (d,k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate("+horSpace+","+vertSpace+")";
});
fmcG.append('circle')
.attr('cx',100)
.attr('cy',100)
.attr('r', r)
.style('fill','none')
.style('stroke','#003366');
fmcG.append('text')
.attr('x',100)
.attr('y',105)
.style('text-anchor','middle')
.text(function(d) {return d.fmc});
fmcG.append('line')
//.data(function(d) {return d.ratings}) why doesnt it workk??????
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
How can I take an 12-item array and append lines about the circle in 30 degree increments (360 divided by 12) while using the value of each item in the array to determine the line's length?
The main issue is that, right now, you're appending a single line. For appending as many lines as data points you have to set up a proper enter selection:
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
//etc...
And that, by the way, is the reason your data is not working (as you ask in your comment "why doesnt it workk??????")
Other issues:
A point scale needs to have a discrete domain, for instance d3.range(12)
For whatever reason you're moving the circles 100px right and down. I'm moving the lines by the same amount.
Here is the snippet with those changes:
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{
'fmc': 'fmc1',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
{
'fmc': 'fmc2',
'ratings': [8, 10, 10, 5, 10, 10, 10, 10, 10, 7, 10, 5]
},
{
'fmc': 'fmc3',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain(d3.range(12)).range([0, 2 * Math.PI]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
fmcG.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', r)
.style('fill', 'none')
.style('stroke', '#003366');
fmcG.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor', 'middle')
.text(function(d) {
return d.fmc
});
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
.attr("x1", 100)
.attr("y1", 100)
.attr("x2", function(d, i) {
return 100 + length(d) * Math.cos(rotationDegree(i));
})
.attr("y2", function(d, i) {
return 100 + length(d) * Math.sin(rotationDegree(i));
})
.style("stroke", function(d) {
return "#003366"
});
<script src="https://d3js.org/d3.v5.min.js"></script>
I have a half donut I based a bit off http://bl.ocks.org/mikeyao/1c5c69b562cc4dc915a7af157e9c967e and some code I already had for a full donut chart, it currently that looks like this:
As shown in the image, the value is zero, but zero should not fill up half the chart. How can I set zero to start at the left most corner? Or in other words, how can I tell d3js that the chart should fill values from left corner to right corner. This is my code:
let initChart = function() {
let width = 148;
let height = 148;
let radius = Math.min(width, height) / 2;
let color = d3.scale.ordinal().range(scope.colors);
let selector = '#half-donut-' + scope.section;
let angle = 0.5 * Math.PI;
let data = [
{
label: 'Data',
value: _data
}
];
let backgroundArc = d3.svg
.arc()
.innerRadius(58)
.outerRadius(radius)
.cornerRadius(20)
.startAngle(angle * -1)
.endAngle(angle);
let mainArc = d3.svg
.arc()
.innerRadius(58)
.outerRadius(radius)
.cornerRadius(20)
.startAngle(angle * -1)
.endAngle(function(d) {
return (d.value / 100) * angle;
});
let svg = d3
.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
let charts = svg
.selectAll('g')
.data(data)
.enter()
.append('g')
.attr('transform', function() {
return (
'translate(' + width / 2 + ',' + height / 2 + ')'
);
});
let legend = svg
.selectAll('.legend')
.data(data)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function() {
return 'translate(' + -21 + ',' + -21 + ')';
});
legend
.append('text')
.attr('x', 22)
.attr('y', 12)
.attr('text-anchor', 'middle')
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
)
.text(function(d) {
return d.value + '%';
});
charts
.append('path')
.attr('d', backgroundArc)
.attr('fill', '#F3F3F4');
charts
.append('path')
.attr('d', mainArc)
.attr('fill', color);
};
Thanks in advance.
Calc endAngle relative to startAngle
.endAngle(function(d) { return -angle + (d.value / 100) * 2 * angle; });
or use the pie method used in the example
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
So, I'm basically trying to make a multilevel circular partition (aka sunburst diagram) with D3.js (v4) and a JSON data.
I placed some labels, which must have different angles depending of their levels (circles) on the partition :
- Level < 3 must be curved and "follow" the arc radius.
- level == 3 must be straight and perpendicular of the arc radius.
I didn't use textPath tags, because I'm not really experienced in SVG and it looks overly complicated to me, and I don't really know how to use it.
here's my code (without the JSON but this is a really classical one, I can add a part of it if it is needed):
var width = 800;
var height = 800;
var radius = 400;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear().range([0, 2 * Math.PI]);
var y = d3.scaleSqrt().range([0, radius]);
var arc = d3.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); })
.innerRadius(function(d) { return setRadius("inner", d.data.level); })
.outerRadius(function(d) { return setRadius("outer", d.data.level); });
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + (height/2) + ")");
var hierarchy = d3.hierarchy(dataset)
.sum(function(d) { return d.size; });
var partition = d3.partition();
svg.selectAll("path")
.data(partition(hierarchy).descendants())
.enter().append("path")
.attr("id", function(d, i){ return "path" + i; })
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "1px")
.style("fill", function(d) { return (d.data.color) ? d.data.color : 'black'; });
svg.selectAll("text")
.data(partition(hierarchy).descendants())
.enter().append("text")
.attr("transform", function(d){ return setLabelPosition(d); })
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("font-size", "18px")
.attr("fill", function(d){ return d.data.textcolor; })
.text(function(d){ if(parseInt(d.data.level) > 0 && parseInt(d.data.level) < 4){ return (d.data.name).toUpperCase(); }});
d3.select(self.frameElement)
.style("height", height + "px");
function setRadius(side, level){
var result = 0;
var innerValues = [0, 120, 180, 240, 365];
var outerValues = [0, 180, 240, 365, 400];
if(!side){
throw error;
}
if(side === "inner"){
result = innerValues[level];
}
if(side === "outer"){
result = outerValues[level];
}
return result;
};
function setLabelPosition(d){
var result = '';
var angle = 0;
var centroid = arc.centroid(d);
if(parseInt(d.data.level) === 3){
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2 - 90);
if(angle > 90){
angle = angle - 180;
}
result = "translate(" + centroid + ")rotate(" + angle + ")";
} else {
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2);
result = "translate(" + centroid + ")rotate(" + angle + ")";
}
return result;
};
And the result :
My problem is, how to curve these level 1 & 2 labels (like the one which have a red border), but keep my lvl 3 labels as they currently are.
It's really a pain in the head, and I did many search (on Google and SO) but I didn't find any satisfying answer.
A solution without using a textPath will be awesome if possible, but any advice is welcome.
Many thanks guys and sorry for my English (as you can probably see it's not my birth language).
PS : This is D3.js v4.
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>