d3v3.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.");
    d3v3.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 = d3v3.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 = d3v3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "brushstart", "axesreorder"].concat(d3v3.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 = d3v3.scale.ordinal(),
    dragging = {},
    line = d3v3.svg.line(),
    axis = d3v3.svg.axis().orient("left").ticks(5),
    g, // groups for axes, brushes
    ctx = {},
    canvas = {},
    clusterCentroids = [];

// side effects for setters
var side_effects = d3v3.dispatch.apply(this,d3v3.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(d3v3.keys(d.value));
    xscale.domain(pc.getOrderedDimensionKeys());
    pc.sortDimensions();
    if (flags.interactive){pc.render().updateAxes();}
  })
  .on("bundleDimension", function(d) {
      if (!d3v3.keys(__.dimensions).length) pc.detectDimensions();
      pc.autoscale();
      if (typeof d.value === "number") {
          if (d.value < d3v3.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
d3v3.rebind(pc, events, "on");

// getter/setter with event firing
function getset(obj,state,events)  {
  d3v3.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 = d3v3.extent(__.data, function(d) {
        return d[k] ? d[k].getTime() : null;
      });

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

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

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

      return d3v3.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 d3v3.scale.ordinal()
        .domain(domain)
        .rangePoints(getRange());
    }
  };

  d3v3.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 (!d3v3.keys(__.dimensions).length) {
    pc.detectDimensions()
  }
  pc.autoscale();

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

  if (global) {
    var extent = d3v3.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(d3v3.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 : d3v3.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 d3v3.keys(__.dimensions).sort(function(x, y){
    return d3v3.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 = {};
  d3v3.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 (!d3v3.keys(__.dimensions).length) {
    pc.detectDimensions()
  }
  pc.autoscale();

  pc.render[__.mode]();

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

pc.renderBrushed = function() {
  if (!d3v3.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 = d3v3.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 = d3v3.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 = d3v3.map();
	var clusterCounts = d3v3.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) {
		d3v3.keys(__.dimensions).map(function(p, i) {
			var scaled = __.dimensions[d].yscale(row[d]);
			if (!clusterCentroids.has(scaled)) {
				var map = d3v3.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 = d3v3.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 = d3v3.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 = d3v3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]);
	__.data.forEach(function(d) {
		d3v3.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) {
	d3v3.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 = d3v3.functor(__.brushedColor)(d, i);
  } else {
    ctx.brushed.strokeStyle = d3v3.functor(__.color)(d, i);
  }
  return color_path(d, ctx.brushed)
};

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

function path_highlight(d, i) {
  ctx.highlight.strokeStyle = d3v3.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;
};
d3v3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");

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

  pc.flip(dimension);

  d3v3.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 = d3v3.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 + ")");
  d3v3.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 = d3v3.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 = d3v3.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) { d3v3.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) { d3v3.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(d3v3.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__ += d3v3.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];
        d3v3.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 = d3v3.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 = d3v3.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 "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 "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 = d3v3.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 "Unknown brush predicate " + __.brushPredicate;
        }
      });
  };

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

			});

			// loop over each dimension and update appropriately (if it was passed in through extents)
			d3v3.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 = d3v3.svg.brush();

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

  function dimensionsForPoint(p) {
    var dims = { i: -1, left: undefined, right: undefined };
    d3v3.keys(__.dimensions).some(function(dim, i) {
      if (xscale(dim) < p[0]) {
        var next = d3v3.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 = d3v3.keys(__.dimensions).length - 1;
      dims.right = dims.left;
      dims.left = pc.getOrderedDimensionKeys()[d3v3.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 = d3v3.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 = d3v3.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 "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 = d3v3.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 = d3v3.keys(__.dimensions).length;
        return d3v3.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 d3v3.svg.multibrush

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

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

  // data within extents
  function selected() {
    var actives = d3v3.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 "Unknown brush predicate " + __.brushPredicate;
      }
    });
  };

  function brushExtents(extents) {
    if (typeof(extents) === 'undefined') {
      extents = {};
      d3v3.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] = d3v3.select(this);
          });

      // loop over each dimension and update appropriately (if it was passed in through extents)
      d3v3.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 = {};
  //  d3v3.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 = d3v3.svg.multibrush();

    brush
      .y(__.dimensions[axis].yscale)
      .on("brushstart", function() {
				if(d3v3.event.sourceEvent !== null) {
                    events.brushstart.call(pc, __.brushed);
					d3v3.event.sourceEvent.stopPropagation();
				}
      })
      .on("brush", function() {
        brushUpdated(selected());
      })
      .on("brushend", function() {
    	// d3v3.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 d3v3.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) {
          d3v3.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) {
        d3v3.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 = d3v3.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 = d3v3.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() {
        d3v3.select(this).style("opacity", 0.8);
      })
      .on("mouseout", function() {
        d3v3.select(this).style("opacity", 0);
      })
      .call(drag);
  }

  function dimensionsForPoint(p) {
    var dims = { i: -1, left: undefined, right: undefined };
    d3v3.keys(__.dimensions).some(function(dim, i) {
      if (xscale(dim) < p[0]) {
        var next = d3v3.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 = d3v3.keys(__.dimensions).length - 1;
      dims.right = dims.left;
      dims.left = pc.getOrderedDimensionKeys()[d3v3.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 = d3v3.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: d3v3.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 = d3v3.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 "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 = d3v3.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 = d3v3.keys(__.dimensions).length;
        return d3v3.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");
  d3v3.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");
  d3v3.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: " + d3v3.keys(__.dimensions).length + " dimensions (" + d3v3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };

  return pc;
};

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

    d3v3.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;
});
