Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?

Hot Progress Bar Widget

Description

This widget is a fork of the original Progress Bar Widget, a widget made for Dashing.

The original widget shows multiple animated progress bars and reacts dynamically to new information being passed in. This forked widget adds the possibility to define warning and critical thresholds for each progress bars, which will change their color depending on the threshold reached.

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.

With the threashold feature, it is typically aimed at being used in simple green/orange/red monitoring dashboards.

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.
  • As a bonus, this widget can also play a sound when status is modified. Just add one or more data-*status*sound attributes with the value set to an absolute or relative url pointing to an audio file. status must be one of ok, critical, warning or unknown.

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="HotProgressBars" data-title="Project Bars" data-zebra="1" data-criticalsound="/mp3/critical.mp3"></div>
</li>

You can disable the zebra stripping feature by removing or setting data-zebra attribute to 0.

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>, critical: <value>, warning: <value>, localScope: <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

critical, warning and localScope are optional values with the following meaning:

  • critical defines the critical threshold, the progress bar will turn red if the value reaches this threashold,
  • warning defines the warning threshold, the progress bar will turn yellow if the value reaches this threshold,
  • localScope can be 0 or 1 and configures whether or not the whole widget should change its color if a threshold is reached for this progress bar.

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

class Dashing.HotProgressBars 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-hot-progress-bar")
if elem.length
@animateProgressBarContent(elem[0], item, 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)
if @zebra
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-hot-progress-bar") )
innerProgressBar = $("<div/>")
.attr("class", "inner-hot-progress-bar")
innerProgressBar.css("width", "0%")
progressBarValue = $("<p/>").text("0%")
# Put it all together.
innerProgressBar.append(progressBarValue)
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.
# @progress_item - an item of the progress_items data received
# @baseDuration - the minimum time the animation will perform.
# /
animateProgressBarContent: (element, item, baseDuration) ->
from = parseFloat(element.style.width)
to = parseFloat(item.progress)
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
progressBars = this
interval = setInterval(
->
currentValue += valueIncrement
if Math.abs(currentValue - from) >= Math.abs(endpointDifference)
setHotProgressBarValue(element, to, item.warning, item.critical, item.localScope)
clearInterval(interval)
else
setHotProgressBarValue(element, currentValue, item.warning, item.critical, item.localScope)
updateHotProgressBarStatus(progressBars)
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.
# @warningThreshold - the treshold at which display a warning visual alert
# @criticalThreshold - the treshold at which display a critical visual alert
# @localScope - whether this item can impact the global status of the widget
# /
setHotProgressBarValue = (element, value, warningThreshold, criticalThreshold, localScope) ->
if (value > 100)
value = 100
else if (value < 0)
value = 0
element.textContent = Math.floor(value) + "%"
element.style.width = value + "%"
newStatus = switch
when criticalThreshold and value >= criticalThreshold then 'critical'
when warningThreshold and value >= warningThreshold then 'warning'
else 'ok'
for status in ['ok', 'critical', 'warning']
do (status) ->
match = (newStatus == status)
$(element).toggleClass("inner-hot-progress-bar-#{status}", match)
$(element).parent().toggleClass("outer-hot-progress-bar-#{status}", match)
$(element).toggleClass("global-alert", not localScope)
#***/
# Update the widget background accorrding to the progress items status
#
# @progressBars - DOM element corresponding to the widget
# /
updateHotProgressBarStatus = (progressBars) ->
progressBars_node = $(progressBars.node)
overallStatus = switch
when progressBars_node.find(".inner-hot-progress-bar-critical.global-alert").length then 'critical'
when progressBars_node.find(".inner-hot-progress-bar-warning.global-alert").length then 'warning'
else 'ok'
lastOverallStatus = progressBars.lastOverallStatus
if lastOverallStatus != overallStatus
progressBars.lastOverallStatus = overallStatus
for status in ['ok', 'critical', 'warning']
do (status) ->
progressBars_node.toggleClass("widget-hot-progress-bars-#{status}", overallStatus == status)
audiosound = progressBars[overallStatus + 'sound']
audioplayer = new Audio(audiosound) if audiosound?
if audioplayer
audioplayer.play()
#***/
# 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)
<h1 class="title" data-bind="title"></h1>
<div class="rows-container">
</div>
// ----------------------------------------------------------------------------
// 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;
$green: #00C176;
$base-color: $green;
$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%);
$warning-color: #FABE28;
$warning-color-dark: darken($warning-color, 10%);
$warning-color-light: lighten($warning-color, 10%);
$warning-color-lighter: lighten($warning-color, 25%);
$critical-color: #FF003C;
$critical-color-dark: darken($critical-color, 10%);
$critical-color-light: lighten($critical-color, 10%);
$critical-color-lighter: lighten($critical-color, 25%);
// $text-color: $base-color-lightest;
$text-color: rgba(255, 255, 255, 0.7);
// ----------------------------------------------------------------------------
// Widget-project-completion styles
// ----------------------------------------------------------------------------
.widget.widget-hot-progress-bars {
height: 100%;
width: 100%;
padding: 5px;
position:relative;
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-hot-progress-bar {
display:inline-block;
width: 65%;
vertical-align: middle;
border-radius: 2 * $row-size;
border-width: ($row-size / 3);
border-style: solid;
.inner-hot-progress-bar {
border-radius: $row-size / 2;
color: $white;
}
.inner-hot-progress-bar-ok {
background-color: $base-color-dark;
}
.inner-hot-progress-bar-warning {
background-color: $warning-color-dark;
}
.inner-hot-progress-bar-critical {
background-color: $critical-color-dark;
}
}
.outer-hot-progress-bar-ok {
background-color: $base-color-lighter;
border-color: $base-color-dark;
}
.outer-hot-progress-bar-warning {
background-color: $warning-color-lighter;
border-color: $warning-color-dark;
}
.outer-hot-progress-bar-critical {
background-color: $critical-color-lighter;
border-color: $critical-color-dark;
}
.zebra-stripe {
#background-color: $base-color-light;
background-color: $base-color;
}
}
.widget.widget-hot-progress-bars-ok {
background-color: $base-color;
.zebra-stripe {
background-color: $base-color-light;
}
}
.widget.widget-hot-progress-bars-warning {
background-color: $warning-color;
.zebra-stripe {
background-color: $warning-color-light;
}
}
.widget.widget-hot-progress-bars-critical {
background-color: $critical-color;
.zebra-stripe {
background-color: $critical-color-light;
}
}

