denolfe
9/26/2013 - 4:48 PM

Progress Bars - An animated progress bar widget for Dashing.

Progress Bars - An animated progress bar widget for Dashing.

// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------

// row-size is a magic number used for scaling. It will make things bigger
// or smaller but always in proportion with each other. Feel free to change
// this to reflect your personal needs.
$row-size: 0.7em;

$blue: #2db4d4;
$white: #ffffff;
$base-color: $blue;

$base-color-dark: darken($base-color, 10%);
$base-color-light: lighten($base-color, 10%);
$base-color-lighter: lighten($base-color, 25%);
$base-color-lightest: lighten($base-color, 35%);

$text-color:       $base-color-lightest;

// ----------------------------------------------------------------------------
// Widget-project-completion styles
// ----------------------------------------------------------------------------
.widget.widget-progress-bars {
  height: 100%;
  width: 100%;
  padding: 5px;
  position:relative;
  background-color: $base-color;
  vertical-align: baseline;

  * {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing:border-box;
  }

  .title {
    color: $text-color;
    margin-bottom: 5px;
  }

  .rows-container {
    height: 85%;
    width:100%;
    color: $text-color;
    font-size: $row-size;
    text-align:center;
  }

  .row {
    height:0%;
    width:100%;
    vertical-align: middle;
    display:table;
    transition-property: height;
    transition-duration: 0.3s;
    transition-timing-function: linear;
  }

  .row-content {
    padding-left: 5px;
    display:table-cell;
    vertical-align: middle;
  }

  .project-name {
    display:inline-block;
    width:35%;
    padding-right: $row-size;
    text-align: left;
    vertical-align: middle;
    text-overflow: ellipsis;
    overflow:hidden;
    white-space: nowrap;
  }

  .outer-progress-bar {
    display:inline-block;
    width: 65%;
    vertical-align: middle;
    border: ($row-size / 3) solid $base-color-dark;
    border-radius: 2 * $row-size;
    background-color: $base-color-lighter;
    
    .inner-progress-bar {
      background-color: $base-color-dark;
      border-radius: $row-size / 2;
      color: $white;
    }
  }

  .zebra-stripe {
    background-color: $base-color-light;
  }



}
<h1 class="title" data-bind="title"></h1>

<div class="rows-container">
</div>
class Dashing.ProgressBars extends Dashing.Widget

  @accessor 'title'

  ready: ->
    @drawWidget( @get('progress_items') )

  onData: (eventData) ->
    @drawWidget(eventData.progress_items)

  drawWidget: (progress_items) ->
    container = $(@node)
    rowsContainer = container.find('.rows-container')

    if progress_items.length == 0
      rowsContainer.empty()
    else
      # Float value used to scale the rows to use the entire space of the widget
      rowHeight = 100 / progress_items.length
      counter = 0
      @clearIntervals()

      # Add or move rows for each project. Checks first if the row already exists.
      progress_items.forEach (item) =>
        normalizedItemName = item.name.replace(/\W+/g, "_")
        referenceRow = rowsContainer.children().eq(counter)
        existingRow = rowsContainer.find("."+normalizedItemName)

        if existingRow.length
          if referenceRow.attr("class").indexOf(normalizedItemName) == -1
            existingRow.detach().insertBefore(referenceRow)
            existingRow.hide().fadeIn(1200)
        else
          row = createRow(item)
          if referenceRow.length
            row.insertBefore(referenceRow)
          else
            rowsContainer.append(row)
          row.hide().fadeIn(1200)

        elem = rowsContainer.find("."+normalizedItemName+" .inner-progress-bar")
        if elem.length
          @animateProgressBarContent(elem[0], parseFloat(elem[0].style.width),
                                    parseFloat(item.progress), 1000)
        ++counter

      # Remove any nodes that were not in the new data, these will be the rows
      # at the end of the widget.
      currentNode = rowsContainer.children().eq(counter-1)
      while currentNode.next().length
        currentNode = currentNode.next()
        currentNode.fadeOut(100, -> $(this).remove() )

      # Set the height after rows were added/removed.
      rows = rowsContainer.children()
      percentageOfTotalHeight = 100 / progress_items.length
      applyCorrectedRowHeight(rows, percentageOfTotalHeight)

      applyZebraStriping(rows)


  #***/
  # Create a JQuery row object with the proper structure and base
  # settings for the item passed in.
  #
  # The Row DOM Hierarchy:
  # Row
  #   Row Content (here so we can use vertical alignment)
  #     Project Name
  #     Outer Bar Container (The border and background)
  #       Inner Bar Container (The progress and text)
  #
  # @item - object representing an item and it's progress
  # /
  createRow = (item) ->

    row = ( $("<div/>")
      .attr("class", "row " + item.name.replace(/\W+/g, "_") ) )

    rowContent = ( $("<div/>")
      .attr("class", "row-content") ) 

    projectName = ( $("<div/>")
      .attr("class", "project-name")
      .text(item.name)
      .attr("title", item.name) )

    outerProgressBar = ( $("<div/>")
      .attr("class", "outer-progress-bar") )

    innerProgressBar = $("<div/>")
      .attr("class", "inner-progress-bar")
      .text("0%")
    innerProgressBar.css("width", "0%")

    # Put it all together.
    outerProgressBar.append(innerProgressBar)
    rowContent.append(projectName)
    rowContent.append(outerProgressBar)
    row.append(rowContent)

    return row


  #***/
  # Does calculations for the animation and sets up the javascript
  # interval to perform the animation.
  #
  # @element - element that is going to be animated.
  # @from - the value that the element starts at.
  # @to - the value that the element is going to.
  # @baseDuration - the minimum time the animation will perform.
  # /
  animateProgressBarContent: (element, from, to, baseDuration) ->
    endpointDifference = (to-from)

    if endpointDifference != 0
      currentValue = from

      # Every x milliseconds, the function should run.
      stepInterval = 16.667

      # Change the duration based on the distance between points.
      duration = baseDuration + Math.abs(endpointDifference) * 25

      numberOfSteps = duration / stepInterval
      valueIncrement = endpointDifference / numberOfSteps
      
      interval = setInterval(
        ->
          currentValue += valueIncrement
          if Math.abs(currentValue - from) >= Math.abs(endpointDifference)
            setProgressBarValue(element, to)
            clearInterval(interval)
          else
            setProgressBarValue(element, currentValue)
        stepInterval)

      @addInterval(interval)

  #***/
  # Sets the text and width of the element in question to the specified value
  # after making sure it is bounded between [0-100]
  #
  # @element - element to be set
  # @value - the numeric value to set the element to. This can be a float.
  # /
  setProgressBarValue = (element, value) ->
    if (value > 100) 
      value = 100
    else if (value < 0) 
      value = 0
    element.textContent = Math.floor(value) + "%"
    element.style.width = value + "%"

  #***/
  # Applies a percentage-based row height to the list of rows passed in.
  #
  # @rows - the elements to apply this height value to
  # @percentageOfTotalHeight - The height to be applied to each row.
  # /
  applyCorrectedRowHeight = (rows, percentageOfTotalHeight) ->
    height = percentageOfTotalHeight + "%"
    for row in rows
      row.style.height = height

  #***/
  # Adds a class to every other row to change the background color. This
  # was done mainly for readability.
  #
  # @rows - list of elements to run zebra-striping on
  # /
  applyZebraStriping = (rows) ->
    isZebraStripe = false
    for row in rows
      # In case elements are moved around, we don't want them to retain this.
      row.classList.remove("zebra-stripe")
      if isZebraStripe
        row.classList.add("zebra-stripe")
      isZebraStripe = !isZebraStripe

  #***/
  # Stops all javascript intervals from running and clears the list.
  #/
  clearIntervals: ->
    if @intervalList
      for interval in @intervalList
        clearInterval(interval)
      @intervalList = []

  #***/
  # Adds a javascript interval to a list so that it can be tracked and cleared
  # ahead of time if the need arises.
  #
  # @interval - the javascript interval to add
  #/
  addInterval: (interval) ->
    if !@intervalList
      @intervalList = []
    @intervalList.push(interval)

Progress Bar Widget

Description

A widget made for Dashing. This widget shows multiple animated progress bars and reacts dynamically to new information being passed in. Anything with a current state and with a projected max/goal state can easily be represented with this widget. Some sample ideas would be to show progress, completion, capacity, load, fundraising, and much more.

Features

  • Animating progress bars - Both the number and bar will grow or shrink based on new data that is being passed to it.
  • Responsive Design - Allows the widget to be resized to any height or width and still fit appropriately. The progress bars will split up all available space amongst each other, squeezing in when additional progress bars fill the widget.
  • Easy Customization - Change the base color in one line in the scss and have the entire widget color scheme react. The font size and progress bar size are handled by a single magic variable in the scss that will scale each bar up proportionally.

Preview

A screenshot showing multiple variations of the widget. A live demo is available here

Dependencies

Needs a job that sends data to the widget.

Usage

With this sample widget code in your dashboard:

<li data-row="1" data-col="1" data-sizex="2" data-sizey="1">
  <div data-id="progress_bars" data-view="ProgressBars" data-title="Project Bars"></div>
</li>

You can send an event through a job like the following: send_event( 'progress_bars', {title: "", progress_items: []} )

progress_items is an array of hashes that follow this design: {name: <value>, progress: <value>} The 'name' key can be any unique string that describes the bar. The 'progress' variable is a value from 0-100 that will represent the percentage of the bar that should be filled. Valid inputs include: 24, "24", "24%", 24.04

Sending a request to a web service for a JSON response or reading from a file can produce this information easily.