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