Last active
November 2, 2016 14:58
-
-
Save nickolas1/485f7e794027642abe76d9497de0e795 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
<!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