Created
September 3, 2013 13:56
-
-
Save atbradley/6424297 to your computer and use it in GitHub Desktop.
Draw an interactive parallel coordinates plot using Paper.js.
This file contains hidden or 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
//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