typeFunction = (objectName) -> (v) -> Object::toString.call(v) == "[object #{objectName}]" # @return [Boolean] true if the given value is an array, false otherwise. # @param v Value to test. Epoch.isArray = Array.isArray ? typeFunction('Array') # @return [Boolean] true if the given value is an object, false otherwise. # @param v Value to test. Epoch.isObject = typeFunction('Object') # @return [Boolean] true if the given value is a string, false otherwise. # @param v Value to test. Epoch.isString = typeFunction('String') # @return [Boolean] true if the given value is a function, false otherwise. # @param v Value to test. Epoch.isFunction = typeFunction('Function') # @return [Boolean] true if the given value is a number, false otherwise. # @param v Value to test. Epoch.isNumber = typeFunction('Number') # Attempts to determine if a given value represents a DOM element. The result is always correct if the # browser implements DOM Level 2, but one can fool it on certain versions of IE. Adapted from: # Stack Overflow #384286. # @return [Boolean] true if the given value is a DOM element, false otherwise. # @param v Value to test. Epoch.isElement = (v) -> if HTMLElement? v instanceof HTMLElement else v? and Epoch.isObject(v) and v.nodeType == 1 and Epoch.isString(v.nodeName) # Determines if a given value is a non-empty array. # @param v Value to test. # @return [Boolean] true if the given value is an array with at least one element. Epoch.isNonEmptyArray = (v) -> Epoch.isArray(v) and v.length > 0 # Generates shallow copy of an object. # @return A shallow copy of the given object. # @param [Object] original Object for which to make the shallow copy. Epoch.Util.copy = (original) -> return null unless original? copy = {} copy[k] = v for own k, v of original return copy # Creates a deep copy of the given options filling in missing defaults. # @param [Object] options Options to copy. # @param [Object] defaults Default values for the options. Epoch.Util.defaults = (options, defaults) -> result = Epoch.Util.copy(options) for own k, v of defaults opt = options[k] def = defaults[k] bothAreObjects = Epoch.isObject(opt) and Epoch.isObject(def) if opt? and def? if bothAreObjects and not Epoch.isArray(opt) result[k] = Epoch.Util.defaults(opt, def) else result[k] = opt else if opt? result[k] = opt else result[k] = def return result # Formats numbers with standard postfixes (e.g. K, M, G) # @param [Number] v Value to format. # @param [Integer] fixed Number of floating point digits to fix after conversion. # @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results. # @example Formatting a very large number # Epoch.Util.formatSI(1120000) == "1.1 M" Epoch.Util.formatSI = (v, fixed=1, fixIntegers=false) -> if v < 1000 q = v q = q.toFixed(fixed) unless (q|0) == q and !fixIntegers return q for own i, label of ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] base = Math.pow(10, ((i|0)+1)*3) if v >= base and v < Math.pow(10, ((i|0)+2)*3) q = v/base q = q.toFixed(fixed) unless (q % 1) == 0 and !fixIntegers return "#{q} #{label}" # Formats large bandwidth and disk space usage numbers with byte postfixes (e.g. KB, MB, GB, etc.) # @param [Number] v Value to format. # @param [Integer] fixed Number of floating point digits to fix after conversion. # @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results. # @example Formatting a large number of bytes # Epoch.Util.formatBytes(5.21 * Math.pow(2, 20)) == "5.2 MB" Epoch.Util.formatBytes = (v, fixed=1, fix_integers=false) -> if v < 1024 q = v q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers return "#{q} B" for own i, label of ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] base = Math.pow(1024, (i|0)+1) if v >= base and v < Math.pow(1024, (i|0)+2) q = v/base q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers return "#{q} #{label}" # @return a "dasherized" css class names from a given string # @example Using dasherize # Epoch.Util.dasherize('My Awesome Name') == 'my-awesome-name' Epoch.Util.dasherize = (str) -> Epoch.Util.trim(str).replace("\n", '').replace(/\s+/g, '-').toLowerCase() # @return the full domain of a given variable from an array of layers # @param [Array] layers Layered plot data. # @param [String] key The key name of the value at on each entry in the layers. Epoch.Util.domain = (layers, key='x') -> set = {} domain = [] for layer in layers for entry in layer.values continue if set[entry[key]]? domain.push(entry[key]) set[entry[key]] = true return domain # Strips whitespace from the beginning and end of a string. # @param [String] string String to trim. # @return [String] The string without leading or trailing whitespace. # Returns null if the given parameter was not a string. Epoch.Util.trim = (string) -> return null unless Epoch.isString(string) string.replace(/^\s+/g, '').replace(/\s+$/g, '') # Returns the computed styles of an element in the document # @param [HTMLElement] Element for which to fetch the styles. # @param [String] pseudoElement Pseudo selectors on which to search for the element. # @return [Object] The styles for the given element. Epoch.Util.getComputedStyle = (element, pseudoElement) -> if Epoch.isFunction(window.getComputedStyle) window.getComputedStyle(element, pseudoElement) else if element.currentStyle? element.currentStyle # Converts a CSS color string into an RGBA string with the given opacity # @param [String] color Color string to convert into an rgba # @param [Number] opacity Opacity to use for the resulting color. # @return the resulting rgba color string. Epoch.Util.toRGBA = (color, opacity) -> if (parts = color.match /^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*[0-9\.]+\)/) [all, r, g, b] = parts result = "rgba(#{r},#{g},#{b},#{opacity})" else if (v = d3.rgb color) result = "rgba(#{v.r},#{v.g},#{v.b},#{opacity})" return result # Obtains a graphics context for the given canvas node. Nice to have # this abstracted out in case we want to support WebGL in the future. # Also allows us to setup a special context when unit testing, as # jsdom doesn't have canvas support, and node-canvas is a pain in the # butt to install properly across different platforms. Epoch.Util.getContext = (node, type='2d') -> node.getContext(type) # Basic eventing base class for all Epoch classes. class Epoch.Events constructor: -> @_events = {} # Registers a callback to a given event. # @param [String] name Name of the event. # @param [Function, String] callback Either a closure to call when the event fires # or a string that denotes a method name to call on this object. on: (name, callback) -> return unless callback? @_events[name] ?= [] @_events[name].push callback # Registers a map of event names to given callbacks. This method calls .on # directly for each of the events given. # @param [Object] map A map of event names to callbacks. onAll: (map) -> return unless Epoch.isObject(map) @on(name, callback) for own name, callback of map # Removes a specific callback listener or all listeners for a given event. # @param [String] name Name of the event. # @param [Function, String] callback (Optional) Callback to remove from the listener list. # If this parameter is not provided then all listeners will be removed for the event. off: (name, callback) -> return unless Epoch.isArray(@_events[name]) return delete(@_events[name]) unless callback? while (i = @_events[name].indexOf(callback)) >= 0 @_events[name].splice(i, 1) # Removes a set of callback listeners for all events given in the map or array of strings. # This method calls .off directly for each event and callback to remove. # @param [Object, Array] mapOrList Either a map that associates event names to specific callbacks # or an array of event names for which to completely remove listeners. offAll: (mapOrList) -> if Epoch.isArray(mapOrList) @off(name) for name in mapOrList else if Epoch.isObject(mapOrList) @off(name, callback) for own name, callback of mapOrList # Triggers an event causing all active listeners to be executed. # @param [String] name Name of the event to fire. trigger: (name) -> return unless @_events[name]? args = (arguments[i] for i in [1...arguments.length]) for callback in @_events[name] fn = null if Epoch.isString(callback) fn = @[callback] else if Epoch.isFunction(callback) fn = callback unless fn? Epoch.exception "Callback for event '#{name}' is not a function or reference to a method." fn.apply @, args # Performs a single pass flatten on a multi-array # @param [Array] multiarray A deep multi-array to flatten # @returns [Array] A single pass flatten of the multi-array Epoch.Util.flatten = (multiarray) -> if !Array.isArray(multiarray) throw new Error('Epoch.Util.flatten only accepts arrays') result = [] for array in multiarray if Array.isArray(array) for item in array result.push item else result.push array result