const d3 = require('d3');
/*
 Copyright (c) 2012, Kai Chang
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice, this
 list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation
 and/or other materials provided with the distribution.

 * The name Kai Chang may not be used to endorse or promote products
 derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
d3.parcoords = function(config) {
    var __ = {
        data: [],
        highlighted: [],
        dimensions: {},
        dimensionTitleRotation: 0,
        brushed: false,
        brushedColor: null,
        alphaOnBrushed: 0.0,
        mode: "default",
        rate: 20,
        width: 600,
        height: 300,
        margin: { top: 24, right: 0, bottom: 12, left: 0 },
        nullValueSeparator: "undefined", // set to "top" or "bottom"
        nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
        color: "#069",
        composite: "source-over",
        alpha: 0.7,
        bundlingStrength: 0.5,
        bundleDimension: null,
        smoothness: 0.0,
        showControlPoints: false,
        hideAxis : [],
        flipAxes: [],
        animationTime: 1100, // How long it takes to flip the axis when you double click
        rotateLabels: false
    };

    extend(__, config);

    if (config && config.dimensionTitles) {
        console.warn("dimensionTitles passed in config is deprecated. Add title to dimension object.");
        d3.entries(config.dimensionTitles).forEach(function(d) {
            if (__.dimensions[d.key]) {
                __.dimensions[d.key].title = __.dimensions[d.key].title ? __.dimensions[d.key].title : d.value;
            } else {
                __.dimensions[d.key] = {
                    title: d.value
                };
            }
        });
    }
    var pc = function(selection) {
        selection = pc.selection = d3.select(selection);

        __.width = selection[0][0].clientWidth;
        __.height = selection[0][0].clientHeight;

        // canvas data layers
        ["marks", "foreground", "brushed", "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)
            .style("font", "14px sans-serif")
            .style("position", "absolute")

            .append("svg:g")
            .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");

        return pc;
    };
    var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "brushstart", "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,
            debug: false
        },
        xscale = d3.scale.ordinal(),
        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;
            ctx.brushed.globalCompositeOperation = d.value;
        })
        .on("alpha", function(d) {
            ctx.foreground.globalAlpha = d.value;
            ctx.brushed.globalAlpha = d.value;
        })
        .on("brushedColor", function (d) {
            ctx.brushed.strokeStyle = d.value;
        })
        .on("width", function(d) { pc.resize(); })
        .on("height", function(d) { pc.resize(); })
        .on("margin", function(d) { pc.resize(); })
        .on("rate", function(d) {
            brushedQueue.rate(d.value);
            foregroundQueue.rate(d.value);
        })
        .on("dimensions", function(d) {
            __.dimensions = pc.applyDimensionDefaults(d3.keys(d.value));
            xscale.domain(pc.getOrderedDimensionKeys());
            pc.sortDimensions();
            if (flags.interactive){pc.render().updateAxes();}
        })
        .on("bundleDimension", function(d) {
            if (!d3.keys(__.dimensions).length) pc.detectDimensions();
            pc.autoscale();
            if (typeof d.value === "number") {
                if (d.value < d3.keys(__.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);
            if (flags.interactive){pc.render();}
        })
        .on("hideAxis", function(d) {
            pc.dimensions(pc.applyDimensionDefaults());
            pc.dimensions(without(__.dimensions, d.value));
        })
        .on("flipAxes", function(d) {
            if (d.value && d.value.length) {
                d.value.forEach(function(axis) {
                    flipAxisAndUpdatePCP(axis);
                });
                pc.updateAxes(0);
            }
        });

// expose the state of the chart
    pc.state = __;
    pc.flags = flags;

// create getter/setters
    getset(pc, __, events);

// expose events
    d3.rebind(pc, events, "on");

// 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];
                }
                if (key === 'dimensions' && Object.prototype.toString.call(x) === '[object Array]') {
                    console.warn("pc.dimensions([]) is deprecated, use pc.dimensions({})");
                    x = pc.applyDimensionDefaults(x);
                }
                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 (var key in source) {
            target[key] = source[key];
        }
        return target;
    };

    function without(arr, items) {
        items.forEach(function (el) {
            delete arr[el];
        });
        return arr;
    };
    /** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
    function getRange() {
        if (__.nullValueSeparator=="bottom") {
            return [h()+1-__.nullValueSeparatorPadding.bottom-__.nullValueSeparatorPadding.top, 1];
        } else if (__.nullValueSeparator=="top") {
            return [h()+1, 1+__.nullValueSeparatorPadding.bottom+__.nullValueSeparatorPadding.top];
        }
        return [h()+1, 1];
    };

    pc.autoscale = function() {
        // yscale
        var defaultScales = {
            "date": function(k) {
                var extent = d3.extent(__.data, function(d) {
                    return d[k] ? d[k].getTime() : null;
                });

                // special case if single value
                if (extent[0] === extent[1]) {
                    return d3.scale.ordinal()
                        .domain([extent[0]])
                        .rangePoints(getRange());
                }

                return d3.time.scale()
                    .domain(extent)
                    .range(getRange());
            },
            "number": function(k) {
                var extent = d3.extent(__.data, function(d) { return +d[k]; });

                // special case if single value
                if (extent[0] === extent[1]) {
                    return d3.scale.ordinal()
                        .domain([extent[0]])
                        .rangePoints(getRange());
                }

                return d3.scale.linear()
                    .domain(extent)
                    .range(getRange());
            },
            "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 (p[k] === undefined && __.nullValueSeparator!== "undefined"){
                        return; // null values will be drawn beyond the horizontal null value separator!
                    }
                    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(getRange());
            }
        };

        d3.keys(__.dimensions).forEach(function(k) {
            if (!__.dimensions[k].yscale){
                __.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
            }
        });

        // xscale
        xscale.rangePoints([0, w()], 1);

        // Retina display, etc.
        var devicePixelRatio = window.devicePixelRatio || 1;

        // canvas sizes
        pc.selection.selectAll("canvas")
            .style("margin-top", __.margin.top + "px")
            .style("margin-left", __.margin.left + "px")
            .style("width", (w()+2) + "px")
            .style("height", (h()+2) + "px")
            .attr("width", (w()+2) * devicePixelRatio)
            .attr("height", (h()+2) * devicePixelRatio);

        // 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.foreground.scale(devicePixelRatio, devicePixelRatio);
        ctx.brushed.strokeStyle = __.brushedColor;
        ctx.brushed.lineWidth = 1.4;
        ctx.brushed.globalCompositeOperation = __.composite;
        ctx.brushed.globalAlpha = __.alpha;
        ctx.brushed.scale(devicePixelRatio, devicePixelRatio);
        ctx.highlight.lineWidth = 3;
        ctx.highlight.scale(devicePixelRatio, devicePixelRatio);

        return this;
    };

    pc.scale = function(d, domain) {
        __.dimensions[d].yscale.domain(domain);

        return this;
    };

    pc.flip = function(d) {
        //__.dimensions[d].yscale.domain().reverse();                               // does not work
        __.dimensions[d].yscale.domain(__.dimensions[d].yscale.domain().reverse()); // works

        return this;
    };

    pc.commonScale = function(global, type) {
        var t = type || "number";
        if (typeof global === 'undefined') {
            global = true;
        }

        // try to autodetect dimensions and create scales
        if (!d3.keys(__.dimensions).length) {
            pc.detectDimensions()
        }
        pc.autoscale();

        // scales of the same type
        var scales = d3.keys(__.dimensions).filter(function(p) {
            return __.dimensions[p].type == t;
        });

        if (global) {
            var extent = d3.extent(scales.map(function(d,i) {
                return __.dimensions[d].yscale.domain();
            }).reduce(function(a,b) {
                return a.concat(b);
            }));

            scales.forEach(function(d) {
                __.dimensions[d].yscale.domain(extent);
            });

        } else {
            scales.forEach(function(d) {
                __.dimensions[d].yscale.domain(d3.extent(__.data, function(d) { return +d[k]; }));
            });
        }

        // update centroids
        if (__.bundleDimension !== null) {
            pc.bundleDimension(__.bundleDimension);
        }

        return this;
    };
    pc.detectDimensions = function() {
        pc.dimensions(pc.applyDimensionDefaults());
        return this;
    };

    pc.applyDimensionDefaults = function(dims) {
        var types = pc.detectDimensionTypes(__.data);
        dims = dims ? dims : d3.keys(types);
        var newDims = {};
        var currIndex = 0;
        dims.forEach(function(k) {
            newDims[k] = __.dimensions[k] ? __.dimensions[k] : {};
            //Set up defaults
            newDims[k].orient= newDims[k].orient ? newDims[k].orient : 'left';
            newDims[k].ticks= newDims[k].ticks != null ? newDims[k].ticks : 5;
            newDims[k].innerTickSize= newDims[k].innerTickSize != null ? newDims[k].innerTickSize : 6;
            newDims[k].outerTickSize= newDims[k].outerTickSize != null ? newDims[k].outerTickSize : 0;
            newDims[k].tickPadding= newDims[k].tickPadding != null ? newDims[k].tickPadding : 3;
            newDims[k].type= newDims[k].type ? newDims[k].type : types[k];

            newDims[k].index = newDims[k].index != null ? newDims[k].index : currIndex;
            currIndex++;
        });
        return newDims;
    };

    pc.getOrderedDimensionKeys = function(){
        return d3.keys(__.dimensions).sort(function(x, y){
            return d3.ascending(__.dimensions[x].index, __.dimensions[y].index);
        });
    };

// 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 = {};
        if(!data[0]) return types;

        d3.keys(data[0])
            .forEach(function(col) {
                types[isNaN(Number(col)) ? col : parseInt(col)] = pc.toTypeCoerceNumbers(data[0][col]);
            });
        return types;
    };
    pc.render = function() {
        // try to autodetect dimensions and create scales
        if (!d3.keys(__.dimensions).length) {
            pc.detectDimensions()
        }
        pc.autoscale();

        pc.render[__.mode]();

        events.render.call(this);
        return this;
    };

    pc.renderBrushed = function() {
        if (!d3.keys(__.dimensions).length) pc.detectDimensions();

        pc.renderBrushed[__.mode]();

        events.render.call(this);
        return this;
    };

    function isBrushed() {
        if (__.brushed && __.brushed.length !== __.data.length)
            return true;

        var object = brush.currentMode().brushState();

        for (var key in object) {
            if (object.hasOwnProperty(key)) {
                return true;
            }
        }
        return false;
    };

    pc.render.default = function() {
        pc.clear('foreground');
        pc.clear('highlight');

        pc.renderBrushed.default();

        __.data.forEach(path_foreground);
    };

    var foregroundQueue = d3.renderQueue(path_foreground)
        .rate(50)
        .clear(function() {
            pc.clear('foreground');
            pc.clear('highlight');
        });

    pc.render.queue = function() {
        pc.renderBrushed.queue();

        foregroundQueue(__.data);
    };

    pc.renderBrushed.default = function() {
        pc.clear('brushed');

        if (isBrushed()) {
            __.brushed.forEach(path_brushed);
        }
    };

    var brushedQueue = d3.renderQueue(path_brushed)
        .rate(50)
        .clear(function() {
            pc.clear('brushed');
        });

    pc.renderBrushed.queue = function() {
        if (isBrushed()) {
            brushedQueue(__.brushed);
        } else {
            brushedQueue([]); // This is needed to clear the currently brushed items
        }
    };
    function compute_cluster_centroids(d) {

        var clusterCentroids = d3.map();
        var clusterCounts = d3.map();
        // determine clusterCounts
        __.data.forEach(function(row) {
            var scaled = __.dimensions[d].yscale(row[d]);
            if (!clusterCounts.has(scaled)) {
                clusterCounts.set(scaled, 0);
            }
            var count = clusterCounts.get(scaled);
            clusterCounts.set(scaled, count + 1);
        });

        __.data.forEach(function(row) {
            d3.keys(__.dimensions).map(function(p, i) {
                var scaled = __.dimensions[d].yscale(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 += __.dimensions[p].yscale(row[p]) / clusterCounts.get(scaled);
                clusterCentroids.get(scaled).set(p, value);
            });
        });

        return clusterCentroids;

    }

    function compute_centroids(row) {
        var centroids = [];

        var p = d3.keys(__.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 = __.dimensions[p[i]].yscale(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 * (__.dimensions[p[i+1]].yscale(row[p[i+1]]) - y);
                if (__.bundleDimension !== null) {
                    var leftCentroid = __.clusterCentroids.get(__.dimensions[__.bundleDimension].yscale(row[__.bundleDimension])).get(p[i]);
                    var rightCentroid = __.clusterCentroids.get(__.dimensions[__.bundleDimension].yscale(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;
    }

    pc.compute_real_centroids = function(row) {
        var realCentroids = [];

        var p = d3.keys(__.dimensions);
        var cols = p.length;
        var a = 0.5;

        for (var i = 0; i < cols; ++i) {
            var x = position(p[i]);
            var y = __.dimensions[p[i]].yscale(row[p[i]]);
            realCentroids.push([x, y]);
        }

        return realCentroids;
    }

    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;
        pc.alphaOnBrushed(0.1);
        pc.render();
        return this;
    };

// draw dots with radius r on the axis line where data intersects
    pc.axisDots = function(r) {
        var r = r || 0.1;
        var ctx = pc.ctx.marks;
        var startAngle = 0;
        var endAngle = 2 * Math.PI;
        ctx.globalAlpha = d3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]);
        __.data.forEach(function(d) {
            d3.entries(__.dimensions).forEach(function(p, i) {
                ctx.beginPath();
                ctx.arc(position(p), __.dimensions[p.key].yscale(d[p]), r, startAngle, endAngle);
                ctx.stroke();
                ctx.fill();
            });
        });
        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, ctx) {
        ctx.beginPath();
        if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
            single_curve(d, ctx);
        } else {
            single_path(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_curve(d, ctx);
            } else {
                single_path(d, ctx);
            }
        });
        ctx.stroke();
    };

// returns the y-position just beyond the separating null value line
    function getNullPosition() {
        if (__.nullValueSeparator=="bottom") {
            return h()+1;
        } else if (__.nullValueSeparator=="top") {
            return 1;
        } else {
            console.log("A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.");
        }
        return h()+1;
    };

    function single_path(d, ctx) {
        d3.entries(__.dimensions).forEach(function(p, i) {  //p isn't really p
            if (i == 0) {
                ctx.moveTo(position(p.key), typeof d[p.key] =='undefined' ? getNullPosition() : __.dimensions[p.key].yscale(d[p.key]));
            } else {
                ctx.lineTo(position(p.key), typeof d[p.key] =='undefined' ? getNullPosition() : __.dimensions[p.key].yscale(d[p.key]));
            }
        });
    };

    function path_brushed(d, i) {
        if (__.brushedColor !== null) {
            ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
        } else {
            ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
        }
        return color_path(d, ctx.brushed)
    };

    function path_foreground(d, i) {
        ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
        return color_path(d, ctx.foreground);
    };

    function path_highlight(d, i) {
        ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
        return color_path(d, ctx.highlight);
    };
    pc.clear = function(layer) {
        ctx[layer].clearRect(0, 0, w() + 2, h() + 2);

        // This will make sure that the foreground items are transparent
        // without the need for changing the opacity style of the foreground canvas
        // as this would stop the css styling from working
        if(layer === "brushed" && isBrushed()) {
            ctx.brushed.fillStyle = pc.selection.style("background-color");
            ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
            ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
            ctx.brushed.globalAlpha = __.alpha;
        }
        return this;
    };
    d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");

    function flipAxisAndUpdatePCP(dimension) {
        var g = pc.svg.selectAll(".dimension");

        pc.flip(dimension);

        d3.select(this.parentElement)
            .transition()
            .duration(__.animationTime)
            .call(axis.scale(__.dimensions[dimension].yscale))
            .call(axis.orient(__.dimensions[dimension].orient))
            .call(axis.ticks(__.dimensions[dimension].ticks))
            .call(axis.innerTickSize(__.dimensions[dimension].innerTickSize))
            .call(axis.outerTickSize(__.dimensions[dimension].outerTickSize))
            .call(axis.tickPadding(__.dimensions[dimension].tickPadding))
            .call(axis.tickFormat(__.dimensions[dimension].tickFormat));

        pc.render();
    }

    function rotateLabels() {
        if (!__.rotateLabels) return;

        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();
    }

    function dimensionLabels(d) {
        return __.dimensions[d].title ? __.dimensions[d].title : d;  // dimension display names
    }

    pc.createAxes = function() {
        if (g) pc.removeAxes();

        // Add a group element for each dimension.
        g = pc.svg.selectAll(".dimension")
            .data(pc.getOrderedDimensionKeys(), 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) {
                var axisElement = d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) );

                axisElement.selectAll("path")
                    .style("fill", "none")
                    .style("stroke", "#222")
                    .style("shape-rendering", "crispEdges");

                axisElement.selectAll("line")
                    .style("fill", "none")
                    .style("stroke", "#222")
                    .style("shape-rendering", "crispEdges");
            })
            .append("svg:text")
            .attr({
                "text-anchor": "middle",
                "y": 0,
                "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
                "x": 0,
                "class": "label"
            })
            .text(dimensionLabels)
            .on("dblclick", flipAxisAndUpdatePCP)
            .on("wheel", rotateLabels);

        if (__.nullValueSeparator=="top") {
            pc.svg.append("line")
                .attr("x1", 0)
                .attr("y1", 1+__.nullValueSeparatorPadding.top)
                .attr("x2", w())
                .attr("y2", 1+__.nullValueSeparatorPadding.top)
                .attr("stroke-width", 1)
                .attr("stroke", "#777")
                .attr("fill", "none")
                .attr("shape-rendering", "crispEdges");
        } else if (__.nullValueSeparator=="bottom") {
            pc.svg.append("line")
                .attr("x1", 0)
                .attr("y1", h()+1-__.nullValueSeparatorPadding.bottom)
                .attr("x2", w())
                .attr("y2", h()+1-__.nullValueSeparatorPadding.bottom)
                .attr("stroke-width", 1)
                .attr("stroke", "#777")
                .attr("fill", "none")
                .attr("shape-rendering", "crispEdges");
        }

        flags.axes= true;
        return this;
    };

    pc.removeAxes = function() {
        g.remove();
        g = undefined;
        return this;
    };

    pc.updateAxes = function(animationTime) {
        if (typeof animationTime === 'undefined') {
            animationTime = __.animationTime;
        }

        var g_data = pc.svg.selectAll(".dimension").data(pc.getOrderedDimensionKeys());

        // 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) {
                var axisElement = d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) );

                axisElement.selectAll("path")
                    .style("fill", "none")
                    .style("stroke", "#222")
                    .style("shape-rendering", "crispEdges");

                axisElement.selectAll("line")
                    .style("fill", "none")
                    .style("stroke", "#222")
                    .style("shape-rendering", "crispEdges");
            })
            .append("svg:text")
            .attr({
                "text-anchor": "middle",
                "y": 0,
                "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
                "x": 0,
                "class": "label"
            })
            .text(dimensionLabels)
            .on("dblclick", flipAxisAndUpdatePCP)
            .on("wheel", rotateLabels);

        // Update
        g_data.attr("opacity", 0);
        g_data.select(".axis")
            .transition()
            .duration(animationTime)
            .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) )
            });
        g_data.select(".label")
            .transition()
            .duration(animationTime)
            .text(dimensionLabels)
            .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");

        // Exit
        g_data.exit().remove();

        g = pc.svg.selectAll(".dimension");
        g.transition().duration(animationTime)
            .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
            .style("opacity", 1);

        pc.svg.selectAll(".axis")
            .transition()
            .duration(animationTime)
            .each(function(d) { d3.select(this).call( pc.applyAxisConfig(axis, __.dimensions[d]) );
            });

        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;
    };

    pc.applyAxisConfig = function(axis, dimension) {
        return axis.scale(dimension.yscale)
            .orient(dimension.orient)
            .ticks(dimension.ticks)
            .tickValues(dimension.tickValues)
            .innerTickSize(dimension.innerTickSize)
            .outerTickSize(dimension.outerTickSize)
            .tickPadding(dimension.tickPadding)
            .tickFormat(dimension.tickFormat)
    };

// Jason Davies, http://bl.ocks.org/1341281
    pc.reorderable = function() {
        if (!g) pc.createAxes();

        g.style("cursor", "move")
            .call(d3.behavior.drag()
                .on("dragstart", function(d) {
                    dragging[d] = this.__origin__ = xscale(d);
                })
                .on("drag", function(d) {
                    dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
                    pc.sortDimensions();
                    xscale.domain(pc.getOrderedDimensionKeys());
                    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 i = 0,
                        j = __.dimensions[d].index,
                        elem = this,
                        parent = this.parentElement;

                    while((elem = elem.previousElementSibling) != null) ++i;
                    if (i !== j) {
                        events.axesreorder.call(pc, pc.getOrderedDimensionKeys());
                        // 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
                        if (i > j) { // Element moved left
                            parent.insertBefore(this, parent.children[j - 1]);
                        } else {     // Element moved right
                            if ((j + 1) < parent.children.length) {
                                parent.insertBefore(this, parent.children[j + 1]);
                            } else {
                                parent.appendChild(this);
                            }
                        }
                    }

                    delete this.__origin__;
                    delete dragging[d];
                    d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
                    pc.render();
                }));
        flags.reorderable = true;
        return this;
    };

// Reorder dimensions, such that the highest value (visually) is on the left and
// the lowest on the right. Visual values are determined by the data values in
// the given row.
    pc.reorder = function(rowdata) {
        var firstDim = pc.getOrderedDimensionKeys()[0];

        pc.sortDimensionsByRowData(rowdata);
        // NOTE: this is relatively cheap given that:
        // number of dimensions < number of data items
        // Thus we check equality of order to prevent rerendering when this is the case.
        var reordered = false;
        reordered = firstDim !== pc.getOrderedDimensionKeys()[0];

        if (reordered) {
            xscale.domain(pc.getOrderedDimensionKeys());
            var highlighted = __.highlighted.slice(0);
            pc.unhighlight();

            g.transition()
                .duration(1500)
                .attr("transform", function(d) {
                    return "translate(" + xscale(d) + ")";
                });
            pc.render();

            // pc.highlight() does not check whether highlighted is length zero, so we do that here.
            if (highlighted.length !== 0) {
                pc.highlight(highlighted);
            }
        }
    }

    pc.sortDimensionsByRowData = function(rowdata) {
        var copy = __.dimensions;
        var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
            var pixelDifference = __.dimensions[a].yscale(rowdata[a]) - __.dimensions[b].yscale(rowdata[b]);

            // Array.sort is not necessarily stable, this means that if pixelDifference is zero
            // the ordering of dimensions might change unexpectedly. This is solved by sorting on
            // variable name in that case.
            if (pixelDifference === 0) {
                return a.localeCompare(b);
            } // else
            return pixelDifference;
        });
        __.dimensions = {};
        positionSortedKeys.forEach(function(p, i){
            __.dimensions[p] = copy[p];
            __.dimensions[p].index = i;
        });
    }

    pc.sortDimensions = function() {
        var copy = __.dimensions;
        var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
            return position(a) - position(b);
        });
        __.dimensions = {};
        positionSortedKeys.forEach(function(p, i){
            __.dimensions[p] = copy[p];
            __.dimensions[p].index = i;
        })
    };

// 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
                brushState: function() { 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.renderBrushed();
    }

    function brushPredicate(predicate) {
        if (!arguments.length) { return brush.predicate; }

        predicate = String(predicate).toUpperCase();
        if (predicate !== "AND" && predicate !== "OR") {
            throw new Error("Invalid predicate " + predicate);
        }

        brush.predicate = predicate;
        __.brushed = brush.currentMode().selected();
        pc.renderBrushed();
        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 new Error("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 = d3.keys(__.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) {
                    if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
                        return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
                    } else {
                        return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
                    }
                },
                "number": function(d,p,dimension) {
                    if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
                        return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
                    } else {
                        return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
                    }
                },
                "string": function(d,p,dimension) {
                    return extents[dimension][0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
                }
            };

            return __.data
                .filter(function(d) {
                    switch(brush.predicate) {
                        case "AND":
                            return actives.every(function(p, dimension) {
                                return within[__.dimensions[p].type](d,p,dimension);
                            });
                        case "OR":
                            return actives.some(function(p, dimension) {
                                return within[__.dimensions[p].type](d,p,dimension);
                            });
                        default:
                            throw new Error("Unknown brush predicate " + __.brushPredicate);
                    }
                });
        };

        function brushExtents(extents) {
            if(typeof(extents) === 'undefined')
            {
                var extents = {};
                d3.keys(__.dimensions).forEach(function(d) {
                    var brush = brushes[d];
                    if (brush !== undefined && !brush.empty()) {
                        var extent = brush.extent();
                        extent.sort(d3.ascending);
                        extents[d] = extent;
                    }
                });
                return extents;
            }
            else
            {
                //first get all the brush selections
                var brushSelections = {};
                g.selectAll('.brush')
                    .each(function(d) {
                        brushSelections[d] = d3.select(this);

                    });

                // loop over each dimension and update appropriately (if it was passed in through extents)
                d3.keys(__.dimensions).forEach(function(d) {
                    if (extents[d] === undefined){
                        return;
                    }

                    var brush = brushes[d];
                    if (brush !== undefined) {
                        //update the extent
                        brush.extent(extents[d]);

                        //redraw the brush
                        brushSelections[d]
                            .transition()
                            .duration(0)
                            .call(brush);

                        //fire some events
                        brush.event(brushSelections[d]);
                    }
                });

                //redraw the chart
                pc.renderBrushed();

                return pc;
            }
        }

        function brushFor(axis) {
            var brush = d3.svg.brush();

            brush
                .y(__.dimensions[axis].yscale)
                .on("brushstart", function() {
                    if(d3.event.sourceEvent !== null) {
                        events.brushstart.call(pc, __.brushed);
                        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) {
            if (dimension===undefined) {
                __.brushed = false;
                if (g) {
                    g.selectAll('.brush')
                        .each(function(d) {
                            d3.select(this)
                                .transition()
                                .duration(0)
                                .call(brushes[d].clear());
                        });
                    pc.renderBrushed();
                }
            }
            else {
                if (g) {
                    g.selectAll('.brush')
                        .each(function(d) {
                            if (d!=dimension) return;
                            d3.select(this)
                                .transition()
                                .duration(0)
                                .call(brushes[d].clear());
                            brushes[d].event(d3.select(this));
                        });
                    pc.renderBrushed();
                }
            }
            return this;
        };

        function install() {
            if (!g) pc.createAxes();

            // Add and store a brush for each axis.
            var brush = g.append("svg:g")
                .attr("class", "brush")
                .each(function(d) {
                    d3.select(this).call(brushFor(d));
                });

            brush.selectAll("rect")
                .style("visibility", null)
                .attr("x", -15)
                .attr("width", 30);

            brush.selectAll("rect.background")
                .style("fill", "transparent");

            brush.selectAll("rect.extent")
                .style("fill", "rgba(255,255,255,0.25)")
                .style("stroke", "rgba(0,0,0,0.6)");

            brush.selectAll(".resize rect")
                .style("fill", "rgba(0,0,0,0.1)");

            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,
            brushState: brushExtents
        }
    })();
// 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 };
            d3.keys(__.dimensions).some(function(dim, i) {
                if (xscale(dim) < p[0]) {
                    var next = d3.keys(__.dimensions)[pc.getOrderedDimensionKeys().indexOf(dim)+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 = pc.getOrderedDimensionKeys()[0];
                dims.right = pc.getOrderedDimensionKeys()[1];
            } else if (dims.right === undefined) {
                // Event on the right side of the last axis
                dims.i = d3.keys(__.dimensions).length - 1;
                dims.right = dims.left;
                dims.left = pc.getOrderedDimensionKeys()[d3.keys(__.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,
                    strum;

                p[0] = p[0] - __.margin.left;
                p[1] = p[1] - __.margin.top;

                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.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 - __.margin.left), 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 = __.dimensions[d1].yscale,
                    y2 = __.dimensions[d2].yscale,
                    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 new Error("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.renderBrushed();
                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 = d3.keys(__.dimensions).length;
                    return d3.keys(__.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,
            brushState: function () { return strums; }
        };

    }());

// brush mode: 1D-Axes with multiple extents
// requires d3.svg.multibrush

    (function() {
        if (typeof d3.svg.multibrush !== 'function') {
            return;
        }
        var brushes = {};

        function is_brushed(p) {
            return !brushes[p].empty();
        }

        // data within extents
        function selected() {
            var actives = d3.keys(__.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,b) {
                    if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
                        return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
                    } else {
                        return b[0] <= d[p] && d[p] <= b[1]
                    }
                },
                "number": function(d,p,dimension,b) {
                    if (typeof __.dimensions[p].yscale.rangePoints === "function") { // if it is ordinal
                        return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
                    } else {
                        return b[0] <= d[p] && d[p] <= b[1]
                    }
                },
                "string": function(d,p,dimension,b) {
                    return b[0] <= __.dimensions[p].yscale(d[p]) && __.dimensions[p].yscale(d[p]) <= b[1]
                }
            };

            return __.data
                .filter(function(d) {
                    switch(brush.predicate) {
                        case "AND":
                            return actives.every(function(p, dimension) {
                                return extents[dimension].some(function(b) {
                                    return within[__.dimensions[p].type](d,p,dimension,b);
                                });
                            });
                        case "OR":
                            return actives.some(function(p, dimension) {
                                return extents[dimension].some(function(b) {
                                    return within[__.dimensions[p].type](d,p,dimension,b);
                                });
                            });
                        default:
                            throw new Error("Unknown brush predicate " + __.brushPredicate);
                    }
                });
        };

        function brushExtents(extents) {
            if (typeof(extents) === 'undefined') {
                extents = {};
                d3.keys(__.dimensions).forEach(function (d) {
                    var brush = brushes[d];
                    if (brush !== undefined && !brush.empty()) {
                        var extent = brush.extent();
                        extents[d] = extent;
                    }
                });
                return extents;
            }
            else {
                //first get all the brush selections
                var brushSelections = {};
                g.selectAll('.brush')
                    .each(function (d) {
                        brushSelections[d] = d3.select(this);
                    });

                // loop over each dimension and update appropriately (if it was passed in through extents)
                d3.keys(__.dimensions).forEach(function (d) {
                    if (extents[d] === undefined) {
                        return;
                    }

                    var brush = brushes[d];
                    if (brush !== undefined) {
                        //update the extent
                        brush.extent(extents[d]);

                        //redraw the brush
                        brushSelections[d]
                            .transition()
                            .duration(0)
                            .call(brush);

                        //fire some events
                        brush.event(brushSelections[d]);
                    }
                });

                //redraw the chart
                pc.renderBrushed();

                return pc;
            }
        }

        //function brushExtents() {
        //  var extents = {};
        //  d3.keys(__.dimensions).forEach(function(d) {
        //    var brush = brushes[d];
        //    if (brush !== undefined && !brush.empty()) {
        //      var extent = brush.extent();
        //      extents[d] = extent;
        //    }
        //  });
        //  return extents;
        //}

        function brushFor(axis) {
            var brush = d3.svg.multibrush();

            brush
                .y(__.dimensions[axis].yscale)
                .on("brushstart", function() {
                    if(d3.event.sourceEvent !== null) {
                        events.brushstart.call(pc, __.brushed);
                        d3.event.sourceEvent.stopPropagation();
                    }
                })
                .on("brush", function() {
                    brushUpdated(selected());
                })
                .on("brushend", function() {
                    // d3.svg.multibrush clears extents just before calling 'brushend'
                    // so we have to update here again.
                    // This fixes issue #103 for now, but should be changed in d3.svg.multibrush
                    // to avoid unnecessary computation.
                    brushUpdated(selected());
                    events.brushend.call(pc, __.brushed);
                })
                .extentAdaption(function(selection) {
                    selection
                        .style("visibility", null)
                        .attr("x", -15)
                        .attr("width", 30)
                        .style("fill", "rgba(255,255,255,0.25)")
                        .style("stroke", "rgba(0,0,0,0.6)");
                })
                .resizeAdaption(function(selection) {
                    selection
                        .selectAll("rect")
                        .attr("x", -15)
                        .attr("width", 30)
                        .style("visibility", null)
                        .style("fill", "rgba(0,0,0,0.1)");
                });

            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.renderBrushed();
            }
            return this;
        };

        function install() {
            if (!g) pc.createAxes();

            // Add and store a brush for each axis.
            var brush = g.append("svg:g")
                .attr("class", "brush")
                .each(function(d) {
                    d3.select(this).call(brushFor(d));
                })

            brush.selectAll("rect")
                .style("visibility", null)
                .attr("x", -15)
                .attr("width", 30);

            brush.selectAll("rect.background")
                .style("fill", "transparent");

            brush.selectAll("rect.extent")
                .style("fill", "rgba(255,255,255,0.25)")
                .style("stroke", "rgba(0,0,0,0.6)");

            brush.selectAll(".resize rect")
                .style("fill", "rgba(0,0,0,0.1)");

            pc.brushExtents = brushExtents;
            pc.brushReset = brushReset;
            return pc;
        }

        brush.modes["1D-axes-multi"] = {
            install: install,
            uninstall: function() {
                g.selectAll(".brush").remove();
                brushes = {};
                delete pc.brushExtents;
                delete pc.brushReset;
            },
            selected: selected,
            brushState: brushExtents
        }
    })();
// brush mode: angular
// code based on 2D.strums.js

    (function() {
        var arcs = {},
            strumRect;

        function drawStrum(arc, activePoint) {
            var svg = pc.selection.select("svg").select("g#arcs"),
                id = arc.dims.i,
                points = [arc.p2, arc.p3],
                line = svg.selectAll("line#arc-" + id).data([{p1:arc.p1,p2:arc.p2},{p1:arc.p1,p2:arc.p3}]),
                circles = svg.selectAll("circle#arc-" + id).data(points),
                drag = d3.behavior.drag(),
                path = svg.selectAll("path#arc-" + id).data([arc]);

            path.enter()
                .append("path")
                .attr("id", "arc-" + id)
                .attr("class", "arc")
                .style("fill", "orange")
                .style("opacity", 0.5);

            path
                .attr("d", arc.arc)
                .attr("transform", "translate(" + arc.p1[0] + "," + arc.p1[1] + ")");

            line.enter()
                .append("line")
                .attr("id", "arc-" + id)
                .attr("class", "arc");

            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,
                        angle = 0;

                    i = i + 2;

                    arc["p" + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
                    arc["p" + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);

                    angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);

                    if ((arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) ||
                        (arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)) {

                        if (i === 2) {
                            arc.endAngle = angle;
                            arc.arc.endAngle(angle);
                        } else if (i === 3) {
                            arc.startAngle = angle;
                            arc.arc.startAngle(angle);
                        }

                    }

                    drawStrum(arc, i - 2);
                })
                .on("dragend", onDragEnd());

            circles.enter()
                .append("circle")
                .attr("id", "arc-" + id)
                .attr("class", "arc");

            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 };
            d3.keys(__.dimensions).some(function(dim, i) {
                if (xscale(dim) < p[0]) {
                    var next = d3.keys(__.dimensions)[pc.getOrderedDimensionKeys().indexOf(dim)+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 = pc.getOrderedDimensionKeys()[0];
                dims.right = pc.getOrderedDimensionKeys()[1];
            } else if (dims.right === undefined) {
                // Event on the right side of the last axis
                dims.i = d3.keys(__.dimensions).length - 1;
                dims.right = dims.left;
                dims.left = pc.getOrderedDimensionKeys()[d3.keys(__.dimensions).length - 2];
            }

            return dims;
        }

        function onDragStart() {
            // First we need to determine between which two axes the arc was started.
            // This will determine the freedom of movement, because a arc 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,
                    arc;

                p[0] = p[0] - __.margin.left;
                p[1] = p[1] - __.margin.top;

                dims = dimensionsForPoint(p),
                    arc = {
                        p1: p,
                        dims: dims,
                        minX: xscale(dims.left),
                        maxX: xscale(dims.right),
                        minY: 0,
                        maxY: h(),
                        startAngle: undefined,
                        endAngle: undefined,
                        arc: d3.svg.arc().innerRadius(0)
                    };

                arcs[dims.i] = arc;
                arcs.active = dims.i;

                // Make sure that the point is within the bounds
                arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
                arc.p2 = arc.p1.slice();
                arc.p3 = arc.p1.slice();
            };
        }

        function onDrag() {
            return function() {
                var ev = d3.event,
                    arc = arcs[arcs.active];

                // Make sure that the point is within the bounds
                arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX);
                arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY);
                arc.p3 = arc.p2.slice();
//      console.log(arcs.angle(arcs.active));
//      console.log(signedAngle(arcs.unsignedAngle(arcs.active)));
                drawStrum(arc, 1);
            };
        }

        // some helper functions
        function hypothenuse(a, b) {
            return Math.sqrt(a*a + b*b);
        }

        var rad = (function() {
            var c = Math.PI / 180;
            return function(angle) {
                return angle * c;
            };
        })();

        var deg = (function() {
            var c = 180 / Math.PI;
            return function(angle) {
                return angle * c;
            };
        })();

        // [0, 2*PI] -> [-PI/2, PI/2]
        var signedAngle = function(angle) {
            var ret = angle;
            if (angle > Math.PI) {
                ret = angle - 1.5 * Math.PI;
                ret = angle - 1.5 * Math.PI;
            } else {
                ret = angle - 0.5 * Math.PI;
                ret = angle - 0.5 * Math.PI;
            }
            return -ret;
        }

        /**
         * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
         * However, one can only select lines from 0 to PI, so we compute the
         * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
         * are 12 and 6 o'clock respectively.
         */
        function containmentTest(arc) {
            var startAngle = signedAngle(arc.startAngle);
            var endAngle = signedAngle(arc.endAngle);

            if (startAngle > endAngle) {
                var tmp = startAngle;
                startAngle = endAngle;
                endAngle = tmp;
            }

            // test if segment angle is contained in angle interval
            return function(a) {

                if (a >= startAngle && a <= endAngle) {
                    return true;
                }

                return false;
            };
        }

        function selected() {
            var ids = Object.getOwnPropertyNames(arcs),
                brushed = __.data;

            // Get the ids of the currently active arcs.
            ids = ids.filter(function(d) {
                return !isNaN(d);
            });

            function crossesStrum(d, id) {
                var arc = arcs[id],
                    test = containmentTest(arc),
                    d1 = arc.dims.left,
                    d2 = arc.dims.right,
                    y1 = __.dimensions[d1].yscale,
                    y2 = __.dimensions[d2].yscale,
                    a = arcs.width(id),
                    b = y1(d[d1]) - y2(d[d2]),
                    c = hypothenuse(a, b),
                    angle = Math.asin(b/c);	// rad in [-PI/2, PI/2]
                return test(angle);
            }

            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 new Error("Unknown brush predicate " + __.brushPredicate);
                }
            });
        }

        function removeStrum() {
            var arc = arcs[arcs.active],
                svg = pc.selection.select("svg").select("g#arcs");

            delete arcs[arcs.active];
            arcs.active = undefined;
            svg.selectAll("line#arc-" + arc.dims.i).remove();
            svg.selectAll("circle#arc-" + arc.dims.i).remove();
            svg.selectAll("path#arc-" + arc.dims.i).remove();
        }

        function onDragEnd() {
            return function() {
                var brushed = __.data,
                    arc = arcs[arcs.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 (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
                    removeStrum(arcs);
                }

                if (arc) {
                    var angle = arcs.startAngle(arcs.active);

                    arc.startAngle = angle;
                    arc.endAngle = angle;
                    arc.arc
                        .outerRadius(arcs.length(arcs.active))
                        .startAngle(angle)
                        .endAngle(angle);
                }

                brushed = selected(arcs);
                arcs.active = undefined;
                __.brushed = brushed;
                pc.renderBrushed();
                events.brushend.call(pc, __.brushed);
            };
        }

        function brushReset(arcs) {
            return function() {
                var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
                    return !isNaN(d);
                });

                ids.forEach(function(d) {
                    arcs.active = d;
                    removeStrum(arcs);
                });
                onDragEnd(arcs)();
            };
        }

        function install() {
            var drag = d3.behavior.drag();

            // Map of current arcs. arcs are stored per segment of the PC. A segment,
            // being the area between two axes. The left most area is indexed at 0.
            arcs.active = undefined;
            // Returns the width of the PC segment where currently a arc 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.
            arcs.width = function(id) {
                var arc = arcs[id];

                if (arc === undefined) {
                    return undefined;
                }

                return arc.maxX - arc.minX;
            };

            // returns angles in [-PI/2, PI/2]
            angle = function(p1, p2) {
                var a = p1[0] - p2[0],
                    b = p1[1] - p2[1],
                    c = hypothenuse(a, b);

                return Math.asin(b/c);
            }

            // returns angles in [0, 2 * PI]
            arcs.endAngle = function(id) {
                var arc = arcs[id];
                if (arc === undefined) {
                    return undefined;
                }
                var sAngle = angle(arc.p1, arc.p2),
                    uAngle = -sAngle + Math.PI / 2;

                if (arc.p1[0] > arc.p2[0]) {
                    uAngle = 2 * Math.PI - uAngle;
                }

                return uAngle;
            }

            arcs.startAngle = function(id) {
                var arc = arcs[id];
                if (arc === undefined) {
                    return undefined;
                }

                var sAngle = angle(arc.p1, arc.p3),
                    uAngle = -sAngle + Math.PI / 2;

                if (arc.p1[0] > arc.p3[0]) {
                    uAngle = 2 * Math.PI - uAngle;
                }

                return uAngle;
            }

            arcs.length = function(id) {
                var arc = arcs[id];

                if (arc === undefined) {
                    return undefined;
                }

                var a = arc.p1[0] - arc.p2[0],
                    b = arc.p1[1] - arc.p2[1],
                    c = hypothenuse(a, b);

                return(c);
            }

            pc.on("axesreorder.arcs", function() {
                var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
                    return !isNaN(d);
                });

                // Checks if the first dimension is directly left of the second dimension.
                function consecutive(first, second) {
                    var length = d3.keys(__.dimensions).length;
                    return d3.keys(__.dimensions).some(function(d, i) {
                        return (d === first)
                            ? i + i < length && __.dimensions[i + 1] === second
                            : false;
                    });
                }

                if (ids.length > 0) { // We have some arcs, which might need to be removed.
                    ids.forEach(function(d) {
                        var dims = arcs[d].dims;
                        arcs.active = d;
                        // If the two dimensions of the current arc are not next to each other
                        // any more, than we'll need to remove the arc. Otherwise we keep it.
                        if (!consecutive(dims.left, dims.right)) {
                            removeStrum(arcs);
                        }
                    });
                    onDragEnd(arcs)();
                }
            });

            // Add a new svg group in which we draw the arcs.
            pc.selection.select("svg").append("g")
                .attr("id", "arcs")
                .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");

            // Install the required brushReset function
            pc.brushReset = brushReset(arcs);

            drag
                .on("dragstart", onDragStart(arcs))
                .on("drag", onDrag(arcs))
                .on("dragend", onDragEnd(arcs));

            // 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#arcs")
                .attr("id", "arc-events")
                .attr("x", __.margin.left)
                .attr("y", __.margin.top)
                .attr("width", w())
                .attr("height", h() + 2)
                .style("opacity", 0)
                .call(drag);
        }

        brush.modes["angular"] = {
            install: install,
            uninstall: function() {
                pc.selection.select("svg").select("g#arcs").remove();
                pc.selection.select("svg").select("rect#arc-events").remove();
                pc.on("axesreorder.arcs", undefined);
                delete pc.brushReset;

                strumRect = undefined;
            },
            selected: selected,
            brushState: function () { return arcs; }
        };

    }());
    pc.interactive = function() {
        flags.interactive = true;
        return this;
    };