will be nice if you can have snapshot for the result

Hi @larrycai. I've integrated the Hot Progress Bars widget, and an example screenshot is shown below:

Hot Progress Bars example - screenshot

I send data from Windows to the widget using cURL. In this example, my cURL command is:

<your-curl.exe> -d "{\"auth_token\": \"<your-auth-token>\", \"progress_items\":[{\"name\": \"Critical item:\", \"progress\": \"50\", \"warning\": \"30\", \"critical\": \"45\", \"localScope\": \"1\"}, {\"name\": \"Warning item:\", \"progress\": \"35\", \"warning\": \"30\", \"critical\": \"45\", \"localScope\": \"1\"}, {\"name\": \"Regular item:\", \"progress\": \"15\", \"warning\": \"30\", \"critical\": \"45\", \"localScope\": \"1\"}]  }" <http://your-dashboard:<port>/widgets/hot_progress_bars

Hope this helps!

What is the easiest way to reverse the colors on this widget so that closer to 100% is green?

ifnull commented Dec 27, 2015

@aspence7 Just change the conditionals in the newStatus switch.

diff --git a/widgets/hot_progress_bars/hot_progress_bars.coffee b/widgets/hot_progress_bars/hot_progress_bars.coffee
index 537e0c5..156536c 100644
--- a/widgets/hot_progress_bars/hot_progress_bars.coffee
+++ b/widgets/hot_progress_bars/hot_progress_bars.coffee
@@ -163,8 +163,8 @@ class Dashing.HotProgressBars extends Dashing.Widget
     element.style.width = value + "%"

     newStatus = switch
-      when criticalThreshold and value >= criticalThreshold then 'critical'
-      when warningThreshold and value >= warningThreshold then 'warning'
+      when criticalThreshold and value <= criticalThreshold then 'critical'
+      when warningThreshold and value <= warningThreshold then 'warning'
       else 'ok'

     for status in ['ok', 'critical', 'warning']
diff --git a/widgets/hot_progress_bars/hot_progress_bars.scss b/widgets/hot_progress_bars/hot_progress_bars.scss

kondalonline commented Nov 26, 2016 edited

@kalenwatermeyer
@yannrouillard

I just created a widget and added the coffee,html and scss file as mentioned above. Also added a list item to dashboard.

Dashboard widget is dumping this code below. Any suggestion as what might have gone wrong. Other widgets are working fine on the same dashboard.

["str", [Tilt::StringTemplate]]["erb", [Tilt::ErubisTemplate, Tilt::ERBTemplate]]["rhtml", [Tilt::ErubisTemplate, Tilt::ERBTemplate]]["erubis", [Tilt::ErubisTemplate]]["etn", [Tilt::EtanniTemplate]]["etanni", [Tilt::EtanniTemplate]]["haml", [Tilt::HamlTemplate]]["mab", [Tilt::MarkabyTemplate]]["liquid", [Tilt::LiquidTemplate]]["radius", [Tilt::RadiusTemplate]]["markdown", [Tilt::RedcarpetTemplate, Tilt::RedcarpetTemplate::Redcarpet2, Tilt::RedcarpetTemplate::Redcarpet1, Tilt::RDiscountTemplate, Tilt::BlueClothTemplate, Tilt::KramdownTemplate, Tilt::MarukuTemplate]]["mkd", [Tilt::RedcarpetTemplate, Tilt::RedcarpetTemplate::Redcarpet2, Tilt::RedcarpetTemplate::Redcarpet1, Tilt::RDiscountTemplate, Tilt::BlueClothTemplate, Tilt::KramdownTemplate, Tilt::MarukuTemplate]]["md", [Tilt::RedcarpetTemplate, Tilt::RedcarpetTemplate::Redcarpet2, Tilt::RedcarpetTemplate::Redcarpet1, Tilt::RDiscountTemplate, Tilt::BlueClothTemplate, Tilt::KramdownTemplate, Tilt::MarukuTemplate]]["textile", [Tilt::RedClothTemplate]]["rdoc", [Tilt::RDocTemplate]]["wiki", [Tilt::WikiClothTemplate, Tilt::CreoleTemplate]]["creole", [Tilt::CreoleTemplate]]["mediawiki", [Tilt::WikiClothTemplate]]["mw", [Tilt::WikiClothTemplate]]["ad", [Tilt::AsciidoctorTemplate]]["adoc", [Tilt::AsciidoctorTemplate]]["asciidoc", [Tilt::AsciidoctorTemplate]]["html", [Tilt::PlainTemplate]]

@ALL.
I made a typo while creating the widget. I have created hot_progress_bar widget instead of hot_progress_bars

It's working fine now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment