Skip to content

Instantly share code, notes, and snippets.

@lorenzopub
Created July 18, 2017 06:57
Show Gist options
  • Save lorenzopub/1d86c5db1e1241a65bf7a8ba14717e1b to your computer and use it in GitHub Desktop.
Save lorenzopub/1d86c5db1e1241a65bf7a8ba14717e1b to your computer and use it in GitHub Desktop.
Line chart scroller II
license: mit
Bud1%  @� @� @� @ E%DSDB`� @� @� @
<!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>
(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;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment