nv.models.multiBarHorizontal = function() { var margin = {top: 0, right: 0, bottom: 0, left: 0}, width = 960, height = 500, id = Math.floor(Math.random() * 10000), //Create semi-unique ID in case user doesn't select one x = d3.scale.ordinal(), y = d3.scale.linear(), getX = function(d) { return d.x }, getY = function(d) { return d.y }, forceY = [0], // 0 is forced by default.. this makes sense for the majority of bar graphs... user can always do chart.forceY([]) to remove color = d3.scale.category20().range(), stacked = false, showValues = false, valuePadding = 60, valueFormat = d3.format(',.2f'), delay = 1200, xDomain, yDomain, x0, y0; var dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout'); function chart(selection) { selection.each(function(data) { var availableWidth = width - margin.left - margin.right, availableHeight = height - margin.top - margin.bottom; if (stacked) data = d3.layout.stack() .offset('zero') .values(function(d){ return d.values }) .y(getY) (data); //add series index to each data point for reference data = data.map(function(series, i) { series.values = series.values.map(function(point) { point.series = i; return point; }); return series; }); var seriesData = (xDomain && yDomain) ? [] : // if we know xDomain and yDomain, no need to calculate data.map(function(d) { return d.values.map(function(d,i) { return { x: getX(d,i), y: getY(d,i), y0: d.y0 } }) }); x .domain(xDomain || d3.merge(seriesData).map(function(d) { return d.x })) .rangeBands([0, availableHeight], .1); y .domain(yDomain || d3.extent(d3.merge(seriesData).map(function(d) { return d.y + (stacked ? d.y0 : 0) }).concat(forceY))) //.range([0, availableWidth]); if (showValues && !stacked) y.range([(y.domain()[0] < 0 ? valuePadding : 0), availableWidth - (y.domain()[1] > 0 ? valuePadding : 0) ]); else y.range([0, availableWidth]); //store old scales if they exist x0 = x0 || x; //y0 = y0 || y; y0 = y0 || d3.scale.linear().domain(y.domain()).range([y(0),y(0)]); var wrap = d3.select(this).selectAll('g.wrap.multibarHorizontal').data([data]); var wrapEnter = wrap.enter().append('g').attr('class', 'wrap nvd3 multibarHorizontal'); var defsEnter = wrapEnter.append('defs'); var gEnter = wrapEnter.append('g'); gEnter.append('g').attr('class', 'groups'); var g = wrap.select('g') wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); var groups = wrap.select('.groups').selectAll('.group') .data(function(d) { return d }, function(d) { return d.key }); groups.enter().append('g') .style('stroke-opacity', 1e-6) .style('fill-opacity', 1e-6); d3.transition(groups.exit()) .style('stroke-opacity', 1e-6) .style('fill-opacity', 1e-6) .remove(); groups .attr('class', function(d,i) { return 'group series-' + i }) .classed('hover', function(d) { return d.hover }) .style('fill', function(d,i){ return color[i % color.length] }) .style('stroke', function(d,i){ return color[i % color.length] }); d3.transition(groups) .style('stroke-opacity', 1) .style('fill-opacity', .75); var bars = groups.selectAll('g.bar') .data(function(d) { return d.values }); bars.exit().remove(); var barsEnter = bars.enter().append('g') .attr('transform', function(d,i,j) { return 'translate(' + y0(stacked ? d.y0 : 0) + ',' + (stacked ? 0 : (j * x.rangeBand() / data.length ) + x(getX(d,i))) + ')' }) .on('mouseover', function(d,i) { //TODO: figure out why j works above, but not here d3.select(this).classed('hover', true); dispatch.elementMouseover({ value: getY(d,i), point: d, series: data[d.series], pos: [ y(getY(d,i) + (stacked ? d.y0 : 0)), x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length) ], pointIndex: i, seriesIndex: d.series, e: d3.event }); }) .on('mouseout', function(d,i) { d3.select(this).classed('hover', false); dispatch.elementMouseout({ value: getY(d,i), point: d, series: data[d.series], pointIndex: i, seriesIndex: d.series, e: d3.event }); }) .on('click', function(d,i) { dispatch.elementClick({ value: getY(d,i), point: d, series: data[d.series], pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted pointIndex: i, seriesIndex: d.series, e: d3.event }); d3.event.stopPropagation(); }) .on('dblclick', function(d,i) { dispatch.elementDblClick({ value: getY(d,i), point: d, series: data[d.series], pos: [x(getX(d,i)) + (x.rangeBand() * (stacked ? data.length / 2 : d.series + .5) / data.length), y(getY(d,i) + (stacked ? d.y0 : 0))], // TODO: Figure out why the value appears to be shifted pointIndex: i, seriesIndex: d.series, e: d3.event }); d3.event.stopPropagation(); }); barsEnter.append('rect') .attr('width', 0) .attr('height', x.rangeBand() / (stacked ? 1 : data.length) ) if (showValues && !stacked) { barsEnter.append('text') .attr('text-anchor', function(d,i) { return getY(d,i) < 0 ? 'end' : 'start' }) bars.selectAll('text') .attr('y', x.rangeBand() / 2) .attr('dy', '-.5em') .text(function(d,i) { return valueFormat(getY(d,i)) }) d3.transition(bars) //.delay(function(d,i) { return i * delay / data[0].values.length }) .selectAll('text') .attr('dx', function(d,i) { return getY(d,i) < 0 ? -4 : y(getY(d,i)) - y(0) + 4 }) } else { bars.selectAll('text').remove(); } bars .attr('class', function(d,i) { return getY(d,i) < 0 ? 'bar negative' : 'bar positive'}) //.attr('transform', function(d,i,j) { //return 'translate(' + y0(stacked ? d.y0 : 0) + ',' + x(getX(d,i)) + ')' //}) if (stacked) d3.transition(bars) //.delay(function(d,i) { return i * delay / data[0].values.length }) .attr('transform', function(d,i) { //return 'translate(' + y(d.y0) + ',0)' return 'translate(' + y(d.y0) + ',' + (stacked ? 0 : (j * x.rangeBand() / data.length )) + ')' }) .selectAll('rect') .attr('width', function(d,i) { return Math.abs(y(getY(d,i) + d.y0) - y(d.y0)) }) .attr('height', x.rangeBand() ); else d3.transition(bars) //.delay(function(d,i) { return i * delay / data[0].values.length }) .attr('transform', function(d,i) { //TODO: stacked must be all positive or all negative, not both? return 'translate(' + (getY(d,i) < 0 ? y(getY(d,i)) : y(0)) + ',' + (d.series * x.rangeBand() / data.length + x(getX(d,i)) ) + ')' }) .selectAll('rect') .attr('height', x.rangeBand() / data.length ) .attr('width', function(d,i) { return Math.abs(y(getY(d,i)) - y(0)) }); /* if (stacked) d3.transition(bars) .delay(function(d,i) { return i * 1000 / data[0].values.length }) .attr('x', function(d,i) { return y(d.y0); }) .attr('width', function(d,i) { return Math.abs(y(getY(d,i) + d.y0) - y(d.y0)) }) .each('end', function() { d3.transition(d3.select(this)) .attr('y', function(d,i) { return 0 }) .attr('height', x.rangeBand() ); }) else d3.transition(bars) .delay(function(d,i) { return i * 1200 / data[0].values.length }) .attr('y', function(d,i) { return d.series * x.rangeBand() / data.length }) .attr('height', x.rangeBand() / data.length ) .each('end', function() { d3.transition(d3.select(this)) .attr('x', function(d,i) { return getY(d,i) < 0 ? //TODO: stacked must be all positive or all negative, not both? y(getY(d,i)) : y(0) }) .attr('width', function(d,i) { return Math.abs(y(getY(d,i)) - y(0)) }); }) */ //TODO: decide if this makes sense to add into all the models for ease of updating (updating without needing the selection) chart.update = function() { selection.transition().call(chart); } //store old scales for use in transitions on update, to animate from old to new positions, and sizes x0 = x.copy(); y0 = y.copy(); }); return chart; } chart.dispatch = dispatch; chart.x = function(_) { if (!arguments.length) return getX; getX = _; return chart; }; chart.y = function(_) { if (!arguments.length) return getY; getY = _; return chart; }; chart.margin = function(_) { if (!arguments.length) return margin; margin = _; 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.xScale = function(_) { if (!arguments.length) return x; x = _; return chart; }; chart.yScale = function(_) { if (!arguments.length) return y; y = _; return chart; }; chart.xDomain = function(_) { if (!arguments.length) return xDomain; xDomain = _; return chart; }; chart.yDomain = function(_) { if (!arguments.length) return yDomain; yDomain = _; return chart; }; chart.forceY = function(_) { if (!arguments.length) return forceY; forceY = _; return chart; }; chart.stacked = function(_) { if (!arguments.length) return stacked; stacked = _; return chart; }; chart.color = function(_) { if (!arguments.length) return color; color = _; return chart; }; chart.id = function(_) { if (!arguments.length) return id; id = _; return chart; }; chart.delay = function(_) { if (!arguments.length) return delay; delay = _; return chart; }; chart.showValues = function(_) { if (!arguments.length) return showValues; showValues = _; return chart; }; chart.valueFormat= function(_) { if (!arguments.length) return valueFormat; valueFormat = _; return chart; }; chart.valuePadding = function(_) { if (!arguments.length) return valuePadding; valuePadding = _; return chart; }; return chart; }