All Downloads are FREE. Search and download functionalities are using the official Maven repository.

statics.widgets.rickshawgraph.rickshawgraph.coffee Maven / Gradle / Ivy

There is a newer version: 0.0.16
Show newest version
# Rickshawgraph v0.1.2

class Dashing.Rickshawgraph extends Dashing.Widget

  DIVISORS = [
    {number: 100000000000000000000000,  label: 'Y'},
    {number: 100000000000000000000,     label: 'Z'},
    {number: 100000000000000000,        label: 'E'},
    {number: 1000000000000000,          label: 'P'},
    {number: 1000000000000,             label: 'T'},
    {number: 1000000000,                label: 'G'},
    {number: 1000000,                   label: 'M'},
    {number: 1000,                      label: 'K'}
  ]

  # Take a long number like "2356352" and turn it into "2.4M"
  formatNumber = (number) ->
    for divisior in DIVISORS
      if number > divisior.number
        number = "#{Math.round(number / (divisior.number/10))/10}#{divisior.label}"
        break

    return number

  getRenderer: () -> return @get('renderer') or @get('graphtype') or 'area'

  # Retrieve the `current` value of the graph.
  @accessor 'current', ->
    answer = null

    # Return the value supplied if there is one.
    if @get('displayedValue') != null and @get('displayedValue') != undefined
      answer = @get('displayedValue')

    if answer == null
      # Compute a value to return based on the summaryMethod
      series = @_parseData {points: @get('points'), series: @get('series')}
      if !(series?.length > 0)
        # No data in series
        answer = ''

      else
        switch @get('summaryMethod')
          when "sum"
            answer = 0
            answer += (point?.y or 0) for point in s.data for s in series

          when "sumLast"
            answer = 0
            answer += s.data[s.data.length - 1].y or 0 for s in series

          when "highest"
            answer = 0
            if @get('unstack') or (@getRenderer() is "line")
              answer = Math.max(answer, (point?.y or 0)) for point in s.data for s in series
            else
              # Compute the sum of values at each point along the graph
              for index in [0...series[0].data.length]
                value = 0
                for s in series
                  value += s.data[index]?.y or 0
                answer = Math.max(answer, value)

          when "none"
            answer = ''

          else
          # Otherwise if there's only one series, pick the most recent value from the series.
            if series.length == 1 and series[0].data?.length > 0
              data = series[0].data
              answer = data[data.length - 1].y
            else
              # Otherwise just return nothing.
              answer = ''

        answer = formatNumber answer

    return answer


  ready: ->
    @assignedColors = @get('colors').split(':') if @get('colors')
    @strokeColors = @get('strokeColors').split(':') if @get('strokeColors')

    @graph = @_createGraph()
    @graph.render()

  clear: ->
    # Remove the old graph/legend if there is one.
    $node = $(@node)
    $node.find('.rickshaw_graph').remove()
    if @$legendDiv
      @$legendDiv.remove()
      @$legendDiv = null

  # Handle new data from Dashing.
  onData: (data) ->
    series = @_parseData data

    if @graph
      # Remove the existing graph if the number of series has changed or any names have changed.
      needClear = false
      needClear |= (series.length != @graph.series.length)
      if @get("legend") then for subseries, index in series
        needClear |= @graph.series[index]?.name != series[index]?.name

      if needClear then @graph = @_createGraph()

      # Copy over the new graph data
      for subseries, index in series
        @graph.series[index] = subseries

      @graph.render()

  # Create a new Rickshaw graph.
  _createGraph: ->
    $node = $(@node)
    $container = $node.parent()

    @clear()

    # Gross hacks. Let's fix this.
    width  = (Dashing.widget_base_dimensions[0] * $container.data("sizex")) + Dashing.widget_margins[0] * 2 * (($container.data("sizex") ? 1) - 1)
    height = (Dashing.widget_base_dimensions[1] * $container.data("sizey")) + Dashing.widget_margins[1] * 2 * (($container.data("sizey") ? 1) - 1)

    if @get("legend")
      # Shave 20px off the bottom of the graph for the legend
      height -= 20

    $graph = $("
") $node.append $graph series = @_parseData {points: @get('points'), series: @get('series')} graphOptions = { element: $graph.get(0), renderer: @getRenderer(), width: width, height: height, series: series } if !!@get('stroke') then graphOptions.stroke = true if @get('min') != null then graphOptions.min = @get('min') if @get('max') != null then graphOptions.max = @get('max') try graph = new Rickshaw.Graph graphOptions catch err nullsFound = false if err.toString() is "x and y properties of points should be numbers instead of number and object" # This will happen with older versions of Rickshaw that don't support nulls in the data set. for s in series for point in s.data if point.y is null nullsFound = true point.y = 0 if nullsFound # Try to create the graph again now that we've patched up the data. graph = new Rickshaw.Graph graphOptions if !@rickshawVersionWarning console.log "#{@get 'id'} - Nulls were found in your data, but Rickshaw didn't like" + " them. Consider upgrading your rickshaw to 1.4.3 or higher." @rickshawVersionWarning = true else # No nulls were found - this is some other problem, so just re-throw the exception. throw err graph.renderer.unstack = !!@get('unstack') xAxisOptions = { graph: graph } if Rickshaw.Fixtures.Time.Local xAxisOptions.timeFixture = new Rickshaw.Fixtures.Time.Local() x_axis = new Rickshaw.Graph.Axis.Time xAxisOptions y_axis = new Rickshaw.Graph.Axis.Y(graph: graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT) if @get("legend") # Add a legend @$legendDiv = $("
") $node.append(@$legendDiv) legend = new Rickshaw.Graph.Legend { graph: graph element: @$legendDiv.get(0) } return graph # Parse a {series, points} object with new data from Dashing. # _parseData: (data) -> series = [] # Figure out what kind of data we've been passed if data.series dataSeries = if isString(data.series) then JSON.parse data.series else data.series for subseries, index in dataSeries try series.push @_parseSeries subseries catch err console.log "Error while parsing series: #{err}" else if data.points points = data.points if isString(points) then points = JSON.parse points if points[0]? and !points[0].x? # Not already in Rickshaw format; assume graphite data points = graphiteDataToRickshaw(points) series.push {data: points} if series.length is 0 # No data - create a dummy series to keep Rickshaw happy series.push {data: [{x:0, y:0}]} @_updateColors(series) # Fix any missing data in the series. if Rickshaw.Series.fill then Rickshaw.Series.fill(series, null) return series # Parse a series of data from an array passed to `_parseData()`. # This accepts both Graphite and Rickshaw style data sets. _parseSeries: (series) -> if series?.datapoints? # This is a Graphite series answer = { name: series.target data: graphiteDataToRickshaw series.datapoints color: series.color stroke: series.stroke } else if series?.data? # Rickshaw data. Need to clone, otherwise we could end up with multiple graphs sharing # the same data, and Rickshaw really doesn't like that. answer = { name: series.name data: series.data color: series.color stroke: series.stroke } else if !series throw new Error("No data received for #{@get 'id'}") else throw new Error("Unknown data for #{@get 'id'}. series: #{series}") answer.data.sort (a,b) -> a.x - b.x return answer # Update the color assignments for a series. This will assign colors to any data that # doesn't have a color already. _updateColors: (series) -> # If no colors were provided, or of there aren't enough colors, then generate a set of # colors to use. if !@defaultColors or @defaultColors?.length != series.length @defaultColors = computeDefaultColors @, @node, series for subseries, index in series # Preferentially pick supplied colors instead of defaults, but don't overwrite a color # if one was supplied with the data. subseries.color ?= @assignedColors?[index] or @defaultColors[index] subseries.stroke ?= @strokeColors?[index] or "#000" # Convert a collection of Graphite data points into data that Rickshaw will understand. graphiteDataToRickshaw = (datapoints) -> answer = [] for datapoint in datapoints # Need to convert potential nulls from Graphite into a real number for Rickshaw. answer.push {x: datapoint[1], y: (datapoint[0] or 0)} answer # Compute a pleasing set of default colors. This works by starting with the background color, # and picking colors of intermediate luminance between the background and white (or the # background and black, for light colored backgrounds.) We use the brightest color for the # first series, because then multiple series will appear to blend in to the background. computeDefaultColors = (self, node, series) -> defaultColors = [] # Use a neutral color if we can't get the background-color for some reason. backgroundColor = parseColor($(node).css('background-color')) or [50, 50, 50, 1.0] hsl = rgbToHsl backgroundColor alpha = if self.get('defaultAlpha')? then self.get('defaultAlpha') else 1 if self.get('colorScheme') in ['rainbow', 'near-rainbow'] saturation = (interpolate hsl[1], 1.0, 3)[1] luminance = if (hsl[2] < 0.6) then 0.7 else 0.3 hueOffset = 0 if self.get('colorScheme') is 'rainbow' # Note the first and last values in `hues` will both have the same hue as the background, # hence the + 2. hues = interpolate hsl[0], hsl[0] + 1, (series.length + 2) hueOffset = 1 else hues = interpolate hsl[0] - 0.25, hsl[0] + 0.25, series.length for hue, index in hues if hue > 1 then hues[index] -= 1 if hue < 0 then hues[index] += 1 for index in [0...series.length] defaultColors[index] = rgbToColor hslToRgb([hues[index + hueOffset], saturation, luminance, alpha]) else hue = if self.get('colorScheme') is "compliment" then hsl[0] + 0.5 else hsl[0] if hsl[0] > 1 then hsl[0] -= 1 saturation = hsl[1] saturationSource = if (saturation < 0.6) then 0.7 else 0.3 saturations = interpolate saturationSource, saturation, (series.length + 1) luminance = hsl[2] luminanceSource = if (luminance < 0.6) then 0.9 else 0.1 luminances = interpolate luminanceSource, luminance, (series.length + 1) for index in [0...series.length] defaultColors[index] = rgbToColor hslToRgb([hue, saturations[index], luminances[index], alpha]) return defaultColors # Helper functions # ================ isString = (obj) -> return toString.call(obj) is "[object String]" # Parse a `rgb(x,y,z)` or `rgba(x,y,z,a)` string. parseRgbaColor = (colorString) -> match = /^rgb\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString) if match return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), 1.0] match = /^rgba\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString) if match return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4])] return null # Parse a color string as RGBA parseColor = (colorString) -> answer = null # Try to use the browser to parse the color for us. div = document.createElement('div') div.style.color = colorString if div.style.color answer = parseRgbaColor div.style.color if !answer match = /^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/.exec(colorString) if match then answer = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16), 1.0] if !answer match = /^#([\da-fA-F])([\da-fA-F])([\da-fA-F])/.exec(colorString) if match then answer = [parseInt(match[1], 16) * 0x11, parseInt(match[2], 16) * 0x11, parseInt(match[3], 16) * 0x11, 1.0] if !answer then answer = parseRgbaColor colorString return answer # Convert an RGB or RGBA color to a CSS color. rgbToColor = (rgb) -> if (!3 of rgb) or (rgb[3] == 1.0) return "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})" else return "rgba(#{rgb[0]},#{rgb[1]},#{rgb[2]},#{rgb[3]})" # Returns an array of size `steps`, where the first value is `source`, the last value is `dest`, # and the intervening values are interpolated. If steps < 2, then returns `[dest]`. # interpolate = (source, dest, steps) -> if steps < 2 answer =[dest] else stepSize = (dest - source) / (steps - 1) answer = (num for num in [source..dest] by stepSize) # Rounding errors can cause us to drop the last value if answer.length < steps then answer.push dest return answer # Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c # # Converts an RGBA color value to HSLA. Conversion formula # adapted from http://en.wikipedia.org/wiki/HSL_color_space. # Assumes r, g, and b are contained in the set [0, 255] and # a in [0, 1]. Returns h, s, l, a in the set [0, 1]. # # Returns the HSLA representation as an array. rgbToHsl = (rgba) -> [r,g,b,a] = rgba r /= 255 g /= 255 b /= 255 max = Math.max(r, g, b) min = Math.min(r, g, b) l = (max + min) / 2 if max == min h = s = 0 # achromatic else d = max - min s = if l > 0.5 then d / (2 - max - min) else d / (max + min) switch max when r then h = (g - b) / d + (g < b ? 6 : 0) when g then h = (b - r) / d + 2 when b then h = (r - g) / d + 4 h /= 6; return [h, s, l, a] # Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c # # Converts an HSLA color value to RGBA. Conversion formula # adapted from http://en.wikipedia.org/wiki/HSL_color_space. # Assumes h, s, l, and a are contained in the set [0, 1] and # returns r, g, and b in the set [0, 255] and a in [0, 1]. # # Retunrs the RGBA representation as an array. hslToRgb = (hsla) -> [h,s,l,a] = hsla if s is 0 r = g = b = l # achromatic else hue2rgb = (p, q, t) -> if(t < 0) then t += 1 if(t > 1) then t -= 1 if(t < 1/6) then return p + (q - p) * 6 * t if(t < 1/2) then return q if(t < 2/3) then return p + (q - p) * (2/3 - t) * 6 return p q = if l < 0.5 then l * (1 + s) else l + s - l * s p = 2 * l - q; r = hue2rgb(p, q, h + 1/3) g = hue2rgb(p, q, h) b = hue2rgb(p, q, h - 1/3) return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a]




© 2015 - 2024 Weber Informatics LLC | Privacy Policy