Skip to content

Instantly share code, notes, and snippets.

@atbradley
Created September 3, 2013 13:56
Show Gist options
  • Save atbradley/6424297 to your computer and use it in GitHub Desktop.
Save atbradley/6424297 to your computer and use it in GitHub Desktop.
Draw an interactive parallel coordinates plot using Paper.js.
//Warning: Ugly code; first project with paper.js.
var lblHeight = 0;
var marX = 30;
var marY = 10;
var marDesc = 3;
var labels = [];
var dataurl = '/data/states.json';
//var dataurl = 'data/mtcars.json';
var linesLayer;
var detailLayer;
var plotArea;
var pointX = {};
var selRect = new Path();
var descBoxes = [];
selStyle = {
strokeColor: "#999999",
strokeWidth: 1,
dashArray: [3, 3]
}
var nextHue = 0;
function nextColor() {
outp = new Color({
hue: nextHue,
saturation: .7,
brightness: 1,
});
nextHue += 30; nextHue = ( nextHue > 360 ) ? 0 : nextHue;
return outp;
}
var labelStyle = {
font: "sans-serif",
fontSize: 14,
fillColor: "#666666"
};
var gridStyle = {
strokeWidth: 3,
strokeColor: 'black',
};
var lineStyles = {
normal: {
strokeColor: '#999999',
strokeWidth: 1,
},
highlighted: {
strokeColor: 'darkorange',
},
selected: {
strokeColor: 'yellow',
},
}
//I suspect this could be improved.
function normalizeData(data) {
var max = [];
var min = [];
var range = [];
for ( ob in data ) {
for ( v in data[ob] ) {
obs = data[ob];
if ( !(v in max) ) max[v] = obs[v];
if ( !(v in min) ) min[v] = obs[v];
max[v] = Math.max(max[v], obs[v]);
min[v] = Math.min(min[v], obs[v]);
}
}
var outp = {};
for ( v in max ) {
range[v] = max[v] - min[v];
}
for ( ob in data ) {
var obsv = {};
for ( v in data[ob] ) {
obsv[v] = (data[ob][v] - min[v]) / range[v];
}
outp[ob] = obsv;
}
//for ( x in data ) for ( y in data[x] ) console.log( 'normalizing', x, y, data[x][y] );
//for ( x in outp ) for ( y in outp[x] ) console.log( 'normalized', x, y, outp[x][y] );
return outp;
}
function setUpLabels(headers) {
lblLayer = new Layer({ name: 'labels', });
for (header in headers) {
var lbl = new PointText({
content: header,
});
lbl.style = labelStyle;
lblHeight = Math.max(lblHeight, lbl.bounds.width);
labels.push(lbl);
}
plotWidth = view.size.width-(marX*2);
lblDist = (plotWidth-lbl.bounds.height)/(labels.length-1);
lblLoc = marX;
pY = view.size.height - marY - lblHeight;
for ( lbl in labels ) {
label = labels[lbl];
tl = label.bounds.topLeft - label.position;
label.position = new Point(lblLoc, pY) + tl;
label.rotate(-90, label.bounds.topRight);
lblLoc += lblDist;
pointX[label.content] = label.position.x;
}
//Draw gridlines.
gridsLayer = new Layer({name: 'grids'});
tl = new Point(marX, marY);
br = new Point(labels[labels.length-1].bounds.topRight.x + 5,
labels[0].bounds.topLeft.y - 10);
plotArea = new Rectangle(tl, br);
gridLines = new Path(plotArea.bottomLeft, plotArea.bottomRight);
gridLines.style = gridStyle;
}
function drawLines(data) {
normdata = normalizeData(data);
//for ( x in data ) for ( y in data[x] ) console.log( x, y, data[x][y] );
linesLayer = new Layer({name: 'lines'});
for ( obs in normdata ) {
var line = new Path();
line.style = lineStyles.normal;
//TODO: add metadata to line.data.
for ( v in normdata[obs] ) {
lX = pointX[v];
lY = plotArea.top + ((1-normdata[obs][v]) * plotArea.height);
line.add([lX, lY]);
//console.log(obs, v, data[obs][v], normdata[obs][v]);
line.data[v] = data[obs][v];
}
line.name = obs;
line.onClick = function(ev) {
selectLines([this]);
}
}
detailLayer = new Layer({name: 'details'});
}
$.getJSON(dataurl, function(data) {
//for ( x in data ) for ( y in data[x] ) console.log( x, y, data[x][y] );
for ( x in data ) {
headers = data[x];
break;
}
//for ( x in data ) for ( y in data[x] ) console.log( x, y, data[x][y] );
setUpLabels(headers);
drawLines(data);
});
//TODO: Improve detail placement.
function selectLines(lns) {
detailLayer.removeChildren();
for ( line in linesLayer.children ) {
linesLayer.children[line].style = lineStyles.normal;
}
if ( lns.length <= 3 ) {
var tX = marX;
for ( x in lns ) {
var thisColor = nextColor();
lns[x].strokeColor = thisColor;
textTitle = new PointText({
content: lns[x].name,
fontSize: 14,
fillColor: '#666666',
fontWeight: 'bold',
});
textCont = "";
for ( v in lns[x].data ) {
textCont += "\n"+v+": "+lns[x].data[v];
}
var textDetails = new PointText({
content: textCont,
fontSize: 12,
fillColor: 'black',
});
descSize = new Size(
Math.max(textTitle.bounds.width, textDetails.bounds.width)+marDesc*2,
textTitle.bounds.height + textDetails.bounds.height)+marDesc;
var tl = new Point(tX, 20);
console.log(tl);
textTitle.point = tl;
newBox = new Path.Rectangle(textTitle.bounds.topLeft-[marDesc, marDesc], descSize);
newBox.style = {
fillColor: 'white',
strokeJoin: 'round',
strokeWidth: 2,
}
newBox.strokeColor = thisColor;
tl = tl + [marDesc, marDesc];
textDetails.point = textTitle.bounds.bottomLeft + [0,marDesc];
textTitle.bringToFront();
textDetails.bringToFront();
tX = newBox.bounds.topRight.x + marDesc*2;
}
} else {
var color = nextColor();
for ( x in lns ) {
lns[x].strokeColor = color;
}
}
for ( x in lns ) {
lns[x].bringToFront();
console.log(lns[x].name, lns[x].data);
}
}
function onMouseDown(ev) {
selectLines([]);
}
//TODO: Show a bounding box.
function onMouseDrag(ev) {
//selRect.remove();
var selRect = new Path.Rectangle(ev.downPoint, ev.lastPoint);
selRect.style = selStyle;
selRect.removeOnDrag();
selRect.removeOnUp();
}
//TODO: Identify lines in the bounding box and selectLines() them.
function onMouseUp(ev) {
var lines = [];
if ( ev.delta.length < 5 ) return false;
selector = new Path.Rectangle(ev.point, ev.lastPoint);
for (var l = 0; l < linesLayer.children.length; l++) {
if ( linesLayer.children[l].getIntersections(selector).length > 0 ) {
lines.push(linesLayer.children[l]);
}
}
selector.remove();
selectLines(lines);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment