//thanks http://dexvis.com/vis/blog/2013/mar/reusable6/html/ParallelCoordinates3.html and http://syntagmatic.github.com/parallel-coordinates/ ParallelCoordinates.prototype = new DexComponent(); ParallelCoordinates.constructor = ParallelCoordinates; function ParallelCoordinates(userConfig) { DexComponent.call(this, userConfig, { 'id' : "ParallelCoordinates", 'class' : "ParallelCoordinates", 'parent' : null, 'height' : 400, 'width' : 600, 'opacity' : 100, 'strokeWidth' : 4, 'axisFontSize' : 16, 'fontSize' : 12, 'color' : d3.scale.category20(), 'title' : '', 'columnNames' : [ "X", "Y" ], 'data' : [[0,0],[1,1],[2,4],[3,9],[4,16]], 'rows' : 0, 'columns' : [], 'xoffset' : 0, 'yoffset' : 0, 'normalize' : false, 'axisLabels' : { 'stagger' : false, 'yoffset' : -9, 'staggeredYOffset' : -18 } }); // Ugly, but my JavaScript is weak. When in handler functions // this seems to be the only way to get linked back to the // this.x variables. this.chart = this; } ParallelCoordinates.prototype.render = function() { this.update(); }; ParallelCoordinates.prototype.update = function () { // If we need to call super: //DexComponent.prototype.update.call(this); var chart = this.chart; var config = this.config; var numericColumns = dex.csv.getNumericColumnNames(config.columnNames, config.data); var jsonData = dex.csv.toJson(config.columnNames, config.data); var x = d3.scale.ordinal() .rangePoints([0, config.width], 1); var y = {}; var line = d3.svg.line(); var axis = d3.svg.axis().orient("left"); var background; var foreground; var chartContainer = config.parent.append("g") .attr("id", config["id"]) .attr("class", config["class"]) .attr("transform", "translate(" + config.xoffset + "," + config.yoffset + ")"); // Extract the list of dimensions and create a scale for each. //x.domain(dimensions = d3.keys(cars[0]).filter(function(d) //{ // return d != "name" && (y[d] = d3.scale.linear() // .domain(d3.extent(cars, function(p) { return +p[d]; })) // .range([height, 0])); //})); var allExtents = [] numericColumns.forEach(function (d) { allExtents = allExtents.concat(d3.extent(jsonData, function (p) { return +p[d]; })); }); var normalizedExtent = d3.extent(allExtents); //console.log("NE: " + normalizedExtent); // REM: Figure out how to switch over to consistent extends. Snapping. x.domain(dimensions = d3.keys(jsonData[0]).filter(function (d) { if (d === "name") return false; if (contains(numericColumns, d)) { var extent = d3.extent(jsonData, function (p) { return +p[d]; }); if (config.normalize) { extent = normalizedExtent; } y[d] = d3.scale.linear() .domain(extent) .range([config.height, 0]); allExtents.concat(extent); } else { y[d] = d3.scale.ordinal() .domain(jsonData.map(function (p) { return p[d]; })) .rangePoints([config.height, 0]); } return true; })); // Add grey background lines for context. background = chartContainer.append("g") .attr("class", "background") .selectAll("path") .data(jsonData) .enter().append("path") .attr("d", path) .attr("id", "fillpath"); foreground = chartContainer.append("g") .attr("fill", "none") .attr("stroke-opacity", config.opacity / 100.0) .selectAll("path") .data(jsonData) .enter().append("path") .attr("d", path) .attr("stroke", function (d, i) { return config.color(i); }) .attr("stroke-width", config.strokeWidth) .attr("title", function (d, i) { var info = ""; for (var key in jsonData[i]) { info += "" } return info + "
" + key + "" + jsonData[i][key] + "
"; }) .on("mouseover", function () { d3.select(this) .style("stroke-width", config.strokeWidth + (config.strokeWidth / 3)) .style("stroke-opacity", config.opacity / 100.0); }) .on("mouseout", function () { d3.select(this) .style("stroke-width", config.strokeWidth) .style("stroke-opacity", config.opacity / 100); }); // Original random colors: // .attr("stroke", function(d) { return '#'+Math.floor(Math.random()*16777215).toString(16); }); // Add a group element for each dimension. var g = chartContainer.selectAll(".dimension") .data(dimensions) .enter().append("g") .attr("font-size", config.fontSize) .attr("class", "dimension") .attr("transform", function (d) { return "translate(" + x(d) + ")"; }); // Add an axis and title. g.append("g") .attr("class", "axis") .each(function (d) { d3.select(this).call(axis.scale(y[d])); }) .append("text") .attr("text-anchor", "middle") .attr("y", function (d, i) { if (config.axisLabels.stagger) { if (i % 2 == 1) { return config.axisLabels.staggeredYOffset; } } return config.axisLabels.yoffset; }) .attr("font-size", config.axisFontSize) .text(String); // Add and store a brush for each axis. g.append("g") .attr("class", "brush") .each(function (d) { d3.select(this).call(y[d].brush = d3.svg.brush().y(y[d]).on("brush", brush)); }) .selectAll("rect") .attr("x", -8) .attr("width", 16); // Returns the path for a given data point. function path(d) { return line(dimensions.map(function (p) { return [x(p), y[p](d[p])]; })); } // Handles a brush event, toggling the display of foreground lines. function brush() { var actives = dimensions.filter(function (p) { return !y[p].brush.empty(); }), extents = actives.map(function (p) { return y[p].brush.extent(); }); foreground.style("display", function (d) { return actives.every( function (p, i) { var displayYesOrNo; // Categorical if (!contains(numericColumns, p)) { displayYesOrNo = extents[i][0] <= y[p](d[p]) && y[p](d[p]) <= extents[i][1]; } // Numeric else { displayYesOrNo = extents[i][0] <= d[p] && d[p] <= extents[i][1]; } //probably not the best place for it but loop through all the states and change the color to green if selected allstates = d3.select("#StateMap").selectAll("path") allstates.data().forEach(function (dd, ii) { dd.id == d.State ? d3.select(allstates[0][ii]).attr("opacity", displayYesOrNo ? 1 : 0.3) : null }); return displayYesOrNo; }) ? null : "none"; }); actives.length == 0 ? d3.select("#StateMap").selectAll("path").attr("opacity",1) : null; } }; function contains(a, obj) { var i = a.length; while (i--) { if (a[i] === obj) { return true; } } return false; }