简体   繁体   中英

Dashing Rickshawgraph to change background color on highest data series

I am trying to have the widget for the rickshawgraph in dashing change the background color depending on the highest value on the highest graph for the newest incoming data. I have it working for one series, but I am having trouble getting it to handle multiple series in one graph. This is a snippet from the rickshawgraph.coffee file. I know I need a loop to get each series and check which one has the highest value, then perform the rest of the logic, but I'm having a really tough time with the syntax. Any help would be appreciated. I included the complete files below as well.

  node = $(@node)
  series = @_parseData {points: @get('points'), series: @get('series')}
  data = series[0].data
  values = data[data.length - 1].y
  #cool = parseInt(@get('cool'))
  cool = parseInt node.data "cool"
  #warm = parseInt(@get('warm'))
  warm = parseInt node.data "warm"
  level = switch
    when values <= cool then 0
    when values >= warm then 4
    else
      bucketSize = (warm - cool) / 3 # Total # of colours in middle
      Math.ceil (values - cool) / bucketSize

  backgroundClass = "hotness#{level}"
  lastClass = @get "lastClass"
  node.toggleClass "#{lastClass} #{backgroundClass}"

  @set "lastClass", backgroundClass

My erb file calls the widget here.

</li>

My scss rickshawgraph.scss is here.

// ----------------------------------------------------------------------------
// Mixins
// ----------------------------------------------------------------------------
@mixin transition($transition-property, $transition-time, $method) {
  -webkit-transition: $transition-property $transition-time $method;
  -moz-transition: $transition-property $transition-time $method;
  -o-transition: $transition-property $transition-time $method;
  transition: $transition-property $transition-time $method;
}

// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color:  #00C176;

// ----------------------------------------------------------------------------
// Widget-graph styles
// ----------------------------------------------------------------------------

.widget-rickshawgraph {
    background-color: #00C176;
    position: relative;
}
.widget-rickshawgraph .rickshaw_graph {
    position: absolute;
    left: 0px;
    top: 0px;
}
.widget-rickshawgraph svg {
    position: absolute;
    left: 0px;
    top: 0px;
}
.widget-rickshawgraph .title, .widget-rickshawgraph .value {
    position: relative;
    z-index: 99;
}
.widget-rickshawgraph .title {
    color: rgba(126, 126, 126, 0.7);
}
.widget-rickshawgraph .more-info {
    color: rgba(0, 0, 0, 0);
    font-weight: 600;
    font-size: 20px;
    margin-top: 0;
    opacity: 0;
}
.widget-rickshawgraph .x_tick {
    position: absolute;
    bottom: 0;
}
.widget-rickshawgraph .x_tick .title {
    font-size: 40px;
    color: rgba(0, 0, 0, 0.4);
    opacity: 0.5;
    padding-bottom: 3px;
}
.widget-rickshawgraph .y_ticks {
    font-size: 40px;
    fill: rgba(0, 0, 0, 0.4);
    color: rgba(0, 0, 0, 0.4);
    font-weight: bold;
}
.widget-rickshawgraph .y_ticks text {
    font-size: 20px;
    color: rgba(0, 0, 0, 0.4);
    fill: rgba(0, 0, 0, 0.4);
    font-weight: bold;
}
.widget-rickshawgraph .domain {
    display: none;
}
.widget-rickshawgraph .rickshaw_legend {
    position: absolute;
    left: 0px;
    bottom: 0px;
    white-space: nowrap;
    overflow-x: scroll;
    font-size: 80px;
    height: 20px;
}
.widget-rickshawgraph .rickshaw_legend ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
    text-align: center;
}
.widget-rickshawgraph .rickshaw_legend ul li {
    display: inline;
}
.widget-rickshawgraph .rickshaw_legend .swatch {
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-left: 5px;
}
.widget-rickshawgraph .rickshaw_legend .label {
    display: inline-block;
    margin-left: 5px;
    /*Change the font size and the text size and make sure the label comes to the front for the legend */
    font-size: 200%;
    color: rgba(255, 255, 255, 0.7);
}