// expose a few objects
    pc.xscale = xscale;
    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.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.selectAll([canvas.foreground, canvas.brushed]).classed("faded", true);
        data.forEach(path_highlight);
        events.highlight.call(this, data);
        return this;
    };

// clear highlighting
    pc.unhighlight = function() {
        __.highlighted = [];
        pc.clear("highlight");
        d3.selectAll([canvas.foreground, canvas.brushed]).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) {
        if (xscale.range().length === 0) {
            xscale.rangePoints([0, w()], 1);
        }
        var v = dragging[d];
        return v == null ? xscale(d) : v;
    }

// Merges the canvases and SVG elements into one canvas element which is then passed into the callback
// (so you can choose to save it to disk, etc.)
    pc.mergeParcoords = function(callback) {
        // Retina display, etc.
        var devicePixelRatio = window.devicePixelRatio || 1;

        // Create a canvas element to store the merged canvases
        var mergedCanvas = document.createElement("canvas");
        mergedCanvas.width = pc.canvas.foreground.clientWidth * devicePixelRatio
        mergedCanvas.height = (pc.canvas.foreground.clientHeight + 30) * devicePixelRatio;
        mergedCanvas.style.width = mergedCanvas.width / devicePixelRatio + "px";
        mergedCanvas.style.height = mergedCanvas.height / devicePixelRatio + "px";

        // Give the canvas a white background
        var context = mergedCanvas.getContext("2d");
        context.fillStyle = "#ffffff";
        context.fillRect(0, 0, mergedCanvas.width, mergedCanvas.height);

        // Merge all the canvases
        for (var key in pc.canvas) {
            context.drawImage(pc.canvas[key], 0, 24 * devicePixelRatio, mergedCanvas.width, mergedCanvas.height - 30 * devicePixelRatio);
        }

        // Add SVG elements to canvas
        var DOMURL = window.URL || window.webkitURL || window;
        var serializer = new XMLSerializer();
        var svgStr = serializer.serializeToString(pc.selection.select("svg")[0][0]);

        // Create a Data URI.
        var src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);
        var img = new Image();
        img.onload = function () {
            context.drawImage(img, 0, 0, img.width * devicePixelRatio, img.height * devicePixelRatio);
            if (typeof callback === "function") {
                callback(mergedCanvas);
            }
        };
        img.src = src;
    }
    pc.version = "0.7.0";
    // this descriptive text should live with other introspective methods
    pc.toString = function() { return "Parallel Coordinates: " + d3.keys(__.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;
});

export default d3.parcoords;