|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
<style> |
|
|
|
* { |
|
box-sizing: border-box; |
|
} |
|
|
|
html, body { |
|
width: 100%; |
|
height: 100%; |
|
margin: 0; |
|
overflow: hidden; |
|
font-family: helvetica, sans-serif; |
|
} |
|
|
|
.table-wrapper { |
|
position: relative; |
|
height: 100%; |
|
padding-top: 20px; |
|
} |
|
|
|
.table-scroll { |
|
height: 100%; |
|
overflow: scroll; |
|
} |
|
|
|
table { |
|
margin-right: 0; |
|
margin-left: auto; |
|
} |
|
|
|
thead { |
|
background: white; |
|
} |
|
|
|
th > div { |
|
position: absolute; |
|
width: 4em; |
|
text-align: left; |
|
background: white; |
|
padding: 3px; |
|
top: 0; |
|
} |
|
|
|
td { |
|
padding: 3px; |
|
width: 4em; |
|
} |
|
|
|
td:last-child, |
|
th:last-child > div { |
|
text-align: right; |
|
} |
|
|
|
svg { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: calc(100% - 8.5em); |
|
height: 100%; |
|
pointer-events: none; |
|
} |
|
|
|
path { |
|
fill: none; |
|
stroke-width: 1; |
|
stroke: black; |
|
} |
|
|
|
</style> |
|
|
|
<body> |
|
|
|
<div class="table-wrapper"> |
|
<div class="table-scroll"> |
|
<table> |
|
<thead> |
|
<th><div>Ticker</div></th> |
|
<th><div>Return</div></th> |
|
</thead> |
|
<tbody></tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<svg></svg> |
|
|
|
</body> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
var data = d3.range(100).map(getRandomSeries) |
|
.sort((a,b) => b.price[b.price.length-1].value - a.price[a.price.length-1].value) |
|
|
|
var x = d3.scaleTime() |
|
.domain(d3.extent(d3.merge(data.map(d => d.price)).map(d => d.date))) |
|
.range([0, d3.select("svg").node().getBoundingClientRect().width]) |
|
|
|
var y = d3.scaleLinear() |
|
.domain(d3.extent(d3.merge(data.map(d => d.price)).map(d => d.value))) |
|
.range([innerHeight, 0]) |
|
|
|
var line = d3.line() |
|
.x(d => x(d.date)) |
|
.y(d => y(d.value)) |
|
|
|
var row = d3.select("tbody") |
|
.selectAll("tr") |
|
.data(data) |
|
.enter() |
|
.append("tr") |
|
.on("mouseenter", function(d,i) { |
|
render(i); |
|
}) |
|
|
|
row.append("td").text(d => d.ticker) |
|
row.append("td").text(d => d3.format(".0%")(d.price[d.price.length-1].value - 1)) |
|
|
|
var path = d3.select("svg") |
|
.selectAll("path") |
|
.data(data) |
|
.enter() |
|
.append("path") |
|
.attr("d", d => line(d.price)) |
|
|
|
var scrollScale = d3.scaleLinear() |
|
.domain([0,d3.select(".table-scroll").node().scrollHeight - (innerHeight - 20)]) |
|
.range([0,data.length-1]) |
|
|
|
var opacityScale = d3.scaleLinear() |
|
.domain([0,4]) |
|
.range([1,0]) |
|
.clamp(true) |
|
|
|
d3.select(".table-scroll").on("scroll", function() { |
|
render(Math.round(scrollScale(this.scrollTop))); |
|
}) |
|
|
|
function render(index) { |
|
row |
|
.style("opacity", (d,i) => opacityScale(Math.abs(index - i))) |
|
.style("font-weight", (d,i) => i == index ? 'bold' : 'normal') |
|
path |
|
.style("opacity", (d,i) => opacityScale(Math.abs(index - i))) |
|
.style("stroke-width", (d,i) => i == index ? 3 : 1) |
|
} |
|
|
|
function getRandomSeries() { |
|
return { |
|
ticker: getRandomTicker(), |
|
price: getRandomTimeSeries(100) |
|
} |
|
} |
|
|
|
function getRandomTicker() { |
|
var length = Math.ceil(Math.random()*4); |
|
var chars = 'abcdefghijklmnopqrstuvwxyz'; |
|
return d3.range(length).map(() => chars[Math.floor(Math.random()*chars.length)].toUpperCase()).join(''); |
|
} |
|
|
|
function getRandomTimeSeries(numPoints) { |
|
var data = d3.range(numPoints).map(d => ({ |
|
date: d3.interpolateDate(new Date("2000/01/01"), new Date("2016/10/01"))(d/numPoints), |
|
value: undefined |
|
})) |
|
data.forEach(function(d,i,arr) { |
|
if(i==0) { |
|
d.value = 1 |
|
} else { |
|
d.value = arr[i-1].value * d3.randomNormal(1, .02)() |
|
} |
|
}) |
|
return data |
|
} |
|
|
|
</script> |