Skip to content

Instantly share code, notes, and snippets.

@peatroot
Created October 10, 2017 14:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peatroot/67b92b3874a5b7bb2788d41b4ab21fc5 to your computer and use it in GitHub Desktop.
Save peatroot/67b92b3874a5b7bb2788d41b4ab21fc5 to your computer and use it in GitHub Desktop.
Parallel lines filter with table
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="filter"></div>
<pre class="table"></pre>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.js"></script>
<script type="text/javascript" src="render-queue.js"></script>
<script type="text/javascript" src="parallel-lines-filter.js"></script>
<script>
// data generation (uniform distribution of theta, phi - interestingly not uniform on sphere's surface)
var dimensions = [
{
key: 'name',
type: ParallelLinesFilter.types['String']
},
{
key: 'a',
type: ParallelLinesFilter.types['Number']
},
{
key: 'b',
type: ParallelLinesFilter.types['Number']
},
{
key: 'c',
type: ParallelLinesFilter.types['Number']
},
{
key: 'd',
type: ParallelLinesFilter.types['Number']
}
];
var data = [];
for (var i = 0; i < 81; i++) {
data.push({
name: 'abc'.split('')[i % 3] + 'defg'.split('')[i % 4] + 'hijkl'.split('')[i % 5] + 'mnopqrs'.split('')[i % 7],
a: Math.random() * 360,
b: (Math.random() - 0.5) * 180,
c: 2 ^ (Math.random() * 5),
d: Math.log(Math.random() * 81 + 1)
})
}
// dimension extents
dimensions.forEach(function (dim) {
if (!('domain' in dim)) {
dim.domain = ParallelLinesFilter.d3Functor(dim.type.extent) (data.map(function (d) {
return d[dim.key];
}))
}
if (!('scale' in dim)) {
dim.scale = dim.type.defaultScale.copy();
}
dim.scale.domain(dim.domain);
})
// get the table
var output = d3.select('pre.table');
// table update callback
function onFilterUpdate (selected) {
output.text(d3.tsvFormat(selected.slice(0, 24)));
}
// create the parallel filter
var parallel = ParallelLinesFilter({parent: d3.select('.filter')});
parallel.data(data)
.dimensions(dimensions)
.margins({top: 20, bottom: 20, left: 40, right: 20})
.width(600)
.height(300)
.profileColour('green')
.onFilter(onFilterUpdate);
// init
parallel.render();
output.text(d3.tsvFormat(data.slice(0, 24)));
</script>
</body>
</html>
function getSet (chart, state, prop, post) {
chart[prop] = function (value) {
if (!arguments.length) return state[prop];
state[prop] = value;
if (post) chart[post]();
return chart;
}
}
function ParallelLinesFilter (opts) {
var chart = {};
var xScale = d3.scalePoint();
var yAxis = d3.axisLeft();
var parent = opts.parent;
var container
var svg;
var canvas;
var ctx;
var renderer;
var gMain;
var gAxes;
var state = {
margins: {top: 50, right: 50, bottom: 50, left: 150},
width: 800,
height: 300,
data: [],
dimensions: [],
profileColour: 'red',
onFilter: function () {}
};
for (prop in state) {
getSet(chart, state, prop);
}
function draw (d) {
var coords = project(d)
ctx.strokeStyle = state.profileColour;
ctx.beginPath()
coords.forEach(function (p, i) {
if (p === null) {
if (i > 0) {
var prev = coords[i - 1];
if (prev !== null) {
ctx.moveTo(prev[0], prev[1]);
ctx.lineTo(prev[0] + 6, prev[1]);
}
}
if (i < coords.length - 1) {
var next = coords[i + 1];
if (next !== null) {
ctx.moveTo(next[0] - 6, next[1]);
}
}
return;
}
if (i === 0) {
ctx.moveTo(p[0], p[1]);
return;
}
ctx.lineTo(p[0], p[1]);
});
ctx.stroke();
}
function dataAreaWidth () {
return state.width - state.margins.left - state.margins.right;
}
function dataAreaHeight () {
return state.height - state.margins.top - state.margins.bottom;
}
function project (d) {
return state.dimensions.map(function (p, i) {
if (!(p.key in d) || (d[p.key] === null)) {
return null;
}
return [xScale(i), p.scale(d[p.key])];
});
}
function renderSvg () {
}
function renderCanvas () {
}
function renderAxes () {
var axes = gAxes.selectAll('.axis')
.data(state.dimensions)
.enter().append('g')
.attr('class', function (d) {
return 'axis ' + d.key.replace(/ /g, '_');
})
.attr('transform', function (d, i) {
return 'translate(' + xScale(i) + ',0)';
});
axes.append('g')
.each(function (d) {
var renderAxis = ('axis' in d) ? d.axis.scale(d.scale) : yAxis.scale(d.scale);
d3.select(this).call(renderAxis);
})
.append('text')
.attr('class', 'title')
.attr('text-anchor', 'start')
.text(function (d) {return ('description' in d) ? d.description : d.key;})
axes.append('g')
.attr('class', 'brush')
.each(function (d) {
d3.select(this).call(d.brush = d3.brushY()
.extent([[-10, 0], [10, dataAreaHeight()]])
.on('start', brushstart)
.on('brush', brush)
.on('end', brush));
})
.selectAll('rect')
.attr('x', -8)
.attr('width', 16);
}
function brushstart () {
d3.event.sourceEvent.stopPropagation();
}
function brush () {
renderer.invalidate();
var actives = [];
svg.selectAll('.axis .brush')
.filter(function (d) {
return d3.brushSelection(this);
})
.each(function (d) {
actives.push({
dimension: d,
extent: d3.brushSelection(this)
});
});
var selected = state.data.filter(function (d) {
if (actives.every(function (active) {
var dim = active.dimension;
return dim.type.within(d[dim.key], active.extent, dim);
})) {
return true;
}
});
ctx.clearRect(0, 0, state.width, state.height);
ctx.globalAlpha = d3.min([0.85/Math.pow(state.data.length,0.3),1]);
renderer(selected);
state.onFilter(selected);
}
chart.render = function () {
xScale.domain(d3.range(state.dimensions.length))
.range([0, dataAreaWidth()]);
state.dimensions.forEach(function (dim) {
dim.scale.range([dataAreaHeight() - 2, 0]);
})
container = parent.select('div.parallel-lines-filter');
if (container.empty()) {
container = parent.append('div')
.attr('width', state.width)
.attr('height', state.height)
.classed('parallel-lines-filter', true);
}
svg = container.select('svg');
if (svg.empty()) {
svg = container.append('svg')
.attr('width', state.width)
.attr('height', state.height);
}
canvas = container.select('canvas');
if (canvas.empty()) {
canvas = container.append('canvas')
.attr('width', dataAreaWidth())
.attr('height', dataAreaHeight())
.style('width', dataAreaWidth() + 'px')
.style('height', dataAreaHeight() + 'px')
.style('margin-top', state.margins.top + 'px')
.style('margin-left', state.margins.left + 'px');
}
ctx = canvas.node().getContext('2d');
// ctx.globalCompositeOperation = 'darken';
// ctx.globalAlpha = 0.15;
// ctx.globalAlpha = 0.95;
// ctx.lineWidth = 1.5;
ctx.lineWidth = 3;
renderer = renderQueue(draw).rate(50);
ctx.clearRect(0, 0, state.width, state.height);
ctx.globalAlpha = d3.min([0.85/Math.pow(state.data.length,0.3),1]);
renderer(state.data);
gAxes = svg.select('g.axes')
if (gAxes.empty()) {
gAxes = svg.append('g')
.classed('axes', true);
}
gAxes.attr('transform', 'translate(' + state.margins.left + ',' + state.margins.top + ')');
renderAxes ()
}
return chart;
}
ParallelLinesFilter.d3Functor = function (v) {
return typeof v === 'function' ? v : function() { return v; };
};
ParallelLinesFilter.types = {
Number: {
key: 'Number',
extent: d3.extent,
within: function (d, extent, dim) {
return (extent[0] <= dim.scale(d)) && (dim.scale(d) < extent[1]);
},
defaultScale: d3.scaleLinear()
},
String: {
key: 'String',
extent: function (data) {
return data.sort();
},
within: function(d, extent, dim) {
return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1];
},
defaultScale: d3.scalePoint()
}
};
/*
* See https://gist.github.com/syntagmatic/3341641
*/
var renderQueue = (function(func) {
var _queue = [], // data to be rendered
_rate = 1000, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
var rq = function(data) {
if (data) rq.data(data);
_invalidate();
_clear();
rq.render();
};
rq.render = function() {
var valid = true;
_invalidate = rq.invalidate = function() {
valid = false;
};
function doFrame() {
if (!valid) return true;
var chunk = _queue.splice(0,_rate);
chunk.map(func);
timer_frame(doFrame);
}
doFrame();
};
rq.data = function(data) {
_invalidate();
_queue = data.slice(0); // creates a copy of the data
return rq;
};
rq.add = function(data) {
_queue = _queue.concat(data);
};
rq.rate = function(value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function() {
return _queue.length;
};
// clear the canvas
rq.clear = function(func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 17); };
return rq;
});
body {
min-width: 760px;
}
.parallel-lines-filter {
display: block;
}
.parallel-lines-filter svg,
.parallel-lines-filter canvas {
font: 10px sans-serif;
position: absolute;
}
.parallel-lines-filter canvas {
opacity: 0.9;
pointer-events: none;
}
.axis .title {
font-size: 10px;
transform: rotate(-21deg) translate(-5px,-6px);
fill: #222;
}
.axis line,
.axis path {
fill: none;
stroke: #eee;
stroke-width: 1px;
}
.axis .tick text {
fill: #222;
opacity: 0;
pointer-events: none;
}
.axis.id .tick text {
opacity: 0.2;
font-size: 6px;
pointer-events: all;
}
.axis.id .tick:hover text {
opacity: 1;
font-size: 10px;
}
.axis.id .tick:nth-of-type() text {
opacity: 1;
font-size: 10px;
}
.axis:hover line,
.axis:hover path,
.axis.active line,
.axis.active path {
fill: none;
stroke: #222;
stroke-width: 1px;
}
.axis:hover .title {
font-weight: bold;
}
.axis:hover .tick text {
opacity: 1;
}
.axis.active .title {
font-weight: bold;
}
.axis.active .tick text {
opacity: 1;
font-weight: bold;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
stroke-width: 1px;
}
pre {
width: 100%;
height: 300px;
margin: 6px 12px;
tab-size: 40;
font-size: 10px;
overflow: auto;
}
.filter {
min-height: 400px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment