nv.models.legend = function() { "use strict"; //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var margin = {top: 5, right: 0, bottom: 5, left: 0} , width = 400 , height = 20 , getKey = function(d) { return d.key } , color = nv.utils.defaultColor() , align = true , rightAlign = true , updateState = true //If true, legend will update data.disabled and trigger a 'stateChange' dispatch. , radioButtonMode = false //If true, clicking legend items will cause it to behave like a radio button. (only one can be selected at a time) , dispatch = d3.dispatch('legendClick', 'legendDblclick', 'legendMouseover', 'legendMouseout', 'stateChange') ; //============================================================ function chart(selection) { selection.each(function(data) { var availableWidth = width - margin.left - margin.right, container = d3.select(this); //------------------------------------------------------------ // Setup containers and skeleton of chart var wrap = container.selectAll('g.nv-legend').data([data]); var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-legend').append('g'); var g = wrap.select('g'); wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); //------------------------------------------------------------ var series = g.selectAll('.nv-series') .data(function(d) { return d }); var seriesEnter = series.enter().append('g').attr('class', 'nv-series') .on('mouseover', function(d,i) { dispatch.legendMouseover(d,i); //TODO: Make consistent with other event objects }) .on('mouseout', function(d,i) { dispatch.legendMouseout(d,i); }) .on('click', function(d,i) { dispatch.legendClick(d,i); if (updateState) { if (radioButtonMode) { //Radio button mode: set every series to disabled, // and enable the clicked series. data.forEach(function(series) { series.disabled = true}); d.disabled = false; } else { d.disabled = !d.disabled; if (data.every(function(series) { return series.disabled})) { //the default behavior of NVD3 legends is, if every single series // is disabled, turn all series' back on. data.forEach(function(series) { series.disabled = false}); } } dispatch.stateChange({ disabled: data.map(function(d) { return !!d.disabled }) }); } }) .on('dblclick', function(d,i) { dispatch.legendDblclick(d,i); if (updateState) { //the default behavior of NVD3 legends, when double clicking one, // is to set all other series' to false, and make the double clicked series enabled. data.forEach(function(series) { series.disabled = true; }); d.disabled = false; dispatch.stateChange({ disabled: data.map(function(d) { return !!d.disabled }) }); } }); seriesEnter.append('circle') .style('stroke-width', 2) .attr('class','nv-legend-symbol') .attr('r', 5); seriesEnter.append('text') .attr('text-anchor', 'start') .attr('class','nv-legend-text') .attr('dy', '.32em') .attr('dx', '8'); series.classed('disabled', function(d) { return d.disabled }); series.exit().remove(); series.select('circle') .style('fill', function(d,i) { return d.color || color(d,i)}) .style('stroke', function(d,i) { return d.color || color(d, i) }); series.select('text').text(getKey); //TODO: implement fixed-width and max-width options (max-width is especially useful with the align option) // NEW ALIGNING CODE, TODO: clean up if (align) { var seriesWidths = []; series.each(function(d,i) { var legendText = d3.select(this).select('text'); var nodeTextLength; try { nodeTextLength = legendText.node().getComputedTextLength(); } catch(e) { nodeTextLength = nv.utils.calcApproxTextWidth(legendText); } seriesWidths.push(nodeTextLength + 28); // 28 is ~ the width of the circle plus some padding }); var seriesPerRow = 0; var legendWidth = 0; var columnWidths = []; while ( legendWidth < availableWidth && seriesPerRow < seriesWidths.length) { columnWidths[seriesPerRow] = seriesWidths[seriesPerRow]; legendWidth += seriesWidths[seriesPerRow++]; } if (seriesPerRow === 0) seriesPerRow = 1; //minimum of one series per row while ( legendWidth > availableWidth && seriesPerRow > 1 ) { columnWidths = []; seriesPerRow--; for (var k = 0; k < seriesWidths.length; k++) { if (seriesWidths[k] > (columnWidths[k % seriesPerRow] || 0) ) columnWidths[k % seriesPerRow] = seriesWidths[k]; } legendWidth = columnWidths.reduce(function(prev, cur, index, array) { return prev + cur; }); } var xPositions = []; for (var i = 0, curX = 0; i < seriesPerRow; i++) { xPositions[i] = curX; curX += columnWidths[i]; } series .attr('transform', function(d, i) { return 'translate(' + xPositions[i % seriesPerRow] + ',' + (5 + Math.floor(i / seriesPerRow) * 20) + ')'; }); //position legend as far right as possible within the total width if (rightAlign) { g.attr('transform', 'translate(' + (width - margin.right - legendWidth) + ',' + margin.top + ')'); } else { g.attr('transform', 'translate(0' + ',' + margin.top + ')'); } height = margin.top + margin.bottom + (Math.ceil(seriesWidths.length / seriesPerRow) * 20); } else { var ypos = 5, newxpos = 5, maxwidth = 0, xpos; series .attr('transform', function(d, i) { var length = d3.select(this).select('text').node().getComputedTextLength() + 28; xpos = newxpos; if (width < margin.left + margin.right + xpos + length) { newxpos = xpos = 5; ypos += 20; } newxpos += length; if (newxpos > maxwidth) maxwidth = newxpos; return 'translate(' + xpos + ',' + ypos + ')'; }); //position legend as far right as possible within the total width g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')'); height = margin.top + margin.bottom + ypos + 15; } }); return chart; } //============================================================ // Expose Public Variables //------------------------------------------------------------ chart.dispatch = dispatch; chart.options = nv.utils.optionsFunc.bind(chart); chart.margin = function(_) { if (!arguments.length) return margin; margin.top = typeof _.top != 'undefined' ? _.top : margin.top; margin.right = typeof _.right != 'undefined' ? _.right : margin.right; margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; margin.left = typeof _.left != 'undefined' ? _.left : margin.left; return chart; }; chart.width = function(_) { if (!arguments.length) return width; width = _; return chart; }; chart.height = function(_) { if (!arguments.length) return height; height = _; return chart; }; chart.key = function(_) { if (!arguments.length) return getKey; getKey = _; return chart; }; chart.color = function(_) { if (!arguments.length) return color; color = nv.utils.getColor(_); return chart; }; chart.align = function(_) { if (!arguments.length) return align; align = _; return chart; }; chart.rightAlign = function(_) { if (!arguments.length) return rightAlign; rightAlign = _; return chart; }; chart.updateState = function(_) { if (!arguments.length) return updateState; updateState = _; return chart; }; chart.radioButtonMode = function(_) { if (!arguments.length) return radioButtonMode; radioButtonMode = _; return chart; }; //============================================================ return chart; }