Created
October 10, 2017 14:57
-
-
Save peatroot/67b92b3874a5b7bb2788d41b4ab21fc5 to your computer and use it in GitHub Desktop.
Parallel lines filter with table
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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