Skip to content

Instantly share code, notes, and snippets.

@element824
Last active June 7, 2018 06:31
Show Gist options
  • Select an option

  • Save element824/c23dbf546ae766ce391a82dfccf0284b to your computer and use it in GitHub Desktop.

Select an option

Save element824/c23dbf546ae766ce391a82dfccf0284b to your computer and use it in GitHub Desktop.

Revisions

  1. element824 revised this gist Jun 7, 2018. No changes.
  2. element824 created this gist Jun 7, 2018.
    Binary file added .DS_Store
    Binary file not shown.
    1 change: 1 addition & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    license: mit
    3 changes: 3 additions & 0 deletions README.md
    Original 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>
    1,325 changes: 1,325 additions & 0 deletions d3.parcoords.js
    Original 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;
    });
    59 changes: 59 additions & 0 deletions divgrid.js
    Original 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;
    };
    56 changes: 56 additions & 0 deletions index.html
    Original 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>
    9 changes: 9 additions & 0 deletions planet.csv
    Original 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
    112 changes: 112 additions & 0 deletions style.css
    Original 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;
    }