Skip to content

Instantly share code, notes, and snippets.

@darthmall
Created October 21, 2017 16:31
Show Gist options
  • Select an option

  • Save darthmall/1346bca1b2d7011b267bd6cbf2ac24d2 to your computer and use it in GitHub Desktop.

Select an option

Save darthmall/1346bca1b2d7011b267bd6cbf2ac24d2 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="style.css">
<title>Simon-Ehrlich Wager</title>
</head>
<body>
<figure id="wager-hx"></figure>
<figure id="metals"></figure>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.js"></script>
<script src="main.js"></script>
</body>
</html>
const betPeriod = 10;
function flatMap(fn, arr) {
return Array.prototype.concat.apply([], arr.map(fn));
}
/**
* Return an array containing the boundary data objects.
*/
function bounds(data, acc) {
function f(res, val) {
if (res.length < 1) return [val, val];
if (acc(val) < acc(res[0])) res[0] = val;
if (acc(val) > acc(res[1])) res[1] = val;
return res;
}
// Default the accessor to the identity function
acc = acc || (d => d);
return data.reduce(f, []);
}
/**
* Render a line chart from data.
*/
function MetalPrices(el) {
let _data = [],
_highlight = [0, 1],
_width = 960,
_height = 500,
_margin = {
top: 8,
right: 0,
bottom: 16,
left: 32
},
_seriesName = d => d.key,
_seriesColor = () => "black";
// Initialize the chart in the element.
el = d3.select(el).append("svg").classed("line-chart chart", true);
const mask = el.append("defs")
.append("clipPath").attr("id", "highlight")
.append("rect");
// Create the layers in the chart.
const margin = el.append("g").attr("class", "margin"),
bg = margin.append("rect").attr("class", "bg"),
xAxis = margin.append("g").attr("class", "x axis"),
yAxis = margin.append("g").attr("class", "y axis"),
series = margin.append("g").attr("class", "series"),
seriesFade = series.append("g"),
seriesHighlight = series.append("g").attr("clip-path", "url(#highlight)"),
highlightBrush = seriesHighlight.append("rect").attr("class", "brush bg"),
annotations = margin.append("g").attr("class", "annotations"),
legend = annotations.append("g").attr("class", "legendOrdinal");
const x = d3.scaleLinear(),
y = d3.scaleLinear();
function chart() {
const w = _width - _margin.left - _margin.right,
h = _height - _margin.top - _margin.bottom;
el.attr("width", _width)
.attr("height", _height);
margin.attr("transform", `translate(${_margin.left}, ${_margin.top})`);
el.selectAll(".bg").attr("width", w)
.attr("height", h);
x.domain(d3.extent(_data, d => d.year)).range([0, w]);
y.domain(d3.extent(_data, d => d.price)).nice().range([h, 0]);
const nest = d3.nest().key(d => d.type);
const line = d3.line().x(d => x(d.year)).y(d => y(d.price));
const path = seriesFade.selectAll("path").data(nest.entries(_data));
path.exit().remove();
path.enter().append("path")
.merge(path)
.attr("d", d => line(d.values))
.attr("stroke-dasharray", "3,1")
.style("stroke", d => _seriesColor(_seriesName(d)))
.style("opacity", 0.6);
const highlightPath = seriesHighlight.selectAll("path").data(nest.entries(_data));
highlightPath.exit().remove();
highlightPath.enter().append("path")
.merge(highlightPath)
.attr("d", d => line(d.values))
.style("stroke", d => _seriesColor(_seriesName(d)))
.style("stroke-width", 2);
mask.transition().attr("x", x(_highlight[0]))
.attr("width", x(_highlight[1]) - x(_highlight[0]))
.attr("height", _height);
xAxis.call(d3.axisBottom()
.scale(x)
.tickSize(h)
.tickPadding(8)
.tickFormat(String));
yAxis.attr("transform", `translate(${w}, 0)`)
.call(d3.axisLeft()
.scale(y)
.tickSize(w));
el.selectAll(".axis .domain").remove();
legend.call(d3.legendColor().scale(_seriesColor))
.attr("transform", function () {
const bbox = this.getBoundingClientRect();
return `translate(${w - bbox.width - 8}, 8)`;
});
}
/**
* Data rendered in the chart.
*
* The data should be a 2D array. The outer array contains each series
* rendered as separate lines; each series is an array of points.
*/
chart.data = function (_) {
if (arguments.length < 1) return _data;
_data = _;
return chart;
};
/**
* A range of years highlighted in the chart.
*/
chart.highlight = function (_) {
if (arguments.length < 1) return _highlight;
_highlight = _;
return chart;
};
chart.margin = function (_) {
if (arguments.length < 1) return _margin;
Object.assign(_margin, _);
return chart;
};
chart.seriesColor = function (_) {
if (arguments.length < 1) return _seriesColor;
_seriesColor = _;
return chart;
};
chart.height = function (_) {
if (arguments.length < 1) return _height;
_height = _;
return chart;
};
chart.width = function (_) {
if (arguments.length < 1) return _width;
_width = _;
return chart;
};
return chart;
}
function RugPlot(el) {
var _data = []
_selected = 0,
_width = 960,
_height = 32,
_margin = {
top: 0,
right: 0,
bottom: 0,
left: 32
},
_x = d => d.year,
_xScale = d3.scaleLinear(),
_y = d => d.winner,
_yScale = d3.scaleBand(),
_fringeWidth = 2;
el = d3.select(el).append("svg").classed("rug-chart chart", true);
const margin = el.append("g").attr("class", "margin"),
yAxis = margin.append("g").attr("class", "y axis"),
rug = margin.append("g");
function chart() {
const w = _width - _margin.left - _margin.right,
h = _height - _margin.top - _margin.bottom;
el.attr("width", _width).attr("height", _height);
margin.attr("transform", `translate(${_margin.left}, ${_margin.top})`);
_xScale.domain(d3.extent(_data, _x)).range([0, w]);
_yScale.domain(_data.map(_y)).range([h, 0]);
const rect = rug.selectAll("rect").data(_data.filter(d => !!_y(d)));
rect.exit().remove();
rect.enter().append("rect")
.attr("width", _fringeWidth)
.style("cursor", "pointer")
.merge(rect)
.attr("x", d => _xScale(_x(d)) - 1)
.attr("y", d => _yScale(_y(d)))
.attr("height", _yScale.bandwidth())
.style("fill", d => (d.year === _selected) ? "red" : "steelblue");
yAxis.call(d3.axisLeft().scale(_yScale))
.selectAll(".domain").remove();
}
chart.data = function (_) {
if (arguments.length < 1) return _data;
_data = _;
return chart;
};
chart.margin = function (_) {
if (arguments.length < 1) return _margin;
Object.assign(_margin, _);
return chart;
};
chart.selected = function (_) {
if (arguments.length < 1) return _selected;
_selected = _;
return chart;
};
return chart;
}
function LineChart(el) {
let _data = [],
_width = 179,
_height = _width * .618,
_margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
},
_x = d => d.year,
_xScale = d3.scaleLinear(),
_y = d => d.price,
_yScale = d3.scaleLinear(),
_seriesName = d => d.key,
_seriesColor = () => "black";
el = d3.select(el).classed("line-chart chart", true);
const margin = el.append("g").attr("class", "margin"),
bg = margin.append("rect").attr("class", "bg"),
yAxis = margin.append("g").attr("class", "y axis"),
series = margin.append("g").attr("class", "series"),
annotations = margin.append("g").attr("class", "annotation");
function chart() {
const w = _width - _margin.left - _margin.right,
h = _height - _margin.top - _margin.bottom;
_xScale.domain(d3.extent(d3.merge(_data), _x)).range([0, w]);
_yScale.domain(d3.extent(d3.merge(_data), _y)).nice().range([h, 0]);
el.attr("width", _width).attr("height", _height);
bg.attr("width", w).attr("height", h);
const line = d3.line()
.x(d => _xScale(_x(d)))
.y(d => _yScale(_y(d)));
const path = series.selectAll(".hx").data(_data);
path.exit().remove();
path.enter().append("path")
.attr("class", "hx")
.attr("stroke-dasharray", "3,1")
.style("opacity", 0.6)
.merge(path)
.attr("d", line)
.style("stroke", d => _seriesColor(_seriesName(d)));
const trend = series.selectAll(".trend")
.data([bounds(d3.merge(_data), d => d.year)]);
trend.exit().remove();
trend.enter().append("path")
.attr("class", "trend")
.merge(trend)
.attr("d", line)
.style("stroke", d => _seriesColor(_seriesName(d)));
yAxis.attr("transform", `translate(0,0)`)
.call(d3.axisRight()
.scale(_yScale)
.tickSize(w)
.ticks(3))
.selectAll(".tick text")
.attr("x", 4)
.attr("dy", "-.35em")
.style("fill", "#aaa");
yAxis.selectAll(".domain").remove();
}
chart.data = function (_) {
if (arguments.length < 1) return _data;
_data = _;
return chart;
};
chart.seriesName = function (_) {
if (arguments.length < 1) return _seriesName;
_seriesName = _;
return chart;
};
chart.seriesColor = function (_) {
if (arguments.length < 1) return _seriesColor;
_seriesColor = _;
return chart;
};
return chart;
}
function BetPeriod(el) {
let _data = [],
_seriesName = d => d.key,
_seriesColor = () => "black";
el = d3.select(el);
function chart() {
const div = el.selectAll("div").data(_data, d => d.key);
div.exit().remove();
div.enter().append("div")
.merge(div)
.attr("class", "small-multiple")
.each(function (d) {
const el = d3.select(this);
const p = el.selectAll("p").data([d.key]);
p.exit().remove();
p.enter().append("p")
.merge(p)
.text(String);
const svg = el.selectAll("svg").data([0]);
svg.exit().remove();
LineChart(svg.enter().append("svg").merge(svg).node())
.data([d.values])
.seriesName(() => _seriesName(d))
.seriesColor(_seriesColor)();
});
}
chart.data = function (_) {
if (arguments.length < 1) return _data;
_data = _;
return chart;
};
chart.seriesColor = function (_) {
if (arguments.length < 1) return _seriesColor;
_seriesColor = _;
return chart;
};
return chart;
}
function Wager(el, metals, winners) {
const color = d3.scaleOrdinal(d3.schemeCategory10);
const sharedMargin = {left: 64, right: 1};
const priceHx = MetalPrices("#wager-hx")
.seriesColor(color)
.margin(sharedMargin)
.data(metals)
.highlight([1980, 1990]);
const winnerRug = RugPlot("#wager-hx")
.margin(sharedMargin)
.data(winners)
.selected(1980);
const betData = d3.nest()
.key(d => d.type)
.entries(metals.filter(d => d.year >= 1980 && d.year <= 1990));
const metalPrices = BetPeriod("#metals")
.data(betData)
.seriesColor(color);
priceHx();
winnerRug();
metalPrices();
d3.selectAll("#wager-hx rect")
.on("click", function (d) {
const start = d.year,
end = start + betPeriod;
const betData = d3.nest()
.key(d => d.type)
.entries(metals.filter(d => d.year >= start && d.year <= end));
priceHx.highlight([start, end])();
winnerRug.selected(d.year)();
metalPrices.data(betData)();
});
}
/**
* Convert all properties in row to numbers.
*/
function parse(row) {
for (const k in row) {
row[k] = +row[k];
}
return row;
}
d3.tsv("metals.tsv", parse, function (data) {
function pivotRow(row) {
const pivot = [];
for (const k in row) {
if (k === "Year") continue
pivot.push({year: row["Year"], type: k, price: row[k]});
}
// Return an array of observations. This will result in a nested array that
// needs to be flattened.
return pivot
}
// Pivot the columns containing the metal prices into a tidy format where
// each row represents a price observation that contains the year, metal,
// and price
let metals = flatMap(pivotRow, data);
// Sort the array by year
data.sort((a, b) => a.Year - b.Year);
// For each 10 year period in the array, calculate the winner of the bet if it
// had started in that year.
const winners = [];
for (let i = 0, n = data.length; i < n; i++) {
const start = data[i],
end = data[i + betPeriod];
// Fill out entries for years where there isn't enough data to construct a
// 10-year period, just so that the x scales on the line chart and the rug
// plot have matching domains.
if (!end) {
winners.push({year: start.Year});
continue;
}
// Skip a 10-year period if we're missing data for that period
if (end.Year - start.Year !== betPeriod) {
console.warn("Missing year", start.Year + betPeriod)
continue;
}
// Track the score for the bettors
let ehrlich = 0,
simon = 0;
// Check each metal for this year
for (const k in start) {
if (k === "Year") continue;
if (end[k] - start[k] > 0) {
++ehrlich;
} else {
++simon;
}
}
winners.push({
year: start.Year,
winner: (ehrlich > simon) ? "Ehrlich" : "Simon"
});
}
Wager(document.body, metals, winners);
});
Year Tin (98$/t) Copper (98$/t) Nickel (98$/t) Tungsten (98$/t) Chromium (98$/t)
1900 12900 7000 22000 11000 1090
1901 7220 7000 24000 7000 1160
1902 11200 4800 19000 8000 882
1903 11300 5300 16000 6300 765
1904 11200 5100 16000 10000 836
1905 12600 6300 16000 14000 769
1906 15900 7700 16000 16000 738
1907 14800 7700 17000 22000 652
1908 11800 5300 18000 14000 731
1909 11900 5300 16000 16000 685
1910 13200 5000 15000 18700 608
1911 16400 4900 15000 15000 603
1912 17200 6200 15000 14800 498
1913 16100 5630 15300 16600 501
1914 12300 4760 14700 16600 449
1915 13700 6180 14600 65800 526
1916 14300 9360 13800 70700 660
1917 17400 8190 11800 32600 750
1918 21100 5890 9760 34900 1280
1919 13200 3780 8320 23600 680
1920 8660 3140 7530 8910 333
1921 5990 2540 8420 10900 232
1922 6980 2900 8140 12600 255
1923 8960 3100 7560 13300 265
1924 10500 2790 6300 11200 280
1925 11900 2920 6800 13700 239
1926 13200 2840 7280 14200 232
1927 13300 2690 7220 13500 235
1928 10600 3110 7770 13800 240
1929 9490 3850 7350 17400 255
1930 6850 2860 7570 16400 334
1931 5790 1980 8270 16400 498
1932 5770 1520 9190 15200 635
1933 10800 2020 9670 16700 503
1934 14000 2330 9390 24600 480
1935 13200 2330 9190 22100 530
1936 12000 2520 9050 24200 529
1937 13600 3350 8740 30700 482
1938 10800 2610 8930 27800 494
1939 13000 2900 9050 27900 477
1940 12800 2960 8990 33400 486
1941 12700 2930 8560 36100 455
1942 11500 2650 7050 33500 562
1943 10800 2500 6650 32900 617
1944 10600 2450 6530 30100 651
1945 10400 2410 6410 29200 634
1946 10000 2580 6430 23400 434
1947 12500 3420 5640 23800 466
1948 14800 3320 5370 24700 513
1949 15000 2940 6040 25100 522
1950 14200 3210 6700 26600 503
1951 17500 3400 7440 53200 501
1952 16300 3310 7730 54300 580
1953 12900 3910 8050 53000 669
1954 12300 4000 8180 52800 516
1955 12700 5040 8900 52300 523
1956 13400 5540 8560 48300 577
1957 12300 3840 9480 19900 652
1958 11800 3280 9210 13800 631
1959 12600 3820 9110 16000 756
1960 12300 3920 8960 17100 476
1961 13700 3630 9400 16200 379
1962 13700 3670 9510 17300 673
1963 13700 3640 9260 15700 619
1964 18300 3750 9160 14800 652
1965 20400 4020 9020 19700 670
1966 18200 3990 8740 23300 609
1967 16500 4100 9460 20000 644
1968 15300 4260 9810 23200 645
1969 16100 4650 10300 23300 614
1970 16100 5380 11900 23700 538
1971 14800 4620 11800 26200 755
1972 15300 4420 12100 22100 767
1973 18400 4810 12400 22000 774
1974 28900 5630 12700 34800 1060
1975 22700 4290 13800 35400 1780
1976 24000 4390 14200 40100 1440
1977 31700 3960 13400 54300 1340
1978 34700 3630 11300 45300 1150
1979 36400 4570 13200 41600 1310
1980 36900 4420 12300 36600 1260
1981 29000 3330 10700 31500 1270
1982 24400 2710 8130 23700 1150
1983 23600 2760 7650 17000 1060
1984 21600 2310 7490 17900 1140
1985 19900 2240 7540 14100 1160
1986 12600 2170 5770 10500 1010
1987 13200 2610 6940 9110 944
1988 13400 3660 19000 10300 1440
1989 15100 3800 17500 13900 1630
1990 10600 3380 11100 10600 1110
1991 9600 2890 9760 11800 1080
1992 10300 2750 8130 10800 1020
1993 8700 2280 5970 7690 777
1994 8950 2690 6970 10400 764
1995 9800 3260 8800 13200 1300
1996 9450 2500 7790 10800 970
1997 8540 2400 7040 9810 1040
1998 8230 1730 4630 8300 909
1999 7900 1640 5880 6920 694
2000 7730 1840 8180 7840 721
2001 6390 1560 5470 11500 786
2002 5830 1510 6130 8230 721
2003 6640 1670 8530 7720 790
2004 10400 2550 11900 10000 1190
2005 8850 3200 12300 24900 1260
2006 10100 5610 19600 29900 1140
2007 15600 5680 29300 28200 1580
2008 18900 5330 16000 26600 2640
2009 14100 4040 11100 19500 1540
2010 20400 5740 16300 20200 1980
2011 25100 6490 16600 33900 1970
2012 20100 5750 12400 40200 1810
2013 16000 5240 10500 32600 1650
2014 15600 4830 11600 31100 1670
2015 11500 3890 8120 26000 1540
html {
font-family: sans-serif;
}
svg {
font-family: sans-serif;
}
.small-multiple {
display: inline-block;
}
.small-multiple + .small-multiple {
margin-left: 16px;
}
path {
fill: none;
stroke: black;
}
.chart .bg {
fill: #f4f7fa;
}
.chart .brush {
mix-blend-mode: multiply;
}
.tick line {
stroke: white
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment