block by tophtucker bfa88584f6855cd2af0fe8bc30a5264a

Line chart scroller II

Full Screen

Hehe

Revising this one

Using Visvalingam line simplification algorithm, hackily adapted from

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;
  width: 8em;
  margin: 0 0 0 auto;
}

.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%;
}

path {
  fill: none;
  stroke-width: 1;
  stroke: black;
}

rect {
  pointer-events: all;
  fill: none;
}

</style>

<body>

  <div class="table-wrapper">
    <div class="table-scroll">
      <table>
        <thead>
          <th><div>Name</div></th>
          <th><div>Value</div></th>
        </thead>
        <tbody></tbody>
      </table>
    </div>
  </div>

  <svg></svg>

</body>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="visvalingam.js"></script>
<script>

var format = d3.format("$.0f"),
    marginLeft = 40,
    indexThreshold = 4;

var data = d3.range(100).map(getRandomSeries)
  .sort((a,b) => b.data[b.data.length-1][1] - a.data[a.data.length-1][1])

var x = d3.scaleTime()
  .domain(d3.extent(d3.merge(data.map(d => d.data)).map(d => d[0])))
  .range([marginLeft, d3.select("svg").node().getBoundingClientRect().width])

var y = d3.scaleLinear()
  .domain(d3.extent(d3.merge(data.map(d => d.data)).map(d => d[1])))
  .range([innerHeight, 0])

// add a third entry to each array element with triangle area for simplify algo
data.forEach(d => {
  simplify(d.data)
})

var line = d3.line()
  .x(d => x(d[0]))
  .y(d => y(d[1]))

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.name)
row.append("td").text(d => format(d.data[d.data.length-1][1] - 1))

var yAxis = d3.axisLeft(y).tickFormat(format)
var yAxisG = d3.select("svg").append("g")
  .attr("class", "axis axis--y")
  .attr("transform", "translate(" + marginLeft + ",0)")
  .call(yAxis)

var path = d3.select("svg")
  .selectAll("path.line")
  .data(data)
  .enter()
  .append("path")
  .classed("line", true)
  .attr("d", d => line(d.data))

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, indexThreshold])
  .range([1, 0])
  .clamp(true)

var zoom = d3.zoom()
  .scaleExtent([1, data.length])
  .on("zoom", zoomed);

d3.select("svg")
  .append("rect")
  .attr("width", innerWidth)
  .attr("height", innerHeight)
  .call(zoom)

function zoomed() {
  indexThreshold = d3.event.transform.k
  opacityScale.domain([0, indexThreshold])
  render(lastIndex)
}


d3.select(".table-scroll").on("scroll", function() {
  render(Math.round(scrollScale(this.scrollTop)));
})

var simplifyAreaScale = d3.scaleLinear()
  .domain([3, data.length])
  .range(d3.extent(d3.merge(data.map(d => d.data)).map(d => d[2])))
  .clamp(true)

var lastIndex = 0;
function render(index) {
  lastIndex = 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)
    .each(d => d.simplified = d.data.filter((d,i,arr) => i == 0 || i == arr.length-1 || d[2] >= simplifyAreaScale(indexThreshold)))
    // .attr("d", d => line(d.simplified))

  y.domain(
    padExtent(
      d3.extent(
        d3.merge(
          data
            .filter((d,i) => Math.abs(index - i) < indexThreshold)
            .map(d => d.data)
        ).map(d => d[1])
      )
    )
  )

  // var t = d3.transition()
  //   .duration(250)
  //   .ease(d3.easeLinear)
  path
    // .transition(t)
    .attr("d", d => line(d.simplified))
  yAxisG
    // .transition(t)
    .call(yAxis)

}

function getRandomSeries() {
  return {
    name: getRandomTicker(),
    data: 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 => [
    d3.interpolateDate(new Date("2000/01/01"), new Date("2016/10/01"))(d/numPoints),
    undefined
  ])
  data.forEach(function(d,i,arr) {
    if(i==0) {
      d[1] = d3.randomNormal(75, 30)()
    } else {
      d[1] = arr[i-1][1] * d3.randomNormal(1, .02)()
    }
  })
  return data
}

function padExtent(extent) {
  var d = extent[1] - extent[0]
  return [
    extent[0] - d * .25,
    extent[1] + d * .25
  ]
}

</script>

visvalingam.js

(function() {

window.simplify = function(points) {

  var heap = minHeap(),
      maxArea = 0,
      triangle;

  var triangles = [];

  if (points.some(function(p) { return p == null; })) return null;

  for (var i = 1, n = points.length - 1; i < n; ++i) {
    triangle = points.slice(i - 1, i + 2);
    if (triangle[1][2] = area(triangle)) {
      triangles.push(triangle);
      heap.push(triangle);
    }
  }

  for (var i = 0, n = triangles.length; i < n; ++i) {
    triangle = triangles[i];
    triangle.previous = triangles[i - 1];
    triangle.next = triangles[i + 1];
  }

  while (triangle = heap.pop()) {

    // If the area of the current point is less than that of the previous point
    // to be eliminated, use the latter’s area instead. This ensures that the
    // current point cannot be eliminated without eliminating previously-
    // eliminated points.
    if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
    else maxArea = triangle[1][2];

    if (triangle.previous) {
      triangle.previous.next = triangle.next;
      triangle.previous[2] = triangle[2];
      update(triangle.previous);
    } else {
      triangle[0][2] = triangle[1][2];
    }

    if (triangle.next) {
      triangle.next.previous = triangle.previous;
      triangle.next[0] = triangle[0];
      update(triangle.next);
    } else {
      triangle[2][2] = triangle[1][2];
    }
  }

  function update(triangle) {
    heap.remove(triangle);
    triangle[1][2] = area(triangle);
    heap.push(triangle);
  }

  return points;
}

function compare(a, b) {
  return a[1][2] - b[1][2];
}

function area(t) {
  return Math.abs((t[0][0] - t[2][0]) * (t[1][1] - t[0][1]) - (t[0][0] - t[1][0]) * (t[2][1] - t[0][1]));
}

function minHeap() {
  var heap = {},
      array = [];

  heap.push = function() {
    for (var i = 0, n = arguments.length; i < n; ++i) {
      var object = arguments[i];
      up(object.index = array.push(object) - 1);
    }
    return array.length;
  };

  heap.pop = function() {
    var removed = array[0],
        object = array.pop();
    if (array.length) {
      array[object.index = 0] = object;
      down(0);
    }
    return removed;
  };

  heap.remove = function(removed) {
    var i = removed.index,
        object = array.pop();
    if (i !== array.length) {
      array[object.index = i] = object;
      (compare(object, removed) < 0 ? up : down)(i);
    }
    return i;
  };

  function up(i) {
    var object = array[i];
    while (i > 0) {
      var up = ((i + 1) >> 1) - 1,
          parent = array[up];
      if (compare(object, parent) >= 0) break;
      array[parent.index = i] = parent;
      array[object.index = i = up] = object;
    }
  }

  function down(i) {
    var object = array[i];
    while (true) {
      var right = (i + 1) << 1,
          left = right - 1,
          down = i,
          child = array[down];
      if (left < array.length && compare(array[left], child) < 0) child = array[down = left];
      if (right < array.length && compare(array[right], child) < 0) child = array[down = right];
      if (down === i) break;
      array[child.index = i] = child;
      array[object.index = i = down] = object;
    }
  }

  return heap;
}

})();