Create a gist now

Instantly share code, notes, and snippets.

Progress Bars - An animated progress bar widget for Dashing.

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 widgetA 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.

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)
<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;
$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;
}
}
@idank
idank commented Oct 10, 2013

line 15 in progress_bars.coffee doesn't work as expected to check for an empty array. changing it to if progress_items.length == 0 should fix it.

@mdirienzo
Owner

Fixed, thanks for catching that!

@cunningham

I'm having trouble getting this to work. My widget just displays ["str", [Tilt::StringTemplate]]["erb", [Tilt::ErubisTemplate, Tilt::ERBTemplate]] and so on. I'm sending {"auth_token": dashing_token, "progress_items": [{"name": key, "progress" : value} for key,value in array_of_key_values]} and not seeing any update. Thoughts?

@shushiej

Hi there, I am also having the same problem as cunningham, can you please give an example of how you send_event please, I think that could be the problem.
cheers

@joenguyen

I am also seeing that as well. Any help would be awesome.

@ghost
ghost commented Nov 29, 2013

A http post example in the readme file would help out when configuring this...

@ghost
ghost commented Nov 29, 2013

This works for sending through PowerShell scripts:

$testvar1 = "34" (or use a cmdlet to collect percentage stats)
$testvar2 = "46" (or use a cmdlet to collect percentage stats)
$uri = "http://localhost:3030/widgets/progress_name_here"
$parameters = "{ " + '"auth_token"' + ":" + '"put_your_auth_token_here"' + "," + '"progress_items": ' + "[{" + '"name"' + ":" + '"Test Name 1"' + "," + '"progress"' + ":" + "$testvar1" + "}" + "," + "{" + '"name"' + ":" + '"Test Name 2"' + "," + '"progress"' + ":" + "$testvar2"  + "}] }"
$parameters | Invoke-RestMethod -Uri $uri -Method Post
@shushiej
shushiej commented Dec 6, 2013

I restarted my server and it ended up fixing the problem.

@saftas
saftas commented Jan 28, 2014

Hi,
I also have that cunningham have/had and tried to restart everything with no luck.
Did someone find a solution for the issue?

@centic9
centic9 commented Feb 28, 2014

See Shopify/dashing#324, seems to be caused by name conflicts/mismatches.

@derrybarry

#324 now closed as name conflicts did occur, does anyone have a copy of a working job??

@Sweenj
Sweenj commented Sep 12, 2014

This is working great for me, but I have a question. If I wanted to show the % with a decimal point, for example, 34.45%, what would I need to change in the progress_bars.coffee file? I'm having trouble making this change. I switched
element.textContent = Math.floor(value) + "%"
to
element.textContent = value + "%"
But it still comes out as a whole number. I checked the history.yml and I'm passing 34.45 so the float is sent to the coffeescript file correctly.

Any help would be appreciated!

@Prashant0608

does any one have a working job file

@liam1027

Also looking for a working copy.

Edit: I got this to work. I was having trouble even getting the bars to load without error as Cunningham and other indicated. Here's how I fixed it:

First start by deleting the widget:
rm -rf progressbars

Next, create a new widget using dashing:
dashing generate widget progressbars

Copy and paste the file contents outlined above into their respective files.

  • NB: In progressbars.coffee, don't paste in the first line. Instead leave it as:
    class Dashing.Progressbars extends Dashing.Widget
    Notice the lower case "b" in "Progressbars".

You may need to restart dashing before this works.

I'm a ruby / coffee script noob, so I'm not sure why this works and what conflict it resolved. Surely there's a better way to fix this, but after some time I did get it to work.

For a sample job file, try this:
SCHEDULER.every '5s', :first_in => 0 do |job|
progress_items = [{ name: "used", value: 10 }, { name: "free", value: 90 }]
send_event( 'progress_bars', {title: "SPDB01", progress_items: []} )
end

For curl requests, try this:
curl -d '{ "auth_token":"YOUR_AUTH_TOKEN", "progress_items": [{"name": "used", "progress": "10"}, {"name": "free", "progress": "90"}] }' http://localhost:3030/widgets/progress_bars

