├── .agignore ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── NOTES.md ├── README.md ├── Rakefile ├── VERSION ├── assets ├── css │ └── style.css ├── js │ ├── Chart.min.js │ ├── dasht.js │ ├── jquery-1.11.3.min.js │ ├── mustache.min.js │ └── underscore-min.js └── plugins │ ├── chart.css │ ├── chart.html │ ├── chart.js │ ├── map.css │ ├── map.html │ ├── map.js │ ├── value.css │ ├── value.html │ └── value.js ├── dasht.gemspec ├── examples ├── simple_heroku_dashboard.rb └── simple_heroku_dashboard_2.rb ├── fivestreet_example.jpg ├── lib ├── dasht.rb └── dasht │ ├── array_monkeypatching.rb │ ├── base.rb │ ├── board.rb │ ├── list.rb │ ├── log_thread.rb │ ├── metric.rb │ ├── metrics.rb │ ├── rack_app.rb │ └── reloader.rb ├── screenshot.png ├── test ├── helper.rb ├── test_list.rb └── test_metric.rb └── views └── dashboard.erb /.agignore: -------------------------------------------------------------------------------- 1 | *min*.js 2 | *min*css 3 | d3.v3.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rack', "~> 1.6" 4 | 5 | # Add dependencies to develop your gem here. 6 | # Include everything needed to run rake, tests, features, etc. 7 | group :development do 8 | gem "shoulda", ">= 0" 9 | gem "rdoc", "~> 3.12" 10 | gem "bundler", "~> 1.0" 11 | gem "jeweler", "~> 2.0.1" 12 | gem "simplecov", ">= 0" 13 | gem "test-unit", "~> 3" 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activesupport (4.2.3) 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 | builder (3.2.2) 12 | descendants_tracker (0.0.4) 13 | thread_safe (~> 0.3, >= 0.3.1) 14 | docile (1.1.5) 15 | faraday (0.9.1) 16 | multipart-post (>= 1.2, < 3) 17 | git (1.2.9.1) 18 | github_api (0.12.4) 19 | addressable (~> 2.3) 20 | descendants_tracker (~> 0.0.4) 21 | faraday (~> 0.8, < 0.10) 22 | hashie (>= 3.4) 23 | multi_json (>= 1.7.5, < 2.0) 24 | nokogiri (~> 1.6.6) 25 | oauth2 26 | hashie (3.4.2) 27 | highline (1.7.3) 28 | i18n (0.7.0) 29 | jeweler (2.0.1) 30 | builder 31 | bundler (>= 1.0) 32 | git (>= 1.2.5) 33 | github_api 34 | highline (>= 1.6.15) 35 | nokogiri (>= 1.5.10) 36 | rake 37 | rdoc 38 | json (1.8.3) 39 | jwt (1.5.1) 40 | mini_portile (0.6.2) 41 | minitest (5.7.0) 42 | multi_json (1.11.2) 43 | multi_xml (0.5.5) 44 | multipart-post (2.0.0) 45 | nokogiri (1.6.6.2) 46 | mini_portile (~> 0.6.0) 47 | oauth2 (1.0.0) 48 | faraday (>= 0.8, < 0.10) 49 | jwt (~> 1.0) 50 | multi_json (~> 1.3) 51 | multi_xml (~> 0.5) 52 | rack (~> 1.2) 53 | power_assert (0.2.2) 54 | rack (1.6.4) 55 | rake (10.4.2) 56 | rdoc (3.12.2) 57 | json (~> 1.4) 58 | shoulda (3.5.0) 59 | shoulda-context (~> 1.0, >= 1.0.1) 60 | shoulda-matchers (>= 1.4.1, < 3.0) 61 | shoulda-context (1.2.1) 62 | shoulda-matchers (2.8.0) 63 | activesupport (>= 3.0.0) 64 | simplecov (0.10.0) 65 | docile (~> 1.1.0) 66 | json (~> 1.8) 67 | simplecov-html (~> 0.10.0) 68 | simplecov-html (0.10.0) 69 | test-unit (3.0.8) 70 | power_assert 71 | thread_safe (0.3.5) 72 | tzinfo (1.2.2) 73 | thread_safe (~> 0.1) 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | bundler (~> 1.0) 80 | jeweler (~> 2.0.1) 81 | rack (~> 1.6) 82 | rdoc (~> 3.12) 83 | shoulda 84 | simplecov 85 | test-unit (~> 3) 86 | 87 | BUNDLED WITH 88 | 1.10.5 89 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Rusty Klophaus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | + Display a dashboard index if no default dashboard is specified. 4 | + Add a container class, for better layout. Get rid of masonry. 5 | + Fix up 'scroll' tile. 6 | + Create a 'delta' tile. (Up / down X percent.) 7 | + Support user-defined plugins. 8 | + Automatically support librato style log statements, ie: 9 | "count#user.clicks=1" - count metric type. 10 | "sample#database.size=40.9MB" - gauge metric type. 11 | "measure#database.query=200ms" - append metric type, support stats on browser side. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dasht 2 | 3 | Dasht is a framework for building beautiful, developer-focused application dashboards. Dasht is especially good at displaying high-level application stats in real-time on a wall-mounted monitor. 4 | 5 | > "You can't manage what you don't measure." - Peter Drucker 6 | 7 | Dasht works best with a Twelve-Factor (Heroku style) app. Specifically, your app should treat [logs as event streams](http://12factor.net/logs). Dasht lets you detect events with regular expressions, turn the events into metrics, then publish the metrics to a dashboard. 8 | 9 | A typical Dasht dashboard takes just a few minutes of coding and is usually less than 100 lines of Ruby. 10 | 11 | Dasht is a Ruby / Rack application by [Rusty Klophaus](http://rusty.io), open-sourced under the MIT license. 12 | 13 | [![Gem Version](https://badge.fury.io/rb/dasht.svg)](http://badge.fury.io/rb/dasht) 14 | 15 | ![Dasht At FiveStreet](fivestreet_example.jpg) 16 | *Two Dasht-powered dashboards at FiveStreet HQ* 17 | 18 | # Getting Started 19 | 20 | Let's make the following dashboard for your Heroku app. 21 | 22 | ![Dasht Screen Shot](screenshot.png) 23 | 24 | First, copy the Ruby code below to a file called `my_dashboard.rb`. 25 | 26 | ```ruby 27 | require 'dasht' 28 | 29 | application = ARGV[0] 30 | 31 | dasht do |d| 32 | # Consume Heroku logs. 33 | d.start "heroku logs --tail --app #{application}" do |l| 34 | # Track some metrics. 35 | l.count :lines, /.+/ 36 | 37 | l.count :bytes, /.+/ do |match| 38 | match[0].length 39 | end 40 | 41 | l.append :visitors, /Started GET .* for (\d+\.\d+\.\d+\.\d+) at/ do |matches| 42 | matches[1] 43 | end 44 | end 45 | 46 | counter = 0 47 | d.interval :counter do 48 | sleep 1 49 | counter += 1 50 | end 51 | 52 | # Publish a board. 53 | d.board do |b| 54 | b.value :counter, :title => "Counter" 55 | b.value :lines, :title => "Number of Lines" 56 | b.value :bytes, :title => "Number of Bytes" 57 | b.chart :bytes, :title => "Chart of Bytes", :periods => 10 58 | b.map :visitors, :title => "Visitors", :width => 12, :height => 9 59 | end 60 | end 61 | ``` 62 | 63 | Then, run the following commands in your shell: 64 | 65 | ```sh 66 | # Install the gem. 67 | gem install dasht 68 | 69 | # Create a dashboard. 70 | vi my_dashboard.rb 71 | 72 | # Run the dashboard. 73 | ruby my_dashboard.rb $APPNAME 74 | 75 | # Open the dashboard. 76 | open http://localhost:8080 77 | ``` 78 | 79 | Look in the [examples](https://github.com/rustyio/dasht/tree/master/examples) folder for more example dashboards.. 80 | 81 | # Documentation 82 | 83 | ## Creating a Dasht Instance 84 | 85 | To create a Dasht instance, simply require the library and then call the global `dasht` method with a block. 86 | 87 | ```ruby 88 | require 'dasht' 89 | 90 | dasht do |d| 91 | # ...Your metrics and dashboards... 92 | end 93 | ``` 94 | 95 | There are a number of global settings that can be specified at the instance level, with their corresponding defaults: 96 | 97 | ```ruby 98 | dasht do |d| 99 | # Set the web port. 100 | b.port = 8080 101 | 102 | # Set the amount of history to keep. 103 | b.history = 60 * 60 104 | 105 | # Set a background color. 106 | b.background = "#334455" 107 | 108 | # Or, set a background image. 109 | # b.background = "url(http://path/to/image.png)" 110 | 111 | # Set the default resolution. 112 | b.default_resolution = 60 113 | 114 | # Set the default refresh rate. 115 | b.default_refresh = 5 116 | 117 | # Set the default tile width. 118 | b.default_width = 3 119 | 120 | # Set the default tile height. 121 | b.default_height = 3 122 | 123 | # Set the default number of periods. 124 | b.default_periods 125 | 126 | # ...Your metrics and dashboards... 127 | end 128 | ``` 129 | 130 | 131 | ## Ingesting Data 132 | 133 | Dasht gets data by running a command (or tailing a log file) and listening to the output. A single Dasht instance can listen to multiple sources. If the command ends for some reason, it is automatically restarted. 134 | 135 | Some examples: 136 | 137 | ```ruby 138 | # Start a command, process the output. 139 | d.start("heroku logs --tail --app my_application") do |l| 140 | ... 141 | end 142 | 143 | # Tail a file, process the new data. 144 | d.tail("/path/to/my_application.log") do |l| 145 | ... 146 | end 147 | ``` 148 | 149 | There is also an interval type meant for querying external data sources. It simply runs in a loop. Remember to include a `sleep` statement!. 150 | 151 | ```ruby 152 | # Query external data. Acts as a gauge, and sets the metric to the 153 | # return value of the block. 154 | d.interval :my_metric do 155 | sleep 5 156 | hash = JSON.parse(Net::HTTP.get("http://website/some/api.json")) 157 | hash["value"] 158 | end 159 | ``` 160 | 161 | ## Metrics 162 | 163 | Dasht tries to apply each new log line against a series of user-defined regular expressions. When a regular expression matches, Dasht updates the corresponding metric. 164 | 165 | There are a number of pre-defined metric types: 166 | 167 | + `gauge` - Set a metric. 168 | + `count` - Increment a metric by some amount. (defaults to 1 if no block is provided). 169 | + `min` - Update the minimum value. 170 | + `max` - Update the maximum value. 171 | + `append` - Create a list of values. Useful for non-numeric data such as geographic locations. 172 | 173 | Unless otherwise noted, all metric definitions require a block. The block should convert the regular expression match into a value. Metrics should be kept as simple and compact as possible to keep memory requirements low. (Dasht uses an extremely simple structure to store time-series data. It doesn't go to great lengths to be especially resource efficient.) 174 | 175 | Some examples: 176 | 177 | ```ruby 178 | d.start "some command" do |l| 179 | # Track the total number of log lines processed. 180 | l.count :lines, /.+/ 181 | 182 | # Track the total size of the logs, in bytes. 183 | l.count :bytes, /.+/ do |match| 184 | match[0].length 185 | end 186 | 187 | # Track the maximum response time. 188 | l.max :max_response, /Completed 200 OK in (\d+)ms/ do |match| 189 | match[1].to_i 190 | end 191 | 192 | # Track visitor IP addresses. 193 | l.append :visitors, /Started GET .* for (\d+\.\d+\.\d+\.\d+) at/ do |matches| 194 | matches[1] 195 | end 196 | end 197 | ``` 198 | 199 | You can also define your own metric types with the `event` command. The `op` parameter is a symbol referring to any [Array](http://ruby-doc.org/core-2.2.0/Array.html) instance method. If the built-in Array methods don't do what you need, you can monkey-patch the array class to add new methods. 200 | 201 | ```ruby 202 | # Format is d.event(metric, regex, op, &block). The definition below 203 | # would set the metric to the first occurance of some metric value 204 | # for a given timeframe. 205 | l.event(:my_metric, /some-regex/, :first) do |match| 206 | match[1].to_i 207 | end 208 | ``` 209 | 210 | ## Boards 211 | 212 | A single Dasht instance can host multiple dashboards. Dashboards are defined like this: 213 | 214 | ```ruby 215 | # Publish a board, accessible through "/boards/my_board" 216 | d.board :my_board do |b| 217 | ... 218 | end 219 | 220 | # Publish the default board, accessible through "/" 221 | d.board do |b| 222 | ... 223 | end 224 | ``` 225 | 226 | Each dashboard can have a number of different settings, documented below with their corresponding defaults: 227 | 228 | ```ruby 229 | d.board do |b| 230 | # Set a background color. 231 | b.background = "#334455" 232 | 233 | # Or, set a background image. 234 | # b.background = "url(http://path/to/image.png)" 235 | 236 | # Set the default resolution. 237 | b.default_resolution = 60 238 | 239 | # Set the default refresh rate. 240 | b.default_refresh = 5 241 | 242 | # Set the default tile width. 243 | b.default_width = 3 244 | 245 | # Set the default tile height. 246 | b.default_height = 3 247 | 248 | # Set the default number of periods. 249 | b.default_periods 250 | end 251 | ``` 252 | Each dashboard can be filled with tiles that display various key metrics about the app. Each dashboard is split into a 12x12 grid. Tiles by default take up a 3x3 spot. The height and width of each tile can be adjusted with a per-tile setting. 253 | 254 | Dasht tries to make dashboards look nice with minimal effort in the following ways: 255 | 256 | + The dashboard itself gracefully stretches to fill the entire screen for most reasonable monitor sizes, even in portrait orientation. 257 | + Tile elements are designed to be slightly transparent, so they look nice with any background color or image. 258 | + Text is automatically scaled up or down to be as large as possible while still fitting into available space. 259 | + Dasht is responsive and looks nice on mobile devices and tables. That said, the target platform is a large monitor. 260 | 261 | ## Tiles 262 | 263 | Metrics can be published by placing tiles on a dashboard. Dasht comes with a handful of tile types. 264 | 265 | All tiles respond to the following options: 266 | 267 | + `:title` - The title of the tile. 268 | + `:resolution` - The resolution of the tile. Defaults to the board or instance default. If none specified, defaults to 60, meaning that it will load data for the last minute. 269 | + `:refresh` - The refresh rate of the tile. Defaults to the board or instance default. If none specified, defaults to 5, meaning that it will load fresh data every 5 seconds. 270 | + `:width` - The width of the tile, an integer from 1 to 12. Defaults to 3. 271 | + `:height` - The height of the tile, an integer from 1 to 12. Defaults to 3. 272 | 273 | Individual tiles have additional attributes. 274 | 275 | Dasht is extensible. New tiles are relatively easy to write. A simple tile plugin takes around 30 lines of Javascript. See the [existing plugins](https://github.com/rustyio/dasht/tree/master/assets/plugins) for examples. 276 | 277 | ### Value Tile 278 | 279 | Display a single numeric value. 280 | 281 | ```ruby 282 | b.value :my_metric, :title => "My Title" 283 | ``` 284 | 285 | ### Map Tile 286 | 287 | Drop pins on a map corresponding to either a physical addresses ("123 Main Street..."), an IP address ("216.58.217.142"), or coordinates ("[-97.7559964, 30.3071816]", longitude then latitude) . The metric attached to the map should use the `append` metric type, and the block should return the address string. See `examples/simple_heroku_dashboard.rb` for an example. 288 | 289 | ```ruby 290 | b.map :visitors, :title => "Visitors" 291 | ``` 292 | 293 | ### Chart Tile 294 | 295 | Show a simple line chart of the metric. This will display the value of the metric for a number of periods in history. For example, if the resolution is 60, and periods is set to 10, then the chart will show the last rolling 10 minutes of history. 296 | 297 | ```ruby 298 | b.chart :my_metric, :title => "My Title", :periods => 10 299 | ``` 300 | 301 | ### A Tile With Many Settings 302 | 303 | Here is how other settings are set. 304 | 305 | ```ruby 306 | b.value :my_metric, :title => "My Title", 307 | :resolution => 10, :refresh => 1, :width => 6, :height => 2 308 | ``` 309 | 310 | # Contributing 311 | 312 | To contribute, please create a pull request with a good description and either a unit test or a sample dashboard that exercises the new feature. 313 | 314 | # License 315 | 316 | The MIT License (MIT) 317 | 318 | Copyright (c) 2015 Rusty Klophaus 319 | 320 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 321 | 322 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 323 | 324 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 325 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gem| 16 | # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options 17 | gem.name = "dasht" 18 | gem.homepage = "http://github.com/rustyio/dasht" 19 | gem.license = "MIT" 20 | gem.summary = "Simple, beautiful, full-screen dashboards." 21 | gem.description = "Dasht is a framework for building beautiful, developer-focused application dashboards. Dasht is especially good at displaying high-level application stats in real-time on a wall-mounted monitor." 22 | gem.email = "rklophaus@gmail.com" 23 | gem.authors = ["Rusty Klophaus"] 24 | # dependencies defined in Gemfile 25 | end 26 | Jeweler::RubygemsDotOrgTasks.new 27 | 28 | require 'rake/testtask' 29 | Rake::TestTask.new(:test) do |test| 30 | test.libs << 'lib' << 'test' 31 | test.pattern = 'test/**/test_*.rb' 32 | test.verbose = true 33 | end 34 | 35 | desc "Code coverage detail" 36 | task :simplecov do 37 | ENV['COVERAGE'] = "true" 38 | Rake::Task['test'].execute 39 | end 40 | 41 | task :default => :test 42 | 43 | require 'rdoc/task' 44 | Rake::RDocTask.new do |rdoc| 45 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 46 | 47 | rdoc.rdoc_dir = 'rdoc' 48 | rdoc.title = "dasht #{version}" 49 | rdoc.rdoc_files.include('README*') 50 | rdoc.rdoc_files.include('lib/**/*.rb') 51 | end 52 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.9 -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Montserrat:400,700); 2 | @import url(http://fonts.googleapis.com/css?family=Lato:400,700); 3 | 4 | html, body { 5 | margin: 0px; 6 | padding: 0px; 7 | border: 0px; 8 | } 9 | 10 | body { 11 | font-family: 'Montserrat', sans-serif; 12 | font-weight: 700; 13 | background-color: #345; 14 | } 15 | 16 | body.waiting { 17 | cursor: progress; 18 | } 19 | 20 | #container { 21 | width: 100%; 22 | height: 100%; 23 | border: 0px; 24 | padding: 0px; 25 | margin: 0px; 26 | } 27 | 28 | .tile { 29 | float: left; 30 | position: relative; 31 | } 32 | 33 | .tile .title { 34 | position: absolute; 35 | margin: 0px 20px 0px 20px; 36 | padding-top: 20px; 37 | padding-bottom: 3px; 38 | border-bottom: solid 8px rgba(0, 0, 0, 0.4); 39 | text-align: left; 40 | color: rgba(255, 255, 255, 0.8); 41 | font-size: 16px; 42 | letter-spacing: 0.05em; 43 | font-weight: bold; 44 | z-index: 10; 45 | } 46 | 47 | .resize-text { 48 | overflow: scroll; 49 | } 50 | 51 | .width-1 { width: 8.33%; } 52 | .width-2 { width: 16.66%; } 53 | .width-3 { width: 25.00%; } 54 | .width-4 { width: 33.33%; } 55 | .width-5 { width: 41.66%; } 56 | .width-6 { width: 50.00%; } 57 | .width-7 { width: 58.33%; } 58 | .width-8 { width: 66.66%; } 59 | .width-9 { width: 75.00%; } 60 | .width-10 { width: 83.33%; } 61 | .width-11 { width: 91.66%; } 62 | .width-12 { width: 100.00%; } 63 | 64 | .height-1 { height: 8.33%; } 65 | .height-2 { height: 16.66%; } 66 | .height-3 { height: 25.00%; } 67 | .height-4 { height: 33.33%; } 68 | .height-5 { height: 41.66%; } 69 | .height-6 { height: 50.00%; } 70 | .height-7 { height: 58.33%; } 71 | .height-8 { height: 66.66%; } 72 | .height-9 { height: 75.00%; } 73 | .height-10 { height: 83.33%; } 74 | .height-11 { height: 91.66%; } 75 | .height-12 { height: 100.00%; } 76 | 77 | 78 | @media (min-width: 640px) { 79 | html, body { 80 | height: 100%; 81 | width: 100%; 82 | } 83 | } 84 | 85 | @media (max-width: 640px) { 86 | .tile { 87 | float: inherit; 88 | display: block; 89 | } 90 | .tile .title { 91 | display: block; 92 | position: relative; 93 | } 94 | .width-1, 95 | .width-2, 96 | .width-3, 97 | .width-4, 98 | .width-5, 99 | .width-6, 100 | .width-7, 101 | .width-8, 102 | .width-9, 103 | .width-10, 104 | .width-11, 105 | .width-12 { 106 | width: 100%; 107 | } 108 | 109 | .height-1, 110 | .height-2, 111 | .height-3, 112 | .height-4, 113 | .height-5, 114 | .height-6, 115 | .height-7, 116 | .height-8, 117 | .height-9, 118 | .height-10, 119 | .height-11, 120 | .height-12 { 121 | height: auto; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /assets/js/Chart.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Chart.js 3 | * http://chartjs.org/ 4 | * Version: 1.0.2 5 | * 6 | * Copyright 2015 Nick Downie 7 | * Released under the MIT license 8 | * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md 9 | */ 10 | (function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); -------------------------------------------------------------------------------- /assets/js/dasht.js: -------------------------------------------------------------------------------- 1 | // Initialize some vars. 2 | Dasht = new function(){ 3 | this.fontsize_small = 12; 4 | this.fontsize_medium = 30; 5 | this.fontsize_large = 80; 6 | this.pending_requests = []; 7 | }(); 8 | 9 | Dasht.add_tile = function(options) { 10 | // Generate the html. 11 | var template_key = "#" + options.type + "-template"; 12 | var html = $(template_key).last().html(); 13 | if (html == undefined) { 14 | alert("Template not found: " + template_key); 15 | } 16 | var el = $.parseHTML($.trim(Mustache.render(html, options)))[0]; 17 | 18 | // Update the page. 19 | $("#container").append(el); 20 | 21 | // Initialize the element. 22 | if (Dasht[options.type + "_init"] != undefined) { 23 | Dasht[options.type + "_init"](el, options); 24 | } 25 | } 26 | 27 | Dasht.init = function() { 28 | Dasht.scale_fontsize_loop(); 29 | Dasht.process_pending_requests_loop(); 30 | Dasht.watchdog_loop(); 31 | } 32 | 33 | Dasht.fill_tile = function(el, do_width, do_height) { 34 | var parent = $(el).parent(); 35 | if (do_width || do_width == undefined) { 36 | var marginsize = parseInt($(el).css("margin-left")) + parseInt($(el).css("margin-right")); 37 | $(el).outerWidth(parent.width() - marginsize); 38 | } 39 | if (do_height || do_height == undefined) { 40 | var marginsize = parseInt($(el).css("margin-top")) + parseInt($(el).css("margin-bottom")); 41 | $(el).outerHeight(parent.height() - marginsize); 42 | } 43 | } 44 | 45 | Dasht._scale_fontsize = function(selector, size, min_size, max_size) { 46 | var elements = $(selector); 47 | 48 | var any_too_small = function() { 49 | return _.any(elements, function(el) { 50 | return (($(el)[0].scrollWidth <= $(el).innerWidth()) || 51 | ($(el)[0].scrollHeight <= $(el).innerHeight())); 52 | }); 53 | } 54 | 55 | var any_too_big = function() { 56 | return _.any(elements, function(el) { 57 | return (($(el)[0].scrollWidth > $(el).innerWidth()) || 58 | ($(el)[0].scrollHeight > $(el).innerHeight())); 59 | }); 60 | } 61 | 62 | while (size <= max_size && any_too_small()) { 63 | size = size + 1; 64 | $(elements).css("font-size", size + "px"); 65 | } 66 | 67 | while (size >= min_size && any_too_big()) { 68 | size = size - 1; 69 | $(elements).css("font-size", size + "px"); 70 | } 71 | 72 | return size; 73 | } 74 | 75 | Dasht.scale_fontsize_loop = function() { 76 | Dasht.fontsize_small = Dasht._scale_fontsize(".fontsize-small", Dasht.fontsize_small, 10, 20); 77 | Dasht.fontsize_medium = Dasht._scale_fontsize(".fontsize-medium", Dasht.fontsize_medium, 25, 45); 78 | Dasht.fontsize_large = Dasht._scale_fontsize(".fontsize-large", Dasht.fontsize_large, 55, 90); 79 | setTimeout(Dasht.scale_fontsize_loop, 1000); 80 | } 81 | 82 | Dasht.get_value = function(options, callback) { 83 | // Add to the list of pending requests. 84 | Dasht.pending_requests.push({ 85 | metric: options["metric"], 86 | resolution: options["resolution"] || 60, 87 | periods: options["periods"] || 1, 88 | refresh: options["refresh"] || 5, 89 | callback: callback 90 | }); 91 | } 92 | 93 | Dasht.process_pending_requests_loop = function() { 94 | // Nothing to do. Return. 95 | if (Dasht.pending_requests.length == 0) { 96 | setTimeout(Dasht.process_pending_requests_loop, 500); 97 | return; 98 | } 99 | 100 | // Split pending_requests into query data and callback data. 101 | var queries = []; 102 | var callbacks = []; 103 | _.each(Dasht.pending_requests, function(o) { 104 | callbacks.push(o.callback); 105 | delete o.callback; 106 | queries.push(o); 107 | }); 108 | Dasht.pending_requests = []; 109 | 110 | var successFN = function(responses) { 111 | Dasht.loaded_data = true; 112 | $("body").removeClass("waiting"); 113 | 114 | // Process the responses. 115 | _.each(responses, function(response, i) { 116 | var query = queries[i]; 117 | var callback = callbacks[i]; 118 | callback(response); 119 | setTimeout(function() { 120 | Dasht.get_value(query, callback); 121 | }, query.refresh * 1000); 122 | }); 123 | 124 | // Loop. 125 | setTimeout(Dasht.process_pending_requests_loop, 500); 126 | } 127 | 128 | // Perform the request. 129 | $("body").addClass("waiting"); 130 | $.ajax({ 131 | url: "/data", 132 | type: 'post', 133 | dataType: 'json', 134 | data: JSON.stringify(queries), 135 | success: successFN 136 | }); 137 | } 138 | 139 | 140 | Dasht.loaded_data = true; 141 | Dasht.watchdog_loop = function() { 142 | // The page could get stuck for a variety of reasons. This is the 143 | // last resort. Run every 2 minutes. If we haven't loaded new data 144 | // in that time, then reload the page. 145 | if (Dasht.loaded_data == true) { 146 | Dasht.loaded_data = false; 147 | setTimeout(Dasht.watchdog_loop, 2 * 60 * 1000); 148 | } else { 149 | document.location.reload(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /assets/js/mustache.min.js: -------------------------------------------------------------------------------- 1 | (function defineMustache(global,factory){if(typeof exports==="object"&&exports&&typeof exports.nodeName!=="string"){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{global.Mustache={};factory(Mustache)}})(this,function mustacheFactory(mustache){var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};function escapeHtml(string){return String(string).replace(/[&<>"'\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){value=context.view;names=name.split(".");index=0;while(value!=null&&index")value=this.renderPartial(token,context,partials,originalTemplate);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /assets/plugins/chart.css: -------------------------------------------------------------------------------- 1 | .chart-tile .chart { 2 | margin: 20px; 3 | } 4 | 5 | @media (max-width: 640px) { 6 | .chart-tile .chart { 7 | height: 80px; 8 | margin: 12px 9 | height: 100px; 10 | } 11 | } -------------------------------------------------------------------------------- /assets/plugins/chart.html: -------------------------------------------------------------------------------- 1 |
    {{title}}
    2 | 3 | -------------------------------------------------------------------------------- /assets/plugins/chart.js: -------------------------------------------------------------------------------- 1 | Dasht.chart_init = function(el, options) { 2 | var chart = $(el).find(".chart"); 3 | var chart_el = chart.get()[0]; 4 | var old_data = 0; 5 | 6 | 7 | // Set the value height to be tile height minus title height. 8 | Dasht.fill_tile($(el).find(".title"), true, false); 9 | Dasht.fill_tile(chart); 10 | 11 | // Create some empty values. 12 | var labels = []; 13 | var data = []; 14 | for (var i = 0; i < options.periods; i++) { 15 | labels.push(""); 16 | data.push(0); 17 | } 18 | 19 | // Create the chart. 20 | var chart_data = { 21 | labels: labels, 22 | datasets: [ 23 | { 24 | fillColor: "rgba(255,255,255,0.2)", 25 | strokeColor: "rgba(255,255,255,0.4)", 26 | data: data 27 | } 28 | ] 29 | }; 30 | 31 | var chart_options = { 32 | showScale: false, 33 | showTooltips: false, 34 | pointDot : false 35 | } 36 | 37 | var ctx = $(".chart").get(0).getContext("2d"); 38 | var chart = new Chart(ctx).Line(chart_data, chart_options); 39 | 40 | // Handle value updates. 41 | setTimeout(function() { 42 | Dasht.get_value(options, function(new_data) { 43 | if (_.isEqual(old_data, new_data)) return; 44 | // Update chart. 45 | for (var i = 0; i < options.periods; i++) { 46 | chart.datasets[0].points[i].value = new_data[i]; 47 | } 48 | chart.update(); 49 | 50 | old_data = new_data; 51 | }); 52 | }, 1000); 53 | } 54 | -------------------------------------------------------------------------------- /assets/plugins/map.css: -------------------------------------------------------------------------------- 1 | .map-tile .title { 2 | padding: 12px 20px; 3 | background-color: rgba(0, 0, 0, 0.4); 4 | border: 0px; 5 | } 6 | 7 | .map-tile .map { 8 | display: block; 9 | height: 20px; 10 | opacity: 0.8; 11 | } 12 | 13 | @media (max-width: 640px) { 14 | .map-tile .map { 15 | height: 500px; 16 | } 17 | } -------------------------------------------------------------------------------- /assets/plugins/map.html: -------------------------------------------------------------------------------- 1 | {{#title}} 2 |
    {{title}}
    3 | {{/title}} 4 |
    5 | -------------------------------------------------------------------------------- /assets/plugins/map.js: -------------------------------------------------------------------------------- 1 | Dasht.map_geocoder_cache = {} 2 | 3 | Dasht.map_plot_address = function(map, markers, geocoder, address) { 4 | // Maybe pull the location from cache. 5 | var location; 6 | if (location = Dasht.map_geocoder_cache[address]) { 7 | Dasht.map_plot_location(map, markers, address, location); 8 | return; 9 | } 10 | 11 | geocoder.geocode({ "address": address }, function(results, status) { 12 | // Don't plot if the lookup failed. 13 | if (status != google.maps.GeocoderStatus.OK) return; 14 | var location = results[0].geometry.location; 15 | Dasht.map_geocoder_cache[address] = location; 16 | Dasht.map_plot_location(map, markers, address, location); 17 | }); 18 | } 19 | 20 | Dasht.map_plot_ip = function(map, markers, ip) { 21 | // Maybe pull the location from cache. 22 | var location; 23 | if (location = Dasht.map_geocoder_cache[ip]) { 24 | Dasht.map_plot_location(map, markers, ip, location); 25 | return; 26 | } 27 | 28 | jQuery.ajax({ 29 | url: 'http://freegeoip.net/json/' + ip, 30 | type: 'POST', 31 | dataType: 'jsonp', 32 | success: function(response) { 33 | var location = new google.maps.LatLng(response.latitude, response.longitude); 34 | Dasht.map_geocoder_cache[ip] = location; 35 | Dasht.map_plot_location(map, markers, ip, location); 36 | }, 37 | error: function (xhr, ajaxOptions, thrownError) { 38 | alert("Failed!"); 39 | } 40 | }); 41 | } 42 | 43 | Dasht.map_plot_coordinates = function(map, markers, coordinates) { 44 | // Maybe pull the location from cache. 45 | var location; 46 | if (location = Dasht.map_geocoder_cache[coordinates]) { 47 | Dasht.map_plot_location(map, markers, coordinates, location); 48 | return; 49 | } 50 | 51 | var coordinate_regex = /\[(-?\d+\.\d+),\s*(-?\d+\.\d+)\]/; 52 | var matches = coordinates.match(coordinate_regex); 53 | var lng = parseFloat(matches[1]); 54 | var lat = parseFloat(matches[2]); 55 | var location = new google.maps.LatLng(lat, lng); 56 | Dasht.map_geocoder_cache[coordinates] = location; 57 | Dasht.map_plot_location(map, markers, coordinates, location); 58 | } 59 | 60 | Dasht.map_plot_location = function(map, markers, item, location) { 61 | var location_exists = _.any(_.values(markers), function(marker) { 62 | return _.isEqual(marker.position, location); 63 | }); 64 | if (location_exists) return; 65 | 66 | // Drop a pin. 67 | var marker = new google.maps.Marker({ 68 | map: map, 69 | animation: google.maps.Animation.DROP, 70 | position: location 71 | }); 72 | 73 | // Keep track of markers. 74 | markers[item] = marker; 75 | } 76 | 77 | Dasht.map_init = function(el, options) { 78 | // Initialize. 79 | var old_data = undefined; 80 | var styles = [ 81 | { 82 | stylers: [ 83 | { hue: "#ffffff" }, 84 | { saturation: -100 }, 85 | { lightness: 20 }, 86 | { gamma: 0.5 } 87 | ] 88 | }, 89 | { 90 | featureType: "water", 91 | stylers: [ 92 | { hue: "#ffffff" }, 93 | { saturation: 80 }, 94 | { lightness: 100 }, 95 | { gamma: 0.43 } 96 | ] 97 | } 98 | ]; 99 | 100 | var mapOptions = { 101 | zoom: 4, 102 | center: new google.maps.LatLng(39.8282, -98.5795), 103 | styles: styles, 104 | disableDefaultUI: true 105 | }; 106 | 107 | var map_el = $(el).find(".map").get()[0]; 108 | var num_entries = options["n"] || 10; 109 | var map = new google.maps.Map(map_el, mapOptions); 110 | var markers = {}; 111 | var geocoder = new google.maps.Geocoder(); 112 | var ip_regex = /\d+\.\d+\.\d+\.\d+/; 113 | var coordinate_regex = /\[(-?\d+\.\d+),\s*(-?\d+\.\d+)\]/; 114 | 115 | Dasht.fill_tile($(el).find(".map")); 116 | 117 | // Update values. 118 | setTimeout(function() { 119 | Dasht.get_value(options, function(new_data) { 120 | new_data = new_data[0]; 121 | if (_.isEqual(old_data, new_data)) return; 122 | 123 | // Remove old markers. 124 | var old_markers = _.difference(_.keys(markers), new_data); 125 | _.each(old_markers, function(address) { 126 | markers[address].setMap(null); 127 | delete markers[address]; 128 | }); 129 | 130 | // Plot each marker. 131 | _.each(new_data, function(item, index) { 132 | if (item.search(ip_regex) >= 0) { 133 | Dasht.map_plot_ip(map, markers, item); 134 | } else if (item.search(coordinate_regex) >= 0) { 135 | Dasht.map_plot_coordinates(map, markers, item); 136 | } else { 137 | Dasht.map_plot_address(map, markers, geocoder, item); 138 | } 139 | }); 140 | 141 | old_data = new_data; 142 | }); 143 | }, 1000); 144 | } 145 | -------------------------------------------------------------------------------- /assets/plugins/value.css: -------------------------------------------------------------------------------- 1 | .value-tile .value { 2 | font-family: 'Lato', sans-serif; 3 | color: rgba(255, 255, 255, 0.9); 4 | margin: auto; 5 | font-size: 80px; 6 | font-weight: bold; 7 | margin: 20px 20px 0px 20px; 8 | display: -webkit-flex; 9 | -webkit-align-items: center; 10 | display: flex; 11 | align-items: center; 12 | } 13 | 14 | @media (max-width: 640px) { 15 | .value-tile .value { 16 | margin-top: 20px; 17 | margin-bottom: 20px; 18 | font-size: 30px; 19 | } 20 | } -------------------------------------------------------------------------------- /assets/plugins/value.html: -------------------------------------------------------------------------------- 1 | {{#title}} 2 |
    {{title}}
    3 | {{/title}} 4 |
    5 | -------------------------------------------------------------------------------- /assets/plugins/value.js: -------------------------------------------------------------------------------- 1 | Dasht.value_update = function(el, value) { 2 | $(el).css("opacity", 0.7); 3 | $(el).html(value.toLocaleString()); 4 | $(el).animate({ "opacity": 1.0 }); 5 | } 6 | 7 | Dasht.value_init = function(el, options) { 8 | // Initialize. 9 | var value = $(el).find(".value"); 10 | var value_el = value.get()[0]; 11 | var old_data = undefined; 12 | 13 | // Set the value height to be tile height minus title height. 14 | Dasht.fill_tile($(el).find(".title"), true, false); 15 | Dasht.fill_tile(value); 16 | 17 | // Update values. 18 | setTimeout(function() { 19 | Dasht.get_value(options, function(new_data) { 20 | new_data = new_data[0]; 21 | if (_.isEqual(old_data, new_data)) return; 22 | Dasht.value_update(value, new_data); 23 | old_data = new_data; 24 | }); 25 | }, 1000); 26 | } 27 | -------------------------------------------------------------------------------- /dasht.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: dasht 0.1.9 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "dasht" 9 | s.version = "0.1.9" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Rusty Klophaus"] 14 | s.date = "2015-10-15" 15 | s.description = "Dasht is a framework for building beautiful, developer-focused application dashboards. Dasht is especially good at displaying high-level application stats in real-time on a wall-mounted monitor." 16 | s.email = "rklophaus@gmail.com" 17 | s.extra_rdoc_files = [ 18 | "LICENSE.txt", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".agignore", 23 | "Gemfile", 24 | "Gemfile.lock", 25 | "LICENSE.txt", 26 | "NOTES.md", 27 | "README.md", 28 | "Rakefile", 29 | "VERSION", 30 | "assets/css/style.css", 31 | "assets/js/Chart.min.js", 32 | "assets/js/dasht.js", 33 | "assets/js/jquery-1.11.3.min.js", 34 | "assets/js/mustache.min.js", 35 | "assets/js/underscore-min.js", 36 | "assets/plugins/chart.css", 37 | "assets/plugins/chart.html", 38 | "assets/plugins/chart.js", 39 | "assets/plugins/map.css", 40 | "assets/plugins/map.html", 41 | "assets/plugins/map.js", 42 | "assets/plugins/value.css", 43 | "assets/plugins/value.html", 44 | "assets/plugins/value.js", 45 | "dasht.gemspec", 46 | "examples/simple_heroku_dashboard.rb", 47 | "examples/simple_heroku_dashboard_2.rb", 48 | "fivestreet_example.jpg", 49 | "lib/dasht.rb", 50 | "lib/dasht/array_monkeypatching.rb", 51 | "lib/dasht/base.rb", 52 | "lib/dasht/board.rb", 53 | "lib/dasht/list.rb", 54 | "lib/dasht/log_thread.rb", 55 | "lib/dasht/metric.rb", 56 | "lib/dasht/metrics.rb", 57 | "lib/dasht/rack_app.rb", 58 | "lib/dasht/reloader.rb", 59 | "screenshot.png", 60 | "test/helper.rb", 61 | "test/test_list.rb", 62 | "test/test_metric.rb", 63 | "views/dashboard.erb" 64 | ] 65 | s.homepage = "http://github.com/rustyio/dasht" 66 | s.licenses = ["MIT"] 67 | s.rubygems_version = "2.4.8" 68 | s.summary = "Simple, beautiful, full-screen dashboards." 69 | 70 | if s.respond_to? :specification_version then 71 | s.specification_version = 4 72 | 73 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 74 | s.add_runtime_dependency(%q, ["~> 1.6"]) 75 | s.add_development_dependency(%q, [">= 0"]) 76 | s.add_development_dependency(%q, ["~> 3.12"]) 77 | s.add_development_dependency(%q, ["~> 1.0"]) 78 | s.add_development_dependency(%q, ["~> 2.0.1"]) 79 | s.add_development_dependency(%q, [">= 0"]) 80 | s.add_development_dependency(%q, ["~> 3"]) 81 | else 82 | s.add_dependency(%q, ["~> 1.6"]) 83 | s.add_dependency(%q, [">= 0"]) 84 | s.add_dependency(%q, ["~> 3.12"]) 85 | s.add_dependency(%q, ["~> 1.0"]) 86 | s.add_dependency(%q, ["~> 2.0.1"]) 87 | s.add_dependency(%q, [">= 0"]) 88 | s.add_dependency(%q, ["~> 3"]) 89 | end 90 | else 91 | s.add_dependency(%q, ["~> 1.6"]) 92 | s.add_dependency(%q, [">= 0"]) 93 | s.add_dependency(%q, ["~> 3.12"]) 94 | s.add_dependency(%q, ["~> 1.0"]) 95 | s.add_dependency(%q, ["~> 2.0.1"]) 96 | s.add_dependency(%q, [">= 0"]) 97 | s.add_dependency(%q, ["~> 3"]) 98 | end 99 | end 100 | 101 | -------------------------------------------------------------------------------- /examples/simple_heroku_dashboard.rb: -------------------------------------------------------------------------------- 1 | require 'dasht' 2 | 3 | application = ARGV[0] 4 | 5 | dasht do |d| 6 | # Consume Heroku logs. 7 | d.start "heroku logs --tail --app #{application}" do |l| 8 | # Track some metrics. 9 | l.count :lines, /.+/ 10 | 11 | l.count :bytes, /.+/ do |match| 12 | match[0].length 13 | end 14 | 15 | l.append :visitors, /Started GET .* for (\d+\.\d+\.\d+\.\d+) at/ do |matches| 16 | matches[1] 17 | end 18 | end 19 | 20 | counter = 0 21 | d.interval :counter do 22 | sleep 1 23 | counter += 1 24 | end 25 | 26 | # Publish a board. 27 | d.board do |b| 28 | b.value :counter, :title => "Counter" 29 | b.value :lines, :title => "Number of Lines" 30 | b.value :bytes, :title => "Number of Bytes" 31 | b.chart :bytes, :title => "Chart of Bytes", :periods => 10 32 | b.map :visitors, :title => "Visitors", :width => 12, :height => 9 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/simple_heroku_dashboard_2.rb: -------------------------------------------------------------------------------- 1 | require 'dasht' 2 | 3 | application = ARGV[0] 4 | 5 | dasht do |d| 6 | # Consume Heroku logs. 7 | d.start "heroku logs --tail --app #{application}" do |l| 8 | # Track some metrics. 9 | l.count :lines, /.+/ 10 | 11 | l.count :bytes, /.+/ do |match| 12 | match[0].length 13 | end 14 | 15 | l.append :visitors, /Started GET .* for (\d+\.\d+\.\d+\.\d+) at/ do |matches| 16 | matches[1] 17 | end 18 | end 19 | 20 | counter = 0 21 | d.interval :counter do 22 | sleep 1 23 | counter += 1 24 | end 25 | 26 | # Publish a board. 27 | d.board do |b| 28 | b.map :visitors, :title => "Visitors", :width => 8, :height => 12 29 | b.value :counter, :title => "Counter", :width => 4 30 | b.value :lines, :title => "Number of Lines", :width => 4 31 | b.value :bytes, :title => "Number of Bytes", :width => 4 32 | b.chart :bytes, :title => "Chart of Bytes", :periods => 10, :width => 4 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /fivestreet_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/dasht/c88d5de86238725943205ce0b22da806e8f4eab5/fivestreet_example.jpg -------------------------------------------------------------------------------- /lib/dasht.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'thread' 3 | require 'erb' 4 | require 'json' 5 | 6 | require 'dasht/array_monkeypatching' 7 | require 'dasht/reloader' 8 | require 'dasht/board' 9 | require 'dasht/list' 10 | require 'dasht/metric' 11 | require 'dasht/metrics' 12 | require 'dasht/rack_app' 13 | require 'dasht/log_thread' 14 | require 'dasht/base' 15 | 16 | class DashtSingleton 17 | def self.run(&block) 18 | @@instance ||= Dasht::Base.new 19 | @@instance.run(&block) 20 | end 21 | end 22 | 23 | def dasht(&block) 24 | DashtSingleton.run(&block) 25 | end 26 | -------------------------------------------------------------------------------- /lib/dasht/array_monkeypatching.rb: -------------------------------------------------------------------------------- 1 | class Array 2 | def dasht_sum 3 | self.compact.inject(:+) || 0; 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/dasht/base.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class Base 3 | attr_accessor :metrics 4 | attr_accessor :rack_app 5 | attr_accessor :reloader 6 | attr_accessor :boards 7 | 8 | # Settings. 9 | attr_accessor :port 10 | attr_accessor :background 11 | attr_accessor :default_resolution 12 | attr_accessor :default_refresh 13 | attr_accessor :default_width 14 | attr_accessor :default_height 15 | attr_accessor :history 16 | 17 | def initialize 18 | @boards = {} 19 | @log_threads = {} 20 | @metrics = Metrics.new(self) 21 | @reloader = Reloader.new(self) 22 | @rack_app = RackApp.new(self) 23 | end 24 | 25 | def log(s) 26 | if s.class < Exception 27 | print "\n#{s}\n" 28 | print s.backtrace.join("\n") 29 | else 30 | print "\r#{s}\n" 31 | end 32 | end 33 | 34 | ### DATA INGESTION ### 35 | 36 | def start(command, &block) 37 | log_thread = @log_threads[command] = LogThread.new(self, command) 38 | yield(log_thread) if block 39 | log_thread 40 | end 41 | 42 | def tail(path) 43 | start("tail -F -n 0 \"#{path}\"") 44 | end 45 | 46 | def interval(metric, &block) 47 | Thread.new do 48 | begin 49 | while true 50 | value = block.call 51 | metrics.set(metric, value, :last, Time.now.to_i) if value 52 | end 53 | rescue => e 54 | log e 55 | raise e 56 | end 57 | end 58 | end 59 | 60 | ### DASHBOARD ### 61 | 62 | def views_path 63 | File.join(File.dirname(__FILE__), "..", "..", "views") 64 | end 65 | 66 | def system_plugins_path 67 | File.join(File.dirname(__FILE__), "..", "..", "assets", "plugins") 68 | end 69 | 70 | def user_plugins_path 71 | File.join(File.dirname($PROGRAM_NAME), "plugins") 72 | end 73 | 74 | def board(name = "default", &block) 75 | name = name.to_s 76 | board = @boards[name] = Board.new(self, name) 77 | yield(board) if block 78 | board 79 | end 80 | 81 | ### RUN & RELOAD ### 82 | 83 | def run(&block) 84 | if @already_running 85 | begin 86 | reload(&block) 87 | rescue => e 88 | log e 89 | end 90 | return 91 | end 92 | 93 | @already_running = true 94 | @reloader.run 95 | 96 | block.call(self) 97 | 98 | @log_threads.values.map(&:run) 99 | @rack_app.run(port) 100 | end 101 | 102 | def reload(&block) 103 | @boards = {} 104 | @log_threads.values.map(&:terminate) 105 | @log_threads = {} 106 | 107 | begin 108 | block.call(self) 109 | rescue => e 110 | log e 111 | raise e 112 | end 113 | 114 | @log_threads.values.map(&:run) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/dasht/board.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class Board 3 | attr_accessor :parent 4 | attr_accessor :name 5 | attr_accessor :tiles 6 | attr_accessor :background 7 | attr_accessor :default_resolution 8 | attr_accessor :default_refresh 9 | attr_accessor :default_width 10 | attr_accessor :default_height 11 | 12 | def initialize(parent, name) 13 | @parent = parent 14 | @name = name 15 | @tiles = [] 16 | end 17 | 18 | def to_html 19 | # Load the erb. 20 | path = File.join(parent.views_path, "dashboard.erb") 21 | @erb = ERB.new(IO.read(path)) 22 | @erb.result(binding) 23 | end 24 | 25 | def emit_plugin_css 26 | _emit_css(parent.system_plugins_path) 27 | end 28 | 29 | def emit_plugin_html 30 | _emit_html(parent.system_plugins_path) 31 | end 32 | 33 | def emit_plugin_js 34 | _emit_js(parent.system_plugins_path) 35 | end 36 | 37 | def method_missing(method, *args, &block) 38 | begin 39 | metric = args.shift 40 | options = args.pop || {} 41 | @tiles << { 42 | :type => method, 43 | :metric => metric, 44 | :resolution => self.default_resolution || parent.default_resolution || 60, 45 | :refresh => self.default_refresh || parent.default_refresh || 5, 46 | :width => self.default_width || parent.default_width || 3, 47 | :height => self.default_height || parent.default_height || 3, 48 | :extra_args => args 49 | }.merge(options) 50 | rescue => e 51 | super(method, *args, &block) 52 | end 53 | end 54 | 55 | private 56 | 57 | def emit_tile_js 58 | s = "\n" 65 | s 66 | end 67 | 68 | def emit_board_js 69 | s = "\n" 74 | s 75 | end 76 | 77 | def _emit_css(plugin_path) 78 | s = "" 79 | Dir[File.join(plugin_path, "*.css")].each do |path| 80 | name = File.basename(path) 81 | s += "\n" 82 | end 83 | return s 84 | end 85 | 86 | def _emit_html(plugin_path) 87 | s = "" 88 | Dir[File.join(plugin_path, "*.html")].each do |path| 89 | name = File.basename(path).gsub(".html", "") 90 | s += "\n" 95 | end 96 | return s 97 | end 98 | 99 | def _emit_js(plugin_path) 100 | s = "" 101 | Dir[File.join(plugin_path, "*.js")].each do |path| 102 | name = File.basename(path) 103 | s += "\n" 104 | end 105 | s 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/dasht/list.rb: -------------------------------------------------------------------------------- 1 | # Dasht::List - Simple list structure following properties: 2 | # 3 | # 1. Fast writes. Appends to a list. 4 | # 2. Fast reads by index. Indexed by position in the list. 5 | # 3. Fast deletes that preserve indexes. Removes items from the front 6 | # of the list. 7 | # 4. Simple (but not necessarily fast) aggregation. Enumerate values 8 | # between pointers. 9 | # 10 | # The Dasht::List structure is formed using a Ruby Array (values), 11 | # plus a counter of how many items have been deleted (offset). 12 | # Whenever data is deleted from the head of the list, the offset is 13 | # incremented. 14 | 15 | module Dasht 16 | class List 17 | attr_accessor :values 18 | attr_accessor :offset 19 | 20 | def initialize 21 | @offset = 0 22 | @values = [] 23 | end 24 | 25 | def to_s 26 | return @values.to_s 27 | end 28 | 29 | # Public: Get a pointer to the first value. 30 | def head_pointer 31 | return offset 32 | end 33 | 34 | # Public: Get a pointer to right after the last value. 35 | def tail_pointer 36 | return offset + @values.length 37 | end 38 | 39 | # Public: Get the value at a given pointer, or nil if the pointer 40 | # is no longer valid. 41 | def get(pointer) 42 | index = _pointer_to_index(pointer) 43 | return @values[index] 44 | end 45 | 46 | # Public: Return an enumerator that walks through the list, yielding 47 | # data. 48 | def enum(start_pointer = nil, end_pointer = nil) 49 | index = _pointer_to_index(start_pointer || head_pointer) 50 | end_index = _pointer_to_index(end_pointer || tail_pointer) 51 | return Enumerator.new do |yielder| 52 | while index < end_index 53 | yielder << @values[index] 54 | index += 1 55 | end 56 | end 57 | end 58 | 59 | # Public: Add data to the list. 60 | # Returns a pointer to the new data. 61 | def append(data) 62 | pointer = self.tail_pointer 63 | @values << data 64 | return pointer 65 | end 66 | 67 | # Public: Remove data up to (but not including) the specified pointer. 68 | def trim_to(pointer) 69 | return if pointer.nil? 70 | index = _pointer_to_index(pointer) 71 | @offset += index 72 | @values = @values.slice(index, @values.length) 73 | return 74 | end 75 | 76 | # Public: Walk through the list, removing links from the list while 77 | # the block returns true. Stop when it returns false. 78 | def trim_while(&block) 79 | while (@values.length > 0) && yield(@values.first) 80 | @values.shift 81 | @offset += 1 82 | end 83 | return 84 | end 85 | 86 | private 87 | 88 | # Convert a pointer to an index in the list. 89 | def _pointer_to_index(pointer) 90 | return [pointer - offset, 0].max 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/dasht/log_thread.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class LogThread 3 | attr_accessor :parent 4 | 5 | def self.update_global_stats(line) 6 | @total_lines ||= 0 7 | @total_bytes ||= 0 8 | @total_lines += 1 9 | @total_bytes += line.length 10 | print "\rConsumed #{@total_lines} lines (#{@total_bytes} bytes)..." 11 | end 12 | 13 | def initialize(parent, command) 14 | @parent = parent 15 | @command = command 16 | @event_definitions = [] 17 | end 18 | 19 | def run 20 | parent.log "Starting `#{@command}`..." 21 | @thread = Thread.new do 22 | begin 23 | while true 24 | begin 25 | IO.popen(@command) do |process| 26 | process.each do |line| 27 | _consume_line(line) 28 | end 29 | end 30 | rescue => e 31 | parent.log e 32 | end 33 | sleep 2 34 | end 35 | rescue => e 36 | parent.log e 37 | raise e 38 | end 39 | end 40 | end 41 | 42 | def event(metric, regex, op, value = nil, &block) 43 | @event_definitions << [metric, regex, op, value, block] 44 | end 45 | 46 | def count(metric, regex, &block) 47 | event(metric, regex, :dasht_sum, 1, &block) 48 | end 49 | 50 | def gauge(metric, regex, &block) 51 | event(metric, regex, :last, nil, &block) 52 | end 53 | 54 | def min(metric, regex, &block) 55 | event(metric, regex, :min, nil, &block) 56 | end 57 | 58 | def max(metric, regex, &block) 59 | event(metric, regex, :max, nil, &block) 60 | end 61 | 62 | def append(metric, regex, &block) 63 | event(metric, regex, :to_a, nil, &block) 64 | end 65 | 66 | def unique(metric, regex, &block) 67 | event(metric, regex, :uniq, nil, &block) 68 | end 69 | 70 | def terminate 71 | @thread.terminate 72 | end 73 | 74 | private 75 | 76 | def _consume_line(line) 77 | self.class.update_global_stats(line) 78 | ts = Time.now.to_i 79 | @event_definitions.each do |metric, regex, op, value, block| 80 | begin 81 | regex.match(line) do |matches| 82 | value = matches[0] if value.nil? 83 | value = block.call(matches) if block 84 | parent.metrics.set(metric, value, op, ts) if value 85 | end 86 | rescue => e 87 | parent.log e 88 | raise e 89 | end 90 | parent.metrics.trim_to(ts - (parent.history || (60 * 60))) 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/dasht/metric.rb: -------------------------------------------------------------------------------- 1 | # Dasht::Metric - Simple in-memory time-series data structure with the 2 | # following properties: 3 | # 4 | # 1. Sparse. Only stores time stamps for intervals with known data, 5 | # and only stores one timestamp per interval. 6 | # 2. Flexible aggregation using Ruby blocks during both read and 7 | # write. 8 | # 3. Read values between two timestamps. 9 | # 10 | # The Dasht::Metric structure is formed using two Dasht::List 11 | # objects. One object tracks data, the other object tracks a list of 12 | # checkpoints and their corresponding index into the data. 13 | module Dasht 14 | class Metric 15 | attr_reader :data, :checkpoints 16 | 17 | def initialize 18 | @checkpoints = List.new 19 | @data = List.new 20 | @last_item = nil 21 | @last_ts = nil 22 | end 23 | 24 | def to_s 25 | return @data.to_s + " (last: #{@last_item})" 26 | end 27 | 28 | def append(data, ts, &block) 29 | # Maybe checkpoint the time. 30 | if @last_ts == ts 31 | @last_item = yield(@last_item, data) 32 | else 33 | if @last_ts 34 | pointer = @data.append(@last_item) 35 | @checkpoints.append([@last_ts, pointer]) 36 | end 37 | @last_ts = ts 38 | @last_item = nil 39 | @last_item = yield(@last_item, data) 40 | end 41 | return 42 | end 43 | 44 | def trim_to(ts) 45 | pointer = nil 46 | @checkpoints.trim_while do |s, p| 47 | pointer = p 48 | (s || 0) < ts 49 | end 50 | @data.trim_to(pointer) 51 | return 52 | end 53 | 54 | def enum(start_ts, end_ts = nil) 55 | # Get a pointer to our location in the data. 56 | start_pointer = nil 57 | end_pointer = nil 58 | prev_p = nil 59 | @checkpoints.enum.each do |s, p| 60 | start_pointer ||= p if start_ts <= (s || 0) 61 | end_pointer ||= prev_p if end_ts && end_ts <= (s || 0) 62 | break if start_pointer && (end_ts.nil? || end_pointer) 63 | prev_p = p 64 | end 65 | start_pointer ||= @data.tail_pointer 66 | end_pointer ||= @data.tail_pointer 67 | 68 | # Enumerate through the data, then tack on the last item. 69 | return Enumerator.new do |yielder| 70 | @data.enum(start_pointer, end_pointer).each do |data| 71 | yielder << data 72 | end 73 | # Maybe include the last item. 74 | if @last_item && 75 | (start_ts <= @last_ts) && 76 | (end_ts.nil? || (@last_ts < end_ts)) 77 | yielder << @last_item 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/dasht/metrics.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class Metrics 3 | attr_accessor :parent 4 | def initialize(parent) 5 | @parent = parent 6 | @metric_values = {} 7 | @metric_operations = {} 8 | end 9 | 10 | def set(metric, value, op, ts) 11 | metric = metric.to_s 12 | @metric_operations[metric] = op 13 | m = (@metric_values[metric] ||= Metric.new) 14 | m.append(value, ts) do |old_value, new_value| 15 | [old_value, new_value].compact.flatten.send(op) 16 | end 17 | end 18 | 19 | def get(metric, start_ts, end_ts) 20 | metric = metric.to_s 21 | m = @metric_values[metric] 22 | return [] if m.nil? 23 | op = @metric_operations[metric] 24 | m.enum(start_ts, end_ts).to_a.flatten.send(op) 25 | end 26 | 27 | def trim_to(ts) 28 | @metric_values.each do |k, v| 29 | v.trim_to(ts) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/dasht/rack_app.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class RackApp 3 | attr_accessor :parent 4 | 5 | def initialize(parent) 6 | @parent = parent 7 | end 8 | 9 | def root_path 10 | File.join(File.dirname(__FILE__), "..", "..") 11 | end 12 | 13 | def run(port) 14 | context = self 15 | app = Rack::Builder.new do 16 | use Rack::Static, :urls => ["/assets"], :root => context.root_path 17 | run lambda { |env| 18 | begin 19 | context._call(env) 20 | rescue => e 21 | context.parent.log e 22 | raise e 23 | end 24 | } 25 | end 26 | Rack::Server.start(:app => app, :Port => (port || 8080)) 27 | end 28 | 29 | def _call(env) 30 | if "/" == env["REQUEST_PATH"] && parent.boards["default"] 31 | return ['200', {'Content-Type' => 'text/html'}, [parent.boards["default"].to_html]] 32 | end 33 | 34 | /^\/boards\/(.+)$/.match(env["REQUEST_PATH"]) do |match| 35 | board = match[1] 36 | if parent.boards[board] 37 | return ['200', {'Content-Type' => 'text/html'}, [parent.boards[board].to_html]] 38 | else 39 | return ['404', {'Content-Type' => 'text/html'}, ["Board #{board} not found."]] 40 | end 41 | end 42 | 43 | /^\/data$/.match(env["REQUEST_PATH"]) do |match| 44 | queries = JSON.parse(env['rack.input'].gets) 45 | now = Time.now.to_i 46 | data = queries.map do |query| 47 | metric = query["metric"] 48 | resolution = query["resolution"] 49 | periods = query["periods"] 50 | ts = now - (resolution * periods) 51 | (1..periods).map do |n| 52 | parent.metrics.get(metric, ts, ts += resolution) || 0 53 | end 54 | end 55 | 56 | return ['200', {'Content-Type' => 'application/json'}, [data.to_json]] 57 | end 58 | 59 | /^\/data\/(.+)/.match(env["REQUEST_PATH"]) do |match| 60 | parts = match[1].split('/') 61 | metric = parts.shift 62 | resolution = parts.shift.to_i 63 | periods = (parts.shift || 1).to_i 64 | ts = Time.now.to_i - (resolution * periods) 65 | data = (1..periods).map do |n| 66 | parent.metrics.get(metric, ts, ts += resolution) || 0 67 | end 68 | return ['200', {'Content-Type' => 'application/json'}, [data.to_json]] 69 | end 70 | 71 | return ['404', {'Content-Type' => 'text/html'}, ["Path not found: #{env['REQUEST_PATH']}"]] 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/dasht/reloader.rb: -------------------------------------------------------------------------------- 1 | module Dasht 2 | class Reloader 3 | attr_accessor :parent 4 | 5 | def initialize(parent) 6 | @parent = parent 7 | @last_modified = File.mtime($PROGRAM_NAME) 8 | end 9 | 10 | def changed? 11 | @last_modified != File.mtime($PROGRAM_NAME) 12 | end 13 | 14 | def run 15 | Thread.new do 16 | while true 17 | unless changed? 18 | sleep 0.3 19 | next 20 | end 21 | parent.log("Reloading #{$PROGRAM_NAME}...") 22 | eval(IO.read($PROGRAM_NAME)) 23 | @last_modified = File.mtime($PROGRAM_NAME) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustyio/dasht/c88d5de86238725943205ce0b22da806e8f4eab5/screenshot.png -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | module SimpleCov::Configuration 4 | def clean_filters 5 | @filters = [] 6 | end 7 | end 8 | 9 | SimpleCov.configure do 10 | clean_filters 11 | load_adapter 'test_frameworks' 12 | end 13 | 14 | ENV["COVERAGE"] && SimpleCov.start do 15 | add_filter "/.rvm/" 16 | end 17 | require 'rubygems' 18 | require 'bundler' 19 | begin 20 | Bundler.setup(:default, :development) 21 | rescue Bundler::BundlerError => e 22 | $stderr.puts e.message 23 | $stderr.puts "Run `bundle install` to install missing gems" 24 | exit e.status_code 25 | end 26 | require 'test/unit' 27 | require 'shoulda' 28 | 29 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 30 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 31 | require 'dasht' 32 | 33 | class Test::Unit::TestCase 34 | end 35 | -------------------------------------------------------------------------------- /test/test_list.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestList < Test::Unit::TestCase 4 | def test_construction 5 | list = Dasht::List.new 6 | ptr1 = list.append(1) 7 | ptr2 = list.append(2) 8 | ptr3 = list.append(3) 9 | assert_equal 6, list.enum(ptr1).to_a.inject(:+) 10 | end 11 | 12 | def test_trim_to 13 | list = Dasht::List.new 14 | ptr1 = list.append(1) 15 | ptr2 = list.append(2) 16 | ptr3 = list.append(3) 17 | 18 | list.trim_to(ptr1) 19 | assert_equal 6, list.enum(ptr1).to_a.inject(:+) 20 | 21 | list.trim_to(ptr2) 22 | assert_equal 5, list.enum(ptr1).to_a.inject(:+) 23 | 24 | list.trim_to(ptr3) 25 | assert_equal 3, list.enum(ptr1).to_a.inject(:+) 26 | 27 | ptr4 = list.append(4) 28 | assert_equal 7, list.enum(ptr1).to_a.inject(:+) 29 | 30 | list.trim_to(ptr4) 31 | assert_equal 4, list.enum(ptr1).to_a.inject(:+) 32 | 33 | list.trim_to(list.tail_pointer) 34 | assert_equal nil, list.enum(ptr1).to_a.inject(:+) 35 | end 36 | 37 | def test_trim_while 38 | list = Dasht::List.new 39 | ptr1 = list.append(1) 40 | ptr2 = list.append(2) 41 | ptr3 = list.append(3) 42 | list.trim_while do |data| 43 | data < 2 44 | end 45 | assert_equal 5, list.enum(ptr1).to_a.inject(:+) 46 | 47 | list.trim_while do |data| 48 | data < 3 49 | end 50 | assert_equal 3, list.enum(ptr1).to_a.inject(:+) 51 | 52 | ptr4 = list.append(4) 53 | assert_equal 7, list.enum(ptr1).to_a.inject(:+) 54 | 55 | list.trim_while do |data| 56 | data < 4 57 | end 58 | assert_equal 4, list.enum(ptr1).to_a.inject(:+) 59 | 60 | list.trim_while do |data| 61 | data < 5 62 | end 63 | assert_equal nil, list.enum(ptr1).to_a.inject(:+) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/test_metric.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestMetric < Test::Unit::TestCase 4 | def test_counting 5 | m = Dasht::Metric.new 6 | proc = Proc.new do |old_value, new_value| 7 | (old_value || 0) + new_value 8 | end 9 | m.append(4, 1, &proc) 10 | m.append(5, 1, &proc) 11 | m.append(6, 2, &proc) 12 | m.append(7, 2, &proc) 13 | 14 | assert_equal 22, m.enum(1).inject(:+) 15 | assert_equal 13, m.enum(2).inject(:+) 16 | assert_equal 9, m.enum(1,2).inject(:+) 17 | end 18 | 19 | def test_lists 20 | m = Dasht::Metric.new 21 | proc = Proc.new do |old_value, new_value| 22 | (old_value || []).push(new_value) 23 | end 24 | m.append(:a, 1, &proc) 25 | m.append(:b, 1, &proc) 26 | m.append(:c, 2, &proc) 27 | m.append(:d, 2, &proc) 28 | 29 | assert_equal [:a, :b, :c, :d], m.enum(1).to_a.flatten 30 | assert_equal [:c, :d], m.enum(2).to_a.flatten 31 | assert_equal [:a, :b], m.enum(1, 2).to_a.flatten 32 | end 33 | 34 | def test_trim_to 35 | m = Dasht::Metric.new 36 | proc = Proc.new do |old_value, new_value| 37 | (old_value || []).push(new_value) 38 | end 39 | m.append(:a, 1, &proc) 40 | m.append(:b, 1, &proc) 41 | m.append(:c, 2, &proc) 42 | m.append(:d, 2, &proc) 43 | 44 | m.trim_to(1) 45 | assert_equal [:a, :b, :c, :d], m.enum(0).to_a.flatten 46 | 47 | m.trim_to(2) 48 | assert_equal [:c, :d], m.enum(0).to_a.flatten 49 | 50 | m.append(:e, 3, &proc) 51 | m.append(:f, 3, &proc) 52 | assert_equal [:c, :d, :e, :f], m.enum(0).to_a.flatten 53 | 54 | 55 | m.trim_to(3) 56 | assert_equal [:e, :f], m.enum(0).to_a.flatten 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /views/dashboard.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= emit_plugin_css %> 8 | 9 | 10 |
    11 | <%= emit_plugin_html %> 12 | 13 | 14 | 15 | 16 | 17 | 19 | <%= emit_board_js %> 20 | <%= emit_plugin_js %> 21 | <%= emit_tile_js %> 22 | 25 | 26 | 27 | --------------------------------------------------------------------------------