block by tophtucker 170914f8196c4c0238d7ef1031eff129

Line chart scroller

Full Screen

(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…

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.

index.html

<!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>