.hotness0 { background-color: #00C176; }
.hotness1 { background-color: #88C100; }
.hotness2 { background-color: #FABE28; }
.hotness3 { background-color: #FF8A00; }
.hotness4 { background-color: #FF003C; }

// // More colour-blind friendly palette
// .hotness0 { background-color: #046D8B; }
// .hotness1 { background-color: #309292; }
// .hotness2 { background-color: #2FB8AC; }
// .hotness3 { background-color: #93A42A; }
// .hotness4 { background-color: #ECBE13; }

My rickshawgraph.coffee is here.

 # Rickshawgraphhot v0.1.0

class Dashing.Rickshawgraphhot 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: 'S'},
      {number: 1,                         label: 'MS'}
  ]

  # 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
          else
           number = " number + 'ms'"

      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 = ''

      if @get('numformat') == 'ms'
        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()

      node = $(@node)
      series = @_parseData {points: @get('points'), series: @get('series')}
      data = series[0].data
      values = data[data.length - 1].y
      #cool = parseInt(@get('cool'))
      cool = parseInt node.data "cool"
      #warm = parseInt(@get('warm'))
      warm = parseInt node.data "warm"
      level = switch
        when values <= cool then 0
        when values >= warm then 4
        else
          bucketSize = (warm - cool) / 3 # Total # of colours in middle
          Math.ceil (values - cool) / bucketSize

      backgroundClass = "hotness#{level}"
      lastClass = @get "lastClass"
      node.toggleClass "#{lastClass} #{backgroundClass}"

      @set "lastClass", backgroundClass
  # 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)
    height = (Dashing.widget_base_dimensions[1] * $container.data("sizey"))

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

    $graph = $("<div style='height: #{height}px;'></div>")
    $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.max = @get('min')
    if @get('max') != null then graphOptions.max = @get('max')

    try
      graph = new Rickshaw.Graph graphOptions
    catch err
      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.
        nullsFound = false
        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.formatMS)

    if @get("legend")
      # Add a legend
      @$legendDiv = $("<div style='position:fixed; z-index:99; width: #{width}px;'></div>")
      $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]

My rickshawgraph.html is here.

<h1 class="title" data-bind="title" style="color:white;"> </h1>
<h2 class="value" data-bind="current | prepend prefix"></h2>
<p class="more-info" data-bind="moreinfo"></p>

I suggest you do this instead. There are warning and danger colors set in "assets/stylesheets/application.scss". You can add new colors in there.

In your rickshaw graph widget add this

onData: (data) ->
 if data.status
    # clear existing "status-*" classes
    $(@get('node')).attr 'class', (i,c) ->
      c.replace /\bstatus-\S+/g, ''
    # add new class
    $(@get('node')).addClass "status-#{data.status}"

In your job's .rb, set a status and send it.

For example:

if count < 50
   status = 'warning'
else
   status = 'danger'
end

send_event('thread-count', { value: count, status: status } )

In the above case if my count is less than 50, it blinks yellow or else its red.

NOTE: the animation doesnt work in Firefox. Works in Safari and Chrome only.

The answer here is a simple and nice one. I had to make a few tweaks though as my charts used data-colors and data-stroke colors. Posting it for sample usage.

plug into rickhawgraph.coffee in the top of onData section:

  onData: (data) ->
    if data.status
      # clear existing "status-*" classes
      $(@get('node')).attr 'class', (i,c) ->
        c.replace /\bstatus-\S+/g, ''
      @assignedColors = ""
      @strokeColors = ""
      # add new class
      $(@get('node')).addClass "status-#{data.status}"
    else
      @assignedColors = @get('colors').split(':') if @get('colors')
      @strokeColors = ""

the html

<li data-row="1" data-col="1" data-sizex="2" data-sizey="4">
    <div data-id="apdex_score_stage" data-view="Rickshawgraph" data-bind-data-min="0" data-max="1" data-title="Apdex Score (1-Excellent)" class="" data-colors="#4D4D94" data-stroke-colors="#707070" data-unstack="false" data-stroke="true" data-default-alpha="0.5" data-legend="false" data-summary-method="last"></div>
</li>

in the job

 apdex_status=""

        if apdex_score_values_array_min.min < 1 #yellow if one of the values is less than 1
    apdex_status="danger"
end

print apdex_score_values_array_min[1]

if apdex_score_values_array_min[1] < 1 #red if last value is less than 1
    apdex_status="warning"
end

#red if last value is less than 1
if apdex_score_values_array_min.min == 1.0 #nothing to worry, no status
  apdex_status=""
end


send_event("apdex_score_stage", min: apdex_score_values_array_min.round(2), status: apdex_status, points: apdex_score_array)

Data-binding is also heavily used here for rickshaw graph.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM