Skip to content

Instantly share code, notes, and snippets.

@pmariac
Last active August 23, 2017 16:24
Show Gist options
  • Save pmariac/44bf394715c9fba770c836a6156ccd21 to your computer and use it in GitHub Desktop.
Save pmariac/44bf394715c9fba770c836a6156ccd21 to your computer and use it in GitHub Desktop.
The console wars
license: mit
/**
* 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
}));
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
/**
* 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;
}
/**
* 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;
}
<!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>
.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