├── .env-dist ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── assets ├── images │ └── google_analytics.svg ├── javascripts │ ├── application.coffee │ ├── d3-3.2.8.js │ ├── dashing.gridster.coffee │ ├── gridster │ │ ├── jquery.gridster.js │ │ └── jquery.leanModal.min.js │ ├── jquery.knob.js │ └── rickshaw-1.4.3.min.js └── stylesheets │ ├── application.scss │ ├── icon-background-fix.scss │ └── jquery.gridster.css ├── config.ru ├── dashboards ├── google_analytics.erb └── layout.erb ├── jobs └── google_analytics.rb ├── public ├── 404.html └── favicon.ico └── widgets ├── clock ├── clock.coffee ├── clock.html └── clock.scss ├── comments ├── comments.coffee ├── comments.html └── comments.scss ├── graph ├── graph.coffee ├── graph.html └── graph.scss ├── iframe ├── iframe.coffee ├── iframe.html └── iframe.scss ├── image ├── image.coffee ├── image.html └── image.scss ├── list ├── list.coffee ├── list.html └── list.scss ├── meter ├── meter.coffee ├── meter.html └── meter.scss ├── number ├── number.coffee ├── number.html └── number.scss └── text ├── text.coffee ├── text.html └── text.scss /.env-dist: -------------------------------------------------------------------------------- 1 | # Dashing 2 | export DASHING_AUTH_TOKEN=26bd6f4b4c3a491fbabdb06b39e85997 3 | #export BASIC_AUTH_USERNAME=some_username 4 | #export BASIC_AUTH_PASSWORD=some_password 5 | 6 | # Google Analytics 7 | export GOOGLE_SERVICE_ACCOUNT_EMAIL=210987654321-3rmagherd99kitt3h5@developer.gserviceaccount.com 8 | export GOOGLE_PRIVATE_KEY="quoted contents of the certificate; very long string" 9 | export GOOGLE_PRIVATE_KEY_SECRET=notasecret 10 | export GOOGLE_ANALYTICS_VIEW_ID=000000000 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .env 3 | .ruby-version 4 | history.yml 5 | vendor/* 6 | *DS_STORE 7 | tmp/* 8 | log/* 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Include: 5 | - '**/Rakefile' 6 | - '**/config.ru' 7 | Exclude: 8 | - '.bundle/**/*' 9 | - 'bin/**/*' 10 | - 'config/**/*' 11 | - 'db/**/*' 12 | - 'log/**/*' 13 | - 'public/**/*' 14 | - 'vendor/**/*' 15 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by `rubocop --auto-gen-config` 2 | # on 2015-05-19 21:05:23 -0500 using RuboCop version 0.31.0. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the offenses are removed from the code base. 5 | # Note that changes in the inspected code, or installation of new 6 | # versions of RuboCop, may require this file to be generated again. 7 | 8 | # Offense count: 7 9 | # Configuration parameters: AllowURI, URISchemes. 10 | Metrics/LineLength: 11 | Max: 139 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'dashing' 4 | gem 'sinatra_cyclist' 5 | gem 'httparty' 6 | gem 'xml-simple' 7 | 8 | # Environment loading while in development 9 | gem 'dotenv', groups: [:development, :test] 10 | 11 | # Job-specific Gems 12 | gem 'google-api-client', '~> 0.8.6' 13 | 14 | # Use Passenger on Heroku instead of Thin. 15 | gem 'passenger' 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.1) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.3.8) 11 | autoparse (0.3.3) 12 | addressable (>= 2.3.1) 13 | extlib (>= 0.9.15) 14 | multi_json (>= 1.0.0) 15 | backports (3.6.4) 16 | coffee-script (2.2.0) 17 | coffee-script-source 18 | execjs 19 | coffee-script-source (1.9.1.1) 20 | daemons (1.2.2) 21 | dashing (1.3.4) 22 | coffee-script (~> 2.2.0) 23 | execjs (~> 2.0.2) 24 | rack (~> 1.5.2) 25 | rufus-scheduler (~> 2.0.24) 26 | sass (~> 3.2.12) 27 | sinatra (~> 1.4.4) 28 | sinatra-contrib (~> 1.4.2) 29 | sprockets (~> 2.10.1) 30 | thin (~> 1.6.1) 31 | thor (~> 0.18.1) 32 | dotenv (2.0.1) 33 | eventmachine (1.0.7) 34 | execjs (2.0.2) 35 | extlib (0.9.16) 36 | faraday (0.9.1) 37 | multipart-post (>= 1.2, < 3) 38 | google-api-client (0.8.6) 39 | activesupport (>= 3.2) 40 | addressable (~> 2.3) 41 | autoparse (~> 0.3) 42 | extlib (~> 0.9) 43 | faraday (~> 0.9) 44 | googleauth (~> 0.3) 45 | launchy (~> 2.4) 46 | multi_json (~> 1.10) 47 | retriable (~> 1.4) 48 | signet (~> 0.6) 49 | googleauth (0.4.1) 50 | faraday (~> 0.9) 51 | jwt (~> 1.4) 52 | logging (~> 2.0) 53 | memoist (~> 0.12) 54 | multi_json (= 1.11) 55 | signet (~> 0.6) 56 | hike (1.2.3) 57 | httparty (0.13.4) 58 | json (~> 1.8) 59 | multi_xml (>= 0.5.2) 60 | i18n (0.7.0) 61 | json (1.8.2) 62 | jwt (1.5.0) 63 | launchy (2.4.3) 64 | addressable (~> 2.3) 65 | little-plugger (1.1.3) 66 | logging (2.0.0) 67 | little-plugger (~> 1.1) 68 | multi_json (~> 1.10) 69 | memoist (0.12.0) 70 | minitest (5.6.1) 71 | multi_json (1.11.0) 72 | multi_xml (0.5.5) 73 | multipart-post (2.0.0) 74 | passenger (5.0.7) 75 | rack 76 | rake (>= 0.8.1) 77 | rack (1.5.3) 78 | rack-protection (1.5.3) 79 | rack 80 | rack-test (0.6.3) 81 | rack (>= 1.0) 82 | rake (10.4.2) 83 | retriable (1.4.1) 84 | rufus-scheduler (2.0.24) 85 | tzinfo (>= 0.3.22) 86 | sass (3.2.19) 87 | signet (0.6.0) 88 | addressable (~> 2.3) 89 | extlib (~> 0.9) 90 | faraday (~> 0.9) 91 | jwt (~> 1.0) 92 | multi_json (~> 1.10) 93 | sinatra (1.4.6) 94 | rack (~> 1.4) 95 | rack-protection (~> 1.4) 96 | tilt (>= 1.3, < 3) 97 | sinatra-contrib (1.4.2) 98 | backports (>= 2.0) 99 | multi_json 100 | rack-protection 101 | rack-test 102 | sinatra (~> 1.4.0) 103 | tilt (~> 1.3) 104 | sinatra_cyclist (0.0.3) 105 | sinatra 106 | sprockets (2.10.2) 107 | hike (~> 1.2) 108 | multi_json (~> 1.0) 109 | rack (~> 1.0) 110 | tilt (~> 1.1, != 1.3.0) 111 | thin (1.6.3) 112 | daemons (~> 1.0, >= 1.0.9) 113 | eventmachine (~> 1.0) 114 | rack (~> 1.0) 115 | thor (0.18.1) 116 | thread_safe (0.3.5) 117 | tilt (1.4.1) 118 | tzinfo (1.2.2) 119 | thread_safe (~> 0.1) 120 | xml-simple (1.1.5) 121 | 122 | PLATFORMS 123 | ruby 124 | 125 | DEPENDENCIES 126 | dashing 127 | dotenv 128 | google-api-client (~> 0.8.6) 129 | httparty 130 | passenger 131 | sinatra_cyclist 132 | xml-simple 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Analytics Dashboard 2 | 3 | ![screenshot](https://cloud.githubusercontent.com/assets/80459/7712656/31a38222-fe38-11e4-998b-8d5d9a5c6956.png) 4 | 5 | This is a [Dashing](http://shopify.github.io/dashing) project that has been customized to add a Google Analytics view. It also has some other visual tweaks to bring Dashing up to speed. 6 | 7 | This project is heavily based on the work [@mtowers](https://github.com/mtowers) did for [his visitor count widget](https://gist.github.com/mtowers/5986576). 8 | 9 | ### Features 10 | 11 | - Configurable to show relevant goals that are set up in Google Analytics 12 | - Single-color (black) for all Dashboard widgets (This can be customized in the Widget SCSS) 13 | - Already set up to go on a wide-screen television 14 | - Configured with [Sinatra Cyclist](https://github.com/vrish88/sinatra_cyclist) to rotate through multiple dashboards 15 | - Uses the latest FontAwesome icons from the CDN 16 | - Fixes an annoying icon positioning issue on most default Dashing installs 17 | 18 | ### Set Up Instructions 19 | 20 | This project has three environment variables you must set. 21 | 22 | - `GOOGLE_SERVICE_ACCOUNT_EMAIL` 23 | - `GOOGLE_PRIVATE_KEY` 24 | - `GOOGLE_PRIVATE_KEY_SECRET` 25 | - `GOOGLE_ANALYTICS_VIEW_ID` 26 | 27 | Obtaining them can be a bit of a chore, particularly if you want to deploy to Heroku and are setting up the project so that *no private credentials are exposed on GitHub*. Be careful with this one. 28 | 29 | These instructions are lifted almost entirely from the original widget. 30 | 31 | #### 1. Create and download a new private key for Google API access 32 | 33 | 1. Go to https://code.google.com/apis/console 34 | 2. Click 'Create Project' 35 | 3. Enable 'Analytics API' service and accept both Terms of Services 36 | 4. Click 'API Access' in the left-hand navigation menu 37 | 5. Click 'Create an OAuth 2.0 Client ID' 38 | 6. Enter a product name (e.g. 'Dashing Widget') - logo and url are optional 39 | 7. Click 'Next' 40 | 8. Under Application Type, select 'Service Account' (Important step -- other account types will not work.) 41 | 9. Click 'Create Client ID' 42 | 10. Click 'Download private key' _NOTE: This will be your only opportunity to download this key._ 43 | 11. Note the password for your new private key, usually 'notasecret'. This will be the value for `GOOGLE_PRIVATE_KEY_SECRET` 44 | 12. Close the download key dialog 45 | 13. Find the details for the service account you just created and copy it's email address which will look something like this: `210987654321-3rmagherd99kitt3h5@developer.gserviceaccount.com`. This will be the value for `GOOGLE_SERVICE_ACCOUNT_EMAIL` 46 | 47 | #### 2. Attach your Google API service account to your Google Analytics profile 48 | 49 | _Note: you will need to be an administrator of the Google Analytics profile_ 50 | 51 | 1. Log in to your Google Analytics account: http://www.google.com/analytics/ 52 | 2. Click 'Admin' in the upper-right corner 53 | 3. Select the account containing the profile you wish to use 54 | 4. Select the property containing the profile you wish to use 55 | 5. Select the profile you wish to use 56 | 6. Click the 'Users' tab 57 | 7. Click '+ New User' 58 | 8. Enter the email address you copied from step 13 above 59 | 9. Click 'Add User' 60 | 61 | #### 3. Locate the ID for your Google Analytics data view 62 | 63 | 1. On your Google Analytics Admin section, click the 'View Settings' panel (Such as "All Data") 64 | 2. Copy your View ID (e.g. 654321). This is the value for `GOOGLE_ANALYTICS_VIEW_ID`. 65 | 66 | #### 4. Convert your Certificate to an ASCII string 67 | 68 | This is a variation from the original widget, and it is mainly so you do not store the certificate with the repository (and put it in an environment variable with other configuration credentials.) From the command line, run this to unlock the certificate. 69 | 70 | ```bash 71 | $ openssl pkcs12 -info -nodes -in YourApp-a0b2c2d3456.p12 > output.txt 72 | ``` 73 | 74 | When prompted for the password, it should be `notasecret`. 75 | 76 | The contents of `output.txt` can now be stored as the value for `GOOGLE_PRIVATE_KEY`. 77 | 78 | 1. Open the file in a text editor 79 | 2. Replace every line break with a `\n` so that it all fits on one line 80 | 3. Put quote marks around it. 81 | 4. In Heroku or other configuration management (locally, `.env`), set this string as the value of `GOOGLE_PRIVATE_KEY` (e.g. `export GOOGLE_PRIVATE_KEY="Bag Attributes\n friendlyName: privatekey\n localKeyID:...`) 82 | 83 | ## Starting the Dashboard 84 | 85 | You should be ready to run the Dashing dashboard as usual: 86 | 87 | ``` 88 | $ dashing start 89 | ``` 90 | 91 | Navigate to http://0.0.0.0:3030/_cycle to see everything in action. 92 | 93 | ## Customizing Your Dashboard 94 | 95 | The general idea of a Dashing dashboard is that a ruby file in the `./jobs` folder runs at a defined interval and publishes through the `send_event()` method. Each "event" is represented by a unique string, meaning that no two events can have the same name. On the other end, an ERB file in the `./dashboards` folder receives the event data. 96 | 97 | In this example, a 1 by 1 unit widget receives the event `ga_sessions`, as defined in `data-id`. The `data-view` parameter tells the browser to use the `Number` widget, and the `data-title` and `data-moreinfo` attributes contain some useful data that is shown with the data. Each type of widget has a different set of data that it is designed to accommodate. 98 | 99 | The `` tag tells the widget what icon to show faded in the background. For this dashboard, all of the possible icons can be found at [FontAwesome](https://fontawesome.io). 100 | 101 | ``` 102 |
  • 103 |
    104 | 105 |
  • 106 | ``` 107 | 108 | ### Events List 109 | 110 | #### Number 111 | 112 | - `ga_sessions` 113 | - `ga_new_sessions` 114 | - `ga_bounce_rate` 115 | - `ga_goal_1_completions` 116 | - `ga_goal_2_completions` 117 | - `ga_goal_3_completions` 118 | - `ga_goal_4_completions` 119 | - `ga_goal_5_completions` 120 | - `ga_goal_6_completions` 121 | - `ga_goal_7_completions` 122 | - `ga_goal_8_completions` 123 | - `ga_goal_9_completions` 124 | - `ga_goal_10_completions` 125 | - `ga_goal_11_completions` 126 | - `ga_goal_12_completions` 127 | - `ga_goal_13_completions` 128 | - `ga_goal_14_completions` 129 | - `ga_goal_15_completions` 130 | - `ga_goal_16_completions` 131 | - `ga_goal_17_completions` 132 | - `ga_goal_18_completions` 133 | - `ga_goal_19_completions` 134 | - `ga_goal_20_completions` 135 | - `ga_goal_1_conversion_rate` 136 | - `ga_goal_2_conversion_rate` 137 | - `ga_goal_3_conversion_rate` 138 | - `ga_goal_4_conversion_rate` 139 | - `ga_goal_5_conversion_rate` 140 | - `ga_goal_6_conversion_rate` 141 | - `ga_goal_7_conversion_rate` 142 | - `ga_goal_8_conversion_rate` 143 | - `ga_goal_9_conversion_rate` 144 | - `ga_goal_10_conversion_rate` 145 | - `ga_goal_11_conversion_rate` 146 | - `ga_goal_12_conversion_rate` 147 | - `ga_goal_13_conversion_rate` 148 | - `ga_goal_14_conversion_rate` 149 | - `ga_goal_15_conversion_rate` 150 | - `ga_goal_16_conversion_rate` 151 | - `ga_goal_17_conversion_rate` 152 | - `ga_goal_18_conversion_rate` 153 | - `ga_goal_19_conversion_rate` 154 | - `ga_goal_20_conversion_rate` 155 | 156 | #### Graph 157 | 158 | - `ga_session_chart` 159 | - `ga_goal_1_completions_chart` 160 | - `ga_goal_2_completions_chart` 161 | - `ga_goal_3_completions_chart` 162 | - `ga_goal_4_completions_chart` 163 | - `ga_goal_5_completions_chart` 164 | - `ga_goal_6_completions_chart` 165 | - `ga_goal_7_completions_chart` 166 | - `ga_goal_8_completions_chart` 167 | - `ga_goal_9_completions_chart` 168 | - `ga_goal_10_completions_chart` 169 | - `ga_goal_11_completions_chart` 170 | - `ga_goal_12_completions_chart` 171 | - `ga_goal_13_completions_chart` 172 | - `ga_goal_14_completions_chart` 173 | - `ga_goal_15_completions_chart` 174 | - `ga_goal_16_completions_chart` 175 | - `ga_goal_17_completions_chart` 176 | - `ga_goal_18_completions_chart` 177 | - `ga_goal_19_completions_chart` 178 | - `ga_goal_20_completions_chart` 179 | 180 | #### List 181 | 182 | - `ga_traffic_sources` 183 | 184 | -------------------------------------------------------------------------------- /assets/images/google_analytics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /assets/javascripts/application.coffee: -------------------------------------------------------------------------------- 1 | # dashing.js is located in the dashing framework 2 | # It includes jquery & batman for you. 3 | #= require dashing.js 4 | 5 | #= require_directory . 6 | #= require_tree ../../widgets 7 | 8 | console.log("Yeah! The dashboard has started!") 9 | 10 | Dashing.on 'ready', -> 11 | Dashing.widget_margins ||= [5, 5] 12 | Dashing.widget_base_dimensions ||= [300, 360] 13 | Dashing.numColumns ||= 4 14 | 15 | contentWidth = (Dashing.widget_base_dimensions[0] + Dashing.widget_margins[0] * 2) * Dashing.numColumns 16 | 17 | Batman.setImmediate -> 18 | $('.gridster').width(contentWidth) 19 | $('.gridster ul:first').gridster 20 | widget_margins: Dashing.widget_margins 21 | widget_base_dimensions: Dashing.widget_base_dimensions 22 | avoid_overlapped_widgets: !Dashing.customGridsterLayout 23 | draggable: 24 | stop: Dashing.showGridsterInstructions 25 | start: -> Dashing.currentWidgetPositions = Dashing.getWidgetPositions() 26 | -------------------------------------------------------------------------------- /assets/javascripts/dashing.gridster.coffee: -------------------------------------------------------------------------------- 1 | #= require_directory ./gridster 2 | 3 | # This file enables gridster integration (http://gridster.net/) 4 | # Delete it if you'd rather handle the layout yourself. 5 | # You'll miss out on a lot if you do, but we won't hold it against you. 6 | 7 | Dashing.gridsterLayout = (positions) -> 8 | Dashing.customGridsterLayout = true 9 | positions = positions.replace(/^"|"$/g, '') 10 | positions = $.parseJSON(positions) 11 | widgets = $("[data-row^=]") 12 | for widget, index in widgets 13 | $(widget).attr('data-row', positions[index].row) 14 | $(widget).attr('data-col', positions[index].col) 15 | 16 | Dashing.getWidgetPositions = -> 17 | $(".gridster ul:first").gridster().data('gridster').serialize() 18 | 19 | Dashing.showGridsterInstructions = -> 20 | newWidgetPositions = Dashing.getWidgetPositions() 21 | 22 | unless JSON.stringify(newWidgetPositions) == JSON.stringify(Dashing.currentWidgetPositions) 23 | Dashing.currentWidgetPositions = newWidgetPositions 24 | $('#save-gridster').slideDown() 25 | $('#gridster-code').text(" 26 | 31 | ") 32 | 33 | $ -> 34 | $('#save-gridster').leanModal() 35 | 36 | $('#save-gridster').click -> 37 | $('#save-gridster').slideUp() 38 | -------------------------------------------------------------------------------- /assets/javascripts/gridster/jquery.leanModal.min.js: -------------------------------------------------------------------------------- 1 | // leanModal v1.1 by Ray Stone - http://finelysliced.com.au 2 | // Dual licensed under the MIT and GPL 3 | 4 | (function($){$.fn.extend({leanModal:function(options){var defaults={top:100,overlay:0.5,closeButton:null};var overlay=$("
    ");$("body").append(overlay);options=$.extend(defaults,options);return this.each(function(){var o=options;$(this).click(function(e){var modal_id=$(this).attr("href");$("#lean_overlay").click(function(){close_modal(modal_id)});$(o.closeButton).click(function(){close_modal(modal_id)});var modal_height=$(modal_id).outerHeight();var modal_width=$(modal_id).outerWidth(); 5 | $("#lean_overlay").css({"display":"block",opacity:0});$("#lean_overlay").fadeTo(200,o.overlay);$(modal_id).css({"display":"block","position":"fixed","opacity":0,"z-index":11000,"left":50+"%","margin-left":-(modal_width/2)+"px","top":o.top+"px"});$(modal_id).fadeTo(200,1);e.preventDefault()})});function close_modal(modal_id){$("#lean_overlay").fadeOut(200);$(modal_id).css({"display":"none"})}}})})(jQuery); 6 | -------------------------------------------------------------------------------- /assets/javascripts/jquery.knob.js: -------------------------------------------------------------------------------- 1 | /*!jQuery Knob*/ 2 | /** 3 | * Downward compatible, touchable dial 4 | * 5 | * Version: 1.2.0 (15/07/2012) 6 | * Requires: jQuery v1.7+ 7 | * 8 | * Copyright (c) 2012 Anthony Terrien 9 | * Under MIT and GPL licenses: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * http://www.gnu.org/licenses/gpl.html 12 | * 13 | * Thanks to vor, eskimoblood, spiffistan, FabrizioC 14 | */ 15 | $(function () { 16 | 17 | /** 18 | * Kontrol library 19 | */ 20 | "use strict"; 21 | 22 | /** 23 | * Definition of globals and core 24 | */ 25 | var k = {}, // kontrol 26 | max = Math.max, 27 | min = Math.min; 28 | 29 | k.c = {}; 30 | k.c.d = $(document); 31 | k.c.t = function (e) { 32 | return e.originalEvent.touches.length - 1; 33 | }; 34 | 35 | /** 36 | * Kontrol Object 37 | * 38 | * Definition of an abstract UI control 39 | * 40 | * Each concrete component must call this one. 41 | * 42 | * k.o.call(this); 43 | * 44 | */ 45 | k.o = function () { 46 | var s = this; 47 | 48 | this.o = null; // array of options 49 | this.$ = null; // jQuery wrapped element 50 | this.i = null; // mixed HTMLInputElement or array of HTMLInputElement 51 | this.g = null; // 2D graphics context for 'pre-rendering' 52 | this.v = null; // value ; mixed array or integer 53 | this.cv = null; // change value ; not commited value 54 | this.x = 0; // canvas x position 55 | this.y = 0; // canvas y position 56 | this.$c = null; // jQuery canvas element 57 | this.c = null; // rendered canvas context 58 | this.t = 0; // touches index 59 | this.isInit = false; 60 | this.fgColor = null; // main color 61 | this.pColor = null; // previous color 62 | this.dH = null; // draw hook 63 | this.cH = null; // change hook 64 | this.eH = null; // cancel hook 65 | this.rH = null; // release hook 66 | 67 | this.run = function () { 68 | var cf = function (e, conf) { 69 | var k; 70 | for (k in conf) { 71 | s.o[k] = conf[k]; 72 | } 73 | s.init(); 74 | s._configure() 75 | ._draw(); 76 | }; 77 | 78 | if(this.$.data('kontroled')) return; 79 | this.$.data('kontroled', true); 80 | 81 | this.extend(); 82 | this.o = $.extend( 83 | { 84 | // Config 85 | min : this.$.data('min') || 0, 86 | max : this.$.data('max') || 100, 87 | stopper : true, 88 | readOnly : this.$.data('readonly'), 89 | 90 | // UI 91 | cursor : (this.$.data('cursor') === true && 30) 92 | || this.$.data('cursor') 93 | || 0, 94 | thickness : this.$.data('thickness') || 0.35, 95 | width : this.$.data('width') || 200, 96 | height : this.$.data('height') || 200, 97 | displayInput : this.$.data('displayinput') == null || this.$.data('displayinput'), 98 | displayPrevious : this.$.data('displayprevious'), 99 | fgColor : this.$.data('fgcolor') || '#87CEEB', 100 | inline : false, 101 | 102 | // Hooks 103 | draw : null, // function () {} 104 | change : null, // function (value) {} 105 | cancel : null, // function () {} 106 | release : null // function (value) {} 107 | }, this.o 108 | ); 109 | 110 | // routing value 111 | if(this.$.is('fieldset')) { 112 | 113 | // fieldset = array of integer 114 | this.v = {}; 115 | this.i = this.$.find('input') 116 | this.i.each(function(k) { 117 | var $this = $(this); 118 | s.i[k] = $this; 119 | s.v[k] = $this.val(); 120 | 121 | $this.bind( 122 | 'change' 123 | , function () { 124 | var val = {}; 125 | val[k] = $this.val(); 126 | s.val(val); 127 | } 128 | ); 129 | }); 130 | this.$.find('legend').remove(); 131 | 132 | } else { 133 | // input = integer 134 | this.i = this.$; 135 | this.v = this.$.val(); 136 | (this.v == '') && (this.v = this.o.min); 137 | 138 | this.$.bind( 139 | 'change' 140 | , function () { 141 | s.val(s.$.val()); 142 | } 143 | ); 144 | } 145 | 146 | (!this.o.displayInput) && this.$.hide(); 147 | 148 | this.$c = $(''); 151 | this.c = this.$c[0].getContext("2d"); 152 | 153 | this.$ 154 | .wrap($('
    ')) 157 | .before(this.$c); 158 | 159 | if (this.v instanceof Object) { 160 | this.cv = {}; 161 | this.copy(this.v, this.cv); 162 | } else { 163 | this.cv = this.v; 164 | } 165 | 166 | this.$ 167 | .bind("configure", cf) 168 | .parent() 169 | .bind("configure", cf); 170 | 171 | this._listen() 172 | ._configure() 173 | ._xy() 174 | .init(); 175 | 176 | this.isInit = true; 177 | 178 | this._draw(); 179 | 180 | return this; 181 | }; 182 | 183 | this._draw = function () { 184 | 185 | // canvas pre-rendering 186 | var d = true, 187 | c = document.createElement('canvas'); 188 | 189 | c.width = s.o.width; 190 | c.height = s.o.height; 191 | s.g = c.getContext('2d'); 192 | 193 | s.clear(); 194 | 195 | s.dH 196 | && (d = s.dH()); 197 | 198 | (d !== false) && s.draw(); 199 | 200 | s.c.drawImage(c, 0, 0); 201 | c = null; 202 | }; 203 | 204 | this._touch = function (e) { 205 | 206 | var touchMove = function (e) { 207 | 208 | var v = s.xy2val( 209 | e.originalEvent.touches[s.t].pageX, 210 | e.originalEvent.touches[s.t].pageY 211 | ); 212 | 213 | if (v == s.cv) return; 214 | 215 | if ( 216 | s.cH 217 | && (s.cH(v) === false) 218 | ) return; 219 | 220 | 221 | s.change(v); 222 | s._draw(); 223 | }; 224 | 225 | // get touches index 226 | this.t = k.c.t(e); 227 | 228 | // First touch 229 | touchMove(e); 230 | 231 | // Touch events listeners 232 | k.c.d 233 | .bind("touchmove.k", touchMove) 234 | .bind( 235 | "touchend.k" 236 | , function () { 237 | k.c.d.unbind('touchmove.k touchend.k'); 238 | 239 | if ( 240 | s.rH 241 | && (s.rH(s.cv) === false) 242 | ) return; 243 | 244 | s.val(s.cv); 245 | } 246 | ); 247 | 248 | return this; 249 | }; 250 | 251 | this._mouse = function (e) { 252 | 253 | var mouseMove = function (e) { 254 | var v = s.xy2val(e.pageX, e.pageY); 255 | if (v == s.cv) return; 256 | 257 | if ( 258 | s.cH 259 | && (s.cH(v) === false) 260 | ) return; 261 | 262 | s.change(v); 263 | s._draw(); 264 | }; 265 | 266 | // First click 267 | mouseMove(e); 268 | 269 | // Mouse events listeners 270 | k.c.d 271 | .bind("mousemove.k", mouseMove) 272 | .bind( 273 | // Escape key cancel current change 274 | "keyup.k" 275 | , function (e) { 276 | if (e.keyCode === 27) { 277 | k.c.d.unbind("mouseup.k mousemove.k keyup.k"); 278 | 279 | if ( 280 | s.eH 281 | && (s.eH() === false) 282 | ) return; 283 | 284 | s.cancel(); 285 | } 286 | } 287 | ) 288 | .bind( 289 | "mouseup.k" 290 | , function (e) { 291 | k.c.d.unbind('mousemove.k mouseup.k keyup.k'); 292 | 293 | if ( 294 | s.rH 295 | && (s.rH(s.cv) === false) 296 | ) return; 297 | 298 | s.val(s.cv); 299 | } 300 | ); 301 | 302 | return this; 303 | }; 304 | 305 | this._xy = function () { 306 | var o = this.$c.offset(); 307 | this.x = o.left; 308 | this.y = o.top; 309 | return this; 310 | }; 311 | 312 | this._listen = function () { 313 | 314 | if (!this.o.readOnly) { 315 | this.$c 316 | .bind( 317 | "mousedown" 318 | , function (e) { 319 | e.preventDefault(); 320 | s._xy()._mouse(e); 321 | } 322 | ) 323 | .bind( 324 | "touchstart" 325 | , function (e) { 326 | e.preventDefault(); 327 | s._xy()._touch(e); 328 | } 329 | ); 330 | this.listen(); 331 | } else { 332 | this.$.attr('readonly', 'readonly'); 333 | } 334 | 335 | return this; 336 | }; 337 | 338 | this._configure = function () { 339 | 340 | // Hooks 341 | if (this.o.draw) this.dH = this.o.draw; 342 | if (this.o.change) this.cH = this.o.change; 343 | if (this.o.cancel) this.eH = this.o.cancel; 344 | if (this.o.release) this.rH = this.o.release; 345 | 346 | if (this.o.displayPrevious) { 347 | this.pColor = this.h2rgba(this.o.fgColor, "0.4"); 348 | this.fgColor = this.h2rgba(this.o.fgColor, "0.6"); 349 | } else { 350 | this.fgColor = this.o.fgColor; 351 | } 352 | 353 | return this; 354 | }; 355 | 356 | this._clear = function () { 357 | this.$c[0].width = this.$c[0].width; 358 | }; 359 | 360 | // Abstract methods 361 | this.listen = function () {}; // on start, one time 362 | this.extend = function () {}; // each time configure triggered 363 | this.init = function () {}; // each time configure triggered 364 | this.change = function (v) {}; // on change 365 | this.val = function (v) {}; // on release 366 | this.xy2val = function (x, y) {}; // 367 | this.draw = function () {}; // on change / on release 368 | this.clear = function () { this._clear(); }; 369 | 370 | // Utils 371 | this.h2rgba = function (h, a) { 372 | var rgb; 373 | h = h.substring(1,7) 374 | rgb = [parseInt(h.substring(0,2),16) 375 | ,parseInt(h.substring(2,4),16) 376 | ,parseInt(h.substring(4,6),16)]; 377 | return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")"; 378 | }; 379 | 380 | this.copy = function (f, t) { 381 | for (var i in f) { t[i] = f[i]; } 382 | }; 383 | }; 384 | 385 | 386 | /** 387 | * k.Dial 388 | */ 389 | k.Dial = function () { 390 | k.o.call(this); 391 | 392 | this.startAngle = null; 393 | this.xy = null; 394 | this.radius = null; 395 | this.lineWidth = null; 396 | this.cursorExt = null; 397 | this.w2 = null; 398 | this.PI2 = 2*Math.PI; 399 | 400 | this.extend = function () { 401 | this.o = $.extend( 402 | { 403 | bgColor : this.$.data('bgcolor') || '#EEEEEE', 404 | angleOffset : this.$.data('angleoffset') || 0, 405 | angleArc : this.$.data('anglearc') || 360, 406 | inline : true 407 | }, this.o 408 | ); 409 | }; 410 | 411 | this.val = function (v) { 412 | if (null != v) { 413 | this.cv = this.o.stopper ? max(min(v, this.o.max), this.o.min) : v; 414 | this.v = this.cv; 415 | this.$.val(this.v); 416 | this._draw(); 417 | } else { 418 | return this.v; 419 | } 420 | }; 421 | 422 | this.xy2val = function (x, y) { 423 | var a, ret; 424 | 425 | a = Math.atan2( 426 | x - (this.x + this.w2) 427 | , - (y - this.y - this.w2) 428 | ) - this.angleOffset; 429 | 430 | if(this.angleArc != this.PI2 && (a < 0) && (a > -0.5)) { 431 | // if isset angleArc option, set to min if .5 under min 432 | a = 0; 433 | } else if (a < 0) { 434 | a += this.PI2; 435 | } 436 | 437 | ret = ~~ (0.5 + (a * (this.o.max - this.o.min) / this.angleArc)) 438 | + this.o.min; 439 | 440 | this.o.stopper 441 | && (ret = max(min(ret, this.o.max), this.o.min)); 442 | 443 | return ret; 444 | }; 445 | 446 | this.listen = function () { 447 | // bind MouseWheel 448 | var s = this, 449 | mw = function (e) { 450 | e.preventDefault(); 451 | 452 | var ori = e.originalEvent 453 | ,deltaX = ori.detail || ori.wheelDeltaX 454 | ,deltaY = ori.detail || ori.wheelDeltaY 455 | ,v = parseInt(s.$.val()) + (deltaX>0 || deltaY>0 ? 1 : deltaX<0 || deltaY<0 ? -1 : 0); 456 | 457 | if ( 458 | s.cH 459 | && (s.cH(v) === false) 460 | ) return; 461 | 462 | s.val(v); 463 | } 464 | , kval, to, m = 1, kv = {37:-1, 38:1, 39:1, 40:-1}; 465 | 466 | this.$ 467 | .bind( 468 | "keydown" 469 | ,function (e) { 470 | var kc = e.keyCode; 471 | kval = parseInt(String.fromCharCode(kc)); 472 | 473 | if (isNaN(kval)) { 474 | 475 | (kc !== 13) // enter 476 | && (kc !== 8) // bs 477 | && (kc !== 9) // tab 478 | && (kc !== 189) // - 479 | && e.preventDefault(); 480 | 481 | // arrows 482 | if ($.inArray(kc,[37,38,39,40]) > -1) { 483 | e.preventDefault(); 484 | 485 | var v = parseInt(s.$.val()) + kv[kc] * m; 486 | 487 | s.o.stopper 488 | && (v = max(min(v, s.o.max), s.o.min)); 489 | 490 | s.change(v); 491 | s._draw(); 492 | 493 | // long time keydown speed-up 494 | to = window.setTimeout( 495 | function () { m*=2; } 496 | ,30 497 | ); 498 | } 499 | } 500 | } 501 | ) 502 | .bind( 503 | "keyup" 504 | ,function (e) { 505 | if (isNaN(kval)) { 506 | if (to) { 507 | window.clearTimeout(to); 508 | to = null; 509 | m = 1; 510 | s.val(s.$.val()); 511 | } 512 | } else { 513 | // kval postcond 514 | (s.$.val() > s.o.max && s.$.val(s.o.max)) 515 | || (s.$.val() < s.o.min && s.$.val(s.o.min)); 516 | } 517 | 518 | } 519 | ); 520 | 521 | this.$c.bind("mousewheel DOMMouseScroll", mw); 522 | this.$.bind("mousewheel DOMMouseScroll", mw) 523 | }; 524 | 525 | this.init = function () { 526 | 527 | if ( 528 | this.v < this.o.min 529 | || this.v > this.o.max 530 | ) this.v = this.o.min; 531 | 532 | this.$.val(this.v); 533 | this.w2 = this.o.width / 2; 534 | this.cursorExt = this.o.cursor / 100; 535 | this.xy = this.w2; 536 | this.lineWidth = this.xy * this.o.thickness; 537 | this.radius = this.xy - this.lineWidth / 2; 538 | 539 | this.o.angleOffset 540 | && (this.o.angleOffset = isNaN(this.o.angleOffset) ? 0 : this.o.angleOffset); 541 | 542 | this.o.angleArc 543 | && (this.o.angleArc = isNaN(this.o.angleArc) ? this.PI2 : this.o.angleArc); 544 | 545 | // deg to rad 546 | this.angleOffset = this.o.angleOffset * Math.PI / 180; 547 | this.angleArc = this.o.angleArc * Math.PI / 180; 548 | 549 | // compute start and end angles 550 | this.startAngle = 1.5 * Math.PI + this.angleOffset; 551 | this.endAngle = 1.5 * Math.PI + this.angleOffset + this.angleArc; 552 | 553 | var s = max( 554 | String(Math.abs(this.o.max)).length 555 | , String(Math.abs(this.o.min)).length 556 | , 2 557 | ) + 2; 558 | 559 | this.o.displayInput 560 | && this.i.css({ 561 | 'width' : ((this.o.width / 2 + 4) >> 0) + 'px' 562 | ,'height' : ((this.o.width / 3) >> 0) + 'px' 563 | ,'position' : 'absolute' 564 | ,'vertical-align' : 'middle' 565 | ,'margin-top' : ((this.o.width / 3) >> 0) + 'px' 566 | ,'margin-left' : '-' + ((this.o.width * 3 / 4 + 2) >> 0) + 'px' 567 | ,'border' : 0 568 | ,'background' : 'none' 569 | ,'font' : 'bold ' + ((this.o.width / s) >> 0) + 'px Arial' 570 | ,'text-align' : 'center' 571 | ,'color' : this.o.fgColor 572 | ,'padding' : '0px' 573 | ,'-webkit-appearance': 'none' 574 | }) 575 | || this.i.css({ 576 | 'width' : '0px' 577 | ,'visibility' : 'hidden' 578 | }); 579 | }; 580 | 581 | this.change = function (v) { 582 | this.cv = v; 583 | this.$.val(v); 584 | }; 585 | 586 | this.angle = function (v) { 587 | return (v - this.o.min) * this.angleArc / (this.o.max - this.o.min); 588 | }; 589 | 590 | this.draw = function () { 591 | 592 | var c = this.g, // context 593 | a = this.angle(this.cv) // Angle 594 | , sat = this.startAngle // Start angle 595 | , eat = sat + a // End angle 596 | , sa, ea // Previous angles 597 | , r = 1; 598 | 599 | c.lineWidth = this.lineWidth; 600 | 601 | this.o.cursor 602 | && (sat = eat - this.cursorExt) 603 | && (eat = eat + this.cursorExt); 604 | 605 | c.beginPath(); 606 | c.strokeStyle = this.o.bgColor; 607 | c.arc(this.xy, this.xy, this.radius, this.endAngle, this.startAngle, true); 608 | c.stroke(); 609 | 610 | if (this.o.displayPrevious) { 611 | ea = this.startAngle + this.angle(this.v); 612 | sa = this.startAngle; 613 | this.o.cursor 614 | && (sa = ea - this.cursorExt) 615 | && (ea = ea + this.cursorExt); 616 | 617 | c.beginPath(); 618 | c.strokeStyle = this.pColor; 619 | c.arc(this.xy, this.xy, this.radius, sa, ea, false); 620 | c.stroke(); 621 | r = (this.cv == this.v); 622 | } 623 | 624 | c.beginPath(); 625 | c.strokeStyle = r ? this.o.fgColor : this.fgColor ; 626 | c.arc(this.xy, this.xy, this.radius, sat, eat, false); 627 | c.stroke(); 628 | }; 629 | 630 | this.cancel = function () { 631 | this.val(this.v); 632 | }; 633 | }; 634 | 635 | $.fn.dial = $.fn.knob = function (o) { 636 | return this.each( 637 | function () { 638 | var d = new k.Dial(); 639 | d.o = o; 640 | d.$ = $(this); 641 | d.run(); 642 | } 643 | ).parent(); 644 | }; 645 | 646 | }); -------------------------------------------------------------------------------- /assets/javascripts/rickshaw-1.4.3.min.js: -------------------------------------------------------------------------------- 1 | var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);this.setRenderer(args.renderer||this.renderer.name,args)};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var tzOffset=(new Date).getTimezoneOffset()*60;var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var nearFuture;var rounded;if(unit.name=="month"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setUTCFullYear(nearFuture.getUTCFullYear());rounded.setUTCMonth(nearFuture.getUTCMonth());rounded.setUTCDate(1);rounded.setUTCHours(0);rounded.setUTCMinutes(0);rounded.setUTCSeconds(0);rounded.setUTCMilliseconds(0);return rounded.getTime()/1e3}if(unit.name=="year"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setUTCFullYear(nearFuture.getUTCFullYear());rounded.setUTCMonth(0);rounded.setUTCDate(1);rounded.setUTCHours(0);rounded.setUTCMinutes(0);rounded.setUTCSeconds(0);rounded.setUTCMilliseconds(0);return rounded.getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var tzOffset=(new Date).getTimezoneOffset()*60;var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var nearFuture;var rounded;if(unit.name=="day"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(1);rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="year"){nearFuture=new Date((time+unit.seconds-1)*1e3);rounded=new Date(0);rounded.setFullYear(nearFuture.getFullYear());rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(1);rounded.setMonth(0);return rounded.getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize));this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize))}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var adjustedScale=this.scale.copy().range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line,index){if(l===line){if(index>0&&self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.length-index-1; 2 | line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.$=="undefined"){throw"couldn't find jQuery at window.$"}if(typeof window.$.ui=="undefined"){throw"couldn't find jQuery UI at window.$.ui"}$(function(){$(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];$(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});$(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)yMax)yMax=y});if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var nodes=vis.selectAll("path").data(data).enter().append("svg:path").attr("d",this.seriesPathFactory());var i=0;series.forEach(function(series){if(series.disabled)return;series.path=nodes[0][i++];this._styleSeries(series)},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);series.path.setAttribute("class",series.className)},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x(series.stack[0].x+frequentInterval.magnitude*(1-this.gapSize));return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}}); -------------------------------------------------------------------------------- /assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | //=require_directory . 3 | //=require_tree ../../widgets 4 | */ 5 | // ---------------------------------------------------------------------------- 6 | // Sass declarations 7 | // ---------------------------------------------------------------------------- 8 | $background-color: #222; 9 | $text-color: #fff; 10 | 11 | $background-warning-color-1: #e82711; 12 | $background-warning-color-2: #9b2d23; 13 | $text-warning-color: #fff; 14 | 15 | $background-danger-color-1: #eeae32; 16 | $background-danger-color-2: #ff9618; 17 | $text-danger-color: #fff; 18 | 19 | @-webkit-keyframes status-warning-background { 20 | 0% { background-color: $background-warning-color-1; } 21 | 50% { background-color: $background-warning-color-2; } 22 | 100% { background-color: $background-warning-color-1; } 23 | } 24 | @-webkit-keyframes status-danger-background { 25 | 0% { background-color: $background-danger-color-1; } 26 | 50% { background-color: $background-danger-color-2; } 27 | 100% { background-color: $background-danger-color-1; } 28 | } 29 | @mixin animation($animation-name, $duration, $function, $animation-iteration-count:""){ 30 | -webkit-animation: $animation-name $duration $function #{$animation-iteration-count}; 31 | -moz-animation: $animation-name $duration $function #{$animation-iteration-count}; 32 | -ms-animation: $animation-name $duration $function #{$animation-iteration-count}; 33 | } 34 | 35 | // ---------------------------------------------------------------------------- 36 | // Base styles 37 | // ---------------------------------------------------------------------------- 38 | html { 39 | font-size: 100%; 40 | -webkit-text-size-adjust: 100%; 41 | -ms-text-size-adjust: 100%; 42 | } 43 | 44 | body { 45 | margin: 0; 46 | background-color: $background-color; 47 | font-size: 20px; 48 | color: $text-color; 49 | font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 50 | } 51 | 52 | b, strong { 53 | font-weight: bold; 54 | } 55 | 56 | a { 57 | text-decoration: none; 58 | color: inherit; 59 | } 60 | 61 | img { 62 | border: 0; 63 | -ms-interpolation-mode: bicubic; 64 | vertical-align: middle; 65 | } 66 | 67 | img, object { 68 | max-width: 100%; 69 | } 70 | 71 | iframe { 72 | max-width: 100%; 73 | } 74 | 75 | table { 76 | border-collapse: collapse; 77 | border-spacing: 0; 78 | width: 100%; 79 | } 80 | 81 | td { 82 | vertical-align: middle; 83 | } 84 | 85 | ul, ol { 86 | padding: 0; 87 | margin: 0; 88 | } 89 | 90 | h1, h2, h3, h4, h5, p { 91 | padding: 0; 92 | margin: 0; 93 | } 94 | h1 { 95 | margin-bottom: 12px; 96 | text-align: center; 97 | font-size: 30px; 98 | font-weight: 400; 99 | } 100 | h2 { 101 | text-transform: uppercase; 102 | font-size: 76px; 103 | font-weight: 700; 104 | color: $text-color; 105 | } 106 | h3 { 107 | font-size: 25px; 108 | font-weight: 600; 109 | color: $text-color; 110 | } 111 | 112 | // ---------------------------------------------------------------------------- 113 | // Base widget styles 114 | // ---------------------------------------------------------------------------- 115 | .gridster { 116 | margin: 0px auto; 117 | } 118 | 119 | .icon-background { 120 | width: 100%!important; 121 | position: absolute; 122 | left: 0; 123 | top: 20px; 124 | opacity: 0.1; 125 | font-size: 275px; 126 | text-align: center; 127 | } 128 | 129 | .list-nostyle { 130 | list-style: none; 131 | } 132 | 133 | .gridster ul { 134 | list-style: none; 135 | } 136 | 137 | .gs_w { 138 | width: 100%; 139 | display: table; 140 | cursor: pointer; 141 | } 142 | 143 | .widget { 144 | padding: 25px 12px; 145 | text-align: center; 146 | width: 100%; 147 | display: table-cell; 148 | vertical-align: middle; 149 | } 150 | 151 | .widget.status-warning { 152 | background-color: $background-warning-color-1; 153 | @include animation(status-warning-background, 2s, ease, infinite); 154 | 155 | .icon-warning-sign { 156 | display: inline-block; 157 | } 158 | 159 | .title, .more-info { 160 | color: $text-warning-color; 161 | } 162 | } 163 | 164 | .widget.status-danger { 165 | color: $text-danger-color; 166 | background-color: $background-danger-color-1; 167 | @include animation(status-danger-background, 2s, ease, infinite); 168 | 169 | .icon-warning-sign { 170 | display: inline-block; 171 | } 172 | 173 | .title, .more-info { 174 | color: $text-danger-color; 175 | } 176 | } 177 | 178 | .more-info { 179 | font-size: 15px; 180 | position: absolute; 181 | bottom: 32px; 182 | left: 0; 183 | right: 0; 184 | } 185 | 186 | .updated-at { 187 | font-size: 15px; 188 | position: absolute; 189 | bottom: 12px; 190 | left: 0; 191 | right: 0; 192 | } 193 | 194 | #save-gridster { 195 | display: none; 196 | position: fixed; 197 | top: 0; 198 | margin: 0px auto; 199 | left: 50%; 200 | z-index: 1000; 201 | background: black; 202 | width: 190px; 203 | text-align: center; 204 | border: 1px solid white; 205 | border-top: 0px; 206 | margin-left: -95px; 207 | padding: 15px; 208 | } 209 | 210 | #save-gridster:hover { 211 | padding-top: 25px; 212 | } 213 | 214 | #saving-instructions { 215 | display: none; 216 | padding: 10px; 217 | width: 500px; 218 | height: 122px; 219 | z-index: 1000; 220 | background: white; 221 | top: 100px; 222 | color: black; 223 | font-size: 15px; 224 | padding-bottom: 4px; 225 | 226 | textarea { 227 | white-space: nowrap; 228 | width: 494px; 229 | height: 80px; 230 | } 231 | } 232 | 233 | #lean_overlay { 234 | position: fixed; 235 | z-index:100; 236 | top: 0px; 237 | left: 0px; 238 | height:100%; 239 | width:100%; 240 | background: #000; 241 | display: none; 242 | } 243 | 244 | #container { 245 | padding-top: 5px; 246 | } 247 | 248 | 249 | // ---------------------------------------------------------------------------- 250 | // Clearfix 251 | // ---------------------------------------------------------------------------- 252 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } 253 | .clearfix:after { clear: both; } 254 | .clearfix { zoom: 1; } 255 | 256 | -------------------------------------------------------------------------------- /assets/stylesheets/icon-background-fix.scss: -------------------------------------------------------------------------------- 1 | div[data-grid-height="984"] { 2 | [data-sizey="1"] .icon-background { 3 | top: 0.1em; 4 | } 5 | 6 | [data-sizey="2"] .icon-background { 7 | top: 0.75em; 8 | } 9 | 10 | [data-sizey="3"] .icon-background { 11 | top: 1.5em; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/stylesheets/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | /*! gridster.js - v0.1.0 - 2012-08-14 2 | * http://gridster.net/ 3 | * Copyright (c) 2012 ducksboard; Licensed MIT */ 4 | 5 | .gridster { 6 | position:relative; 7 | } 8 | 9 | .gridster > * { 10 | margin: 0 auto; 11 | -webkit-transition: height .4s; 12 | -moz-transition: height .4s; 13 | -o-transition: height .4s; 14 | -ms-transition: height .4s; 15 | transition: height .4s; 16 | } 17 | 18 | .gridster .gs_w{ 19 | z-index: 2; 20 | position: absolute; 21 | } 22 | 23 | .ready .gs_w:not(.preview-holder) { 24 | -webkit-transition: opacity .3s, left .3s, top .3s; 25 | -moz-transition: opacity .3s, left .3s, top .3s; 26 | -o-transition: opacity .3s, left .3s, top .3s; 27 | transition: opacity .3s, left .3s, top .3s; 28 | } 29 | 30 | .gridster .preview-holder { 31 | z-index: 1; 32 | position: absolute; 33 | background-color: #fff; 34 | border-color: #fff; 35 | opacity: 0.3; 36 | } 37 | 38 | .gridster .player-revert { 39 | z-index: 10!important; 40 | -webkit-transition: left .3s, top .3s!important; 41 | -moz-transition: left .3s, top .3s!important; 42 | -o-transition: left .3s, top .3s!important; 43 | transition: left .3s, top .3s!important; 44 | } 45 | 46 | .gridster .dragging { 47 | z-index: 10!important; 48 | -webkit-transition: all 0s !important; 49 | -moz-transition: all 0s !important; 50 | -o-transition: all 0s !important; 51 | transition: all 0s !important; 52 | } 53 | 54 | /* Uncomment this if you set helper : "clone" in draggable options */ 55 | /*.gridster .player { 56 | opacity:0; 57 | }*/ -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | if File.exist? '.env' 2 | require 'dotenv' 3 | Dotenv.load 4 | end 5 | 6 | require 'sinatra/cyclist' 7 | require 'dashing' 8 | 9 | configure do 10 | set :auth_token, ENV['DASHING_AUTH_TOKEN'] 11 | set :routes_to_cycle_through, [ 12 | :google_analytics 13 | ] 14 | set :cycle_duration, 60 15 | set :default_dashboard, '_cycle' 16 | 17 | helpers do 18 | def protected! 19 | return if authorized? 20 | response['WWW-Authenticate'] = %(Basic realm="Restricted Area") 21 | throw(:halt, [401, "Not authorized\n"]) 22 | end 23 | 24 | def authorized? 25 | return true if ENV['BASIC_AUTH_USERNAME'].nil? || ENV['BASIC_AUTH_PASSWORD'].nil? 26 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 27 | @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [ENV['BASIC_AUTH_USERNAME'], ENV['BASIC_AUTH_PASSWORD']] 28 | end 29 | end 30 | end 31 | 32 | map Sinatra::Application.assets_prefix do 33 | run Sinatra::Application.sprockets 34 | end 35 | 36 | run Sinatra::Application 37 | -------------------------------------------------------------------------------- /dashboards/google_analytics.erb: -------------------------------------------------------------------------------- 1 | 7 | 8 | <% content_for :title do %>Google Analytics<% end %> 9 | 10 |
    11 |
      12 | 13 |
    • 14 |
      15 |
    • 16 | 17 |
    • 18 |
      19 | 20 |
    • 21 | 22 |
    • 23 |
      24 | 25 |
    • 26 | 27 |
    • 28 |
      29 | 30 |
    • 31 | 32 |
    • 33 |
      34 | 35 |
    • 36 | 37 |
    • 38 |
      39 |
    • 40 | 41 |
    • 42 |
      43 | 44 |
    • 45 | 46 |
    • 47 |
      48 |
    • 49 | 50 |
    • 51 |
      52 | 53 |
    • 54 | 55 |
    56 |
    -------------------------------------------------------------------------------- /dashboards/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= yield_content(:title) %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
    24 | <%= yield %> 25 |
    26 | 27 | <% if development? %> 28 |
    29 |

    Paste the following at the top of <%= params[:dashboard] %>.erb

    30 | 31 |
    32 | Save this layout 33 | <% end %> 34 | 35 | -------------------------------------------------------------------------------- /jobs/google_analytics.rb: -------------------------------------------------------------------------------- 1 | require 'google/api_client' 2 | require 'date' 3 | 4 | # Update these to match your own apps credentials 5 | service_account_email = ENV['GOOGLE_SERVICE_ACCOUNT_EMAIL'] 6 | google_private_key = ENV['GOOGLE_PRIVATE_KEY'] 7 | google_private_key_secret = ENV['GOOGLE_PRIVATE_KEY_SECRET'] 8 | view_id = ENV['GOOGLE_ANALYTICS_VIEW_ID'] 9 | 10 | # Get the Google API client 11 | client = Google::APIClient.new( 12 | application_name: 'Dashing', 13 | application_version: '0.01' 14 | ) 15 | 16 | # Load your credentials for the service account 17 | begin 18 | OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE 19 | key = OpenSSL::PKey::RSA.new google_private_key, google_private_key_secret 20 | client.authorization = Signet::OAuth2::Client.new( 21 | token_credential_uri: 'https://accounts.google.com/o/oauth2/token', 22 | audience: 'https://accounts.google.com/o/oauth2/token', 23 | scope: 'https://www.googleapis.com/auth/analytics.readonly', 24 | issuer: service_account_email, 25 | signing_key: key 26 | ) 27 | rescue StandardError => e 28 | puts "\e[33mFor the Google Analytics widget to work, you must complete"\ 29 | " the configuration steps (including a private key).\e[0m" 30 | puts e.inspect 31 | end 32 | 33 | # Start the scheduler 34 | SCHEDULER.every '60m', first_in: 0 do 35 | # Request a token for our service account 36 | client.authorization.fetch_access_token! 37 | 38 | # Get the analytics API 39 | analytics = client.discovered_api('analytics', 'v3') 40 | 41 | # Start and end dates 42 | start_date = (DateTime.now - 31).strftime('%Y-%m-%d') 43 | start_date_display = DateTime.parse(start_date).strftime('%-m/%-d/%Y') 44 | end_date = (DateTime.now - 1).strftime('%Y-%m-%d') 45 | end_date_display = DateTime.parse(end_date).strftime('%-m/%-d/%Y') 46 | 47 | # Sessions 48 | sessions = client.execute( 49 | api_method: analytics.data.ga.get, parameters: { 50 | 'ids' => 'ga:' + view_id, 51 | 'start-date' => start_date, 52 | 'end-date' => end_date, 53 | 'metrics' => 'ga:sessions' 54 | } 55 | ) 56 | send_event( 57 | 'ga_sessions', 58 | current: sessions.data.rows.count > 0 ? sessions.data.rows[0][0] : 0 59 | ) 60 | 61 | # New Sessions 62 | new_sessions = client.execute( 63 | api_method: analytics.data.ga.get, parameters: { 64 | 'ids' => 'ga:' + view_id, 65 | 'start-date' => start_date, 66 | 'end-date' => end_date, 67 | 'metrics' => 'ga:sessions', 68 | 'filters' => 'ga:userType==New Visitor' 69 | } 70 | ) 71 | send_event( 72 | 'ga_new_sessions', 73 | current: new_sessions.data.rows.count > 0 ? new_sessions.data.rows[0][0] : 0 74 | ) 75 | 76 | # Traffic Sources 77 | traffic_sources = client.execute( 78 | api_method: analytics.data.ga.get, parameters: { 79 | 'ids' => 'ga:' + view_id, 80 | 'start-date' => start_date, 81 | 'end-date' => end_date, 82 | 'metrics' => 'ga:sessions', 83 | 'dimensions' => 'ga:medium', 84 | 'sort' => '-ga:sessions' 85 | } 86 | ) 87 | # Reformat for List: Traffic Sources 88 | traffic_sources_list = traffic_sources.data.rows.map do |row| 89 | row[0] = row[0][0...50] + ' ...' if row[0].length > 50 90 | { 91 | label: row[0], 92 | value: row[1] 93 | } 94 | end 95 | send_event( 96 | 'ga_traffic_sources', 97 | items: traffic_sources.data.rows.count > 0 ? traffic_sources_list.take(10) : [] 98 | ) 99 | 100 | # Bounce Rate 101 | bounce_rate = client.execute( 102 | api_method: analytics.data.ga.get, parameters: { 103 | 'ids' => 'ga:' + view_id, 104 | 'start-date' => start_date, 105 | 'end-date' => end_date, 106 | 'metrics' => 'ga:bounceRate' 107 | } 108 | ) 109 | send_event( 110 | 'ga_bounce_rate', 111 | current: bounce_rate.data.rows.count > 0 ? bounce_rate.data.rows[0][0].to_f.round(2) : 0, 112 | suffix: '%' 113 | ) 114 | 115 | # Goals 116 | (1..20).each do |goal_id| 117 | # Completions 118 | goal_completions = client.execute( 119 | api_method: analytics.data.ga.get, parameters: { 120 | 'ids' => 'ga:' + view_id, 121 | 'start-date' => start_date, 122 | 'end-date' => end_date, 123 | 'metrics' => "ga:goal#{goal_id}Completions" 124 | } 125 | ) 126 | send_event( 127 | "ga_goal_#{goal_id}_completions", 128 | current: goal_completions.data.rows.count > 0 ? goal_completions.data.rows[0][0] : 0 129 | ) 130 | 131 | # Conversion Rate 132 | goal_conversion_rate = client.execute( 133 | api_method: analytics.data.ga.get, parameters: { 134 | 'ids' => 'ga:' + view_id, 135 | 'start-date' => start_date, 136 | 'end-date' => end_date, 137 | 'metrics' => "ga:goal#{goal_id}ConversionRate" 138 | } 139 | ) 140 | send_event( 141 | "ga_goal_#{goal_id}_conversion_rate", 142 | current: goal_conversion_rate.data.rows.count > 0 ? goal_conversion_rate.data.rows[0][0].to_f.round(2) : 0, 143 | suffix: '%' 144 | ) 145 | 146 | # Completions by Date 147 | goal_completions_dates = client.execute( 148 | api_method: analytics.data.ga.get, parameters: { 149 | 'ids' => 'ga:' + view_id, 150 | 'start-date' => start_date, 151 | 'end-date' => end_date, 152 | 'metrics' => "ga:goal#{goal_id}Completions", 153 | 'dimensions' => 'ga:date' 154 | } 155 | ) 156 | goal_completions_plot = [] 157 | index = 0 158 | goal_completions_dates.data.rows.each do |row| 159 | goal_completions_plot << { x: index, y: row[1].to_i } 160 | index += 1 161 | end 162 | send_event( 163 | "ga_goal_#{goal_id}_completions_chart", 164 | points: goal_completions_dates.data.rows.count > 0 ? goal_completions_plot : [], 165 | moreinfo: "Chart: #{start_date_display} - #{end_date_display}" 166 | ) 167 | end 168 | 169 | # Sessions by Date 170 | sessions_dates = client.execute( 171 | api_method: analytics.data.ga.get, parameters: { 172 | 'ids' => 'ga:' + view_id, 173 | 'start-date' => start_date, 174 | 'end-date' => end_date, 175 | 'metrics' => 'ga:sessions', 176 | 'dimensions' => 'ga:date' 177 | } 178 | ) 179 | sessions_dates_plot = [] 180 | index = 0 181 | sessions_dates.data.rows.each do |row| 182 | sessions_dates_plot << { x: index, y: row[1].to_i } 183 | index += 1 184 | end 185 | send_event( 186 | 'ga_session_chart', 187 | points: sessions_dates.data.rows.count > 0 ? sessions_dates_plot : [], 188 | moreinfo: "Chart: #{start_date_display} - #{end_date_display}" 189 | ) 190 | end 191 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This Dashboard doesn't exist. 5 | 17 | 18 | 19 | 20 | 21 |
    22 |

    Drats! That Dashboard doesn't exist.

    23 |

    You may have mistyped the address or the page may have moved.

    24 |
    25 | 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raventools/dashing-google-analytics/942709c7464eaed719862be7e48833374297c59a/public/favicon.ico -------------------------------------------------------------------------------- /widgets/clock/clock.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Clock extends Dashing.Widget 2 | 3 | ready: -> 4 | setInterval(@startTime, 500) 5 | 6 | startTime: => 7 | today = new Date() 8 | 9 | h = today.getHours() 10 | m = today.getMinutes() 11 | s = today.getSeconds() 12 | m = @formatTime(m) 13 | s = @formatTime(s) 14 | @set('time', h + ":" + m + ":" + s) 15 | @set('date', today.toDateString()) 16 | 17 | formatTime: (i) -> 18 | if i < 10 then "0" + i else i -------------------------------------------------------------------------------- /widgets/clock/clock.html: -------------------------------------------------------------------------------- 1 |

    2 |

    -------------------------------------------------------------------------------- /widgets/clock/clock.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | 6 | // ---------------------------------------------------------------------------- 7 | // Widget-clock styles 8 | // ---------------------------------------------------------------------------- 9 | .widget-clock { 10 | 11 | background-color: $background-color; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /widgets/comments/comments.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Comments extends Dashing.Widget 2 | 3 | @accessor 'quote', -> 4 | "“#{@get('current_comment')?.body}”" 5 | 6 | ready: -> 7 | @currentIndex = 0 8 | @commentElem = $(@node).find('.comment-container') 9 | @nextComment() 10 | @startCarousel() 11 | 12 | onData: (data) -> 13 | @currentIndex = 0 14 | 15 | startCarousel: -> 16 | setInterval(@nextComment, 8000) 17 | 18 | nextComment: => 19 | comments = @get('comments') 20 | if comments 21 | @commentElem.fadeOut => 22 | @currentIndex = (@currentIndex + 1) % comments.length 23 | @set 'current_comment', comments[@currentIndex] 24 | @commentElem.fadeIn() 25 | -------------------------------------------------------------------------------- /widgets/comments/comments.html: -------------------------------------------------------------------------------- 1 |

    2 |
    3 |

    4 |

    5 |
    6 | 7 |

    8 | -------------------------------------------------------------------------------- /widgets/comments/comments.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.7); 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Widget-comment styles 11 | // ---------------------------------------------------------------------------- 12 | .widget-comments { 13 | 14 | background-color: $background-color; 15 | 16 | .title { 17 | color: $title-color; 18 | margin-bottom: 15px; 19 | } 20 | 21 | .name { 22 | padding-left: 5px; 23 | } 24 | 25 | .comment-container { 26 | display: none; 27 | } 28 | 29 | .more-info { 30 | color: $moreinfo-color; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /widgets/graph/graph.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Graph extends Dashing.Widget 2 | 3 | @accessor 'current', -> 4 | return @get('displayedValue') if @get('displayedValue') 5 | points = @get('points') 6 | if points 7 | points[points.length - 1].y 8 | 9 | ready: -> 10 | container = $(@node).parent() 11 | # Gross hacks. Let's fix this. 12 | width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1) 13 | height = (Dashing.widget_base_dimensions[1] * container.data("sizey")) 14 | @graph = new Rickshaw.Graph( 15 | element: @node 16 | width: width 17 | height: height 18 | renderer: @get("graphtype") 19 | series: [ 20 | { 21 | color: "#fff", 22 | data: [{x:0, y:0}] 23 | } 24 | ] 25 | ) 26 | 27 | @graph.series[0].data = @get('points') if @get('points') 28 | 29 | x_axis = new Rickshaw.Graph.Axis.Time(graph: @graph) 30 | y_axis = new Rickshaw.Graph.Axis.Y(graph: @graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT) 31 | @graph.render() 32 | 33 | onData: (data) -> 34 | if @graph 35 | @graph.series[0].data = data.points 36 | @graph.render() 37 | -------------------------------------------------------------------------------- /widgets/graph/graph.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 | 5 |

    6 | -------------------------------------------------------------------------------- /widgets/graph/graph.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.3); 8 | $tick-color: rgba(0, 0, 0, 0.4); 9 | 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-graph styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-graph { 15 | 16 | background-color: $background-color; 17 | position: relative; 18 | 19 | 20 | svg { 21 | position: absolute; 22 | opacity: 0.4; 23 | fill-opacity: 0.4; 24 | left: 0px; 25 | top: 0px; 26 | } 27 | 28 | .title, .value { 29 | position: relative; 30 | z-index: 99; 31 | } 32 | 33 | .title { 34 | color: $title-color; 35 | } 36 | 37 | .more-info { 38 | color: $moreinfo-color; 39 | font-weight: 600; 40 | font-size: 20px; 41 | margin-top: 0; 42 | } 43 | 44 | .x_tick { 45 | position: absolute; 46 | bottom: 0; 47 | .title { 48 | font-size: 20px; 49 | color: $tick-color; 50 | opacity: 0.5; 51 | padding-bottom: 3px; 52 | } 53 | } 54 | 55 | .y_ticks { 56 | font-size: 20px; 57 | fill: $tick-color; 58 | fill-opacity: 1; 59 | } 60 | 61 | .domain { 62 | display: none; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /widgets/iframe/iframe.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Iframe extends Dashing.Widget 2 | 3 | ready: -> 4 | # This is fired when the widget is done being rendered 5 | 6 | onData: (data) -> 7 | # Handle incoming data 8 | # You can access the html node of this widget with `@node` 9 | # Example: $(@node).fadeOut().fadeIn() will make the node flash each time data comes in. 10 | -------------------------------------------------------------------------------- /widgets/iframe/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /widgets/iframe/iframe.scss: -------------------------------------------------------------------------------- 1 | .widget-iframe { 2 | padding: 3px 0px 0px 0px !important; 3 | 4 | iframe { 5 | width: 100%; 6 | height: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /widgets/image/image.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Image extends Dashing.Widget 2 | 3 | ready: -> 4 | # This is fired when the widget is done being rendered 5 | 6 | onData: (data) -> 7 | # Handle incoming data 8 | # You can access the html node of this widget with `@node` 9 | # Example: $(@node).fadeOut().fadeIn() will make the node flash each time data comes in. 10 | -------------------------------------------------------------------------------- /widgets/image/image.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /widgets/image/image.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | 6 | // ---------------------------------------------------------------------------- 7 | // Widget-image styles 8 | // ---------------------------------------------------------------------------- 9 | .widget-image { 10 | 11 | background-color: $background-color; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /widgets/list/list.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.List extends Dashing.Widget 2 | ready: -> 3 | if @get('unordered') 4 | $(@node).find('ol').remove() 5 | else 6 | $(@node).find('ul').remove() 7 | -------------------------------------------------------------------------------- /widgets/list/list.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |
      4 |
    1. 5 | 6 | 7 |
    2. 8 |
    9 | 10 |
      11 |
    • 12 | 13 | 14 |
    • 15 |
    16 | 17 |

    18 |

    19 | -------------------------------------------------------------------------------- /widgets/list/list.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | $value-color: #fff; 6 | 7 | $title-color: rgba(255, 255, 255, 0.7); 8 | $label-color: rgba(255, 255, 255, 0.7); 9 | $moreinfo-color: rgba(255, 255, 255, 0.7); 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-list styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-list { 15 | 16 | background-color: $background-color; 17 | vertical-align: top; 18 | 19 | .title { 20 | color: $title-color; 21 | } 22 | 23 | ol, ul { 24 | margin: 0 15px; 25 | text-align: left; 26 | color: $label-color; 27 | } 28 | 29 | ol { 30 | 31 | counter-reset: li; 32 | list-style: none; 33 | *list-style: decimal; 34 | 35 | > li { 36 | position: relative; 37 | padding-left: 50px; 38 | min-height: 50px; 39 | } 40 | 41 | > li:before { 42 | content: counter(li); 43 | counter-increment: li; 44 | color: #fff; 45 | background-color: rgba(255, 255, 255, 0.1); 46 | border-color: #357ebd; 47 | border-radius: 50%; 48 | font-size: 14px; 49 | width: 38px; 50 | height: 38px; 51 | line-height: 38px; 52 | text-align: center; 53 | display: block; 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | } 58 | 59 | } 60 | 61 | li { 62 | margin-bottom: 5px; 63 | } 64 | 65 | .list-nostyle { 66 | list-style: none; 67 | } 68 | 69 | .label { 70 | color: $label-color; 71 | } 72 | 73 | .value { 74 | float: right; 75 | margin-left: 12px; 76 | font-weight: 600; 77 | color: $value-color; 78 | } 79 | 80 | .updated-at { 81 | color: rgba(0, 0, 0, 0.3); 82 | } 83 | 84 | .more-info { 85 | color: $moreinfo-color; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /widgets/meter/meter.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Meter extends Dashing.Widget 2 | 3 | @accessor 'value', Dashing.AnimatedValue 4 | 5 | constructor: -> 6 | super 7 | @observe 'value', (value) -> 8 | $(@node).find(".meter").val(value).trigger('change') 9 | 10 | ready: -> 11 | meter = $(@node).find(".meter") 12 | meter.attr("data-bgcolor", meter.css("background-color")) 13 | meter.attr("data-fgcolor", meter.css("color")) 14 | meter.knob() 15 | -------------------------------------------------------------------------------- /widgets/meter/meter.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 | 4 | 5 |

    6 | 7 |

    8 | -------------------------------------------------------------------------------- /widgets/meter/meter.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #9c4274; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.3); 8 | 9 | $meter-background: darken($background-color, 15%); 10 | 11 | // ---------------------------------------------------------------------------- 12 | // Widget-meter styles 13 | // ---------------------------------------------------------------------------- 14 | .widget-meter { 15 | 16 | background-color: $background-color; 17 | 18 | input.meter { 19 | background-color: $meter-background; 20 | color: #fff; 21 | } 22 | 23 | .title { 24 | color: $title-color; 25 | } 26 | 27 | .more-info { 28 | color: $moreinfo-color; 29 | } 30 | 31 | .updated-at { 32 | color: rgba(0, 0, 0, 0.3); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /widgets/number/number.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Number extends Dashing.Widget 2 | @accessor 'current', Dashing.AnimatedValue 3 | 4 | @accessor 'difference', -> 5 | if @get('last') 6 | last = parseInt(@get('last')) 7 | current = parseInt(@get('current')) 8 | if last != 0 9 | diff = Math.abs(Math.round((current - last) / last * 100)) 10 | "#{diff}%" 11 | else 12 | "" 13 | 14 | @accessor 'arrow', -> 15 | if @get('last') 16 | if parseInt(@get('current')) > parseInt(@get('last')) then 'icon-arrow-up' else 'icon-arrow-down' 17 | 18 | onData: (data) -> 19 | if data.status 20 | # clear existing "status-*" classes 21 | $(@get('node')).attr 'class', (i,c) -> 22 | c.replace /\bstatus-\S+/g, '' 23 | # add new class 24 | $(@get('node')).addClass "status-#{data.status}" 25 | -------------------------------------------------------------------------------- /widgets/number/number.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 | 5 |

    6 | 7 |

    8 | 9 |

    10 | 11 |

    12 | -------------------------------------------------------------------------------- /widgets/number/number.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #000; 5 | $value-color: #fff; 6 | 7 | $title-color: rgba(255, 255, 255, 0.7); 8 | $moreinfo-color: rgba(255, 255, 255, 0.7); 9 | 10 | // ---------------------------------------------------------------------------- 11 | // Widget-number styles 12 | // ---------------------------------------------------------------------------- 13 | .widget-number { 14 | 15 | background-color: $background-color; 16 | 17 | .title { 18 | color: $title-color; 19 | } 20 | 21 | .value { 22 | color: $value-color; 23 | } 24 | 25 | .change-rate { 26 | font-weight: 500; 27 | font-size: 30px; 28 | color: $value-color; 29 | } 30 | 31 | .more-info { 32 | color: $moreinfo-color; 33 | } 34 | 35 | .updated-at { 36 | color: rgba(0, 0, 0, 0.3); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /widgets/text/text.coffee: -------------------------------------------------------------------------------- 1 | class Dashing.Text extends Dashing.Widget 2 | -------------------------------------------------------------------------------- /widgets/text/text.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 | 5 |

    6 | 7 |

    8 | -------------------------------------------------------------------------------- /widgets/text/text.scss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Sass declarations 3 | // ---------------------------------------------------------------------------- 4 | $background-color: #ec663c; 5 | 6 | $title-color: rgba(255, 255, 255, 0.7); 7 | $moreinfo-color: rgba(255, 255, 255, 0.7); 8 | 9 | // ---------------------------------------------------------------------------- 10 | // Widget-text styles 11 | // ---------------------------------------------------------------------------- 12 | .widget-text { 13 | 14 | background-color: $background-color; 15 | 16 | .title { 17 | color: $title-color; 18 | } 19 | 20 | .more-info { 21 | color: $moreinfo-color; 22 | } 23 | 24 | .updated-at { 25 | color: rgba(255, 255, 255, 0.7); 26 | } 27 | 28 | 29 | &.large h3 { 30 | font-size: 65px; 31 | } 32 | } 33 | --------------------------------------------------------------------------------