UNPKG

nvd3-fork

Version:

FORK! of NVD3, a reusable charting library written in d3.js

492 lines (439 loc) 23.6 kB
// Code adapted from Jason Davies' "Parallel Coordinates" // http://bl.ocks.org/jasondavies/1341281 nv.models.parallelCoordinates = function() { "use strict"; //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var margin = {top: 30, right: 0, bottom: 10, left: 0} , width = null , height = null , availableWidth = null , availableHeight = null , x = d3.scale.ordinal() , y = {} , undefinedValuesLabel = "undefined values" , dimensionData = [] , enabledDimensions = [] , dimensionNames = [] , displayBrush = true , color = nv.utils.defaultColor() , filters = [] , active = [] , dragging = [] , axisWithUndefinedValues = [] , lineTension = 1 , foreground , background , dimensions , line = d3.svg.line() , axis = d3.svg.axis() , dispatch = d3.dispatch('brushstart', 'brush', 'brushEnd', 'dimensionsOrder', "stateChange", 'elementClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd', 'activeChanged') ; //============================================================ // Private Variables //------------------------------------------------------------ var renderWatch = nv.utils.renderWatch(dispatch); function chart(selection) { renderWatch.reset(); selection.each(function(data) { var container = d3.select(this); availableWidth = nv.utils.availableWidth(width, container, margin); availableHeight = nv.utils.availableHeight(height, container, margin); nv.utils.initSVG(container); //Convert old data to new format (name, values) if (data[0].values === undefined) { var newData = []; data.forEach(function (d) { var val = {}; var key = Object.keys(d); key.forEach(function (k) { if (k !== "name") val[k] = d[k] }); newData.push({ key: d.name, values: val }); }); data = newData; } var dataValues = data.map(function (d) {return d.values}); if (active.length === 0) { active = data; }; //set all active before first brush call dimensionNames = dimensionData.sort(function (a, b) { return a.currentPosition - b.currentPosition; }).map(function (d) { return d.key }); enabledDimensions = dimensionData.filter(function (d) { return !d.disabled; }); // Setup Scales x.rangePoints([0, availableWidth], 1).domain(enabledDimensions.map(function (d) { return d.key; })); //Set as true if all values on an axis are missing. // Extract the list of dimensions and create a scale for each. var oldDomainMaxValue = {}; var displayMissingValuesline = false; var currentTicks = []; dimensionNames.forEach(function(d) { var extent = d3.extent(dataValues, function (p) { return +p[d]; }); var min = extent[0]; var max = extent[1]; var onlyUndefinedValues = false; //If there is no values to display on an axis, set the extent to 0 if (isNaN(min) || isNaN(max)) { onlyUndefinedValues = true; min = 0; max = 0; } //Scale axis if there is only one value if (min === max) { min = min - 1; max = max + 1; } var f = filters.filter(function (k) { return k.dimension == d; }); if (f.length !== 0) { //If there is only NaN values, keep the existing domain. if (onlyUndefinedValues) { min = y[d].domain()[0]; max = y[d].domain()[1]; } //If the brush extent is > max (< min), keep the extent value. else if (!f[0].hasOnlyNaN && displayBrush) { min = min > f[0].extent[0] ? f[0].extent[0] : min; max = max < f[0].extent[1] ? f[0].extent[1] : max; } //If there is NaN values brushed be sure the brush extent is on the domain. else if (f[0].hasNaN) { max = max < f[0].extent[1] ? f[0].extent[1] : max; oldDomainMaxValue[d] = y[d].domain()[1]; displayMissingValuesline = true; } } //Use 90% of (availableHeight - 12) for the axis range, 12 reprensenting the space necessary to display "undefined values" text. //The remaining 10% are used to display the missingValue line. y[d] = d3.scale.linear() .domain([min, max]) .range([(availableHeight - 12) * 0.9, 0]); axisWithUndefinedValues = []; y[d].brush = d3.svg.brush().y(y[d]).on('brushstart', brushstart).on('brush', brush).on('brushend', brushend); }); // Setup containers and skeleton of chart var wrap = container.selectAll('g.nv-wrap.nv-parallelCoordinates').data([data]); var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-parallelCoordinates'); var gEnter = wrapEnter.append('g'); var g = wrap.select('g'); gEnter.append('g').attr('class', 'nv-parallelCoordinates background'); gEnter.append('g').attr('class', 'nv-parallelCoordinates foreground'); gEnter.append('g').attr('class', 'nv-parallelCoordinates missingValuesline'); wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); line.interpolate('cardinal').tension(lineTension); axis.orient('left'); var axisDrag = d3.behavior.drag() .on('dragstart', dragStart) .on('drag', dragMove) .on('dragend', dragEnd); //Add missing value line at the bottom of the chart var missingValuesline, missingValueslineText; var step = x.range()[1] - x.range()[0]; step = isNaN(step) ? x.range()[0] : step; if (!isNaN(step)) { var lineData = [0 + step / 2, availableHeight - 12, availableWidth - step / 2, availableHeight - 12]; missingValuesline = wrap.select('.missingValuesline').selectAll('line').data([lineData]); missingValuesline.enter().append('line'); missingValuesline.exit().remove(); missingValuesline.attr("x1", function(d) { return d[0]; }) .attr("y1", function(d) { return d[1]; }) .attr("x2", function(d) { return d[2]; }) .attr("y2", function(d) { return d[3]; }); //Add the text "undefined values" under the missing value line missingValueslineText = wrap.select('.missingValuesline').selectAll('text').data([undefinedValuesLabel]); missingValueslineText.append('text').data([undefinedValuesLabel]); missingValueslineText.enter().append('text'); missingValueslineText.exit().remove(); missingValueslineText.attr("y", availableHeight) //To have the text right align with the missingValues line, substract 92 representing the text size. .attr("x", availableWidth - 92 - step / 2) .text(function(d) { return d; }); } // Add grey background lines for context. background = wrap.select('.background').selectAll('path').data(data); background.enter().append('path'); background.exit().remove(); background.attr('d', path); // Add blue foreground lines for focus. foreground = wrap.select('.foreground').selectAll('path').data(data); foreground.enter().append('path') foreground.exit().remove(); foreground.attr('d', path) .style("stroke-width", function (d, i) { if (isNaN(d.strokeWidth)) { d.strokeWidth = 1;} return d.strokeWidth;}) .attr('stroke', function (d, i) { return d.color || color(d, i); }); foreground.on("mouseover", function (d, i) { d3.select(this).classed('hover', true).style("stroke-width", d.strokeWidth + 2 + "px").style("stroke-opacity", 1); dispatch.elementMouseover({ label: d.name, color: d.color || color(d, i), values: d.values, dimensions: enabledDimensions }); }); foreground.on("mouseout", function (d, i) { d3.select(this).classed('hover', false).style("stroke-width", d.strokeWidth + "px").style("stroke-opacity", 0.7); dispatch.elementMouseout({ label: d.name, index: i }); }); foreground.on('mousemove', function (d, i) { dispatch.elementMousemove(); }); foreground.on('click', function (d) { dispatch.elementClick({ id: d.id }); }); // Add a group element for each dimension. dimensions = g.selectAll('.dimension').data(enabledDimensions); var dimensionsEnter = dimensions.enter().append('g').attr('class', 'nv-parallelCoordinates dimension'); dimensions.attr('transform', function(d) { return 'translate(' + x(d.key) + ',0)'; }); dimensionsEnter.append('g').attr('class', 'nv-axis'); // Add an axis and title. dimensionsEnter.append('text') .attr('class', 'nv-label') .style("cursor", "move") .attr('dy', '-1em') .attr('text-anchor', 'middle') .on("mouseover", function(d, i) { dispatch.elementMouseover({ label: d.tooltip || d.key, color: d.color }); }) .on("mouseout", function(d, i) { dispatch.elementMouseout({ label: d.tooltip }); }) .on('mousemove', function (d, i) { dispatch.elementMousemove(); }) .call(axisDrag); dimensionsEnter.append('g').attr('class', 'nv-brushBackground'); dimensions.exit().remove(); dimensions.select('.nv-label').text(function (d) { return d.key }); // Add and store a brush for each axis. restoreBrush(displayBrush); var actives = dimensionNames.filter(function (p) { return !y[p].brush.empty(); }), extents = actives.map(function (p) { return y[p].brush.extent(); }); var formerActive = active.slice(0); //Restore active values active = []; foreground.style("display", function (d) { var isActive = actives.every(function (p, i) { if ((isNaN(d.values[p]) || isNaN(parseFloat(d.values[p]))) && extents[i][0] == y[p].brush.y().domain()[0]) { return true; } return (extents[i][0] <= d.values[p] && d.values[p] <= extents[i][1]) && !isNaN(parseFloat(d.values[p])); }); if (isActive) active.push(d); return !isActive ? "none" : null; }); if (filters.length > 0 || !nv.utils.arrayEquals(active, formerActive)) { dispatch.activeChanged(active); } // Returns the path for a given data point. function path(d) { return line(enabledDimensions.map(function (p) { //If value if missing, put the value on the missing value line if (isNaN(d.values[p.key]) || isNaN(parseFloat(d.values[p.key])) || displayMissingValuesline) { var domain = y[p.key].domain(); var range = y[p.key].range(); var min = domain[0] - (domain[1] - domain[0]) / 9; //If it's not already the case, allow brush to select undefined values if (axisWithUndefinedValues.indexOf(p.key) < 0) { var newscale = d3.scale.linear().domain([min, domain[1]]).range([availableHeight - 12, range[1]]); y[p.key].brush.y(newscale); axisWithUndefinedValues.push(p.key); } if (isNaN(d.values[p.key]) || isNaN(parseFloat(d.values[p.key]))) { return [x(p.key), y[p.key](min)]; } } //If parallelCoordinate contain missing values show the missing values line otherwise, hide it. if (missingValuesline !== undefined) { if (axisWithUndefinedValues.length > 0 || displayMissingValuesline) { missingValuesline.style("display", "inline"); missingValueslineText.style("display", "inline"); } else { missingValuesline.style("display", "none"); missingValueslineText.style("display", "none"); } } return [x(p.key), y[p.key](d.values[p.key])]; })); } function restoreBrush(visible) { filters.forEach(function (f) { //If filter brushed NaN values, keep the brush on the bottom of the axis. var brushDomain = y[f.dimension].brush.y().domain(); if (f.hasOnlyNaN) { f.extent[1] = (y[f.dimension].domain()[1] - brushDomain[0]) * (f.extent[1] - f.extent[0]) / (oldDomainMaxValue[f.dimension] - f.extent[0]) + brushDomain[0]; } if (f.hasNaN) { f.extent[0] = brushDomain[0]; } if (visible) y[f.dimension].brush.extent(f.extent); }); dimensions.select('.nv-brushBackground') .each(function (d) { d3.select(this).call(y[d.key].brush); }) .selectAll('rect') .attr('x', -8) .attr('width', 16); updateTicks(); } // Handles a brush event, toggling the display of foreground lines. function brushstart() { //If brush aren't visible, show it before brushing again. if (displayBrush === false) { displayBrush = true; restoreBrush(true); } } // Handles a brush event, toggling the display of foreground lines. function brush() { actives = dimensionNames.filter(function (p) { return !y[p].brush.empty(); }); extents = actives.map(function(p) { return y[p].brush.extent(); }); filters = []; //erase current filters actives.forEach(function(d,i) { filters[i] = { dimension: d, extent: extents[i], hasNaN: false, hasOnlyNaN: false } }); active = []; //erase current active list foreground.style('display', function(d) { var isActive = actives.every(function(p, i) { if ((isNaN(d.values[p]) || isNaN(parseFloat(d.values[p]))) && extents[i][0] == y[p].brush.y().domain()[0]) return true; return (extents[i][0] <= d.values[p] && d.values[p] <= extents[i][1]) && !isNaN(parseFloat(d.values[p])); }); if (isActive) active.push(d); return isActive ? null : 'none'; }); updateTicks(); dispatch.brush({ filters: filters, active: active }); } function brushend() { var hasActiveBrush = actives.length > 0 ? true : false; filters.forEach(function (f) { if (f.extent[0] === y[f.dimension].brush.y().domain()[0] && axisWithUndefinedValues.indexOf(f.dimension) >= 0) f.hasNaN = true; if (f.extent[1] < y[f.dimension].domain()[0]) f.hasOnlyNaN = true; }); dispatch.brushEnd(active, hasActiveBrush); } function updateTicks() { dimensions.select('.nv-axis') .each(function (d, i) { var f = filters.filter(function (k) { return k.dimension == d.key; }); currentTicks[d.key] = y[d.key].domain(); //If brush are available, display brush extent if (f.length != 0 && displayBrush) { currentTicks[d.key] = []; if (f[0].extent[1] > y[d.key].domain()[0]) currentTicks[d.key] = [f[0].extent[1]]; if (f[0].extent[0] >= y[d.key].domain()[0]) currentTicks[d.key].push(f[0].extent[0]); } d3.select(this).call(axis.scale(y[d.key]).tickFormat(d.format).tickValues(currentTicks[d.key])); }); } function dragStart(d) { dragging[d.key] = this.parentNode.__origin__ = x(d.key); background.attr("visibility", "hidden"); } function dragMove(d) { dragging[d.key] = Math.min(availableWidth, Math.max(0, this.parentNode.__origin__ += d3.event.x)); foreground.attr("d", path); enabledDimensions.sort(function (a, b) { return dimensionPosition(a.key) - dimensionPosition(b.key); }); enabledDimensions.forEach(function (d, i) { return d.currentPosition = i; }); x.domain(enabledDimensions.map(function (d) { return d.key; })); dimensions.attr("transform", function(d) { return "translate(" + dimensionPosition(d.key) + ")"; }); } function dragEnd(d, i) { delete this.parentNode.__origin__; delete dragging[d.key]; d3.select(this.parentNode).attr("transform", "translate(" + x(d.key) + ")"); foreground .attr("d", path); background .attr("d", path) .attr("visibility", null); dispatch.dimensionsOrder(enabledDimensions); } function dimensionPosition(d) { var v = dragging[d]; return v == null ? x(d) : v; } }); return chart; } //============================================================ // Expose Public Variables //------------------------------------------------------------ chart.dispatch = dispatch; chart.options = nv.utils.optionsFunc.bind(chart); chart._options = Object.create({}, { // simple options, just get/set the necessary values width: {get: function(){return width;}, set: function(_){width= _;}}, height: {get: function(){return height;}, set: function(_){height= _;}}, dimensionData: { get: function () { return dimensionData; }, set: function (_) { dimensionData = _; } }, displayBrush: { get: function () { return displayBrush; }, set: function (_) { displayBrush = _; } }, filters: { get: function () { return filters; }, set: function (_) { filters = _; } }, active: { get: function () { return active; }, set: function (_) { active = _; } }, lineTension: {get: function(){return lineTension;}, set: function(_){lineTension = _;}}, undefinedValuesLabel : {get: function(){return undefinedValuesLabel;}, set: function(_){undefinedValuesLabel=_;}}, // deprecated options dimensions: {get: function () { return dimensionData.map(function (d){return d.key}); }, set: function (_) { // deprecated after 1.8.1 nv.deprecated('dimensions', 'use dimensionData instead'); if (dimensionData.length === 0) { _.forEach(function (k) { dimensionData.push({ key: k }) }) } else { _.forEach(function (k, i) { dimensionData[i].key= k }) } }}, dimensionNames: {get: function () { return dimensionData.map(function (d){return d.key}); }, set: function (_) { // deprecated after 1.8.1 nv.deprecated('dimensionNames', 'use dimensionData instead'); dimensionNames = []; if (dimensionData.length === 0) { _.forEach(function (k) { dimensionData.push({ key: k }) }) } else { _.forEach(function (k, i) { dimensionData[i].key = k }) } }}, dimensionFormats: {get: function () { return dimensionData.map(function (d) { return d.format }); }, set: function (_) { // deprecated after 1.8.1 nv.deprecated('dimensionFormats', 'use dimensionData instead'); if (dimensionData.length === 0) { _.forEach(function (f) { dimensionData.push({ format: f }) }) } else { _.forEach(function (f, i) { dimensionData[i].format = f }) } }}, // options that require extra logic in the setter margin: {get: function(){return margin;}, set: function(_){ margin.top = _.top !== undefined ? _.top : margin.top; margin.right = _.right !== undefined ? _.right : margin.right; margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; margin.left = _.left !== undefined ? _.left : margin.left; }}, color: {get: function(){return color;}, set: function(_){ color = nv.utils.getColor(_); }} }); nv.utils.initOptions(chart); return chart; };