Skip to content

Instantly share code, notes, and snippets.

@lee1043
Forked from mostaphaRoudsari/.block
Last active October 15, 2018 20:10
Show Gist options
  • Save lee1043/81a6ba6464d20213e89bbf8f13b68288 to your computer and use it in GitHub Desktop.
Save lee1043/81a6ba6464d20213e89bbf8f13b68288 to your computer and use it in GitHub Desktop.
Parallel Coordinates with mouseover highlight and tooltip (ENSO)
license: mit
/* unvisited link */
a:link {
//color: lightgreen;
//color: blue;
color: steelblue;
}
/* visited link */
a:visited {
color: steelblue;
//color: cyan;
//color: blue;
}
body {
//background-color: #26292E;
background-color: white;
font-family: 'droid_sans', sans-serif;
font-size: 12px;
font-weight: normal;
line-height: 28px;
width: 1700px;
}
text {
//fill: white;
//fill: black;
}
.group:after {
content: "";
display: table;
clear: both;
}
/* Document styling */
#wrapper {
position: relative;
float: left;
top: 20px;
}
#title {
font-size: 24px;
font-weight: 100;
//color: white;
color: black;
}
#title div.large, #title span.large {
font-size: 36px;
}
#footer {
font-size: 14px;
//color: #666;
//color: white;
//color: darkgrey;
width: 80%;
}
#desc {
position: relative;
//float:right;
z-index:-999;
//left: 400px;
//width: 400px;
}
button {
background: none repeat scroll 0 0 #3C3F45;
//color: #23262C;
color: white;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: 500;
margin-top: 30px;
padding: 5px 40px;
text-align: center;
transition: background 0.1s linear 0s, color 0.1s linear 0s;
vertical-align: middle;
width: auto;
border: 0;
}
button:hover {
background: none repeat scroll 0 0 #DE5E60;
color: #FFFFFF;
}
/* data table styles */
//#grid { height: 198px; color:white; clear: both; position:relative; top: 60px; left: 15px }
#grid { height: 198px; color:black; clear: both; position:relative; top: 60px; left: 15px }
.row, .header { clear: left; font-size: 12px; line-height: 18px; height: 15px; margin: 5px; width:1700px }
//.row:hover {text-shadow: -1px 1px 8px #ffc, 1px -1px 8px #fff;}
.row:hover {text-shadow: -1px 1px 8px black, 1px -1px 8px #ffc;}
.row:nth-child(odd) { background: rgba(0,0,0,0.05); } // different background color for odd rows
.header { font-weight: bold; }
.cell { float: left; overflow: hidden; white-space: nowrap; width: 120px; height: 22px; }
.col-0 { float: left; width: 120px; }
.parcoords > svg, .parcoords > canvas {
/*font: 14px sans-serif;*/
position: absolute;
}
.parcoords > canvas {
pointer-events: none;
}
.parcoords rect.background {
fill: transparent;
}
.parcoords rect.background:hover {
fill: rgba(120,120,120,0.2);
}
.parcoords .resize rect {
fill: rgba(0,0,0,0.1);
}
.parcoords rect.extent {
fill: rgba(255,255,255,0.25);
stroke: rgba(0,0,0,0.6);
}
.parcoords .axis line, .parcoords .axis path {
fill: none;
stroke: #222;
shape-rendering: crispEdges;
}
.parcoords canvas {
opacity: 1;
-moz-transition: opacity 0.3s;
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
}
.parcoords canvas.faded {
opacity: 0.25;
}
d3.parcoords = function(config) {
var __ = {
data: [],
highlighted: [],
dimensions: [],
dimensionTitles: {},
dimensionTitleRotation: 0,
types: {},
brushed: false,
mode: "default",
rate: 20,
width: 600,
height: 300,
margin: { top: 30, right: 0, bottom: 12, left: 0 },
color: "#069",
composite: "source-over",
alpha: 0.7,
bundlingStrength: 0.5,
bundleDimension: null,
smoothness: 0.25,
showControlPoints: false,
hideAxis : []
};
extend(__, config);
var pc = function(selection) {
selection = pc.selection = d3.select(selection);
__.width = selection[0][0].clientWidth;
__.height = selection[0][0].clientHeight;
// canvas data layers
["shadows", "marks", "foreground", "highlight"].forEach(function(layer) {
canvas[layer] = selection
.append("canvas")
.attr("class", layer)[0][0];
ctx[layer] = canvas[layer].getContext("2d");
});
// svg tick and brush layers
pc.svg = selection
.append("svg")
.attr("width", __.width)
.attr("height", __.height)
.append("svg:g")
.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
return pc;
};
var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "axesreorder"].concat(d3.keys(__))),
w = function() { return __.width - __.margin.right - __.margin.left; },
h = function() { return __.height - __.margin.top - __.margin.bottom; },
flags = {
brushable: false,
reorderable: false,
axes: false,
interactive: false,
shadows: false,
debug: false
},
xscale = d3.scale.ordinal(),
yscale = {},
dragging = {},
line = d3.svg.line(),
axis = d3.svg.axis().orient("left").ticks(5),
g, // groups for axes, brushes
ctx = {},
canvas = {},
clusterCentroids = [];
// side effects for setters
var side_effects = d3.dispatch.apply(this,d3.keys(__))
.on("composite", function(d) { ctx.foreground.globalCompositeOperation = d.value; })
.on("alpha", function(d) { ctx.foreground.globalAlpha = d.value; })
.on("width", function(d) { pc.resize(); })
.on("height", function(d) { pc.resize(); })
.on("margin", function(d) { pc.resize(); })
.on("rate", function(d) { rqueue.rate(d.value); })
.on("data", function(d) {
if (flags.shadows){paths(__.data, ctx.shadows);}
})
.on("dimensions", function(d) {
xscale.domain(__.dimensions);
if (flags.interactive){pc.render().updateAxes();}
})
.on("bundleDimension", function(d) {
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
if (typeof d.value === "number") {
if (d.value < __.dimensions.length) {
__.bundleDimension = __.dimensions[d.value];
} else if (d.value < __.hideAxis.length) {
__.bundleDimension = __.hideAxis[d.value];
}
} else {
__.bundleDimension = d.value;
}
__.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
})
.on("hideAxis", function(d) {
if (!__.dimensions.length) pc.detectDimensions();
pc.dimensions(without(__.dimensions, d.value));
});
// expose the state of the chart
pc.state = __;
pc.flags = flags;
// create getter/setters
getset(pc, __, events);
// expose events
d3.rebind(pc, events, "on");
// tick formatting
d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
// getter/setter with event firing
function getset(obj,state,events) {
d3.keys(state).forEach(function(key) {
obj[key] = function(x) {
if (!arguments.length) {
return state[key];
}
var old = state[key];
state[key] = x;
side_effects[key].call(pc,{"value": x, "previous": old});
events[key].call(pc,{"value": x, "previous": old});
return obj;
};
});
};
function extend(target, source) {
for (key in source) {
target[key] = source[key];
}
return target;
};
function without(arr, item) {
return arr.filter(function(elem) { return item.indexOf(elem) === -1; })
};
pc.autoscale = function() {
// yscale
var defaultScales = {
"date": function(k) {
return d3.time.scale()
.domain(d3.extent(__.data, function(d) {
return d[k] ? d[k].getTime() : null;
}))
.range([h()+1, 1]);
},
"number": function(k) {
return d3.scale.linear()
.domain(d3.extent(__.data, function(d) { return +d[k]; }))
.range([h()+1, 1]);
},
"string": function(k) {
var counts = {},
domain = [];
// Let's get the count for each value so that we can sort the domain based
// on the number of items for each value.
__.data.map(function(p) {
if (counts[p[k]] === undefined) {
counts[p[k]] = 1;
} else {
counts[p[k]] = counts[p[k]] + 1;
}
});
domain = Object.getOwnPropertyNames(counts).sort(function(a, b) {
return counts[a] - counts[b];
});
return d3.scale.ordinal()
.domain(domain)
.rangePoints([h()+1, 1]);
}
};
__.dimensions.forEach(function(k) {
yscale[k] = defaultScales[__.types[k]](k);
});
__.hideAxis.forEach(function(k) {
yscale[k] = defaultScales[__.types[k]](k);
});
// hack to remove ordinal dimensions with many values
pc.dimensions(pc.dimensions().filter(function(p,i) {
var uniques = yscale[p].domain().length;
if (__.types[p] == "string" && (uniques > 60 || uniques < 2)) {
return false;
}
return true;
}));
// xscale
xscale.rangePoints([0, w()], 1);
// canvas sizes
pc.selection.selectAll("canvas")
.style("margin-top", __.margin.top + "px")
.style("margin-left", __.margin.left + "px")
.attr("width", w()+2)
.attr("height", h()+2);
// default styles, needs to be set when canvas width changes
ctx.foreground.strokeStyle = __.color;
ctx.foreground.lineWidth = 1.4;
ctx.foreground.globalCompositeOperation = __.composite;
ctx.foreground.globalAlpha = __.alpha;
ctx.highlight.lineWidth = 3;
ctx.shadows.strokeStyle = "#dadada";
return this;
};
pc.scale = function(d, domain) {
yscale[d].domain(domain);
return this;
};
pc.flip = function(d) {
//yscale[d].domain().reverse(); // does not work
yscale[d].domain(yscale[d].domain().reverse()); // works
return this;
};
pc.commonScale = function(global, type) {
var t = type || "number";
if (typeof global === 'undefined') {
global = true;
}
// scales of the same type
var scales = __.dimensions.concat(__.hideAxis).filter(function(p) {
return __.types[p] == t;
});
if (global) {
var extent = d3.extent(scales.map(function(p,i) {
return yscale[p].domain();
}).reduce(function(a,b) {
return a.concat(b);
}));
scales.forEach(function(d) {
yscale[d].domain(extent);
});
} else {
scales.forEach(function(k) {
yscale[k].domain(d3.extent(__.data, function(d) { return +d[k]; }));
});
}
// update centroids
if (__.bundleDimension !== null) {
pc.bundleDimension(__.bundleDimension);
}
return this;
};pc.detectDimensions = function() {
pc.types(pc.detectDimensionTypes(__.data));
pc.dimensions(d3.keys(pc.types()));
return this;
};
// a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
pc.toType = function(v) {
return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
};
// try to coerce to number before returning type
pc.toTypeCoerceNumbers = function(v) {
if ((parseFloat(v) == v) && (v != null)) {
return "number";
}
return pc.toType(v);
};
// attempt to determine types of each dimension based on first row of data
pc.detectDimensionTypes = function(data) {
var types = {};
d3.keys(data[0])
.forEach(function(col) {
types[col] = pc.toTypeCoerceNumbers(data[0][col]);
});
return types;
};
pc.render = function() {
// try to autodetect dimensions and create scales
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
pc.render[__.mode]();
events.render.call(this);
return this;
};
pc.render['default'] = function() {
pc.clear('foreground');
if (__.brushed) {
__.brushed.forEach(path_foreground);
__.highlighted.forEach(path_highlight);
} else {
__.data.forEach(path_foreground);
__.highlighted.forEach(path_highlight);
}
};
var rqueue = d3.renderQueue(path_foreground)
.rate(50)
.clear(function() {
pc.clear('foreground');
pc.clear('highlight');
});
pc.render.queue = function() {
if (__.brushed) {
rqueue(__.brushed);
__.highlighted.forEach(path_highlight);
} else {
rqueue(__.data);
__.highlighted.forEach(path_highlight);
}
};
function compute_cluster_centroids(d) {
var clusterCentroids = d3.map();
var clusterCounts = d3.map();
// determine clusterCounts
__.data.forEach(function(row) {
var scaled = yscale[d](row[d]);
if (!clusterCounts.has(scaled)) {
clusterCounts.set(scaled, 0);
}
var count = clusterCounts.get(scaled);
clusterCounts.set(scaled, count + 1);
});
__.data.forEach(function(row) {
__.dimensions.map(function(p, i) {
var scaled = yscale[d](row[d]);
if (!clusterCentroids.has(scaled)) {
var map = d3.map();
clusterCentroids.set(scaled, map);
}
if (!clusterCentroids.get(scaled).has(p)) {
clusterCentroids.get(scaled).set(p, 0);
}
var value = clusterCentroids.get(scaled).get(p);
value += yscale[p](row[p]) / clusterCounts.get(scaled);
clusterCentroids.get(scaled).set(p, value);
});
});
return clusterCentroids;
}
function compute_centroids(row) {
var centroids = [];
var p = __.dimensions;
var cols = p.length;
var a = 0.5; // center between axes
for (var i = 0; i < cols; ++i) {
// centroids on 'real' axes
var x = position(p[i]);
var y = yscale[p[i]](row[p[i]]);
centroids.push([x, y]);
//centroids.push($V([x, y]));
// centroids on 'virtual' axes
if (i < cols - 1) {
var cx = x + a * (position(p[i+1]) - x);
var cy = y + a * (yscale[p[i+1]](row[p[i+1]]) - y);
if (__.bundleDimension !== null) {
var leftCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i]);
var rightCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i+1]);
var centroid = 0.5 * (leftCentroid + rightCentroid);
cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
}
centroids.push([cx, cy]);
//centroids.push($V([cx, cy]));
}
}
return centroids;
}
pc.compute_centroids = compute_centroids;
function compute_control_points(centroids) {
var cols = centroids.length;
var a = __.smoothness;
var cps = [];
cps.push(centroids[0]);
cps.push($V([centroids[0].e(1) + a*2*(centroids[1].e(1)-centroids[0].e(1)), centroids[0].e(2)]));
for (var col = 1; col < cols - 1; ++col) {
var mid = centroids[col];
var left = centroids[col - 1];
var right = centroids[col + 1];
var diff = left.subtract(right);
cps.push(mid.add(diff.x(a)));
cps.push(mid);
cps.push(mid.subtract(diff.x(a)));
}
cps.push($V([centroids[cols-1].e(1) + a*2*(centroids[cols-2].e(1)-centroids[cols-1].e(1)), centroids[cols-1].e(2)]));
cps.push(centroids[cols - 1]);
return cps;
};pc.shadows = function() {
flags.shadows = true;
if (__.data.length > 0) {
paths(__.data, ctx.shadows);
}
return this;
};
// draw little dots on the axis line where data intersects
pc.axisDots = function() {
var ctx = pc.ctx.marks;
ctx.globalAlpha = d3.min([ 1 / Math.pow(data.length, 1 / 2), 1 ]);
__.data.forEach(function(d) {
__.dimensions.map(function(p, i) {
ctx.fillRect(position(p) - 0.75, yscale[p](d[p]) - 0.75, 1.5, 1.5);
});
});
return this;
};
// draw single cubic bezier curve
function single_curve(d, ctx) {
var centroids = compute_centroids(d);
var cps = compute_control_points(centroids);
ctx.moveTo(cps[0].e(1), cps[0].e(2));
for (var i = 1; i < cps.length; i += 3) {
if (__.showControlPoints) {
for (var j = 0; j < 3; j++) {
ctx.fillRect(cps[i+j].e(1), cps[i+j].e(2), 2, 2);
}
}
ctx.bezierCurveTo(cps[i].e(1), cps[i].e(2), cps[i+1].e(1), cps[i+1].e(2), cps[i+2].e(1), cps[i+2].e(2));
}
};
// draw single polyline
function color_path(d, i, ctx) {
ctx.strokeStyle = d3.functor(__.color)(d, i);
ctx.beginPath();
if (__.bundleDimension === null || (__.bundlingStrength === 0 && __.smoothness == 0)) {
single_path(d, ctx);
} else {
single_curve(d, ctx);
}
ctx.stroke();
};
// draw many polylines of the same color
function paths(data, ctx) {
ctx.clearRect(-1, -1, w() + 2, h() + 2);
ctx.beginPath();
data.forEach(function(d) {
if (__.bundleDimension === null || (__.bundlingStrength === 0 && __.smoothness == 0)) {
single_path(d, ctx);
} else {
single_curve(d, ctx);
}
});
ctx.stroke();
};
function single_path(d, ctx) {
__.dimensions.map(function(p, i) {
if (i == 0) {
ctx.moveTo(position(p), yscale[p](d[p]));
} else {
ctx.lineTo(position(p), yscale[p](d[p]));
}
});
}
function path_foreground(d, i) {
return color_path(d, i, ctx.foreground);
};
function path_highlight(d, i) {
return color_path(d, i, ctx.highlight);
};
pc.clear = function(layer) {
ctx[layer].clearRect(0,0,w()+2,h()+2);
return this;
};
function flipAxisAndUpdatePCP(dimension, i) {
var g = pc.svg.selectAll(".dimension");
pc.flip(dimension);
d3.select(g[0][i])
.transition()
.duration(1100)
.call(axis.scale(yscale[dimension]));
pc.render();
if (flags.shadows) paths(__.data, ctx.shadows);
}
function rotateLabels() {
var delta = d3.event.deltaY;
delta = delta < 0 ? -5 : delta;
delta = delta > 0 ? 5 : delta;
__.dimensionTitleRotation += delta;
pc.svg.selectAll("text.label")
.attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
d3.event.preventDefault();
}
pc.createAxes = function() {
if (g) pc.removeAxes();
// Add a group element for each dimension.
g = pc.svg.selectAll(".dimension")
.data(__.dimensions, function(d) { return d; })
.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + xscale(d) + ")"; });
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.attr("transform", "translate(0,0)")
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
.append("svg:text")
.attr({
"text-anchor": "middle",
"y": 0,
"transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
"x": 0,
"class": "label"
})
.text(function(d) {
return d in __.dimensionTitles ? __.dimensionTitles[d] : d; // dimension display names
})
.on("dblclick", flipAxisAndUpdatePCP)
.on("wheel", rotateLabels);
flags.axes= true;
return this;
};
pc.removeAxes = function() {
g.remove();
return this;
};
pc.updateAxes = function() {
var g_data = pc.svg.selectAll(".dimension").data(__.dimensions);
// Enter
g_data.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
.style("opacity", 0)
.append("svg:g")
.attr("class", "axis")
.attr("transform", "translate(0,0)")
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
.append("svg:text")
.attr({
"text-anchor": "middle",
"y": 0,
"transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
"x": 0,
"class": "label"
})
.text(String)
.on("dblclick", flipAxisAndUpdatePCP)
.on("wheel", rotateLabels);
// Update
g_data.attr("opacity", 0);
g_data.select(".axis")
.transition()
.duration(1100)
.each(function(d) {
d3.select(this).call(axis.scale(yscale[d]));
});
g_data.select(".label")
.transition()
.duration(1100)
.text(String)
.attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
// Exit
g_data.exit().remove();
g = pc.svg.selectAll(".dimension");
g.transition().duration(1100)
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
.style("opacity", 1);
pc.svg.selectAll(".axis")
.transition()
.duration(1100)
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); });
if (flags.shadows) paths(__.data, ctx.shadows);
if (flags.brushable) pc.brushable();
if (flags.reorderable) pc.reorderable();
if (pc.brushMode() !== "None") {
var mode = pc.brushMode();
pc.brushMode("None");
pc.brushMode(mode);
}
return this;
};
// Jason Davies, http://bl.ocks.org/1341281
pc.reorderable = function() {
if (!g) pc.createAxes();
// Keep track of the order of the axes to verify if the order has actually
// changed after a drag ends. Changed order might have consequence (e.g.
// strums that need to be reset).
var dimsAtDragstart;
g.style("cursor", "move")
.call(d3.behavior.drag()
.on("dragstart", function(d) {
dragging[d] = this.__origin__ = xscale(d);
dimsAtDragstart = __.dimensions.slice();
})
.on("drag", function(d) {
dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
__.dimensions.sort(function(a, b) { return position(a) - position(b); });
xscale.domain(__.dimensions);
pc.render();
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; });
})
.on("dragend", function(d, i) {
// Let's see if the order has changed and send out an event if so.
var j = __.dimensions.indexOf(d),
parent = this.parentElement;
if (i !== j) {
events.axesreorder.call(pc, __.dimensions);
// We now also want to reorder the actual dom elements that represent
// the axes. That is, the g.dimension elements. If we don't do this,
// we get a weird and confusing transition when updateAxes is called.
// This is due to the fact that, initially the nth g.dimension element
// represents the nth axis. However, after a manual reordering,
// without reordering the dom elements, the nth dom elements no longer
// necessarily represents the nth axis.
//
// i is the original index of the dom element
// j is the new index of the dom element
parent.insertBefore(this, parent.children[j + 1])
}
delete this.__origin__;
delete dragging[d];
d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
pc.render();
if (flags.shadows) paths(__.data, ctx.shadows);
}));
flags.reorderable = true;
return this;
};
// pairs of adjacent dimensions
pc.adjacent_pairs = function(arr) {
var ret = [];
for (var i = 0; i < arr.length-1; i++) {
ret.push([arr[i],arr[i+1]]);
};
return ret;
};
var brush = {
modes: {
"None": {
install: function(pc) {}, // Nothing to be done.
uninstall: function(pc) {}, // Nothing to be done.
selected: function() { return []; } // Nothing to return
}
},
mode: "None",
predicate: "AND",
currentMode: function() {
return this.modes[this.mode];
}
};
// This function can be used for 'live' updates of brushes. That is, during the
// specification of a brush, this method can be called to update the view.
//
// @param newSelection - The new set of data items that is currently contained
// by the brushes
function brushUpdated(newSelection) {
__.brushed = newSelection;
events.brush.call(pc,__.brushed);
pc.render();
}
function brushPredicate(predicate) {
if (!arguments.length) { return brush.predicate; }
predicate = String(predicate).toUpperCase();
if (predicate !== "AND" && predicate !== "OR") {
throw "Invalid predicate " + predicate;
}
brush.predicate = predicate;
__.brushed = brush.currentMode().selected();
pc.render();
return pc;
}
pc.brushModes = function() {
return Object.getOwnPropertyNames(brush.modes);
};
pc.brushMode = function(mode) {
if (arguments.length === 0) {
return brush.mode;
}
if (pc.brushModes().indexOf(mode) === -1) {
throw "pc.brushmode: Unsupported brush mode: " + mode;
}
// Make sure that we don't trigger unnecessary events by checking if the mode
// actually changes.
if (mode !== brush.mode) {
// When changing brush modes, the first thing we need to do is clearing any
// brushes from the current mode, if any.
if (brush.mode !== "None") {
pc.brushReset();
}
// Next, we need to 'uninstall' the current brushMode.
brush.modes[brush.mode].uninstall(pc);
// Finally, we can install the requested one.
brush.mode = mode;
brush.modes[brush.mode].install();
if (mode === "None") {
delete pc.brushPredicate;
} else {
pc.brushPredicate = brushPredicate;
}
}
return pc;
};
// brush mode: 1D-Axes
(function() {
var brushes = {};
function is_brushed(p) {
return !brushes[p].empty();
}
// data within extents
function selected() {
var actives = __.dimensions.filter(is_brushed),
extents = actives.map(function(p) { return brushes[p].extent(); });
// We don't want to return the full data set when there are no axes brushed.
// Actually, when there are no axes brushed, by definition, no items are
// selected. So, let's avoid the filtering and just return false.
//if (actives.length === 0) return false;
// Resolves broken examples for now. They expect to get the full dataset back from empty brushes
if (actives.length === 0) return __.data;
// test if within range
var within = {
"date": function(d,p,dimension) {
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
},
"number": function(d,p,dimension) {
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
},
"string": function(d,p,dimension) {
return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
}
};
return __.data
.filter(function(d) {
switch(brush.predicate) {
case "AND":
return actives.every(function(p, dimension) {
return within[__.types[p]](d,p,dimension);
});
case "OR":
return actives.some(function(p, dimension) {
return within[__.types[p]](d,p,dimension);
});
default:
throw "Unknown brush predicate " + __.brushPredicate;
}
});
};
function brushExtents() {
var extents = {};
__.dimensions.forEach(function(d) {
var brush = brushes[d];
if (!brush.empty()) {
var extent = brush.extent();
extent.sort(d3.ascending);
extents[d] = extent;
}
});
return extents;
}
function brushFor(axis) {
var brush = d3.svg.brush();
brush
.y(yscale[axis])
.on("brushstart", function() { d3.event.sourceEvent.stopPropagation() })
.on("brush", function() {
brushUpdated(selected());
})
.on("brushend", function() {
events.brushend.call(pc, __.brushed);
});
brushes[axis] = brush;
return brush;
}
function brushReset(dimension) {
__.brushed = false;
if (g) {
g.selectAll('.brush')
.each(function(d) {
d3.select(this).call(
brushes[d].clear()
);
});
pc.render();
}
return this;
};
function install() {
if (!g) pc.createAxes();
// Add and store a brush for each axis.
g.append("svg:g")
.attr("class", "brush")
.each(function(d) {
d3.select(this).call(brushFor(d));
})
.selectAll("rect")
.style("visibility", null)
.attr("x", -15)
.attr("width", 30);
pc.brushExtents = brushExtents;
pc.brushReset = brushReset;
return pc;
}
brush.modes["1D-axes"] = {
install: install,
uninstall: function() {
g.selectAll(".brush").remove();
brushes = {};
delete pc.brushExtents;
delete pc.brushReset;
},
selected: selected
}
})();
// brush mode: 2D-strums
// bl.ocks.org/syntagmatic/5441022
(function() {
var strums = {},
strumRect;
function drawStrum(strum, activePoint) {
var svg = pc.selection.select("svg").select("g#strums"),
id = strum.dims.i,
points = [strum.p1, strum.p2],
line = svg.selectAll("line#strum-" + id).data([strum]),
circles = svg.selectAll("circle#strum-" + id).data(points),
drag = d3.behavior.drag();
line.enter()
.append("line")
.attr("id", "strum-" + id)
.attr("class", "strum");
line
.attr("x1", function(d) { return d.p1[0]; })
.attr("y1", function(d) { return d.p1[1]; })
.attr("x2", function(d) { return d.p2[0]; })
.attr("y2", function(d) { return d.p2[1]; })
.attr("stroke", "black")
.attr("stroke-width", 2);
drag
.on("drag", function(d, i) {
var ev = d3.event;
i = i + 1;
strum["p" + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
strum["p" + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
drawStrum(strum, i - 1);
})
.on("dragend", onDragEnd());
circles.enter()
.append("circle")
.attr("id", "strum-" + id)
.attr("class", "strum");
circles
.attr("cx", function(d) { return d[0]; })
.attr("cy", function(d) { return d[1]; })
.attr("r", 5)
.style("opacity", function(d, i) {
return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
})
.on("mouseover", function() {
d3.select(this).style("opacity", 0.8);
})
.on("mouseout", function() {
d3.select(this).style("opacity", 0);
})
.call(drag);
}
function dimensionsForPoint(p) {
var dims = { i: -1, left: undefined, right: undefined };
__.dimensions.some(function(dim, i) {
if (xscale(dim) < p[0]) {
var next = __.dimensions[i + 1];
dims.i = i;
dims.left = dim;
dims.right = next;
return false;
}
return true;
});
if (dims.left === undefined) {
// Event on the left side of the first axis.
dims.i = 0;
dims.left = __.dimensions[0];
dims.right = __.dimensions[1];
} else if (dims.right === undefined) {
// Event on the right side of the last axis
dims.i = __.dimensions.length - 1;
dims.right = dims.left;
dims.left = __.dimensions[__.dimensions.length - 2];
}
return dims;
}
function onDragStart() {
// First we need to determine between which two axes the sturm was started.
// This will determine the freedom of movement, because a strum can
// logically only happen between two axes, so no movement outside these axes
// should be allowed.
return function() {
var p = d3.mouse(strumRect[0][0]),
dims = dimensionsForPoint(p),
strum = {
p1: p,
dims: dims,
minX: xscale(dims.left),
maxX: xscale(dims.right),
minY: 0,
maxY: h()
};
strums[dims.i] = strum;
strums.active = dims.i;
// Make sure that the point is within the bounds
strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
strum.p1[1] = p[1] - __.margin.top;
strum.p2 = strum.p1.slice();
};
}
function onDrag() {
return function() {
var ev = d3.event,
strum = strums[strums.active];
// Make sure that the point is within the bounds
strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY);
drawStrum(strum, 1);
};
}
function containmentTest(strum, width) {
var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
m1 = 1 - width / p1[0],
b1 = p1[1] * (1 - m1),
m2 = 1 - width / p2[0],
b2 = p2[1] * (1 - m2);
// test if point falls between lines
return function(p) {
var x = p[0],
y = p[1],
y1 = m1 * x + b1,
y2 = m2 * x + b2;
if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
return true;
}
return false;
};
}
function selected() {
var ids = Object.getOwnPropertyNames(strums),
brushed = __.data;
// Get the ids of the currently active strums.
ids = ids.filter(function(d) {
return !isNaN(d);
});
function crossesStrum(d, id) {
var strum = strums[id],
test = containmentTest(strum, strums.width(id)),
d1 = strum.dims.left,
d2 = strum.dims.right,
y1 = yscale[d1],
y2 = yscale[d2],
point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
return test(point);
}
if (ids.length === 0) { return brushed; }
return brushed.filter(function(d) {
switch(brush.predicate) {
case "AND":
return ids.every(function(id) { return crossesStrum(d, id); });
case "OR":
return ids.some(function(id) { return crossesStrum(d, id); });
default:
throw "Unknown brush predicate " + __.brushPredicate;
}
});
}
function removeStrum() {
var strum = strums[strums.active],
svg = pc.selection.select("svg").select("g#strums");
delete strums[strums.active];
strums.active = undefined;
svg.selectAll("line#strum-" + strum.dims.i).remove();
svg.selectAll("circle#strum-" + strum.dims.i).remove();
}
function onDragEnd() {
return function() {
var brushed = __.data,
strum = strums[strums.active];
// Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
// considered a drag without move. So we have to deal with that case
if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) {
removeStrum(strums);
}
brushed = selected(strums);
strums.active = undefined;
__.brushed = brushed;
pc.render();
events.brushend.call(pc, __.brushed);
};
}
function brushReset(strums) {
return function() {
var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
return !isNaN(d);
});
ids.forEach(function(d) {
strums.active = d;
removeStrum(strums);
});
onDragEnd(strums)();
};
}
function install() {
var drag = d3.behavior.drag();
// Map of current strums. Strums are stored per segment of the PC. A segment,
// being the area between two axes. The left most area is indexed at 0.
strums.active = undefined;
// Returns the width of the PC segment where currently a strum is being
// placed. NOTE: even though they are evenly spaced in our current
// implementation, we keep for when non-even spaced segments are supported as
// well.
strums.width = function(id) {
var strum = strums[id];
if (strum === undefined) {
return undefined;
}
return strum.maxX - strum.minX;
};
pc.on("axesreorder.strums", function() {
var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
return !isNaN(d);
});
// Checks if the first dimension is directly left of the second dimension.
function consecutive(first, second) {
var length = __.dimensions.length;
return __.dimensions.some(function(d, i) {
return (d === first)
? i + i < length && __.dimensions[i + 1] === second
: false;
});
}
if (ids.length > 0) { // We have some strums, which might need to be removed.
ids.forEach(function(d) {
var dims = strums[d].dims;
strums.active = d;
// If the two dimensions of the current strum are not next to each other
// any more, than we'll need to remove the strum. Otherwise we keep it.
if (!consecutive(dims.left, dims.right)) {
removeStrum(strums);
}
});
onDragEnd(strums)();
}
});
// Add a new svg group in which we draw the strums.
pc.selection.select("svg").append("g")
.attr("id", "strums")
.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
// Install the required brushReset function
pc.brushReset = brushReset(strums);
drag
.on("dragstart", onDragStart(strums))
.on("drag", onDrag(strums))
.on("dragend", onDragEnd(strums));
// NOTE: The styling needs to be done here and not in the css. This is because
// for 1D brushing, the canvas layers should not listen to
// pointer-events.
strumRect = pc.selection.select("svg").insert("rect", "g#strums")
.attr("id", "strum-events")
.attr("x", __.margin.left)
.attr("y", __.margin.top)
.attr("width", w())
.attr("height", h() + 2)
.style("opacity", 0)
.call(drag);
}
brush.modes["2D-strums"] = {
install: install,
uninstall: function() {
pc.selection.select("svg").select("g#strums").remove();
pc.selection.select("svg").select("rect#strum-events").remove();
pc.on("axesreorder.strums", undefined);
delete pc.brushReset;
strumRect = undefined;
},
selected: selected
};
}());
pc.interactive = function() {
flags.interactive = true;
return this;
};
// expose a few objects
pc.xscale = xscale;
pc.yscale = yscale;
pc.ctx = ctx;
pc.canvas = canvas;
pc.g = function() { return g; };
// rescale for height, width and margins
// TODO currently assumes chart is brushable, and destroys old brushes
pc.resize = function() {
// selection size
pc.selection.select("svg")
.attr("width", __.width)
.attr("height", __.height)
pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
// FIXME: the current brush state should pass through
if (flags.brushable) pc.brushReset();
// scales
pc.autoscale();
// axes, destroys old brushes.
if (g) pc.createAxes();
if (flags.shadows) paths(__.data, ctx.shadows);
if (flags.brushable) pc.brushable();
if (flags.reorderable) pc.reorderable();
events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
return this;
};
// highlight an array of data
pc.highlight = function(data) {
if (arguments.length === 0) {
return __.highlighted;
}
__.highlighted = data;
pc.clear("highlight");
d3.select(canvas.foreground).classed("faded", true);
data.forEach(path_highlight);
events.highlight.call(this, data);
return this;
};
// clear highlighting
pc.unhighlight = function() {
__.highlighted = [];
pc.clear("highlight");
d3.select(canvas.foreground).classed("faded", false);
return this;
};
// calculate 2d intersection of line a->b with line c->d
// points are objects with x and y properties
pc.intersection = function(a, b, c, d) {
return {
x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
};
};
function position(d) {
var v = dragging[d];
return v == null ? xscale(d) : v;
}
pc.version = "0.5.0";
// this descriptive text should live with other introspective methods
pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
return pc;
};
d3.renderQueue = (function(func) {
var _queue = [], // data to be rendered
_rate = 10, // number of calls per frame
_clear = function() {}, // clearing function
_i = 0; // current iteration
var rq = function(data) {
if (data) rq.data(data);
rq.invalidate();
_clear();
rq.render();
};
rq.render = function() {
_i = 0;
var valid = true;
rq.invalidate = function() { valid = false; };
function doFrame() {
if (!valid) return true;
if (_i > _queue.length) return true;
// Typical d3 behavior is to pass a data item *and* its index. As the
// render queue splits the original data set, we'll have to be slightly
// more carefull about passing the correct index with the data item.
var end = Math.min(_i + _rate, _queue.length);
for (var i = _i; i < end; i++) {
func(_queue[i], i);
}
_i += _rate;
}
d3.timer(doFrame);
};
rq.data = function(data) {
rq.invalidate();
_queue = data.slice(0);
return rq;
};
rq.rate = function(value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function() {
return _queue.length - _i;
};
// clear the canvas
rq.clear = function(func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = function() {};
return rq;
});
// http://bl.ocks.org/3687826
d3.divgrid = function(config) {
var columns = [];
var dg = function(selection) {
if (columns.length == 0) columns = d3.keys(selection.data()[0][0]);
// header
selection.selectAll(".header")
.data([true])
.enter().append("div")
.attr("class", "header")
var header = selection.select(".header")
.selectAll(".cell")
.data(columns);
header.enter().append("div")
.attr("class", function(d,i) { return "col-" + i; })
.classed("cell", true)
selection.selectAll(".header .cell")
.text(function(d) { return d; });
header.exit().remove();
// rows
var rows = selection.selectAll(".row")
.data(function(d) { return d; })
rows.enter().append("div")
.attr("class", "row")
rows.exit().remove();
var cells = selection.selectAll(".row").selectAll(".cell")
.data(function(d) { return columns.map(function(col){return d[col];}) })
// cells
cells.enter().append("div")
.attr("class", function(d,i) { return "col-" + i; })
.classed("cell", true)
cells.exit().remove();
selection.selectAll(".cell")
.text(function(d) { return d; });
return dg;
};
dg.columns = function(_) {
if (!arguments.length) return columns;
columns = _;
return this;
};
return dg;
};
model name BiasPrLatRmse BiasPrLonRmse BiasSstLatRmse BiasSstLonRmse BiasTauxLatRmse BiasTauxLonRmse NinaSstLonRmse NinaSstTsRmse NinoSstLonRmse NinoSstTsRmse SeasonalPrLatRmse SeasonalSstLatRmse SeasonalSstLonRmse
ACCESS1-0 1.67 1.45 0.66 0.68 11.38 10.15 0.27 0.31 0.38 0.28 1.22 0.13 0.28
ACCESS1-3 1.35 2.06 0.54 0.73 11.47 10.27 0.32 0.37 0.5 0.36 1.33 0.19 0.25
BCC-CSM1-1 2.44 1.7 0.66 0.97 5.82 12.63 0.23 0.47 0.45 0.4 1.56 0.21 0.24
BCC-CSM1-1-M 3.13 1.29 1.26 0.59 9.49 6.99 0.27 0.49 0.28 0.57 1.72 0.2 0.2
CanCM4 0.74 2.01 0.31 1.1 9.86 9.3 0.51 0.32 0.61 0.33 0.99 0.19 0.34
CanESM2 0.85 2.06 0.31 1.16 8.92 9.25 0.34 0.37 0.62 0.27 1.1 0.2 0.34
CCSM4 1.34 1.54 0.4 0.43 3.31 6.46 0.29 0.26 0.33 0.38 1.78 0.18 0.22
CESM1-BGC 1.48 1.45 0.46 0.38 3.67 6.62 0.27 0.26 0.43 0.28 1.88 0.19 0.21
CESM1-CAM5 1.42 1.74 0.69 0.95 4.72 6.95 0.41 0.3 0.56 0.27 1.4 0.2 0.2
CESM1-FASTCHEM 1.37 1.42 0.43 0.4 3.34 6.66 0.3 0.23 0.4 0.36 1.84 0.18 0.22
CESM1-WACCM 1.73 1.72 0.44 0.57 5.5 7.04 0.39 0.3 0.39 0.28 1.55 0.21 0.14
CMCC-CESM 2.43 1.95 1.24 0.91 3.88 7.83 0.27 0.44 0.44 0.54 1.41 0.09 0.18
CMCC-CM 2.0 1.52 0.58 0.36 7.04 10.58 0.23 0.38 0.48 0.42 1.43 0.17 0.26
CMCC-CMS 2.04 1.47 0.67 0.38 3.03 4.26 0.23 0.34 0.37 0.29 1.43 0.2 0.2
CNRM-CM5 1.83 1.25 0.48 1.04 6.82 7.88 0.16 0.33 0.28 0.39 0.97 0.43 0.22
CNRM-CM5-2 1.73 1.32 0.71 1.16 6.21 7.56 0.22 0.32 0.3 0.37 1.0 0.46 0.22
CSIRO-Mk3-6-0 1.26 3.69 1.51 2.29 7.71 14.95 0.72 0.33 0.92 0.36 1.01 0.28 0.24
FGOALS-g2 1.89 0.99 0.22 1.23 5.91 11.72 0.29 0.24 0.43 0.36 0.92 0.26 0.24
GFDL-CM3 2.2 2.12 0.77 1.0 4.67 7.96 0.38 0.53 0.37 0.39 2.26 0.19 0.15
GFDL-ESM2G 2.31 3.6 1.3 1.57 9.42 17.17 0.39 0.36 0.7 0.3 1.61 0.44 0.3
GFDL-ESM2M 2.24 1.96 0.57 1.05 6.95 11.09 0.6 0.55 0.6 0.65 1.83 0.21 0.09
GISS-E2-H 3.41 1.6 1.07 1.01 13.45 18.06 0.42 0.26 0.62 0.37 1.03 0.25 0.32
GISS-E2-H-CC 3.62 1.62 1.18 1.18 12.85 19.28 0.51 0.37 0.6 0.45 1.06 0.25 0.3
GISS-E2-R 2.41 1.99 1.1 1.18 7.9 14.69 0.25 0.27 0.47 0.31 0.78 0.17 0.29
GISS-E2-R-CC 2.45 2.02 1.12 1.2 8.12 15.03 0.29 0.3 0.43 0.56 0.79 0.17 0.3
HadCM3 0.89 2.99 0.67 1.18 6.94 15.93 0.41 0.33 0.63 0.36 0.53 0.19 0.3
HadGEM2-AO 1.18 1.31 0.64 0.74 8.14 12.78 0.27 0.41 0.53 0.48 1.72 0.2 0.18
HadGEM2-CC 1.0 1.21 1.04 1.15 6.81 11.8 0.34 0.39 0.49 0.4 1.52 0.19 0.19
HadGEM2-ES 1.08 1.45 0.97 1.05 7.98 12.86 0.27 0.42 0.49 0.39 1.6 0.19 0.19
INMCM4 2.32 2.61 0.82 1.41 8.17 12.13 0.29 0.37 0.76 0.39 1.04 0.36 0.41
IPSL-CM5A-LR 1.82 3.08 0.97 1.24 5.98 9.22 0.31 0.27 0.58 0.32 1.66 0.34 0.1
IPSL-CM5A-MR 1.72 3.32 0.78 1.01 6.8 8.16 0.37 0.29 0.69 0.3 1.52 0.32 0.12
IPSL-CM5B-LR 1.54 1.76 0.38 0.91 8.04 7.16 0.17 0.21 0.45 0.39 1.19 0.21 0.24
MIROC-ESM 0.74 2.17 1.53 1.96 12.19 16.99 0.46 0.37 0.86 0.45 0.37 0.3 0.57
MIROC-ESM-CHEM 0.74 2.15 1.54 1.95 11.95 16.88 0.44 0.49 0.85 0.5 0.37 0.29 0.57
MIROC4h 1.46 1.67 0.61 1.03 8.79 11.66 0.34 0.5 0.59 0.46 1.35 0.4 0.29
MIROC5 0.89 1.89 0.72 0.98 5.83 11.53 0.45 0.51 0.71 0.75 0.74 0.1 0.2
MPI-ESM-LR 1.96 3.6 1.1 1.45 8.22 8.75 0.36 0.32 0.64 0.41 1.56 0.27 0.22
MPI-ESM-MR 2.03 3.19 1.06 1.22 6.37 10.3 0.3 0.31 0.63 0.37 1.62 0.4 0.2
MPI-ESM-P 1.93 3.62 1.1 1.47 8.53 9.04 0.35 0.3 0.55 0.39 1.57 0.26 0.22
NorESM1-M 1.25 0.93 0.94 0.81 9.05 9.97 0.24 0.28 0.38 0.29 1.52 0.26 0.19
NorESM1-ME 1.23 1.1 1.19 0.99 10.08 9.85 0.24 0.3 0.32 0.35 1.41 0.23 0.14
model name BiasPrLatRmse BiasPrLonRmse BiasSstLatRmse BiasSstLonRmse BiasTauxLatRmse BiasTauxLonRmse NinaSstLonRmse NinaSstTsRmse NinoSstLonRmse NinoSstTsRmse SeasonalPrLatRmse SeasonalSstLatRmse SeasonalSstLonRmse
ACCESS1-0 1.67 1.45 0.66 0.68 11.38 10.15 0.27 0.31 0.38 0.28 1.22 0.13 0.28
ACCESS1-3 1.35 2.06 0.54 0.73 11.47 10.27 0.32 0.37 0.50 0.36 1.33 0.19 0.25
BCC-CSM1-1 2.44 1.70 0.66 0.97 5.82 12.63 0.23 0.47 0.45 0.40 1.56 0.21 0.24
BCC-CSM1-1-M 3.13 1.29 1.26 0.59 9.49 6.99 0.27 0.49 0.28 0.57 1.72 0.20 0.20
BNU-ESM 2.03 1.40 0.70 0.73 90.33 60.95 0.55 0.41 0.35 0.51 1.79 0.30 0.11
CanCM4 0.74 2.01 0.31 1.10 9.86 9.30 0.51 0.32 0.61 0.33 0.99 0.19 0.34
CanESM2 0.85 2.06 0.31 1.16 8.92 9.25 0.34 0.37 0.62 0.27 1.10 0.20 0.34
CCSM4 1.34 1.54 0.40 0.43 3.31 6.46 0.29 0.26 0.33 0.38 1.78 0.18 0.22
CESM1-BGC 1.48 1.45 0.46 0.38 3.67 6.62 0.27 0.26 0.43 0.28 1.88 0.19 0.21
CESM1-CAM5 1.42 1.74 0.69 0.95 4.72 6.95 0.41 0.30 0.56 0.27 1.40 0.20 0.20
CESM1-FASTCHEM 1.37 1.42 0.43 0.40 3.34 6.66 0.30 0.23 0.40 0.36 1.84 0.18 0.22
CESM1-WACCM 1.73 1.72 0.44 0.57 5.50 7.04 0.39 0.30 0.39 0.28 1.55 0.21 0.14
CMCC-CESM 2.43 1.95 1.24 0.91 3.88 7.83 0.27 0.44 0.44 0.54 1.41 0.09 0.18
CMCC-CM 2.00 1.52 0.58 0.36 7.04 10.58 0.23 0.38 0.48 0.42 1.43 0.17 0.26
CMCC-CMS 2.04 1.47 0.67 0.38 3.03 4.26 0.23 0.34 0.37 0.29 1.43 0.20 0.20
CNRM-CM5 1.83 1.25 0.48 1.04 6.82 7.88 0.16 0.33 0.28 0.39 0.97 0.43 0.22
CNRM-CM5-2 1.73 1.32 0.71 1.16 6.21 7.56 0.22 0.32 0.30 0.37 1.00 0.46 0.22
CSIRO-Mk3-6-0 1.26 3.69 1.51 2.29 7.71 14.95 0.72 0.33 0.92 0.36 1.01 0.28 0.24
FGOALS-g2 1.89 0.99 0.22 1.23 5.91 11.72 0.29 0.24 0.43 0.36 0.92 0.26 0.24
FIO-ESM 2.13 1.25 1.06 0.78 88.59 55.31 0.35 0.38 0.34 0.60 1.25 0.16 0.31
GFDL-CM3 2.20 2.12 0.77 1.00 4.67 7.96 0.38 0.53 0.37 0.39 2.26 0.19 0.15
GFDL-ESM2G 2.31 3.60 1.30 1.57 9.42 17.17 0.39 0.36 0.70 0.30 1.61 0.44 0.30
GFDL-ESM2M 2.24 1.96 0.57 1.05 6.95 11.09 0.60 0.55 0.60 0.65 1.83 0.21 0.09
GISS-E2-H 3.41 1.60 1.07 1.01 13.45 18.06 0.42 0.26 0.62 0.37 1.03 0.25 0.32
GISS-E2-H-CC 3.62 1.62 1.18 1.18 12.85 19.28 0.51 0.37 0.60 0.45 1.06 0.25 0.30
GISS-E2-R 2.41 1.99 1.10 1.18 7.90 14.69 0.25 0.27 0.47 0.31 0.78 0.17 0.29
GISS-E2-R-CC 2.45 2.02 1.12 1.20 8.12 15.03 0.29 0.30 0.43 0.56 0.79 0.17 0.30
HadCM3 0.89 2.99 0.67 1.18 6.94 15.93 0.41 0.33 0.63 0.36 0.53 0.19 0.30
HadGEM2-AO 1.18 1.31 0.64 0.74 8.14 12.78 0.27 0.41 0.53 0.48 1.72 0.20 0.18
HadGEM2-CC 1.00 1.21 1.04 1.15 6.81 11.80 0.34 0.39 0.49 0.40 1.52 0.19 0.19
HadGEM2-ES 1.08 1.45 0.97 1.05 7.98 12.86 0.27 0.42 0.49 0.39 1.60 0.19 0.19
INMCM4 2.32 2.61 0.82 1.41 8.17 12.13 0.29 0.37 0.76 0.39 1.04 0.36 0.41
IPSL-CM5A-LR 1.82 3.08 0.97 1.24 5.98 9.22 0.31 0.27 0.58 0.32 1.66 0.34 0.10
IPSL-CM5A-MR 1.72 3.32 0.78 1.01 6.80 8.16 0.37 0.29 0.69 0.30 1.52 0.32 0.12
IPSL-CM5B-LR 1.54 1.76 0.38 0.91 8.04 7.16 0.17 0.21 0.45 0.39 1.19 0.21 0.24
MIROC-ESM 0.74 2.17 1.53 1.96 12.19 16.99 0.46 0.37 0.86 0.45 0.37 0.30 0.57
MIROC-ESM-CHEM 0.74 2.15 1.54 1.95 11.95 16.88 0.44 0.49 0.85 0.50 0.37 0.29 0.57
MIROC4h 1.46 1.67 0.61 1.03 8.79 11.66 0.34 0.50 0.59 0.46 1.35 0.40 0.29
MIROC5 0.89 1.89 0.72 0.98 5.83 11.53 0.45 0.51 0.71 0.75 0.74 0.10 0.20
MPI-ESM-LR 1.96 3.60 1.10 1.45 8.22 8.75 0.36 0.32 0.64 0.41 1.56 0.27 0.22
MPI-ESM-MR 2.03 3.19 1.06 1.22 6.37 10.30 0.30 0.31 0.63 0.37 1.62 0.40 0.20
MPI-ESM-P 1.93 3.62 1.10 1.47 8.53 9.04 0.35 0.30 0.55 0.39 1.57 0.26 0.22
NorESM1-M 1.25 0.93 0.94 0.81 9.05 9.97 0.24 0.28 0.38 0.29 1.52 0.26 0.19
NorESM1-ME 1.23 1.10 1.19 0.99 10.08 9.85 0.24 0.30 0.32 0.35 1.41 0.23 0.14
<!DOCTYPE html>
<style>
#wrapper {
position: relative;
float: left;
top: 20px;
font-family: sans-serif;
font-size: 10px;
}
#tooltip{
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
color:black;
}
</style>
<body>
<link rel="stylesheet" type="text/css" href="./bigdata.css">
<div id="title">
<h1>
<a href="https://pcmdi.llnl.gov/" target="_blank"><img src="PCMDILogo_200x65px_72dpi.png" height="60" align="bottom" title="Program for Climate Model Diagnosis and Intercomparison"></a>
<u>ENSO Performance Metrics for CMIP5 Models</u>
<a href="https://github.com/PCMDI/pcmdi_metrics" target="_blank"><img src="PMPLogo_1359x1146px_300dpi.png" align="bottom" height="60" title="PCMDI Metrics Package"></a>
</h1>
</div>
<div id="footer">This is a <font color="red"><b>PROTOTYPE</b></font> of interactive parallel coordinate plot that is for ENSO performance metrics for <a href="https://pcmdi.llnl.gov/mips/cmip5/" target="_blank">CMIP5</a> models. The <a href="https://github.com/eguil/ENSO_metrics" target="_blank">ENSO metrics</a> is developed in collaboration of Program for Climate Model Diagnosis and Intercomparison (<a href="https://pcmdi.llnl.gov/" target="_blank">PCMDI</a>) of Lawrence Livermore Natioanal Laboratory (<a href="https://www.llnl.gov/" target="_blank">LLNL</a>) and Institute Pierre Simon Laplace (<a href="https://www.ipsl.fr/" target="_blank">IPSL</a>). Visualization is done by <b><i>Jiwoo Lee</i></b> (LLNL) and the ENSO metrics collection has code developed by <b><i>Yann Planton</i></b> (IPSL), <b><i>Jiwoo Lee</i></b> (LLNL), <b><i>Eric Guilyardi</i></b> (IPSL), and <b><i>Peter Gleckler</i></b> (LLNL). Detailed descriptions for each metric can be found <a href="https://github.com/eguil/ENSO_metrics/wiki" target="_blank">here</a>. Last updated in October 2018.<br>
<b><font color="blue">Usuage:</font></b><br>
- Click and drag down on vertical axis to begin brush<br>
- Click vertical axis to clear brush<br>
- Click a label to color data by z-scores<br>
<font color="red">* Please note this page is optimized for screen resolution wider than 1700 pixels.<br></font>
</div>
<button id="refresh-page">Refresh-page</button>
<!--
<link rel="stylesheet" type="text/css" href="http://mostapharoudsari.github.io/honeybee/pc_source_files/css/d3.parcoords.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src = "http://mostapharoudsari.github.io/honeybee/pc_source_files/d3/d3.parcoords.js"></script>
-->
<link rel="stylesheet" type="text/css" href="./d3.parcoords.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src = "./d3.parcoords.js"></script>
<script src="./divgrid.js"></script>
<div id="wrapper" class="parcoords" style="width:100%; height:600px"></div>
<div id="grid"></div>
<script>
var color_set = d3.scale.linear()
.range(["#3182bd", "#f33"]);
// load default chart
//d3.csv("results.csv", function(data){
//d3.csv("ENSO_perf_roundup_20181001.csv", function(data){
d3.csv("ENSO_perf_ERAinterim+HadISST+Tropflux_20181009.csv", function(data){
// collect text for first column to adjust left margin
var firstCell = data.map(function(d){return d3.values(d)[0]});
// find the longest text size in the first row to adjust left margin
var textLength = 0;
firstCell.forEach(function(d){
if (d.length > textLength) textLength = d.length;
});
// get parallel coordinates
graph = d3.parcoords()('#wrapper')
.data(data)
.margin({ top: 30, left: 3 * textLength, bottom: 40, right: 0 })
.alpha(0.6)
.mode("queue")
.rate(5)
.render()
.brushMode("1D-axes") // enable brushing
//.reorderable() // I removed this for now as it can mess up with tooltips
.interactive();
// add instruction text
var instructions = "-Drag around axis to begin brush. -Click axis to clear brush. -Click a label to color data based on axis values. -Hover on each line to highlight."
d3.select("#wrapper svg").append("text")
.text(instructions)
.attr("text-anchor", "middle")
.attr("text-decoration", "overline")
.attr("transform", "translate(" + graph.width()/2 + "," + (graph.height()-5) + ")");;
// set the initial coloring based on the 3rd column --> 2nd
//update_colors(d3.keys(data[0])[2]);
update_colors(d3.keys(data[0])[1]);
// click label to activate coloring
graph.svg.selectAll(".dimension")
.on("click", update_colors)
.selectAll(".label")
.style("font-size", "14px"); // change font sizes of selected lable
// create data table, row hover highlighting
var grid = d3.divgrid();
d3.select("#grid")
//.datum(data.slice(0,10))
.datum(data)
.call(grid)
.selectAll(".row")
.on({
//"mouseover": function(d) { graph.highlight([d]) },
//"mouseout": graph.unhighlight
"click": function(d) {
cleanTooltip();
graph.highlight([d]) },
});
// update data table on brush event
graph.on("brush", function(d) {
d3.select("#grid")
//.datum(d.slice(0,10))
.datum(d)
.call(grid)
.selectAll(".row")
.on({
"mouseover": function(d) { graph.highlight([d]) },
"mouseout": graph.unhighlight
//"click": function(d) {
// cleanTooltip();
// graph.highlight([d]) },
});
});
//add hover event
d3.select("#wrapper svg")
.on("mousemove", function() {
var mousePosition = d3.mouse(this);
highlightLineOnClick(mousePosition, true); //true will also add tooltip
})
.on("mouseout", function(){
cleanTooltip();
graph.unhighlight();
//})
//.on("click", function() {
// var mousePosition = d3.mouse(this);
// highlightLineOnClick(mousePosition, true); //true will also add tooltip
});
// Refresh page
d3.select("#refresh-page")
.on("click", function() {
window.location.reload();
});
});
// update color and font weight of chart based on axis selection
// modified from here: https://syntagmatic.github.io/parallel-coordinates/
function update_colors(dimension) {
// change the fonts to bold
graph.svg.selectAll(".dimension")
.style("font-weight", "normal")
.filter(function(d) { return d == dimension; })
.style("font-weight", "bold");
// change color of lines
// set domain of color scale
var values = graph.data().map(function(d){return parseFloat(d[dimension])});
color_set.domain([d3.min(values), d3.max(values)]);
// change colors for each line
graph.color(function(d){return color_set([d[dimension]])}).render();
};
// Add highlight for every line on click
function getCentroids(data){
// this function returns centroid points for data. I had to change the source
// for parallelcoordinates and make compute_centroids public.
// I assume this should be already somewhere in graph and I don't need to recalculate it
// but I couldn't find it so I just wrote this for now
var margins = graph.margin();
var graphCentPts = [];
data.forEach(function(d){
var initCenPts = graph.compute_centroids(d).filter(function(d, i){return i%2==0;});
// move points based on margins
var cenPts = initCenPts.map(function(d){
return [d[0] + margins["left"], d[1]+ margins["top"]];
});
graphCentPts.push(cenPts);
});
return graphCentPts;
}
function getActiveData(){
// I'm pretty sure this data is already somewhere in graph
if (graph.brushed()!=false) return graph.brushed();
return graph.data();
}
function isOnLine(startPt, endPt, testPt, tol){
// check if test point is close enough to a line
// between startPt and endPt. close enough means smaller than tolerance
var x0 = testPt[0];
var y0 = testPt[1];
var x1 = startPt[0];
var y1 = startPt[1];
var x2 = endPt[0];
var y2 = endPt[1];
var Dx = x2 - x1;
var Dy = y2 - y1;
var delta = Math.abs(Dy*x0 - Dx*y0 - x1*y2+x2*y1)/Math.sqrt(Math.pow(Dx, 2) + Math.pow(Dy, 2));
//console.log(delta);
if (delta <= tol) return true;
return false;
}
function findAxes(testPt, cenPts){
// finds between which two axis the mouse is
var x = testPt[0];
var y = testPt[1];
// make sure it is inside the range of x
if (cenPts[0][0] > x) return false;
if (cenPts[cenPts.length-1][0] < x) return false;
// find between which segment the point is
for (var i=0; i<cenPts.length; i++){
if (cenPts[i][0] > x) return i;
}
}
function cleanTooltip(){
// removes any object under #tooltip is
graph.svg.selectAll("#tooltip")
.remove();
}
function addTooltip(clicked, clickedCenPts){
// sdd tooltip to multiple clicked lines
var clickedDataSet = [];
var margins = graph.margin()
// get all the values into a single list
// I'm pretty sure there is a better way to write this is Javascript
for (var i=0; i<clicked.length; i++){
for (var j=0; j<clickedCenPts[i].length; j++){
var text = d3.values(clicked[i])[j];
// not clean at all!
var x = clickedCenPts[i][j][0] - margins.left;
var y = clickedCenPts[i][j][1] - margins.top;
clickedDataSet.push([x, y, text]);
}
};
// add rectangles
var fontSize = 14;
var padding = 2;
var rectHeight = fontSize + 2 * padding; //based on font size
graph.svg.selectAll("rect[id='tooltip']")
.data(clickedDataSet).enter()
.append("rect")
.attr("x", function(d) { return d[0] - d[2].length * 5;})
.attr("y", function(d) { return d[1] - rectHeight + 2 * padding; })
.attr("rx", "2")
.attr("ry", "2")
.attr("id", "tooltip")
.attr("fill", "grey")
.attr("opacity", 0.9)
.attr("width", function(d){return d[2].length * 10;})
.attr("height", rectHeight);
// add text on top of rectangle
graph.svg.selectAll("text[id='tooltip']")
.data(clickedDataSet).enter()
.append("text")
.attr("x", function(d) { return d[0];})
.attr("y", function(d) { return d[1]; })
.attr("id", "tooltip")
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-size", fontSize)
.text( function (d){ return d[2];})
}
function getClickedLines(mouseClick){
var clicked = [];
var clickedCenPts = [];
// find which data is activated right now
var activeData = getActiveData();
// find centriod points
var graphCentPts = getCentroids(activeData);
if (graphCentPts.length==0) return false;
// find between which axes the point is
var axeNum = findAxes(mouseClick, graphCentPts[0]);
if (!axeNum) return false;
graphCentPts.forEach(function(d, i){
if (isOnLine(d[axeNum-1], d[axeNum], mouseClick, 2)){
clicked.push(activeData[i]);
clickedCenPts.push(graphCentPts[i]); // for tooltip
}
});
return [clicked, clickedCenPts]
}
function highlightLineOnClick(mouseClick, drawTooltip){
var clicked = [];
var clickedCenPts = [];
clickedData = getClickedLines(mouseClick);
if (clickedData && clickedData[0].length!=0){
clicked = clickedData[0];
clickedCenPts = clickedData[1];
// highlight clicked line
graph.highlight(clicked);
if (drawTooltip){
// clean if anything is there
cleanTooltip();
// add tooltip
addTooltip(clicked, clickedCenPts);
}
}
};
</script>
</body>
Case_Description Heating Cooling Pumps & Aux Vent. Fans
Base Design 132573 60471 13158 149825
0+ASHRAE Walls 139144 61356 12793 151225
0+ASHRAE Window 150893 65522 12194 154333
0+ASHRAE C factor 133043 57580 13080 149926
0+ASHRAE Fridge 131315 61327 13252 150119
0+ASHRAE Dishwasher 132258 60717 13184 149868
0+ASHRAE Rf 136384 60390 12972 150420
0+ASHRAE fan speed 132572 60474 13158 150275
0+ASHRAE fan power 136552 58833 13019 90551
0+Single Speed Compressor 143919 74848 12806 149825
0+ASHRAE Clng Eff 134399 72331 13821 147832
0+ASHRAE Htng Eff 136286 60471 13025 150450
0+ASHRAE Ext light 132573 60471 13158 149825
0+ASHRAE DHW 132573 60471 13158 149825
0+ASHRAE DHW eff 132573 60471 13158 149825
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment