summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src/core/util.coffee
blob: 30995bf4a5364440ba38cd5cc6cd8f92a70400d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
typeFunction = (objectName) -> (v) ->
  Object::toString.call(v) == "[object #{objectName}]"

# @return [Boolean] <code>true</code> if the given value is an array, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isArray = Array.isArray ? typeFunction('Array')

# @return [Boolean] <code>true</code> if the given value is an object, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isObject = typeFunction('Object')

# @return [Boolean] <code>true</code> if the given value is a string, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isString = typeFunction('String')

# @return [Boolean] <code>true</code> if the given value is a function, <code>false</code> otherwise.
# @param v Value to test.
Epoch.isFunction = typeFunction('Function')

# @return [Boolean] <code>true</code> if the given value is a number, <code>false</code> 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:
# <a href="http://goo.gl/yaD9hV">Stack Overflow #384286</a>.
# @return [Boolean] <code>true</code> if the given value is a DOM element, <code>false</code> 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] <code>true</code> 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 <code>.on</code>
  # 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 <code>.off</code> 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