HTH someone else

@G4ce2020

Hi Awesome widget, has anyone tried to edit the progress bars to change color based on the result? Say 0-24 green, 25-49 yellow, 51-74 orange and 75+ red?

@Wernervdmerwe

is it possible to change the base color within the erb?

@makenstein

Hi guys,
i tried to create a job file that extracts data from an excel file and send it to the widget but unfortunately i am not able to get it to work :(
i can see my data on the History.yml, but the widget does not show any data or any bars even.
can anyone help me with that please?

this is my code:

require 'date'
require 'roo'
require 'active_support'
require 'active_support/core_ext'

PROGRESSION_FILENAME = 'progressbars.xlsx'

SCHEDULER.every '30s', :first_in => '1s' do
progress = []
progress_report = Roo::Excelx.new(PROGRESSION_FILENAME)
(2..progress_report.last_row).each do |row|
name = progress_report.cell(row, 1)
name.sub!(/(\S+),\s_(\S+)._/, '\2 \1')

prog = progress_report.cell(row, 4)


progress.push({name: name, prog: prog})

end

assemble data to send to client

progress_data = []
progress_data.each do |progress|
progress_data.push({
'label' => progress[:name],
'value' => progress[:prog]
})
end

send_event('progression', { items: progress_data,
class: 'icon icon-background icon-calendar'})
end

@Solarlight1

Good Afternoon All,

Sorry to ask a really stupid question, however I am totally new to Ruby and Debian and have managed to make most things work.

Would someone mind giving me some example programming for the job

I need an array or urls, which will return 1 figure per url already in % which I can then use to build the array for the Progress Bar and then how to pass this to the progress bar.

Sorry im i NOOB!

Regards
Mark

@i601254
i601254 commented Apr 16, 2016

Just discovered your awesome widget everything seems to be running great except the widget is very small. I have three data sets feeding the widget but all three bars are less than an inch long and none of the "names" are visible. Here's how I added to the dashboard:

`

`
@i601254
i601254 commented Apr 19, 2016

figured out that it seems to be a .css issue. When I remove height and width from .widget.widget-progress-bars widget seems to scale perfectly. When I add the Progress Bars widget to the sample dashboard it stops working, doesn't display background color, or bar colors. However I can see the text scrolling and updating with an "invisible" bar for each.

Any tips?

@treesloth

I have a working example of this for some internal dashboards. I'm hoping to make one change. With some widget types, I can pass a color as part of the JSON dataset; for example, I can send "status": [55.0, "#4C4C4C"] to a meter widget and change both the value and the color in which the meter is rendered. Is there any way to do something similar for this widget? Any other reasonably flexible method would also be great. I'm having a pretty tough time navigating the code for it... very much not my specialty. Thanks in advance.

@roysv
roysv commented Sep 25, 2016

Her is my working example of an test job.

SCHEDULER.every '5s', :first_in => 0 do |job|
        progress_items = [{ name: "used", progress: 10 }, { name: "free", progress: 90 }]
        send_event 'progress_bars', {title: "Test Job", progress_items: progress_items}
end

@augustf
augustf commented Feb 7, 2017

I've noticed an issue with the sizing of the widget overall and the spacing between items. It seems to occur under both Dashing and the Smashing fork, and I can't find the CSS that might be causing this.
screen shot 2017-02-06 at 9 56 04 pm

@Zerotouch
Zerotouch commented Feb 9, 2017 edited

I've noticed an issue with the sizing of the widget overall and the spacing between items. It seems to occur under both Dashing and the Smashing fork, and I can't find the CSS that might be causing this.

i have removed the bolded ".widget" part from the scss and seems working to me:
UPDATE: i also modified the padding, also bolded
.widget.widget-progress-bars {
height: 100%;
width: 100%;
padding-right: 5px;
padding-left: 5px;
padding-top: 0px;
padding-bottom: 0px;

position:relative;
background-color: $base-color;
vertical-align: baseline;

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