Last active
August 23, 2017 16:24
-
-
Save pmariac/44bf394715c9fba770c836a6156ccd21 to your computer and use it in GitHub Desktop.
The console wars
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
license: mit |
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
/** | |
* d3.tip | |
* Copyright (c) 2013-2017 Justin Palmer | |
* | |
* Tooltips for d3.js SVG visualizations | |
*/ | |
// eslint-disable-next-line no-extra-semi | |
;(function(root, factory) { | |
if (typeof define === 'function' && define.amd) { | |
// AMD. Register as an anonymous module with d3 as a dependency. | |
define([ | |
'd3-collection', | |
'd3-selection' | |
], factory) | |
} else if (typeof module === 'object' && module.exports) { | |
/* eslint-disable global-require */ | |
// CommonJS | |
var d3Collection = require('d3-collection'), | |
d3Selection = require('d3-selection') | |
module.exports = factory(d3Collection, d3Selection) | |
/* eslint-enable global-require */ | |
} else { | |
// Browser global. | |
var d3 = root.d3 | |
// eslint-disable-next-line no-param-reassign | |
root.d3.tip = factory(d3, d3) | |
} | |
}(this, function(d3Collection, d3Selection) { | |
// Public - contructs a new tooltip | |
// | |
// Returns a tip | |
return function() { | |
var direction = d3TipDirection, | |
offset = d3TipOffset, | |
html = d3TipHTML, | |
rootElement = document.body, | |
node = initNode(), | |
svg = null, | |
point = null, | |
target = null | |
function tip(vis) { | |
svg = getSVGNode(vis) | |
if (!svg) return | |
point = svg.createSVGPoint() | |
rootElement.appendChild(node) | |
} | |
// Public - show the tooltip on the screen | |
// | |
// Returns a tip | |
tip.show = function() { | |
var args = Array.prototype.slice.call(arguments) | |
if (args[args.length - 1] instanceof SVGElement) target = args.pop() | |
var content = html.apply(this, args), | |
poffset = offset.apply(this, args), | |
dir = direction.apply(this, args), | |
nodel = getNodeEl(), | |
i = directions.length, | |
coords, | |
scrollTop = document.documentElement.scrollTop || | |
rootElement.scrollTop, | |
scrollLeft = document.documentElement.scrollLeft || | |
rootElement.scrollLeft | |
nodel.html(content) | |
.style('opacity', 1).style('pointer-events', 'all') | |
while (i--) nodel.classed(directions[i], false) | |
coords = directionCallbacks.get(dir).apply(this) | |
nodel.classed(dir, true) | |
.style('top', (coords.top + poffset[0]) + scrollTop + 'px') | |
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px') | |
return tip | |
} | |
// Public - hide the tooltip | |
// | |
// Returns a tip | |
tip.hide = function() { | |
var nodel = getNodeEl() | |
nodel.style('opacity', 0).style('pointer-events', 'none') | |
return tip | |
} | |
// Public: Proxy attr calls to the d3 tip container. | |
// Sets or gets attribute value. | |
// | |
// n - name of the attribute | |
// v - value of the attribute | |
// | |
// Returns tip or attribute value | |
// eslint-disable-next-line no-unused-vars | |
tip.attr = function(n, v) { | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().attr(n) | |
} | |
var args = Array.prototype.slice.call(arguments) | |
d3Selection.selection.prototype.attr.apply(getNodeEl(), args) | |
return tip | |
} | |
// Public: Proxy style calls to the d3 tip container. | |
// Sets or gets a style value. | |
// | |
// n - name of the property | |
// v - value of the property | |
// | |
// Returns tip or style property value | |
// eslint-disable-next-line no-unused-vars | |
tip.style = function(n, v) { | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().style(n) | |
} | |
var args = Array.prototype.slice.call(arguments) | |
d3Selection.selection.prototype.style.apply(getNodeEl(), args) | |
return tip | |
} | |
// Public: Set or get the direction of the tooltip | |
// | |
// v - One of n(north), s(south), e(east), or w(west), nw(northwest), | |
// sw(southwest), ne(northeast) or se(southeast) | |
// | |
// Returns tip or direction | |
tip.direction = function(v) { | |
if (!arguments.length) return direction | |
direction = v == null ? v : functor(v) | |
return tip | |
} | |
// Public: Sets or gets the offset of the tip | |
// | |
// v - Array of [x, y] offset | |
// | |
// Returns offset or | |
tip.offset = function(v) { | |
if (!arguments.length) return offset | |
offset = v == null ? v : functor(v) | |
return tip | |
} | |
// Public: sets or gets the html value of the tooltip | |
// | |
// v - String value of the tip | |
// | |
// Returns html value or tip | |
tip.html = function(v) { | |
if (!arguments.length) return html | |
html = v == null ? v : functor(v) | |
return tip | |
} | |
// Public: sets or gets the root element anchor of the tooltip | |
// | |
// v - root element of the tooltip | |
// | |
// Returns root node of tip | |
tip.rootElement = function(v) { | |
if (!arguments.length) return rootElement | |
rootElement = v == null ? v : functor(v) | |
return tip | |
} | |
// Public: destroys the tooltip and removes it from the DOM | |
// | |
// Returns a tip | |
tip.destroy = function() { | |
if (node) { | |
getNodeEl().remove() | |
node = null | |
} | |
return tip | |
} | |
function d3TipDirection() { return 'n' } | |
function d3TipOffset() { return [0, 0] } | |
function d3TipHTML() { return ' ' } | |
var directionCallbacks = d3Collection.map({ | |
n: directionNorth, | |
s: directionSouth, | |
e: directionEast, | |
w: directionWest, | |
nw: directionNorthWest, | |
ne: directionNorthEast, | |
sw: directionSouthWest, | |
se: directionSouthEast | |
}), | |
directions = directionCallbacks.keys() | |
function directionNorth() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.n.y - node.offsetHeight, | |
left: bbox.n.x - node.offsetWidth / 2 | |
} | |
} | |
function directionSouth() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.s.y, | |
left: bbox.s.x - node.offsetWidth / 2 | |
} | |
} | |
function directionEast() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.e.y - node.offsetHeight / 2, | |
left: bbox.e.x | |
} | |
} | |
function directionWest() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.w.y - node.offsetHeight / 2, | |
left: bbox.w.x - node.offsetWidth | |
} | |
} | |
function directionNorthWest() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.nw.y - node.offsetHeight, | |
left: bbox.nw.x - node.offsetWidth | |
} | |
} | |
function directionNorthEast() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.ne.y - node.offsetHeight, | |
left: bbox.ne.x | |
} | |
} | |
function directionSouthWest() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.sw.y, | |
left: bbox.sw.x - node.offsetWidth | |
} | |
} | |
function directionSouthEast() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.se.y, | |
left: bbox.se.x | |
} | |
} | |
function initNode() { | |
var div = d3Selection.select(document.createElement('div')) | |
div | |
.style('position', 'absolute') | |
.style('top', 0) | |
.style('opacity', 0) | |
.style('pointer-events', 'none') | |
.style('box-sizing', 'border-box') | |
return div.node() | |
} | |
function getSVGNode(element) { | |
var svgNode = element.node() | |
if (!svgNode) return null | |
if (svgNode.tagName.toLowerCase() === 'svg') return svgNode | |
return svgNode.ownerSVGElement | |
} | |
function getNodeEl() { | |
if (node == null) { | |
node = initNode() | |
// re-add node to DOM | |
rootElement.appendChild(node) | |
} | |
return d3Selection.select(node) | |
} | |
// Private - gets the screen coordinates of a shape | |
// | |
// Given a shape on the screen, will return an SVGPoint for the directions | |
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), | |
// nw(northwest), sw(southwest). | |
// | |
// +-+-+ | |
// | | | |
// + + | |
// | | | |
// +-+-+ | |
// | |
// Returns an Object {n, s, e, w, nw, sw, ne, se} | |
function getScreenBBox() { | |
var targetel = target || d3Selection.event.target | |
while (targetel.getScreenCTM == null && targetel.parentNode == null) { | |
targetel = targetel.parentNode | |
} | |
var bbox = {}, | |
matrix = targetel.getScreenCTM(), | |
tbbox = targetel.getBBox(), | |
width = tbbox.width, | |
height = tbbox.height, | |
x = tbbox.x, | |
y = tbbox.y | |
point.x = x | |
point.y = y | |
bbox.nw = point.matrixTransform(matrix) | |
point.x += width | |
bbox.ne = point.matrixTransform(matrix) | |
point.y += height | |
bbox.se = point.matrixTransform(matrix) | |
point.x -= width | |
bbox.sw = point.matrixTransform(matrix) | |
point.y -= height / 2 | |
bbox.w = point.matrixTransform(matrix) | |
point.x += width | |
bbox.e = point.matrixTransform(matrix) | |
point.x -= width / 2 | |
point.y -= height / 2 | |
bbox.n = point.matrixTransform(matrix) | |
point.y += height | |
bbox.s = point.matrixTransform(matrix) | |
return bbox | |
} | |
// Private - replace D3JS 3.X d3.functor() function | |
function functor(v) { | |
return typeof v === 'function' ? v : function() { | |
return v | |
} | |
} | |
return tip | |
} | |
// eslint-disable-next-line semi | |
})); |
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
Platform | shortName | man | gen | Manufacturer | Launch | End | NA | EU | JP | RW | TOTAL | image | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Color TV-Game | CTV | 1 | 1 | Nintendo | 1977 | 1980 | 0 | 0 | 3 | 0 | 3 | http://imgur.com/jI6P3wh.png | |
Atari 2600 | 2600 | 0 | 2 | Atari | 1978 | 1983 | 23.54 | 3.35 | 0 | 0.75 | 27.64 | http://imgur.com/XiNbx0V.png | |
Intellivision | INT | 0 | 2 | Mattel | 1980 | 1985 | 3.5 | http://i.imgur.com/OoorNvJ.png | |||||
Nintendo Entertainment System | NES | 1 | 3 | Nintendo | 1983 | 1995 | 33.49 | 8.3 | 19.35 | 0.77 | 61.91 | http://i.imgur.com/LOyHDue.png | |
Sega Master System | SMS | 2 | 3 | Sega | 1986 | 1992 | 3 | 6.8 | 1 | 8 | 18.8 | http://imgur.com/5tPyAD1.png | |
PC-Engine | PCE | 0 | 4 | NEC | 1987 | 1994 | 0 | 0 | 3.9 | 1.9 | 5.8 | http://i.imgur.com/Ik0bRDL.png | |
Game Boy | GB | 1 | 4 | Nintendo | 1989 | 2003 | 43.18 | 40.05 | 32.47 | 2.99 | 118.69 | http://imgur.com/Yvl4Dyf.png | |
Mega Drive | MD | 2 | 4 | Sega | 1989 | 1997 | 16.98 | 8.39 | 3.58 | 0.59 | 29.54 | http://imgur.com/B4J6zZX.png | |
Super Nintendo Entertainment System | SNES | 1 | 4 | Nintendo | 1990 | 2003 | 22.88 | 8.15 | 17.17 | 0.9 | 49.1 | http://imgur.com/8rzdjLy.png | |
GameGear | GG | 2 | 4 | Sega | 1991 | 1997 | 5.4 | 3.23 | 1.78 | 0.21 | 10.62 | http://i.imgur.com/MMbDu8A.png | |
PlayStation | PS1 | 3 | 5 | Sony | 1995 | 2000 | 38.94 | 36.91 | 19.36 | 9.04 | 104.25 | http://i.imgur.com/7C8UpMh.png | |
Sega Saturn | SAT | 2 | 5 | Sega | 1995 | 1999 | 1.83 | 1.12 | 5.8 | 0.07 | 8.82 | http://imgur.com/Ter5o4Y.png | |
Nintendo 64 | N64 | 1 | 5 | Nintendo | 1996 | 2003 | 20.11 | 6.35 | 5.54 | 0.93 | 32.93 | http://i.imgur.com/SiyU6zI.png | |
Dreamcast | DC | 2 | 6 | Sega | 1998 | 2001 | 3.9 | 1.91 | 2.25 | 0.14 | 8.2 | http://imgur.com/Ter5o4Y.png | |
PlayStation 2 | PS2 | 3 | 6 | Sony | 2000 | 2012 | 53.65 | 55.28 | 23.18 | 25.57 | 157.68 | http://i.imgur.com/dZQZZFR.png | |
Game Boy Advance | GBA | 1 | 6 | Nintendo | 2001 | 2010 | 40.39 | 21.31 | 16.96 | 2.85 | 81.51 | http://imgur.com/N9Kjrfa.png | |
Xbox | XBOX | 4 | 6 | Microsoft | 2001 | 2009 | 15.77 | 7.17 | 0.53 | 1.18 | 24.65 | http://imgur.com/fFs3qk5.png | |
GameCube | NGC | 1 | 6 | Nintendo | 2001 | 2007 | 12.55 | 4.44 | 4.04 | 0.71 | 21.74 | http://imgur.com/9okzFq1.png | |
Nintendo DS | NDS | 1 | 7 | Nintendo | 2004 | 2014 | 57.37 | 52.07 | 33.01 | 12.43 | 154.88 | http://imgur.com/NFpoKEU.png | |
Xbox 360 | X360 | 4 | 7 | Microsoft | 2005 | 2016 | 49.11 | 25.87 | 1.66 | 9.16 | 85.8 | http://imgur.com/KhbdjNb.png | |
PlayStation Portable | PSP | 3 | 7 | Sony | 2005 | 2014 | 21.41 | 24.14 | 20.01 | 15.26 | 80.82 | http://imgur.com/7D7oKxi.png | |
Wii | Wii | 1 | 7 | Nintendo | 2006 | 2013 | 45.38 | 33.75 | 12.77 | 9.28 | 101.18 | http://imgur.com/KfwzFvq.png | |
PlayStation 3 | PS3 | 3 | 7 | Sony | 2006 | 2017 | 29.42 | 34.54 | 10.47 | 12.45 | 86.88 | http://imgur.com/FOiP973.png | |
Nintendo 3DS | 3DS | 1 | 8 | Nintendo | 2011 | 2017 | 21.05 | 17.45 | 22.8 | 3.75 | 65.05 | http://imgur.com/Ifpai0L.png | |
PlayStation Vita | PSV | 3 | 8 | Sony | 2012 | 2017 | 2.54 | 5.13 | 5.56 | 2.35 | 15.58 | http://imgur.com/wEvIkrq.png | |
Wii U | WiiU | 1 | 8 | Nintendo | 2012 | 2017 | 6.23 | 3.52 | 3.32 | 0.86 | 13.93 | http://imgur.com/hOcRDz5.png | |
PlayStation 4 | PS4 | 3 | 8 | Sony | 2013 | 2017 | 20.89 | 23.7 | 4.78 | 10.14 | 59.51 | http://imgur.com/ZWpleGO.png | |
Xbox One | XOne | 4 | 8 | Microsoft | 2013 | 2017 | 18.5 | 8.06 | 0.08 | 3.34 | 29.98 | http://imgur.com/4sKaJzN.png | |
Nintendo Switch | SWITCH | 1 | 8 | Nintendo | 2017 | 2017 | 1.8 | 1.3 | 1 | 0.6 | 4.7 | http://imgur.com/4WziWbL.png |
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
/** | |
* Pulls nodes toward a specified `(x, y)` target point. | |
*/ | |
function forceAttract(target) { | |
let nodes, | |
targets, | |
strength, | |
strengths; | |
function force (alpha) { | |
let node, target, strength; | |
for (let i=0; i<nodes.length; i++) { | |
node = nodes[i]; | |
target = targets[i]; | |
strength = strengths[i]; | |
node.vx += (target[0] - node.x) * strength * alpha; | |
node.vy += (target[1] - node.y) * strength * alpha; | |
} | |
} | |
function initialize () { | |
if (!nodes) return; | |
// populate local `strengths` using `strength` accessor | |
strengths = new Array(nodes.length); | |
for (let i=0; i<nodes.length; i++) strengths[i] = strength(nodes[i], i, nodes); | |
// populate local `targets` using `target` accessor | |
targets = new Array(nodes.length); | |
for (let i=0; i<nodes.length; i++) targets[i] = target(nodes[i], i, nodes); | |
} | |
force.initialize = _ => { | |
nodes = _; | |
initialize(); | |
}; | |
force.strength = _ => { | |
// return existing value if no value passed | |
if (_ == null) return strength; | |
// coerce `strength` accessor into a function | |
strength = typeof _ === 'function' ? _ : () => +_; | |
// reinitialize | |
initialize(); | |
// allow chaining | |
return force; | |
}; | |
force.target = _ => { | |
// return existing value if no value passed | |
if (_ == null) return target; | |
// coerce `target` accessor into a function | |
target = typeof _ === 'function' ? _ : () => _; | |
// reinitialize | |
initialize(); | |
// allow chaining | |
return force; | |
}; | |
if (!strength) force.strength(0.1); | |
if (!target) force.target([ 0, 0 ]); | |
return force; | |
} |
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
/** | |
* Pulls nodes toward a set of cluster center nodes / points. | |
* Adapted from Mike Bostock's Clustered Force Layout III: | |
* https://bl.ocks.org/mbostock/7881887 | |
*/ | |
function forceCluster(centers) { | |
var nodes, | |
centerpoints = [], | |
strength = 0.1, | |
centerInertia = 0.0; | |
// coerce centers accessor into a function | |
//if (typeof centers !== 'function') centers = () => centers; | |
function force (alpha) { | |
// scale + curve alpha value | |
alpha *= strength * alpha; | |
var c, x, y, l, r; | |
nodes.forEach(function(d, i) { | |
c = centerpoints[i]; | |
if (!c || c === d) return; | |
x = d.x - c.x, | |
y = d.y - c.y, | |
l = Math.sqrt(x * x + y * y), | |
r = d.radius + (c.radius || 0); | |
if (l && l != r) { | |
l = (l - r) / l * alpha; | |
d.x -= x *= l; | |
d.y -= y *= l; | |
c.x += (1 - centerInertia) * x; | |
c.y += (1 - centerInertia) * y; | |
} | |
}); | |
} | |
function initialize () { | |
if (!nodes) return; | |
// populate local `centerpoints` using `centers` accessor | |
var i, n = nodes.length; | |
centerpoints = new Array(n); | |
for (i = 0; i < n; i++) centerpoints[i] = centers(nodes[i], i, nodes); | |
} | |
/** | |
* Reinitialize the force with the specified nodes. | |
*/ | |
force.initialize = function(_) { | |
nodes = _; | |
initialize(); | |
}; | |
/** | |
* An array of objects representing the centerpoint of each cluster, | |
* or a function that returns such an array. | |
* Each object must have `x` and `y` values, and optionally `radius`. | |
*/ | |
force.centers = function(_) { | |
// return existing value if no value passed | |
if (_ == null) return centers; | |
// coerce centers accessor into a function | |
centers = typeof _ === 'function' ? _ : function (n, i) {return _[i]}; | |
// reinitialize | |
initialize(); | |
// allow chaining | |
return force; | |
}; | |
/** | |
* Strength of attraction to the cluster center node/position. | |
*/ | |
force.strength = function(_) { | |
return _ == null ? strength : (strength = +_, force); | |
}; | |
/** | |
* Inertia of cluster center nodes/positions. | |
* Higher values mean the cluster center moves less; | |
* lower values mean the cluster center is more easily | |
* pulled around by other nodes in the cluster. | |
* Typical values range from 0.0 (cluster centers move as much as all other nodes) | |
* to 1.0 (cluster centers are not moved at all by the clustering force). | |
*/ | |
force.centerInertia = function(_) { | |
return _ == null ? centerInertia : (centerInertia = +_, force); | |
}; | |
return force; | |
} |
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> | |
<html> | |
<head> | |
<title>The Console Wars - Hardware Sales</title> | |
<link href="slider.css" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Catamaran" rel="stylesheet"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v0.3.min.js"></script> | |
<!--script src="force-attract.js"></script--> | |
<script src="force-cluster.js"></script> | |
<script src="d3-tip.js"></script> | |
<style> | |
body { | |
font-family: 'Catamaran', sans-serif; | |
margin: 20px; | |
top: 20px; | |
right: 20px; | |
bottom: 20px; | |
left: 20px; | |
} | |
.d3-tip { | |
line-height: 1; | |
padding: 12px; | |
background: rgba(220, 220, 220, 0.97); | |
color: #444; | |
border-radius: 8px; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.d3-tip:after { | |
box-sizing: border-box; | |
display: inline; | |
font-size: 10px; | |
width: 100%; | |
line-height: 1; | |
color: rgba(220, 220, 220, 0.97); | |
content: "\25BC"; | |
position: absolute; | |
text-align: center; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n:after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
} | |
.d3-tip img { | |
display: block; | |
max-width: 300px; | |
max-height: 200px; | |
width: auto; | |
height: auto; | |
margin: auto; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="explain"><h1>The console wars - hardware sales</h1> | |
<p>Only consoles which sold over 3M units worldwide are presented.</p> | |
<p> | |
<button id="groupButton" onclick="groupBy()">Group by Generation / Release date</button> | |
</p> | |
</div> | |
<svg id="slider" width="1600" height="70"></svg> | |
<div id="chart"></div> | |
<script> | |
var width = 1600, | |
height = 700, | |
rightLegendPadding = 200, | |
padding = 7; // separation between nodes | |
var animated = false; | |
var currentYear = 0; | |
var totalSales = 0; | |
var groupedByGeneration = false; | |
var colorByGeneration = d3.scaleSequential(d3.interpolateBlues); | |
var clusterForce; | |
// The largest node for each cluster. | |
var clusters = new Array(10); | |
var nodes; | |
var simulation; | |
var node; | |
var text; | |
var svg; | |
var consoleGenerations = [ | |
{year: 1972, name: '1st Gen.'}, | |
{year: 1976, name: '2nd Gen.'}, | |
{year: 1983, name: '3rd Gen.'}, | |
{year: 1987, name: '4th Gen.'}, | |
{year: 1993, name: '5th Gen.'}, | |
{year: 1998, name: '6th Gen.'}, | |
{year: 2005, name: '7th Gen.'}, | |
{year: 2012, name: '8th Gen.'}, | |
{year: 2017, name: ''} | |
]; | |
var consoleManufacturers = [ | |
{name: 'Others', color: "#ff6724"}, | |
{name: 'Nintendo', color: "#e60012"}, | |
{name: 'Sega', color: "#0060a8"}, | |
{name: 'Sony', color: "#9100d7"}, | |
{name: 'Microsoft', color: "#107c0f"} | |
]; | |
var tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function (d) { | |
return "<strong>" + d.name + "</strong>" | |
+ "<br/><br/>Manufacturer: <span style='color: " + consoleManufacturers[d.manufacturer].color + "'>" + d.manufacturerName + "</span>" | |
+ "<br/>Total sales: " + d.totalSales + " millions units" | |
+ "<br/>Retailed: " + d.yearFrom + "-" + d.yearTo | |
+ "<br/>Generation: " + consoleGenerations[d.generation - 1].name | |
+ "<br/><br/><img src='" + d.image + "'/>"; | |
}); | |
function strokeWidth(d) { | |
return (currentYear > d.yearTo) ? "0" : "3"; | |
} | |
function salesInTime(t) { | |
return 1 - (Math.pow(1 - t, 2)) | |
} | |
function color(d) { | |
var c; | |
if (!groupedByGeneration) { | |
// console.log(d.manufacturer) | |
c = d3.color(consoleManufacturers[d.manufacturer].color); | |
} else { | |
c = d3.color(colorByGeneration(d.generation / 8)); | |
} | |
return c; | |
} | |
function recomputeClusters(nbClusters) { | |
clusterForce.strength(groupedByGeneration ? 0 : 1); | |
clusters = new Array(nbClusters); | |
for (i = 0; i < nodes.length; i++) { | |
var index = (groupedByGeneration ? nodes[i].generation : nodes[i].manufacturer) - 1; | |
if (!clusters[index] || (nodes[i].totalSales > clusters[index].totalSales)) clusters[index] = nodes[i]; | |
} | |
} | |
function getCluster(d) { | |
return groupedByGeneration ? clusters[d.generation - 1] : clusters[d.manufacturer - 1]; | |
} | |
d3.csv("data.csv", function (error, data) { | |
if (error) { | |
throw error; | |
} | |
nodes = d3.range(data.length).map(function (index) { | |
var dat = data[index]; | |
var i = +dat.man, | |
r = (+dat.TOTAL); | |
return { | |
shortName: dat.shortName, | |
image: dat.image, | |
name: dat.Platform, | |
generation: +dat.gen, | |
manufacturer: +dat.man, | |
manufacturerName: dat.Manufacturer, | |
yearFrom: dat.Launch, | |
yearTo: dat.End, | |
totalSales: +(dat.TOTAL), | |
radius: 0, | |
x: Math.cos(i / 5 * 2 * Math.PI) * 200 + (width - rightLegendPadding) / 2, | |
y: Math.sin(i / 5 * 2 * Math.PI) * 200 + height / 2 | |
}; | |
}); | |
clusterForce = forceCluster() | |
.centers(function (d) { | |
return getCluster(d); | |
}) | |
.strength(1) | |
.centerInertia(0); | |
recomputeClusters(5); | |
simulation = d3.forceSimulation() | |
// keep entire simulation balanced around screen center | |
.force('x', d3.forceX().x(function (d) { | |
return groupedByGeneration ? xTime(new Date(d.yearFrom, 1, 1)) + marg.left + (Math.sqrt(d.radius) * 6) : (width - rightLegendPadding) / 2; | |
}).strength(function (d) { | |
return groupedByGeneration ? 2 : 0.1; | |
})) | |
.force('y', d3.forceY().y(height / 2).strength(function (d) { | |
return groupedByGeneration ? 0 : 0.1; | |
})) | |
// cluster by section | |
.force('cluster', clusterForce) | |
// apply collision with padding | |
.force('collide', d3.forceCollide(function (d) { | |
return d.radius + padding; | |
}) | |
.strength(3)) | |
.velocityDecay(0.8) | |
.on('tick', layoutTick) | |
.nodes(nodes); | |
var group = svg.selectAll('.consoleGroup') | |
.data(nodes) | |
.enter().append('g').attr('class', 'consoleGroup'); | |
node = group.append('circle') | |
.attr("class", "consoleSale") | |
.style('fill', function (d) { | |
return color(d); | |
}) | |
.style('stroke', '#000') | |
.style('stroke-width', '2') | |
.call(d3.drag() | |
.on('start', dragstarted) | |
.on('drag', dragged) | |
.on('end', dragended) | |
) | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide); | |
text = group.append('text') | |
.text(function (d) { | |
return d.shortName; | |
}) | |
.attr('text-anchor', 'middle') | |
.attr('dominant-baseline', 'middle') | |
.attr("font-family", "sans-serif") | |
.attr("font-size", "11px") | |
//.attr("cursor", "default") | |
.attr("pointer-events", "none"); | |
// PRELOADING IMAGEs | |
group.append('image') | |
.attr("href", function (d) { | |
return d.image; | |
}) | |
.attr("visibility", "hidden"); | |
// ramp up collision strength to provide smooth transition | |
var transitionTime = 3000; | |
var t = d3.timer(function (elapsed) { | |
var dt = elapsed / transitionTime; | |
simulation.force('collide').strength(Math.pow(dt, 2) * 0.7); | |
if (dt >= 1.0) t.stop(); | |
}); | |
svg.call(tip); | |
animated = true; | |
} | |
); | |
svg = d3.select('#chart').append('svg') | |
.attr('width', width) | |
.attr('height', height); | |
var g = svg.append('g').attr('class', 'manufacturersLegend'); | |
for (i = 0; i < consoleManufacturers.length; i++) { | |
g.append('circle') | |
.style('fill', d3.color(consoleManufacturers[i].color)) | |
.attr('cx', width - rightLegendPadding) | |
.attr('cy', i * 40 + 320) | |
.attr('r', '15'); | |
g.append("text") | |
.text(consoleManufacturers[i].name) | |
.attr('x', width - rightLegendPadding + 30) | |
.attr('y', i * 40 + 325); | |
} | |
g = svg.append('g').attr('class', 'generationsLegend'); | |
for (i = 0; i < consoleGenerations.length - 1; i++) { | |
g.append('circle') | |
.style('fill', colorByGeneration(i / 8)) | |
.attr('cx', width - rightLegendPadding) | |
.attr('cy', i * 40 + 320) | |
.attr('r', '15'); | |
g.append("text") | |
.text(consoleGenerations[i].name) | |
.attr('x', width - rightLegendPadding + 30) | |
.attr('y', i * 40 + 325); | |
} | |
g.attr('opacity', '0'); | |
g = svg.append('g'); | |
/*g.append('circle') | |
.style('fill', "#ccc") | |
.attr('cx', width - rightLegendPadding) | |
.attr('cy', 30) | |
.attr('r', '15') | |
.style('stroke', '#000') | |
.style('stroke-width', '3'); | |
g.append("text") | |
.text("Retail still available") | |
.attr('x', width - rightLegendPadding + 30) | |
.attr('y', 35);*/ | |
g.append('circle') | |
.style('fill', "#ccc") | |
.attr('cx', width - rightLegendPadding) | |
.attr('cy', 70) | |
.attr('r', '15'); | |
g.append("text") | |
.text("Retail unavailable") | |
.attr('x', width - rightLegendPadding + 30) | |
.attr('y', 78); | |
g.append("text") | |
.text("World sales (in million units)") | |
.attr("text-anchor", "middle") | |
.attr('x', width - rightLegendPadding + 70) | |
.attr('y', 125); | |
g.append('circle') | |
.style('fill', "#ccc") | |
.attr('cx', width - rightLegendPadding + 70) | |
.attr('cy', 215) | |
.style('stroke', '#000') | |
.attr('r', Math.sqrt(150) * 6); | |
g.append("text") | |
.text("150") | |
.attr("text-anchor", "middle") | |
.attr('x', width - rightLegendPadding + 70) | |
.attr('y', 162); | |
g.append('circle') | |
.style('fill', "#ccc") | |
.attr('cx', width - rightLegendPadding + 70) | |
.attr('cy', 215) | |
.style('stroke', '#000') | |
.attr('r', Math.sqrt(50) * 6); | |
g.append("text") | |
.text("50") | |
.attr("text-anchor", "middle") | |
.attr('x', width - rightLegendPadding + 70) | |
.attr('y', 192); | |
g.append('circle') | |
.style('fill', "#ccc") | |
.attr('cx', width - rightLegendPadding + 70) | |
.attr('cy', 215) | |
.style('stroke', '#000') | |
.attr('r', Math.sqrt(5) * 6); | |
g.append("text") | |
.text("5") | |
.attr("text-anchor", "middle") | |
.attr('x', width - rightLegendPadding + 70) | |
.attr('y', 218); | |
function dragstarted(d) { | |
if (!d3.event.active) simulation.alphaTarget(0.1).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
if (!d3.event.active) simulation.alphaTarget(0); | |
d.fx = null; | |
d.fy = null; | |
} | |
function layoutTick(e) { | |
// console.log(simulation.alpha()); | |
node | |
.attr('cx', function (d) { | |
return (currentYear >= d.yearFrom) ? Math.max(d.radius, Math.min(width - rightLegendPadding - d.radius, d.x)) : 0; | |
}) | |
.attr('cy', function (d) { | |
return (currentYear >= d.yearFrom) ? Math.max(d.radius, Math.min(height - d.radius, d.y)) : 0; | |
}) | |
.attr('r', function (d) { | |
return (currentYear >= d.yearFrom) ? d.radius : 0; | |
}); | |
text | |
.attr('x', function (d) { | |
return (currentYear >= d.yearFrom) ? Math.max(d.radius, Math.min(width - rightLegendPadding - d.radius, d.x)) : 0; | |
}) | |
.attr('y', function (d) { | |
return (currentYear >= d.yearFrom) ? Math.max(d.radius, Math.min(height - d.radius, d.y)) : 0; | |
}) | |
.attr('opacity', function (d) { | |
return (d.radius > 10) ? 1 : 0; | |
}) | |
} | |
var sliderSVG = d3.select("#slider"), | |
marg = {right: 50, left: 50}, | |
sliderWidth = +sliderSVG.attr("width") - marg.left - marg.right - rightLegendPadding, | |
sliderHeight = +sliderSVG.attr("height"); | |
var handle; | |
var playPauseButton = sliderSVG.append("svg:image") | |
.attr('x', 0) | |
.attr('y', 10) | |
.attr('width', 32) | |
.attr('height', 32) | |
.attr("xlink:href", "http://i.imgur.com/GQYRQQv.png") | |
.on("click", playPause); | |
var xTime = d3.scaleTime() | |
.domain([new Date(1972, 0, 1), new Date(2017, 11, 1)]) | |
.range([0, sliderWidth]) | |
.clamp(true); | |
var xGenerations = d3.scaleOrdinal() | |
.domain(consoleGenerations.map(function (d) { | |
return d.name | |
})) | |
.range(consoleGenerations.map(function (d) { | |
return ((d.year - 1972) / 45) * sliderWidth | |
})); | |
var slider = sliderSVG.append("g") | |
.attr("class", "slider") | |
.attr("transform", "translate(" + marg.left + "," + sliderHeight / 2 + ")"); | |
var track = slider.append("line") | |
.attr("class", "track") | |
.attr("x1", xTime.range()[0]) | |
.attr("x2", xTime.range()[1]); | |
for (i = 0; i < 9; i++) { | |
slider.append("line") | |
.attr("class", "trackGeneration") | |
.attr("x1", xGenerations.range()[i - 1]) | |
.attr("x2", xGenerations.range()[i]) | |
.attr("stroke", "" + colorByGeneration(i / 8)); | |
} | |
track.select(function () { | |
return this.parentNode.appendChild(this.cloneNode(true)); | |
}) | |
.attr("class", "track-overlay") | |
.call(d3.drag() | |
.on("start.interrupt", function () { | |
slider.interrupt(); | |
}) | |
.on("start drag", function () { | |
if (animated) playPause(); | |
handle.transition() | |
.ease(d3.easeCubicOut) | |
.duration(2 * Math.abs(d3.event.x - handle.attr("cx"))) | |
.attr("cx", Math.min(sliderWidth, Math.max(0, d3.event.x))) | |
.tween("attr.fill", function () { | |
var node = this; | |
return function (t) { | |
dateChange(xTime.invert(node.getAttribute("cx"))); | |
} | |
}); | |
})); | |
slider.insert("g", ".track-overlay") | |
.attr("class", "ticks") | |
.attr("transform", "translate(0," + 18 + ")") | |
.selectAll("text") | |
.data(xTime.ticks(d3.timeYear.filter(function (d) { | |
return d.getYear() % 5 === 2 | |
}))) | |
.enter().append("text") | |
.attr("x", xTime) | |
.attr("text-anchor", "middle") | |
.text(function (d) { | |
return d.getFullYear(); | |
}); | |
slider.insert("g", ".track-overlay") | |
.attr("class", "ticks") | |
.attr("transform", "translate(0," + -10 + ")") | |
.selectAll("text") | |
.data(consoleGenerations) | |
.enter().append("text") | |
.attr("x", function (d) { | |
return xGenerations(d.name); | |
}) | |
.attr("text-anchor", "middle") | |
.text(function (d) { | |
return d.name; | |
}); | |
handle = slider.insert("circle", ".track-overlay") | |
.attr("class", "handle") | |
.attr("r", 9) | |
.attr("cx", 80); | |
slider.append("text") | |
.attr("class", "currentYearDisplay") | |
.attr("x", handle.attr('cx')) | |
.attr("text-anchor", "middle") | |
.attr("y", 30) | |
.text("" + currentYear); | |
d3.interval(function (elapsed) { | |
if (animated && (+handle.attr("cx") < sliderWidth)) { | |
handle.attr("cx", Math.min(+handle.attr("cx") + sliderWidth / 500, sliderWidth)); | |
dateChange(xTime.invert(handle.attr("cx"))); | |
} | |
}, 50); | |
function playPause() { | |
animated = !animated; | |
playPauseButton.attr("xlink:href", animated ? "http://i.imgur.com/GQYRQQv.png" : "http://i.imgur.com/twrr0MN.png"); | |
//d3.event.stopPropagation(); | |
} | |
function repaintCircles() { | |
svg.selectAll('.consoleSale') | |
.style('fill', function (d) { | |
return color(d); | |
}) | |
/*.style('stroke-width', function (d) { | |
return strokeWidth(d); | |
});*/ | |
} | |
function dateChange(h) { | |
currentYear = h.getFullYear(); | |
var month = h.getMonth(); | |
totalSales = 0; | |
for (i = 0; i < nodes.length; i++) { | |
// console not appeared | |
if (nodes[i].yearFrom > currentYear) { | |
nodes[i].radius = 0; | |
nodes[i].x = getCluster(nodes[i]).x; | |
nodes[i].y = getCluster(nodes[i]).y; | |
} | |
// console no more retailed | |
else if (currentYear > nodes[i].yearTo) { | |
nodes[i].radius = nodes[i].totalSales; | |
// console currently in retail | |
} else { | |
var fullMonthsOfRetail = (nodes[i].yearTo - nodes[i].yearFrom + 1) * 12; | |
var currentMonthsOfRetail = ((currentYear - nodes[i].yearFrom) * 12) + month + 1; | |
var timeRatio = currentMonthsOfRetail / fullMonthsOfRetail; | |
nodes[i].radius = nodes[i].totalSales * salesInTime(timeRatio); | |
} | |
totalSales += nodes[i].radius; | |
} | |
//var radiusRatio = Math.sqrt(maxRadius / totalSales) * 15; | |
var radiusRatio = 6; | |
// sales are represented by a surface so you need to sqrt the radius | |
for (i = 0; i < nodes.length; i++) { | |
nodes[i].radius = Math.sqrt(nodes[i].radius) * radiusRatio; | |
} | |
simulation.nodes(nodes.filter(function (d) { | |
return d.radius > 0 | |
})).alpha(.1).restart(); | |
repaintCircles(); | |
d3.select(".currentYearDisplay") | |
.text("" + currentYear) | |
.attr('x', handle.attr('cx')); | |
} | |
function groupBy() { | |
groupedByGeneration = !groupedByGeneration; | |
d3.select(".generationsLegend").attr('opacity', groupedByGeneration ? '1' : '0'); | |
d3.select(".manufacturersLegend").attr('opacity', groupedByGeneration ? '0' : '1'); | |
d3.select("#groupButton").text(groupedByGeneration ? "Group by Manufacturer" : "Group by Generation / Release date"); | |
recomputeClusters(groupedByGeneration ? 8 : 5); | |
simulation.nodes(nodes.filter(function (d) { | |
return d.radius > 0 | |
})).alpha(.5).restart(); | |
repaintCircles(); | |
} | |
</script> | |
</body> | |
</html> |
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
.ticks { | |
font: 10px sans-serif; | |
} | |
.track, | |
.track-inset, | |
.track-overlay { | |
stroke-linecap: round; | |
} | |
.trackGeneration { | |
stroke-width: 8px; | |
stroke-linecap: round; | |
} | |
.track { | |
stroke: #000; | |
stroke-opacity: 1; | |
stroke-width: 10px; | |
} | |
/*.track-inset { | |
stroke-opacity: 0; | |
stroke: #ddd; | |
stroke-width: 8px; | |
}*/ | |
.track-overlay { | |
pointer-events: stroke; | |
stroke-width: 50px; | |
cursor: ew-resize; | |
} | |
.handle { | |
fill: #fff; | |
stroke: #000; | |
stroke-opacity: 0.5; | |
stroke-width: 1.25px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment