function viz19() {

    var data  = [],
	lines,
	line,
	path,
	circles,
	circle,
	areas,
	tip   = 0,  // Highest Y point so far
	max   = 500 // Maximum length of data

    var graph = function(s) {

	if (s == undefined) {
	    console.error('selection is undefined.')
	    return
	}

	data    = []

	svg.append("defs")
	    .append("clipPath")
	    .attr("id", "clip")
	    .append("rect")
	    .attr('width', W)
	    .attr('height', H)
	
	graph.render()
    }

    graph.datum  = function(_) {
	if (arguments.length == 0) return datum
	datum = _
	data.push(datum)
	return graph
    }

    function sum_y1_up_to_i(data, d, i) {
	var sum = 0
	var n   = d.id
	var p   = d.id > 0 ? d.id - 1 : d3.max(data, function(d) { return d.id })
	for (let j=0; j<i; j++) {
	    if (data[j].id != p) continue
	    sum += data[j].y
	}
	return sum;
    }
    
    graph.max = function(x, y) {
	return (x > y) ? x : y;
    }

    graph.transition = function(_) {
	if (arguments.length == 0) return transition
	transition = d3.transition().ease(d3.easeLinear).duration(_ * Math.random())
	return graph
    }
    
    // Update SVG
    graph.render = function() {

	if (data.length <= 1)   return
	if (data.length > max)  data.shift(data.length - max)

	// Update time scale to zoom in the latest steps
	xmin = new Date(d3.min(data, function(d) { return d.x })).getTime()
	xmax = new Date(d3.max(data, function(d) { return d.x })).getTime()
	xScale.domain([xmin, xmax]).nice()

	// Keep all Y values in sight
	tip = graph.max(tip, sum_y1_up_to_i(data, data[0], data.length))
	yScale.domain([ d3.min(data, function(d) { return d.y }), tip ]).nice()

	// Find how many players are in
	var players = d3.set(data, function(d) { return d.id })
	var update  = svg.selectAll('g.player').data(players.values())
	var enter   = update.enter().append('g')
	var exit    = update.exit()
	
	exit.attr('class', 'inactive player')
	    .transition(graph.transition).style('opacity', 0).remove()
	update.attr('class', 'active player')
	enter.attr('class', 'new player')
	    .attr('id', function(d) { return 'player-' + d[0] })

	// Play each player
	d3.timeout(function() {	    
	    players.each(graph.play)
	}, 300)
    }

    // Update SVG for one player
    graph.play   = function(player_id) {

	// Only take the first datum
	var pdata = data.filter(function(d) { return d.id == player_id })

	// Update transition duration
	var t     = graph.transition(timeout * (0.5 + Math.random() / 2))
	
	// Current player group
	var player_group = svg.select('g.player#player-' + player_id)

	// Selection
	var update = player_group.selectAll('path').data([pdata])
	var enter  = update.enter().append('path')
	var exit   = update.exit()

	function max(x, y) {
	    if (x <= 0) x = 0
	    if (y <= 0) y = 0

	    return Math.max(x, y)
	}
	function min(x, y) {
	    if (x <= 0) x = 0
	    if (y <= 0) y = 0

	    return Math.min(x, y)
	}
	
	var area = d3.area()
	    .x(function(d, i) { return xScale(d.x) })
	    .y0(function(d, i) { return yScale(sum_y1_up_to_i(data, d, i)) })
	    .y1(function(d, i) { return yScale(d3.sum(data.slice(0, max(0, i)),
						      function(d) { return d.y })) })
	    .curve(d3.curveBasisOpen)
	
	// UPDATE existing nodes
	update
	    .attr('class', 'update')
	    .attr('data-move', function(d, i) { return d[i].n })
	    .attr('d', area)

	// ENTER new nodes
	enter
	    .attr('class', 'enter')
	    .attr('data-move', function(d, i) { return d[i].n })
	    .transition(t).duration(400)
	    .attr('d', area)
	    .attr('fill', function(d, i) { return color(d[i].id) })

	// EXIT removed nodes
	exit.style('fill', 'red').remove()
    }

    graph.run = function() {
	graph.render()
	d3.interval(graph.render, 50)
    }

    return graph
}

function randomInteger(min, max) {
    return min + Math.floor(Math.random() * (max - min + 1))
}

function genDatum() {

    if (halted) {
	clearTimeout(gen)
	console.log('genDatum: halted.')
	return
    }

    // Pick a random timestamp within one second of now
    var now = new Date(new Date().getTime() + (Math.random() - 0.5) * 1000)
    var obj = {
	n: n++,
	id: randomInteger(1, 5),
	x: now.getTime(),
	y: d3.randomNormal(12, 3)()
    }

    // Append datum to data array
    graph.datum(obj)

    // Update delay to next object
    timeout = Math.round(normal())

    // TODO: Use d3.timeInterval
    clearTimeout(gen)
    gen = setTimeout(genDatum, timeout)
    
}

// Variables

var halted  = false,
    timeout = 1000,
    now     = new Date().getTime(),
    n       = 0,
    gen     = setTimeout(genDatum, timeout)

// Random delay for timeout
//var normal  = d3.randomNormal(1000, 200)
var normal  = d3.randomNormal(750, 200)

// Constants

const W      = 1366,  // SVG Width
      H      = 320,  // SVG Height
      R      = 32,   // Point Radius
      D      = 50    // Duration in ms

const color  = d3.scaleOrdinal(d3.schemeCategory10)
const xScale = d3.scaleLinear().domain([0, 1]).range([W, 0])
const yScale = d3.scaleLinear().domain([0, 1]).range([H, 0])

const graph  = viz19()
const view   = d3.select('#view')
const svg    = view.append('svg').attr('width', W).attr('height', H)

// Runtime
console.clear()

view.call(graph)

graph.run()