Skip to content

Instantly share code, notes, and snippets.

@nickolas1
Last active November 2, 2016 14:58
Show Gist options
  • Save nickolas1/485f7e794027642abe76d9497de0e795 to your computer and use it in GitHub Desktop.
Save nickolas1/485f7e794027642abe76d9497de0e795 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<head>
<meta charset='utf-8'>
<style>
body {
font-family: sans-serif;
font-size: 11px;
font-weight: 300;
fill: #242424;
text-align: center;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
cursor: default;
}
.grid-circle {
fill: #f9f9f9;
stroke: #cdcdcd;
fill-opacity: 1;
}
.radial-axis .line{
stroke-width: 2px;
stroke: #fff;
}
.radar-area {
fill-opacity: 0.0;
stroke-width: 1px;
}
.line {
stroke-width: 1px;
stroke: black;
fill: none;
}
.axis path {
display: none;
}
.legend {
font-size: 10px;
fill: #ababab;
}
</style>
</head>
<body>
<div class='line-chart-container'></div>
<div class='radar-chart-container'></div>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='https://npmcdn.com/babel-core@5.8.34/browser.min.js'></script>
<script lang='babel' type='text/babel'>
const margin = { top: 50, right: 50, bottom: 50, left: 50 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
d3.select('body')
.style('font', '10px sans-serif');
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
//const data = [];
//for (let i = 0; i < 3; i++) {
// data.push(months.map(m => { return {month: m, value: getFakeValue(m), year: 2015 + i, group: i}}));
//}
const data = [];
data.push(getFakeTimeseries(10, 12, 2011, 0, 1));
data.push(getFakeTimeseries(1, 12, 2014, 1, 0.6));
data.push(getFakeTimeseries(6, 30, 2014, 2, 1));
LineChart(d3.select('.line-chart-container'), data);
RadarChart(d3.select('.radar-chart-container'), data, months);
function getFakeTimeseries(i0, l, y0, g, s) {
let series = [];
for (let i = i0; i < i0 + l; i++) {
series.push({month: months[i%12], value: getFakeValue(months[i%12], s), year: y0 + Math.floor(i/12), group: g});
}
return series;
}
function getFakeValue(m, s) {
const im = months.indexOf(m);
return s * 0.25 + Math.random() * 0.05 + (0.1 + Math.random() * 0.02) * Math.sin(im * Math.PI / months.length);
}
function LineChart(el, data, options) {
// configure
const cfg = {
w: 600, //Width of the circle
h: 60, //Height of the circle
margin: {top: 20, right: 20, bottom: 20, left: 50}, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scaleOrdinal(d3.schemeCategory10) //Color function
};
if (options !== undefined) {
for (let key in options) {
if (options[key] !== undefined) cfg[key] = options[key];
}
}
const parseTime = d3.timeParse('%B %Y');
data.forEach(d => {
d.forEach(dd => {
dd.parsedDate = parseTime(dd.month + ' ' + dd.year);
});
});
const valMin = d3.min(data, d => {return d3.min(d.map(o => o.value))});
const valMax = d3.max(data, d => {return d3.max(d.map(o => o.value))});
const dateMin = d3.min(data, d => {return d3.min(d.map(o => o.parsedDate))});
const dateMax = d3.max(data, d => {return d3.max(d.map(o => o.parsedDate))});
const x = d3.scaleTime()
.rangeRound([0, cfg.w])
.domain([dateMin, dateMax]);
const y = d3.scaleLinear()
.rangeRound([cfg.h, 0])
.domain([valMin, valMax]);
const axisY = d3.axisLeft(y).ticks(3);
const line = d3.line()
.x(d => x(d.parsedDate))
.y(d => y(d.value));
const svg = el.append('svg')
.attr('width', cfg.w + cfg.margin.left + cfg.margin.right)
.attr('height', cfg.h + cfg.margin.top + cfg.margin.bottom)
.attr('class', 'line-chart');
const g = svg.append('g')
.attr('transform', 'translate(' + cfg.margin.left + ',' + cfg.margin.top + ')');
g.append('g')
.attr('class', 'axis x-axis')
.attr('transform', 'translate(0,' + cfg.h + ')')
.call(d3.axisBottom(x));
g.append('g')
.attr('class', 'axis y-axis')
.call(axisY);
const dataWrapper = g.selectAll('.data-wrapper')
.data(data)
.enter().append('g')
.attr('class', 'data-wrapper');
dataWrapper.append('path')
.attr('d', d => line(d))
.attr('class', 'line')
.style('stroke', (d, i) => { return cfg.color(i); });
}
function RadarChart(el, data, axes, options) {
// configure
const cfg = {
w: 400, //Width of the circle
h: 400, //Height of the circle
margin: {top: 40, right: 40, bottom: 40, left: 40}, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.1, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scaleOrdinal(d3.schemeCategory10) //Color function
};
if (options !== undefined) {
for (let key in options) {
if (options[key] !== undefined) cfg[key] = options[key];
}
}
// sanity check supplied limits
let maxValue = Math.max(cfg.maxValue, d3.max(data, d => {return d3.max(d.map(o => o.value))}));
const nAxis = axes.length;
const radius = Math.min(cfg.w / 2, cfg.h / 2);
const format = d3.format('.' + Math.max(0, d3.precisionFixed(0.01) - 2) + '%');
const angleSlice = Math.PI * 2 / nAxis;
const rScale = d3.scaleLinear()
.range([0, radius]);
// connect contiguous groups
//if (data.length > 1) {
// for (let i = 1; i < data.length; i++) {
// if (data[i-1][0].year === data[i][0].year - 1) data[i-1].push(data[i][0]);
// }
// console.log(data)
//}
// kill any existing chart and create the chart
el.select('svg').remove();
const svg = el.append('svg')
.attr('width', cfg.w + cfg.margin.left + cfg.margin.right)
.attr('height', cfg.h + cfg.margin.top + cfg.margin.bottom)
.attr('class', 'radar-chart');
const g = svg.append('g')
.attr('transform', 'translate(' + (cfg.w/2 + cfg.margin.left) + ',' + (cfg.h/2 + cfg.margin.top) + ')');
const axisGrid = g.append('g')
.attr('class', 'axis-wrapper');
// find good levels
const steps = [1, 2, 5, 10, 15, 20, 25, 30, 40, 50, 100];
const magnitude = Math.pow(10, orderOfMagnitude(maxValue) - 1);
let rStep = steps[0] * magnitude;
for (let i = steps.length - 1; i >= 0; i--) {
let step = steps[i] * magnitude;
let running = 0;
let count = 0;
console.log(step)
while (running < maxValue) {
running += step;
count++;
console.log(' ',running, magnitude, count, running - 0.75 * step)
}
if (count >= 3 && count <= 5 && running - 0.75 * step < maxValue) {
rStep = step;
break;
}
}
const rSteps = [rStep];
while (rSteps[rSteps.length - 1] < maxValue) rSteps.push(rSteps.length * rStep);
maxValue = rSteps[rSteps.length - 1];
rScale.domain([0, maxValue])
// level circles
axisGrid.selectAll('.levels')
.data(rSteps.reverse())
.enter().append('circle')
.attr('class', 'grid-circle')
.attr('r', d => rScale(d));
axisGrid.selectAll('.level-label')
.data(rSteps)
.enter().append('text')
.attr('class', 'level-label')
.attr('x', 4)
.attr('y', d => -rScale(d))
.attr('dy', '0.4em')
.text(d => format(d));
// radial category lines
const axis = axisGrid.selectAll('.month')
.data(axes)
.enter().append('g')
.attr('class', 'radial-axis');
axis.append('line')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', (d, i) => { return rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI/2); })
.attr('y2', (d, i) => { return rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI/2); })
.attr('class', 'line');
//Append the labels at each axis
axis.append('text')
.attr('class', 'legend')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.attr('x', (d, i) => rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice * i - Math.PI/2))
.attr('y', (d, i) => rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice * i - Math.PI/2))
.text(function(d){return d})
.call(wrap, cfg.wrapWidth);
// draw data
const radarLine = d3.radialLine()
.radius(d => rScale(d.value))
.angle(d => angleSlice * axes.indexOf(d.month))
//.curve(d3.curveCatmullRom.alpha(0.5));
const dataWrapper = g.selectAll('.data-wrapper')
.data(data)
.enter().append('g')
.attr('class', 'data-wrapper');
dataWrapper.append('path')
.attr('class', 'radar-area')
.attr('d', d => radarLine(d))
.style('fill', (d, i) => { return cfg.color(i); })
.style('stroke', (d, i) => { return cfg.color(i); });
dataWrapper.selectAll('radar-circle')
.data(d => d)
.enter().append('circle')
.attr('class', 'radar-circle')
.attr('r', cfg.dotRadius)
.attr('cx', (d, i) => { return rScale(d.value) * Math.cos(angleSlice * axes.indexOf(d.month) - Math.PI/2); })
.attr('cy', (d, i) => { return rScale(d.value) * Math.sin(angleSlice * axes.indexOf(d.month) - Math.PI/2); })
.style('fill', d => cfg.color(d.group));
}
function orderOfMagnitude(n) {
let order = Math.floor(Math.log(n) / Math.LN10 + 0.000000001);
return order;
}
//Taken from http://bl.ocks.org/mbostock/7555321
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment