/* Gets the browser window size Returns object with height and width properties */ nv.utils.windowSize = function() { // Sane defaults var size = {width: 640, height: 480}; // Most recent browsers use if (window.innerWidth && window.innerHeight) { size.width = window.innerWidth; size.height = window.innerHeight; return (size); } // IE can use depending on mode it is in if (document.compatMode=='CSS1Compat' && document.documentElement && document.documentElement.offsetWidth ) { size.width = document.documentElement.offsetWidth; size.height = document.documentElement.offsetHeight; return (size); } // Earlier IE uses Doc.body if (document.body && document.body.offsetWidth) { size.width = document.body.offsetWidth; size.height = document.body.offsetHeight; return (size); } return (size); }; /* handle dumb browser quirks... isinstance breaks if you use frames typeof returns 'object' for null, NaN is a number, etc. */ nv.utils.isArray = Array.isArray; nv.utils.isObject = function(a) { return a !== null && typeof a === 'object'; }; nv.utils.isFunction = function(a) { return typeof a === 'function'; }; nv.utils.isDate = function(a) { return toString.call(a) === '[object Date]'; }; nv.utils.isNumber = function(a) { return !isNaN(a) && typeof a === 'number'; }; /* Binds callback function to run when window is resized */ nv.utils.windowResize = function(handler) { if (window.addEventListener) { window.addEventListener('resize', handler); } else { nv.log("ERROR: Failed to bind to window.resize with: ", handler); } // return object with clear function to remove the single added callback. return { callback: handler, clear: function() { window.removeEventListener('resize', handler); } } }; /* Backwards compatible way to implement more d3-like coloring of graphs. Can take in nothing, an array, or a function/scale To use a normal scale, get the range and pass that because we must be able to take two arguments and use the index to keep backward compatibility */ nv.utils.getColor = function(color) { //if you pass in nothing, get default colors back if (color === undefined) { return nv.utils.defaultColor(); //if passed an array, turn it into a color scale } else if(nv.utils.isArray(color)) { var color_scale = d3.scale.ordinal().range(color); return function(d, i) { var key = i === undefined ? d : i; return d.color || color_scale(key); }; //if passed a function or scale, return it, or whatever it may be //external libs, such as angularjs-nvd3-directives use this } else { //can't really help it if someone passes rubbish as color return color; } }; /* Default color chooser uses a color scale of 20 colors from D3 https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors */ nv.utils.defaultColor = function() { // get range of the scale so we'll turn it into our own function. return nv.utils.getColor(d3.scale.category20().range()); }; /* Returns a color function that takes the result of 'getKey' for each series and looks for a corresponding color from the dictionary */ nv.utils.customTheme = function(dictionary, getKey, defaultColors) { // use default series.key if getKey is undefined getKey = getKey || function(series) { return series.key }; defaultColors = defaultColors || d3.scale.category20().range(); // start at end of default color list and walk back to index 0 var defIndex = defaultColors.length; return function(series, index) { var key = getKey(series); if (nv.utils.isFunction(dictionary[key])) { return dictionary[key](); } else if (dictionary[key] !== undefined) { return dictionary[key]; } else { // no match in dictionary, use a default color if (!defIndex) { // used all the default colors, start over defIndex = defaultColors.length; } defIndex = defIndex - 1; return defaultColors[defIndex]; } }; }; /* From the PJAX example on d3js.org, while this is not really directly needed it's a very cool method for doing pjax, I may expand upon it a little bit, open to suggestions on anything that may be useful */ nv.utils.pjax = function(links, content) { var load = function(href) { d3.html(href, function(fragment) { var target = d3.select(content).node(); target.parentNode.replaceChild( d3.select(fragment).select(content).node(), target); nv.utils.pjax(links, content); }); }; d3.selectAll(links).on("click", function() { history.pushState(this.href, this.textContent, this.href); load(this.href); d3.event.preventDefault(); }); d3.select(window).on("popstate", function() { if (d3.event.state) { load(d3.event.state); } }); }; /* For when we want to approximate the width in pixels for an SVG:text element. Most common instance is when the element is in a display:none; container. Forumla is : text.length * font-size * constant_factor */ nv.utils.calcApproxTextWidth = function (svgTextElem) { if (nv.utils.isFunction(svgTextElem.style) && nv.utils.isFunction(svgTextElem.text)) { var fontSize = parseInt(svgTextElem.style("font-size").replace("px",""), 10); var textLength = svgTextElem.text().length; return nv.utils.NaNtoZero(textLength * fontSize * 0.5); } return 0; }; /* Numbers that are undefined, null or NaN, convert them to zeros. */ nv.utils.NaNtoZero = function(n) { if (!nv.utils.isNumber(n) || isNaN(n) || n === null || n === Infinity || n === -Infinity) { return 0; } return n; }; /* Add a way to watch for d3 transition ends to d3 */ d3.selection.prototype.watchTransition = function(renderWatch){ var args = [this].concat([].slice.call(arguments, 1)); return renderWatch.transition.apply(renderWatch, args); }; /* Helper object to watch when d3 has rendered something */ nv.utils.renderWatch = function(dispatch, duration) { if (!(this instanceof nv.utils.renderWatch)) { return new nv.utils.renderWatch(dispatch, duration); } var _duration = duration !== undefined ? duration : 250; var renderStack = []; var self = this; this.models = function(models) { models = [].slice.call(arguments, 0); models.forEach(function(model){ model.__rendered = false; (function(m){ m.dispatch.on('renderEnd', function(arg){ m.__rendered = true; self.renderEnd('model'); }); })(model); if (renderStack.indexOf(model) < 0) { renderStack.push(model); } }); return this; }; this.reset = function(duration) { if (duration !== undefined) { _duration = duration; } renderStack = []; }; this.transition = function(selection, args, duration) { args = arguments.length > 1 ? [].slice.call(arguments, 1) : []; if (args.length > 1) { duration = args.pop(); } else { duration = _duration !== undefined ? _duration : 250; } selection.__rendered = false; if (renderStack.indexOf(selection) < 0) { renderStack.push(selection); } if (duration === 0) { selection.__rendered = true; selection.delay = function() { return this; }; selection.duration = function() { return this; }; return selection; } else { if (selection.length === 0) { selection.__rendered = true; } else if (selection.every( function(d){ return !d.length; } )) { selection.__rendered = true; } else { selection.__rendered = false; } var n = 0; return selection .transition() .duration(duration) .each(function(){ ++n; }) .each('end', function(d, i) { if (--n === 0) { selection.__rendered = true; self.renderEnd.apply(this, args); } }); } }; this.renderEnd = function() { if (renderStack.every( function(d){ return d.__rendered; } )) { renderStack.forEach( function(d){ d.__rendered = false; }); dispatch.renderEnd.apply(this, arguments); } } }; /* Takes multiple objects and combines them into the first one (dst) example: nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4}); gives: {a: 2, b: 3, c: 4} */ nv.utils.deepExtend = function(dst){ var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : []; sources.forEach(function(source) { for (var key in source) { var isArray = nv.utils.isArray(dst[key]); var isObject = nv.utils.isObject(dst[key]); var srcObj = nv.utils.isObject(source[key]); if (isObject && !isArray && srcObj) { nv.utils.deepExtend(dst[key], source[key]); } else { dst[key] = source[key]; } } }); }; /* state utility object, used to track d3 states in the models */ nv.utils.state = function(){ if (!(this instanceof nv.utils.state)) { return new nv.utils.state(); } var state = {}; var _self = this; var _setState = function(){}; var _getState = function(){ return {}; }; var init = null; var changed = null; this.dispatch = d3.dispatch('change', 'set'); this.dispatch.on('set', function(state){ _setState(state, true); }); this.getter = function(fn){ _getState = fn; return this; }; this.setter = function(fn, callback) { if (!callback) { callback = function(){}; } _setState = function(state, update){ fn(state); if (update) { callback(); } }; return this; }; this.init = function(state){ init = init || {}; nv.utils.deepExtend(init, state); }; var _set = function(){ var settings = _getState(); if (JSON.stringify(settings) === JSON.stringify(state)) { return false; } for (var key in settings) { if (state[key] === undefined) { state[key] = {}; } state[key] = settings[key]; changed = true; } return true; }; this.update = function(){ if (init) { _setState(init, false); init = null; } if (_set.call(this)) { this.dispatch.change(state); } }; }; /* Snippet of code you can insert into each nv.models.* to give you the ability to do things like: chart.options({ showXAxis: true, tooltips: true }); To enable in the chart: chart.options = nv.utils.optionsFunc.bind(chart); */ nv.utils.optionsFunc = function(args) { if (args) { d3.map(args).forEach((function(key,value) { if (nv.utils.isFunction(this[key])) { this[key](value); } }).bind(this)); } return this; }; /* numTicks: requested number of ticks data: the chart data returns the number of ticks to actually use on X axis, based on chart data to avoid duplicate ticks with the same value */ nv.utils.calcTicksX = function(numTicks, data) { // find max number of values from all data streams var numValues = 1; var i = 0; for (i; i < data.length; i += 1) { var stream_len = data[i] && data[i].values ? data[i].values.length : 0; numValues = stream_len > numValues ? stream_len : numValues; } nv.log("Requested number of ticks: ", numTicks); nv.log("Calculated max values to be: ", numValues); // make sure we don't have more ticks than values to avoid duplicates numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks; // make sure we have at least one tick numTicks = numTicks < 1 ? 1 : numTicks; // make sure it's an integer numTicks = Math.floor(numTicks); nv.log("Calculating tick count as: ", numTicks); return numTicks; }; /* returns number of ticks to actually use on Y axis, based on chart data */ nv.utils.calcTicksY = function(numTicks, data, getY) { if (getY) { var numValues = 1; for (var i=0; i < data.length; i += 1) { var values = data[i] && data[i].values ? data[i].values : []; var maxValue; for (var j=0; j < values.length; j += 1) { maxValue = values[j] && getY(values[j]) ? getY(values[j]): 0; numValues = maxValue > numValues ? maxValue : numValues; } } nv.log("Requested number of ticks: ", numTicks); nv.log("Calculated max values to be: ", numValues); // make sure we don't have more ticks than values to avoid duplicates numTicks = numTicks > numValues ? numValues - 1 : numTicks; // make sure we have at least one tick numTicks = numTicks < 1 ? 1 : numTicks; // make sure it's an integer numTicks = Math.floor(numTicks); nv.log("Calculating tick count as: ", numTicks); return numTicks; } else { return nv.utils.calcTicksX(numTicks, data); } }; /* Add a particular option from an options object onto chart Options exposed on a chart are a getter/setter function that returns chart on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b'); option objects should be generated via Object.create() to provide the option of manipulating data via get/set functions. */ nv.utils.initOption = function(chart, name) { // if it's a call option, just call it directly, otherwise do get/set if (chart._calls && chart._calls[name]) { chart[name] = chart._calls[name]; } else { chart[name] = function (_) { if (!arguments.length) return chart._options[name]; chart._overrides[name] = true; chart._options[name] = _; return chart; }; // calling the option as _option will ignore if set by option already // so nvd3 can set options internally but the stop if set manually chart['_' + name] = function(_) { if (!arguments.length) return chart._options[name]; if (!chart._overrides[name]) { chart._options[name] = _; } return chart; } } }; /* Add all options in an options object to the chart */ nv.utils.initOptions = function(chart) { chart._overrides = chart._overrides || {}; var ops = Object.getOwnPropertyNames(chart._options || {}); var calls = Object.getOwnPropertyNames(chart._calls || {}); ops = ops.concat(calls); for (var i in ops) { nv.utils.initOption(chart, ops[i]); } }; /* Inherit options from a D3 object d3.rebind makes calling the function on target actually call it on source Also use _d3options so we can track what we inherit for documentation and chained inheritance */ nv.utils.inheritOptionsD3 = function(target, d3_source, oplist) { target._d3options = oplist.concat(target._d3options || []); // Find unique d3 options (string) and update d3options target._d3options = (target._d3options || []).filter(function(item, i, ar){ return ar.indexOf(item) === i; }); oplist.unshift(d3_source); oplist.unshift(target); d3.rebind.apply(this, oplist); }; /* Remove duplicates from an array */ nv.utils.arrayUnique = function(a) { return a.sort().filter(function(item, pos) { return !pos || item != a[pos - 1]; }); }; /* Keeps a list of custom symbols to draw from in addition to d3.svg.symbol Necessary since d3 doesn't let you extend its list -_- Add new symbols by doing nv.utils.symbols.set('name', function(size){...}); */ nv.utils.symbolMap = d3.map(); /* Replaces d3.svg.symbol so that we can look both there and our own map */ nv.utils.symbol = function() { var type, size = 64; function symbol(d,i) { var t = type.call(this,d,i); var s = size.call(this,d,i); if (d3.svg.symbolTypes.indexOf(t) !== -1) { return d3.svg.symbol().type(t).size(s)(); } else { return nv.utils.symbolMap.get(t)(s); } } symbol.type = function(_) { if (!arguments.length) return type; type = d3.functor(_); return symbol; }; symbol.size = function(_) { if (!arguments.length) return size; size = d3.functor(_); return symbol; }; return symbol; }; /* Inherit option getter/setter functions from source to target d3.rebind makes calling the function on target actually call it on source Also track via _inherited and _d3options so we can track what we inherit for documentation generation purposes and chained inheritance */ nv.utils.inheritOptions = function(target, source) { // inherit all the things var ops = Object.getOwnPropertyNames(source._options || {}); var calls = Object.getOwnPropertyNames(source._calls || {}); var inherited = source._inherited || []; var d3ops = source._d3options || []; var args = ops.concat(calls).concat(inherited).concat(d3ops); args.unshift(source); args.unshift(target); d3.rebind.apply(this, args); // pass along the lists to keep track of them, don't allow duplicates target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || [])); target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || [])); }; /* Runs common initialize code on the svg before the chart builds */ nv.utils.initSVG = function(svg) { svg.classed({'nvd3-svg':true}); }; /* Sanitize and provide default for the container height. */ nv.utils.sanitizeHeight = function(height, container) { return (height || parseInt(container.style('height'), 10) || 400); }; /* Sanitize and provide default for the container width. */ nv.utils.sanitizeWidth = function(width, container) { return (width || parseInt(container.style('width'), 10) || 960); }; /* Calculate the available height for a chart. */ nv.utils.availableHeight = function(height, container, margin) { return Math.max(0,nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom); }; /* Calculate the available width for a chart. */ nv.utils.availableWidth = function(width, container, margin) { return Math.max(0,nv.utils.sanitizeWidth(width, container) - margin.left - margin.right); }; /* Clear any rendered chart components and display a chart's 'noData' message */ nv.utils.noData = function(chart, container) { var opt = chart.options(), margin = opt.margin(), noData = opt.noData(), data = (noData == null) ? ["No Data Available."] : [noData], height = nv.utils.availableHeight(null, container, margin), width = nv.utils.availableWidth(null, container, margin), x = margin.left + width/2, y = margin.top + height/2; //Remove any previously created chart components container.selectAll('g').remove(); var noDataText = container.selectAll('.nv-noData').data(data); noDataText.enter().append('text') .attr('class', 'nvd3 nv-noData') .attr('dy', '-.7em') .style('text-anchor', 'middle'); noDataText .attr('x', x) .attr('y', y) .text(function(t){ return t; }); }; /* Wrap long labels. */ nv.utils.wrapTicks = function (text, width) { text.each(function() { var text = d3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.1, y = text.attr("y"), dy = parseFloat(text.attr("dy")), tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em"); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word); } } }); }; /* Check equality of 2 array */ nv.utils.arrayEquals = function (array1, array2) { if (array1 === array2) return true; if (!array1 || !array2) return false; // compare lengths - can save a lot of time if (array1.length != array2.length) return false; for (var i = 0, l = array1.length; i < l; i++) { // Check if we have nested arrays if (array1[i] instanceof Array && array2[i] instanceof Array) { // recurse into the nested arrays if (!nv.arrayEquals(array1[i], array2[i])) return false; } else if (array1[i] != array2[i]) { // Warning - two different object instances will never be equal: {x:20} != {x:20} return false; } } return true; }; /* Check if a point within an arc */ nv.utils.pointIsInArc = function(pt, ptData, d3Arc) { // Center of the arc is assumed to be 0,0 // (pt.x, pt.y) are assumed to be relative to the center var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius r2 = d3Arc.outerRadius()(ptData), theta1 = d3Arc.startAngle()(ptData), theta2 = d3Arc.endAngle()(ptData); var dist = pt.x * pt.x + pt.y * pt.y, angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system. angle = (angle < 0) ? (angle + Math.PI * 2) : angle; return (r1 * r1 <= dist) && (dist <= r2 * r2) && (theta1 <= angle) && (angle <= theta2); };