Last active
June 7, 2018 06:31
-
-
Save element824/c23dbf546ae766ce391a82dfccf0284b to your computer and use it in GitHub Desktop.
Revisions
-
element824 revised this gist
Jun 7, 2018 . No changes.There are no files selected for viewing
-
element824 created this gist
Jun 7, 2018 .There are no files selected for viewing
Binary file not shown.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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1 @@ license: mit 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,3 @@ Displaying planetary data from [nasa.gov](http://nssdc.gsfc.nasa.gov/planetary/factsheet/) to test Parallel Coordinates in d3 with the awesome [parcoords library](http://syntagmatic.github.io/parallel-coordinates/) forked from <a href='http://bl.ocks.org/eesur/'>eesur</a>'s block: <a href='http://bl.ocks.org/eesur/1a2514440351ec22f176'>d3 | Parallel Coordinates</a> 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,1325 @@ d3.parcoords = function(config) { var __ = { data: [], highlighted: [], dimensions: [], dimensionTitles: {}, dimensionTitleRotation: 0, types: {}, brushed: false, mode: "default", rate: 20, width: 940, height: 450, margin: { top: 24, right: 0, bottom: 24, 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) { return d3.scale.ordinal() .domain(__.data.map(function(p) { return p[k]; })) .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($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($V([cx, cy])); } } return 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) { // Let's see if the order has changed and send out an event if so. var orderChanged = dimsAtDragstart.some(function(d, i) { return d !== __.dimensions[i]; }); if (orderChanged) { events.axesreorder.call(pc, __.dimensions); } 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; }); 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,59 @@ // 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; }; 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,56 @@ <!doctype html> <title>Linking to Data Table</title> <!-- http://syntagmatic.github.com/parallel-coordinates/ --> <link rel="stylesheet" type="text/css" href="style.css"> <script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.min.js"></script> <script src="d3.parcoords.js"></script> <script src="divgrid.js"></script> <div id="example" class="parcoords"></div> <div id="grid"></div> <script id="brushing">// quantitative colour scale var green_to_blue = d3.scale.linear() .domain([9, 50]) .range(["#7AC143", "#00B0DD"]) .interpolate(d3.interpolateLab); var color = function(d) { return green_to_blue(d['Length of Day (hours)']); }; var parcoords = d3.parcoords()("#example") .color(color) .alpha(0.4); // load csv file and create the chart d3.csv('planet.csv', function(data) { parcoords .data(data) .render() .brushMode("1D-axes"); // enable brushing // create data table, row hover highlighting var grid = d3.divgrid(); d3.select("#grid") .datum(data.slice(0,10)) .call(grid) .selectAll(".row") .on({ "mouseover": function(d) { parcoords.highlight([d]) }, "mouseout": parcoords.unhighlight }); // update data table on brush event parcoords.on("brush", function(d) { d3.select("#grid") .datum(d.slice(0,10)) .call(grid) .selectAll(".row") .on({ "mouseover": function(d) { parcoords.highlight([d]) }, "mouseout": parcoords.unhighlight }); }); }); </script> 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,9 @@ Planet,Length of Day (hours),Distance from Sun (km),Orbital Period (years),Mass (ME),Diameter (km) Mercury,4222.6,57.9,0.2408467,0.05527,4879 Venus,2802,108.2,0.61519726,0.815,12104 Earth,24,149.6,1.0000174,1,12756 Mars,24.7,227.9,1.8808158,0.10745,6792 Jupiter,9.9,778.6,11.862615,317.83,142984 Saturn,10.7,1433.5,29.447498,95.159,120536 Uranus,17.2,2872.5,84.016846,14.5,51118 Neptune,16.1,4495.1,164.79132,17.204,49528 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,112 @@ @import url(http://fonts.googleapis.com/css?family=Source+Code+Pro:400,600); body { font-size: 14px; font-family: "Source Code Pro", Consolas, monaco, monospace; margin: 20px auto 20px; width: 960px; line-height: 1.45em; } a { color: #454545; } a:hover { color: #000; } ul { margin: 0 20px; padding: 0; } .dark { background: #222; } #example { min-height: 300px; margin: 12px 0; } p { width: 560px; } pre { color: #444; font-family: Ubuntu Mono, Monaco, monospace; padding: 4px 8px; background: #f2f2f2; border: 1px solid #ccc; } h1 small { font-weight: normal; font-size: 0.5em; } h3 { margin-top: 40px; } .float { float: left; } .centered { text-align: center; } .hide { display: none; } input { font-size: 16px; } /* data table styles */ #grid { height: 240px; } .row, .header { clear: left; font-size: 11px; line-height: 24px; height: 24px; } .row:nth-child(odd) { background: rgba(0,0,0,0.05); } .header { font-weight: bold; } .cell { float: left; overflow: hidden; white-space: nowrap; width: 160px; height: 18px; } .col-0 { width: 120px; } /* parcoords styles */ .parcoords > svg, .parcoords > canvas { font: 11px "Source Code Pro", Consolas, monaco, monospace; position: absolute; } .parcoords > canvas { pointer-events: none; } .parcoords text.label { cursor: default; } .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: #454545; shape-rendering: crispEdges; } .parcoords canvas { opacity: 1; -moz-transition: opacity 0.5s; -webkit-transition: opacity 0.5s; -o-transition: opacity 0.5s; } .parcoords canvas.faded { opacity: 0.25; } .parcoords { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }