nv.models.axis = function() { "use strict"; //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var axis = d3.svg.axis(); var scale = d3.scale.linear(); var margin = {top: 0, right: 0, bottom: 0, left: 0} , width = 75 //only used for tickLabel currently , height = 60 //only used for tickLabel currently , axisLabelText = null , showMaxMin = true //TODO: showMaxMin should be disabled on all ordinal scaled axes , rotateLabels = 0 , rotateYLabel = true , staggerLabels = false , isOrdinal = false , ticks = null , axisLabelDistance = 0 , fontSize = undefined , duration = 250 , dispatch = d3.dispatch('renderEnd') , tickFormatMaxMin ; axis .scale(scale) .orient('bottom') .tickFormat(function(d) { return d }) ; //============================================================ // Private Variables //------------------------------------------------------------ var scale0; var renderWatch = nv.utils.renderWatch(dispatch, duration); function chart(selection) { renderWatch.reset(); selection.each(function(data) { var container = d3.select(this); nv.utils.initSVG(container); // Setup containers and skeleton of chart var wrap = container.selectAll('g.nv-wrap.nv-axis').data([data]); var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-axis'); var gEnter = wrapEnter.append('g'); var g = wrap.select('g'); if (ticks !== null) axis.ticks(ticks); else if (axis.orient() == 'top' || axis.orient() == 'bottom') axis.ticks(Math.abs(scale.range()[1] - scale.range()[0]) / 100); //TODO: consider calculating width/height based on whether or not label is added, for reference in charts using this component g.watchTransition(renderWatch, 'axis').call(axis); scale0 = scale0 || axis.scale(); var fmt = axis.tickFormat(); if (fmt == null) { fmt = scale0.tickFormat(); } var axisLabel = g.selectAll('text.nv-axislabel') .data([axisLabelText || null]); axisLabel.exit().remove(); //only skip when fontSize is undefined so it can be cleared with a null or blank string if (fontSize !== undefined) { g.selectAll('g').select("text").style('font-size', fontSize); } var xLabelMargin; var axisMaxMin; var w; switch (axis.orient()) { case 'top': xLabelMargin = axisLabelDistance + 36; axisLabel.enter().append('text').attr('class', 'nv-axislabel'); w = 0; if (scale.range().length === 1) { w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0; } else if (scale.range().length === 2) { w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1]; } else if ( scale.range().length > 2){ w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]); }; axisLabel .attr('text-anchor', 'middle') .attr('y', -xLabelMargin) .attr('x', w/2); if (showMaxMin) { axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') .data(scale.domain()); axisMaxMin.enter().append('g').attr('class',function(d,i){ return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ') }).append('text'); axisMaxMin.exit().remove(); axisMaxMin .attr('transform', function(d,i) { return 'translate(' + nv.utils.NaNtoZero(scale(d)) + ',0)' }) .select('text') .attr('dy', '-0.5em') .attr('y', -axis.tickPadding()) .attr('text-anchor', 'middle') .text(function(d,i) { var formatter = tickFormatMaxMin || fmt; var v = formatter(d); return ('' + v).match('NaN') ? '' : v; }); axisMaxMin.watchTransition(renderWatch, 'min-max top') .attr('transform', function(d,i) { return 'translate(' + nv.utils.NaNtoZero(scale.range()[i]) + ',0)' }); } break; case 'bottom': xLabelMargin = axisLabelDistance + 36; var maxTextWidth = 30; var textHeight = 0; var xTicks = g.selectAll('g').select("text"); var rotateLabelsRule = ''; if (rotateLabels%360) { //Reset transform on ticks so textHeight can be calculated correctly xTicks.attr('transform', ''); //Calculate the longest xTick width xTicks.each(function(d,i){ var box = this.getBoundingClientRect(); var width = box.width; textHeight = box.height; if(width > maxTextWidth) maxTextWidth = width; }); rotateLabelsRule = 'rotate(' + rotateLabels + ' 0,' + (textHeight/2 + axis.tickPadding()) + ')'; //Convert to radians before calculating sin. Add 30 to margin for healthy padding. var sin = Math.abs(Math.sin(rotateLabels*Math.PI/180)); xLabelMargin = (sin ? sin*maxTextWidth : maxTextWidth)+30; //Rotate all xTicks xTicks .attr('transform', rotateLabelsRule) .style('text-anchor', rotateLabels%360 > 0 ? 'start' : 'end'); } else { if (staggerLabels) { xTicks .attr('transform', function(d,i) { return 'translate(0,' + (i % 2 == 0 ? '0' : '12') + ')' }); } else { xTicks.attr('transform', "translate(0,0)"); } } axisLabel.enter().append('text').attr('class', 'nv-axislabel'); w = 0; if (scale.range().length === 1) { w = isOrdinal ? scale.range()[0] * 2 + scale.rangeBand() : 0; } else if (scale.range().length === 2) { w = isOrdinal ? scale.range()[0] + scale.range()[1] + scale.rangeBand() : scale.range()[1]; } else if ( scale.range().length > 2){ w = scale.range()[scale.range().length-1]+(scale.range()[1]-scale.range()[0]); }; axisLabel .attr('text-anchor', 'middle') .attr('y', xLabelMargin) .attr('x', w/2); if (showMaxMin) { //if (showMaxMin && !isOrdinal) { axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') //.data(scale.domain()) .data([scale.domain()[0], scale.domain()[scale.domain().length - 1]]); axisMaxMin.enter().append('g').attr('class',function(d,i){ return ['nv-axisMaxMin','nv-axisMaxMin-x',(i == 0 ? 'nv-axisMin-x':'nv-axisMax-x')].join(' ') }).append('text'); axisMaxMin.exit().remove(); axisMaxMin .attr('transform', function(d,i) { return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)' }) .select('text') .attr('dy', '.71em') .attr('y', axis.tickPadding()) .attr('transform', rotateLabelsRule) .style('text-anchor', rotateLabels ? (rotateLabels%360 > 0 ? 'start' : 'end') : 'middle') .text(function(d,i) { var formatter = tickFormatMaxMin || fmt; var v = formatter(d); return ('' + v).match('NaN') ? '' : v; }); axisMaxMin.watchTransition(renderWatch, 'min-max bottom') .attr('transform', function(d,i) { return 'translate(' + nv.utils.NaNtoZero((scale(d) + (isOrdinal ? scale.rangeBand() / 2 : 0))) + ',0)' }); } break; case 'right': axisLabel.enter().append('text').attr('class', 'nv-axislabel'); axisLabel .style('text-anchor', rotateYLabel ? 'middle' : 'begin') .attr('transform', rotateYLabel ? 'rotate(90)' : '') .attr('y', rotateYLabel ? (-Math.max(margin.right, width) + 12 - (axisLabelDistance || 0)) : -10) //TODO: consider calculating this based on largest tick width... OR at least expose this on chart .attr('x', rotateYLabel ? (d3.max(scale.range()) / 2) : axis.tickPadding()); if (showMaxMin) { axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') .data(scale.domain()); axisMaxMin.enter().append('g').attr('class',function(d,i){ return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ') }).append('text') .style('opacity', 0); axisMaxMin.exit().remove(); axisMaxMin .attr('transform', function(d,i) { return 'translate(0,' + nv.utils.NaNtoZero(scale(d)) + ')' }) .select('text') .attr('dy', '.32em') .attr('y', 0) .attr('x', axis.tickPadding()) .style('text-anchor', 'start') .text(function(d, i) { var formatter = tickFormatMaxMin || fmt; var v = formatter(d); return ('' + v).match('NaN') ? '' : v; }); axisMaxMin.watchTransition(renderWatch, 'min-max right') .attr('transform', function(d,i) { return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')' }) .select('text') .style('opacity', 1); } break; case 'left': /* //For dynamically placing the label. Can be used with dynamically-sized chart axis margins var yTicks = g.selectAll('g').select("text"); yTicks.each(function(d,i){ var labelPadding = this.getBoundingClientRect().width + axis.tickPadding() + 16; if(labelPadding > width) width = labelPadding; }); */ axisLabel.enter().append('text').attr('class', 'nv-axislabel'); axisLabel .style('text-anchor', rotateYLabel ? 'middle' : 'end') .attr('transform', rotateYLabel ? 'rotate(-90)' : '') .attr('y', rotateYLabel ? (-Math.max(margin.left, width) + 25 - (axisLabelDistance || 0)) : -10) .attr('x', rotateYLabel ? (-d3.max(scale.range()) / 2) : -axis.tickPadding()); if (showMaxMin) { axisMaxMin = wrap.selectAll('g.nv-axisMaxMin') .data(scale.domain()); axisMaxMin.enter().append('g').attr('class',function(d,i){ return ['nv-axisMaxMin','nv-axisMaxMin-y',(i == 0 ? 'nv-axisMin-y':'nv-axisMax-y')].join(' ') }).append('text') .style('opacity', 0); axisMaxMin.exit().remove(); axisMaxMin .attr('transform', function(d,i) { return 'translate(0,' + nv.utils.NaNtoZero(scale0(d)) + ')' }) .select('text') .attr('dy', '.32em') .attr('y', 0) .attr('x', -axis.tickPadding()) .attr('text-anchor', 'end') .text(function(d,i) { var formatter = tickFormatMaxMin || fmt; var v = formatter(d); return ('' + v).match('NaN') ? '' : v; }); axisMaxMin.watchTransition(renderWatch, 'min-max right') .attr('transform', function(d,i) { return 'translate(0,' + nv.utils.NaNtoZero(scale.range()[i]) + ')' }) .select('text') .style('opacity', 1); } break; } axisLabel.text(function(d) { return d }); if (showMaxMin && (axis.orient() === 'left' || axis.orient() === 'right')) { //check if max and min overlap other values, if so, hide the values that overlap g.selectAll('g') // the g's wrapping each tick .each(function(d,i) { d3.select(this).select('text').attr('opacity', 1); if (scale(d) < scale.range()[1] + 10 || scale(d) > scale.range()[0] - 10) { // 10 is assuming text height is 16... if d is 0, leave it! if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL d3.select(this).attr('opacity', 0); d3.select(this).select('text').attr('opacity', 0); // Don't remove the ZERO line!! } }); //if Max and Min = 0 only show min, Issue #281 if (scale.domain()[0] == scale.domain()[1] && scale.domain()[0] == 0) { wrap.selectAll('g.nv-axisMaxMin').style('opacity', function (d, i) { return !i ? 1 : 0 }); } } if (showMaxMin && (axis.orient() === 'top' || axis.orient() === 'bottom')) { var maxMinRange = []; wrap.selectAll('g.nv-axisMaxMin') .each(function(d,i) { try { if (i) // i== 1, max position maxMinRange.push(scale(d) - this.getBoundingClientRect().width - 4); //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case) else // i==0, min position maxMinRange.push(scale(d) + this.getBoundingClientRect().width + 4) }catch (err) { if (i) // i== 1, max position maxMinRange.push(scale(d) - 4); //assuming the max and min labels are as wide as the next tick (with an extra 4 pixels just in case) else // i==0, min position maxMinRange.push(scale(d) + 4); } }); // the g's wrapping each tick g.selectAll('g').each(function(d, i) { if (scale(d) < maxMinRange[0] || scale(d) > maxMinRange[1]) { if (d > 1e-10 || d < -1e-10) // accounts for minor floating point errors... though could be problematic if the scale is EXTREMELY SMALL d3.select(this).remove(); else d3.select(this).select('text').remove(); // Don't remove the ZERO line!! } }); } //Highlight zero tick line g.selectAll('.tick') .filter(function (d) { /* The filter needs to return only ticks at or near zero. Numbers like 0.00001 need to count as zero as well, and the arithmetic trick below solves that. */ return !parseFloat(Math.round(d * 100000) / 1000000) && (d !== undefined) }) .classed('zero', true); //store old scales for use in transitions on update scale0 = scale.copy(); }); renderWatch.renderEnd('axis immediate'); return chart; } //============================================================ // Expose Public Variables //------------------------------------------------------------ // expose chart's sub-components chart.axis = axis; chart.dispatch = dispatch; chart.options = nv.utils.optionsFunc.bind(chart); chart._options = Object.create({}, { // simple options, just get/set the necessary values axisLabelDistance: {get: function(){return axisLabelDistance;}, set: function(_){axisLabelDistance=_;}}, staggerLabels: {get: function(){return staggerLabels;}, set: function(_){staggerLabels=_;}}, rotateLabels: {get: function(){return rotateLabels;}, set: function(_){rotateLabels=_;}}, rotateYLabel: {get: function(){return rotateYLabel;}, set: function(_){rotateYLabel=_;}}, showMaxMin: {get: function(){return showMaxMin;}, set: function(_){showMaxMin=_;}}, axisLabel: {get: function(){return axisLabelText;}, set: function(_){axisLabelText=_;}}, height: {get: function(){return height;}, set: function(_){height=_;}}, ticks: {get: function(){return ticks;}, set: function(_){ticks=_;}}, width: {get: function(){return width;}, set: function(_){width=_;}}, fontSize: {get: function(){return fontSize;}, set: function(_){fontSize=_;}}, tickFormatMaxMin: {get: function(){return tickFormatMaxMin;}, set: function(_){tickFormatMaxMin=_;}}, // 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; }}, duration: {get: function(){return duration;}, set: function(_){ duration=_; renderWatch.reset(duration); }}, scale: {get: function(){return scale;}, set: function(_){ scale = _; axis.scale(scale); isOrdinal = typeof scale.rangeBands === 'function'; nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']); }} }); nv.utils.initOptions(chart); nv.utils.inheritOptionsD3(chart, axis, ['orient', 'tickValues', 'tickSubdivide', 'tickSize', 'tickPadding', 'tickFormat']); nv.utils.inheritOptionsD3(chart, scale, ['domain', 'range', 'rangeBand', 'rangeBands']); return chart; };