Last active
December 11, 2017 01:36
-
-
Save juliobguedes/45593c23a4237ec2a753ec2692ca5063 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
maxPessoas = (data) => { | |
let max = parseInt(data[0].pessoas); | |
for (let i = 0; i < data.length; i++) { | |
let pessoa = parseInt(data[i].pessoas); | |
if (pessoa > max) { | |
max = pessoa; | |
} | |
} | |
return max; | |
} | |
minPessoas = (data) => { | |
let max = parseInt(data[0].pessoas); | |
for (let i = 0; i < data.length; i++) { | |
let pessoa = parseInt(data[i].pessoas); | |
if (pessoa < max) { | |
max = pessoa; | |
} | |
} | |
return max; | |
} | |
const radians = 0.0174532925; | |
//CHART CONSTANTS | |
const chartRadius = 250; | |
const chartWidth = chartRadius * 2; | |
const chartHeight = chartRadius * 2; | |
const labelRadius = chartRadius + 5; | |
const margin = { "top": 40, "bottom": 40, "left": 40, "right": 40 }; | |
const hours = ["+0:00", '+0:15', '+0:30', '+0:45', '+1:00', '+1:15', '+1:30', '+1:45', '+2:00', '+2:15', '+2:30', '+2:45', '+3:00', '+3:15', '+3:30', '+3:45', '+4:00', '+4:15', '+4:30', '+4:45']; | |
//CHART OPTIONS | |
const holeRadiusProportion = 0.3; //fraction of chartRadius. 0 gives you some pointy arcs in the centre. | |
const holeRadius = holeRadiusProportion * chartRadius; | |
const segmentsPerCoil = hours.length; //number of coils. for this example, I have 12 hours per year. But you change to whatever suits your data. | |
const segmentAngle = 360 / segmentsPerCoil; | |
var coils; //number of coils, based on data.length / segmentsPerCoil | |
var coilWidth; //remaining chartRadius (after holeRadius removed), divided by coils + 1. I add 1 as the end of the coil moves out by 1 each time | |
//SCALES | |
var colour = d3.scaleSequential(d3.interpolatePlasma); | |
//CREATE SVG AND A G PLACED IN THE CENTRE OF THE SVG | |
const svg = d3.select("#chart") | |
.append("svg") | |
.attr("width", chartWidth + margin.left + margin.right) | |
.attr("height", chartHeight + margin.top + margin.bottom); | |
const g = svg.append("g") | |
.attr("transform", "translate(" | |
+ (margin.left + chartRadius) | |
+ "," | |
+ (margin.top + chartRadius) + ")"); | |
//LOAD THE DATA | |
d3.csv("data.csv", convertTextToNumbers, function (error, data) { | |
if (error) { throw error; }; | |
//ENSURE THE DATA IS SORTED CORRECTLY, IN THIS CASE BY YEAR AND MONTH | |
//THE SPIRAL WILL START IN THE MIDDLE AND WORK OUTWARDS | |
data.sort(function (a, b) { | |
return a.hora - b.hora || a.minuto - b.minuto; | |
}); | |
var dataLength = data.length; | |
coils = Math.ceil(dataLength / segmentsPerCoil); | |
coilWidth = (chartRadius * (1 - holeRadiusProportion)) / (coils + 1); | |
var dataExtent = []; | |
dataExtent.push(maxPessoas(data)); | |
dataExtent.push(minPessoas(data)); | |
colour.domain(dataExtent); | |
//ADD LABELS AND GRIDS FOR EACH MONTH FIRST | |
//SO THE GRID LINES APPEAR BEHIND THE SPIRAL | |
var monthLabels = g.selectAll(".month-label") | |
.data(hours) | |
.enter() | |
.append("g") | |
.attr("class", "month-label"); | |
monthLabels.append("text") | |
.text(function (d) { return d; }) | |
.attr("x", function (d, i) { | |
let labelAngle = (i * segmentAngle) + (segmentAngle / 2); | |
return x(labelAngle, labelRadius); | |
}) | |
.attr("y", function (d, i) { | |
let labelAngle = (i * segmentAngle) + (segmentAngle / 2); | |
return y(labelAngle, labelRadius); | |
}) | |
.style("text-anchor", function (d, i) { | |
return i < (hours.length / 2) ? "start" : "end"; | |
}); | |
monthLabels.append("line") | |
.attr("x2", function (d, i) { | |
let lineAngle = (i * segmentAngle); | |
let lineRadius = chartRadius + 10; | |
return x(lineAngle, lineRadius); | |
}) | |
.attr("y2", function (d, i) { | |
let lineAngle = (i * segmentAngle); | |
let lineRadius = chartRadius + 10; | |
return y(lineAngle, lineRadius); | |
}); | |
//ASSUMING DATA IS SORTED, CALCULATE EACH DATA POINT'S SEGMENT VERTICES | |
data.forEach(function (d, i) { | |
let coil = Math.floor(i / segmentsPerCoil); | |
let position = i - (coil * segmentsPerCoil); | |
let startAngle = position * segmentAngle; | |
let endAngle = (position + 1) * segmentAngle; | |
let startInnerRadius = holeRadius + ((i / segmentsPerCoil) * coilWidth) | |
let startOuterRadius = holeRadius + ((i / segmentsPerCoil) * coilWidth) + coilWidth; | |
let endInnerRadius = holeRadius + (((i + 1) / segmentsPerCoil) * coilWidth) | |
let endOuterRadius = holeRadius + (((i + 1) / segmentsPerCoil) * coilWidth) + coilWidth; | |
//vertices of each segment | |
d.x1 = x(startAngle, startInnerRadius); | |
d.y1 = y(startAngle, startInnerRadius); | |
d.x2 = x(endAngle, endInnerRadius); | |
d.y2 = y(endAngle, endInnerRadius); | |
d.x3 = x(endAngle, endOuterRadius); | |
d.y3 = y(endAngle, endOuterRadius); | |
d.x4 = x(startAngle, startOuterRadius); | |
d.y4 = y(startAngle, startOuterRadius); | |
//CURVE CONTROL POINTS | |
let midAngle = startAngle + (segmentAngle / 2) | |
let midInnerRadius = holeRadius + (((i + 0.5) / segmentsPerCoil) * coilWidth) | |
let midOuterRadius = holeRadius + (((i + 0.5) / segmentsPerCoil) * coilWidth) + coilWidth; | |
//MID POINTS, WHERE THE CURVE WILL PASS THRU | |
d.mid1x = x(midAngle, midInnerRadius); | |
d.mid1y = y(midAngle, midInnerRadius); | |
d.mid2x = x(midAngle, midOuterRadius); | |
d.mid2y = y(midAngle, midOuterRadius); | |
//FROM https://stackoverflow.com/questions/5634460/quadratic-b%C3%A9zier-curve-calculate-points | |
d.controlPoint1x = (d.mid1x - (0.25 * d.x1) - (0.25 * d.x2)) / 0.5; | |
d.controlPoint1y = (d.mid1y - (0.25 * d.y1) - (0.25 * d.y2)) / 0.5; | |
d.controlPoint2x = (d.mid2x - (0.25 * d.x3) - (0.25 * d.x4)) / 0.5; | |
d.controlPoint2y = (d.mid2y - (0.25 * d.y3) - (0.25 * d.y4)) / 0.5; | |
}); | |
var arcs = g.selectAll(".arc") | |
.data(data) | |
.enter() | |
.append("g") | |
.attr("class", "arc"); | |
//CURVED EDGES | |
arcs.append("path") | |
.attr("d", function (d) { | |
//start at vertice 1 | |
let start = "M " + d.x1 + " " + d.y1; | |
//inner curve to vertice 2 | |
let side1 = " Q " + d.controlPoint1x + " " + d.controlPoint1y + " " + d.x2 + " " + d.y2; | |
//straight line to vertice 3 | |
let side2 = "L " + d.x3 + " " + d.y3; | |
//outer curve vertice 4 | |
let side3 = " Q " + d.controlPoint2x + " " + d.controlPoint2y + " " + d.x4 + " " + d.y4; | |
//combine into string, with closure (Z) to vertice 1 | |
return start + " " + side1 + " " + side2 + " " + side3 + " Z" | |
}) | |
.style("fill", function (d) { return colour(d.pessoas); }) | |
.style("stroke", "white") | |
var yearLabels = arcs.filter(function (d) { | |
return (d.horario == "6:00" || d.horario == "11:00" || d.horario == "16:00"); | |
}).raise(); | |
yearLabels.append("path") | |
.attr("id", function (d) { return "path-" + d.horario; }) | |
.attr("d", function (d) { | |
//start at vertice 1 | |
let start = "M " + d.x1 + " " + d.y1; | |
//inner curve to vertice 2 | |
let side1 = " Q " + d.controlPoint1x + " " + d.controlPoint1y + " " + d.x2 + " " + d.y2; | |
return start + side1; | |
}) | |
.style("fill", "none") | |
//.style("opacity", 0); | |
yearLabels.append("text") | |
.attr("class", "year-label") | |
.attr("x", 3) | |
.attr("dy", -5) | |
.append("textPath") | |
.attr("xlink:href", function (d) { | |
return "#path-" + d.horario; | |
}) | |
.text(function (d) { return d.horario; }) | |
//DRAW LEGEND | |
const legendWidth = chartRadius; | |
const legendHeight = 20; | |
const legendPadding = 40; | |
var legendSVG = d3.select("#legend") | |
.append("svg") | |
.attr("width", legendWidth + legendPadding + legendPadding) | |
.attr("height", legendHeight + legendPadding + legendPadding); | |
var defs = legendSVG.append("defs"); | |
var legendGradient = defs.append("linearGradient") | |
.attr("id", "linear-gradient") | |
.attr("x1", "0%") | |
.attr("y1", "0%") | |
.attr("x2", "100%") | |
.attr("y2", "0%"); | |
let noOfSamples = 20; | |
let dataRange = dataExtent[1] - dataExtent[0]; | |
let stepSize = dataRange / noOfSamples; | |
for (i = 0; i < noOfSamples; i++) { | |
legendGradient.append("stop") | |
.attr("offset", (i / (noOfSamples - 1))) | |
.attr("stop-color", colour(dataExtent[0] + (i * stepSize))); | |
} | |
var legendG = legendSVG.append("g") | |
.attr("class", "legendLinear") | |
.attr("transform", "translate(" + legendPadding + "," + legendPadding + ")"); | |
legendG.append("rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", legendWidth) | |
.attr("height", legendHeight) | |
.style("fill", "url(#linear-gradient)"); | |
legendG.append("text") | |
.text("Mais Pessoas") | |
.attr("x", 0) | |
.attr("y", legendHeight - 35) | |
.style("font-size", "12px"); | |
legendG.append("text") | |
.text("Menos Pessoas") | |
.attr("x", legendWidth) | |
.attr("y", legendHeight - 35) | |
.style("text-anchor", "end") | |
.style("font-size", "12px"); | |
}); | |
function x(angle, radius) { | |
//change to clockwise | |
let a = 360 - angle; | |
//start from 12 o'clock | |
a = a + 180; | |
return radius * Math.sin(a * radians); | |
}; | |
function y(angle, radius) { | |
//change to clockwise | |
let a = 360 - angle; | |
//start from 12 o'clock | |
a = a + 180; | |
return radius * Math.cos(a * radians); | |
}; | |
function convertTextToNumbers(d) { | |
d.pessoas = +d.pessoas | |
d.hora = +(d.horario[0] + d.horario[1]) | |
d.minuto = +(d.horario[3] + d.horario[4]) | |
return d; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
horario | pessoas | |
---|---|---|
06:00 | 215 | |
06:15 | 681 | |
06:30 | 405 | |
06:45 | 401 | |
07:00 | 395 | |
07:15 | 371 | |
07:30 | 373 | |
07:45 | 296 | |
08:00 | 256 | |
08:15 | 264 | |
08:30 | 225 | |
08:45 | 165 | |
09:00 | 179 | |
09:15 | 128 | |
09:30 | 160 | |
09:45 | 145 | |
10:00 | 127 | |
10:15 | 95 | |
10:30 | 122 | |
10:45 | 129 | |
11:00 | 212 | |
11:15 | 98 | |
11:30 | 154 | |
11:45 | 139 | |
12:00 | 140 | |
12:15 | 120 | |
12:30 | 119 | |
12:45 | 143 | |
13:00 | 147 | |
13:15 | 192 | |
13:30 | 136 | |
13:45 | 118 | |
14:00 | 131 | |
14:15 | 129 | |
14:30 | 126 | |
14:45 | 147 | |
15:00 | 149 | |
15:15 | 155 | |
15:30 | 182 | |
15:45 | 241 | |
16:00 | 355 | |
16:15 | 380 | |
16:30 | 372 | |
16:45 | 542 | |
17:00 | 649 | |
17:15 | 655 | |
17:30 | 707 | |
17:45 | 634 | |
18:00 | 580 | |
18:15 | 591 | |
18:30 | 520 | |
18:45 | 550 | |
19:00 | 514 | |
19:15 | 415 | |
19:30 | 388 | |
19:45 | 314 | |
20:00 | 246 | |
20:15 | 172 | |
20:30 | 153 | |
20:45 | 232 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment