Skip to content

Instantly share code, notes, and snippets.

@juliobguedes
Last active December 11, 2017 01:36
Show Gist options
  • Save juliobguedes/45593c23a4237ec2a753ec2692ca5063 to your computer and use it in GitHub Desktop.
Save juliobguedes/45593c23a4237ec2a753ec2692ca5063 to your computer and use it in GitHub Desktop.
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;
};
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