Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active December 29, 2016 18:49
Show Gist options
  • Save tophtucker/170914f8196c4c0238d7ef1031eff129 to your computer and use it in GitHub Desktop.
Save tophtucker/170914f8196c4c0238d7ef1031eff129 to your computer and use it in GitHub Desktop.
Line chart scroller

(Scroll or mouse over table.)

It is hard to show a lot of lines at once on a line chart. It's especially hard when the lines are rough and jaggedy and nowhere-differentiable like, say, stock price charts (because in a true random walk it's impossible to disambiguate an intersection — neither line has any "momentum", so as your eye goes past, it's equiprobable that the lines crossed vs. just touched and bounced away). It gets basically illegible after, like, 3 paths. So here's an approach to scrolling through a set of time series inspired by @armollica's lovely 2D/3D scatterplot.

You can imagine a lot of fixes and variations...

  • Don't totally hide other table rows?
  • Fix jumpiness from scrolling-while-mousing?
  • Mouseovers in plot-space as well as table-space?
  • Position labels at path endpoints instead of as table rows?
  • Scroll through different dimensions?
  • Zoom and pan y-axis as you scroll?

I wanna try some kind of analogue to "adaptive resampling" (drawing cruder lines as you add lines to hold the total 'entropy' of the visualization constant, eventually degenerating to a slopegraph of arbitrarily many lines). Also some kind of abstract analogue to (or just application of?) van Wijk Smooth Zooming.

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment