├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── app.js ├── app ├── .buildignore ├── .htaccess ├── 404.html ├── favicon.ico ├── images │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── index.html ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ └── main.js │ ├── directives │ │ ├── gauge.js │ │ └── lineChart.js │ ├── services │ │ └── websocket.js │ └── vendor │ │ └── gauge.js ├── styles │ ├── bootstrap.css │ └── main.css └── views │ └── main.html ├── bower.json ├── doc └── realtimecharts.png ├── karma-e2e.conf.js ├── karma.conf.js ├── package.json └── test ├── .jshintrc ├── runner.html └── spec ├── controllers └── main.js ├── directives ├── gauge.js └── lineChart.js └── services └── websocket.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | .idea 6 | .DS_Store 7 | app/bower_components 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "jQuery": false, 23 | "angular": false, 24 | "google": false, 25 | "Gauge": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2013-10-27 using generator-angular 0.5.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | require('load-grunt-tasks')(grunt); 12 | require('time-grunt')(grunt); 13 | 14 | grunt.initConfig({ 15 | yeoman: { 16 | // configurable paths 17 | app: require('./bower.json').appPath || 'app', 18 | dist: 'dist' 19 | }, 20 | watch: { 21 | coffee: { 22 | files: ['<%= yeoman.app %>/scripts/{,*/}*.coffee'], 23 | tasks: ['coffee:dist'] 24 | }, 25 | coffeeTest: { 26 | files: ['test/spec/{,*/}*.coffee'], 27 | tasks: ['coffee:test'] 28 | }, 29 | styles: { 30 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 31 | tasks: ['copy:styles', 'autoprefixer'] 32 | }, 33 | livereload: { 34 | options: { 35 | livereload: '<%= connect.options.livereload %>' 36 | }, 37 | files: [ 38 | '<%= yeoman.app %>/{,*/}*.html', 39 | '.tmp/styles/{,*/}*.css', 40 | '{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js', 41 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 42 | ] 43 | } 44 | }, 45 | autoprefixer: { 46 | options: ['last 1 version'], 47 | dist: { 48 | files: [{ 49 | expand: true, 50 | cwd: '.tmp/styles/', 51 | src: '{,*/}*.css', 52 | dest: '.tmp/styles/' 53 | }] 54 | } 55 | }, 56 | connect: { 57 | options: { 58 | port: 9000, 59 | // Change this to '0.0.0.0' to access the server from outside. 60 | hostname: 'localhost', 61 | livereload: 35729 62 | }, 63 | livereload: { 64 | options: { 65 | open: true, 66 | base: [ 67 | '.tmp', 68 | '<%= yeoman.app %>' 69 | ] 70 | } 71 | }, 72 | test: { 73 | options: { 74 | port: 9001, 75 | base: [ 76 | '.tmp', 77 | 'test', 78 | '<%= yeoman.app %>' 79 | ] 80 | } 81 | }, 82 | dist: { 83 | options: { 84 | base: '<%= yeoman.dist %>' 85 | } 86 | } 87 | }, 88 | clean: { 89 | dist: { 90 | files: [{ 91 | dot: true, 92 | src: [ 93 | '.tmp', 94 | '<%= yeoman.dist %>/*', 95 | '!<%= yeoman.dist %>/.git*' 96 | ] 97 | }] 98 | }, 99 | server: '.tmp' 100 | }, 101 | jshint: { 102 | options: { 103 | jshintrc: '.jshintrc', 104 | ignores: ['<%= yeoman.app %>/scripts/vendor/gauge.js'] 105 | }, 106 | all: [ 107 | 'Gruntfile.js', 108 | '<%= yeoman.app %>/scripts/{,*/}*.js' 109 | ] 110 | }, 111 | coffee: { 112 | options: { 113 | sourceMap: true, 114 | sourceRoot: '' 115 | }, 116 | dist: { 117 | files: [{ 118 | expand: true, 119 | cwd: '<%= yeoman.app %>/scripts', 120 | src: '{,*/}*.coffee', 121 | dest: '.tmp/scripts', 122 | ext: '.js' 123 | }] 124 | }, 125 | test: { 126 | files: [{ 127 | expand: true, 128 | cwd: 'test/spec', 129 | src: '{,*/}*.coffee', 130 | dest: '.tmp/spec', 131 | ext: '.js' 132 | }] 133 | } 134 | }, 135 | // not used since Uglify task does concat, 136 | // but still available if needed 137 | /*concat: { 138 | dist: {} 139 | },*/ 140 | rev: { 141 | dist: { 142 | files: { 143 | src: [ 144 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 145 | '<%= yeoman.dist %>/styles/{,*/}*.css', 146 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 147 | '<%= yeoman.dist %>/styles/fonts/*' 148 | ] 149 | } 150 | } 151 | }, 152 | useminPrepare: { 153 | html: '<%= yeoman.app %>/index.html', 154 | options: { 155 | dest: '<%= yeoman.dist %>' 156 | } 157 | }, 158 | usemin: { 159 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 160 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 161 | options: { 162 | dirs: ['<%= yeoman.dist %>'] 163 | } 164 | }, 165 | imagemin: { 166 | dist: { 167 | files: [{ 168 | expand: true, 169 | cwd: '<%= yeoman.app %>/images', 170 | src: '{,*/}*.{png,jpg,jpeg}', 171 | dest: '<%= yeoman.dist %>/images' 172 | }] 173 | } 174 | }, 175 | svgmin: { 176 | dist: { 177 | files: [{ 178 | expand: true, 179 | cwd: '<%= yeoman.app %>/images', 180 | src: '{,*/}*.svg', 181 | dest: '<%= yeoman.dist %>/images' 182 | }] 183 | } 184 | }, 185 | cssmin: { 186 | // By default, your `index.html` will take care of 187 | // minification. This option is pre-configured if you do not wish to use 188 | // Usemin blocks. 189 | // dist: { 190 | // files: { 191 | // '<%= yeoman.dist %>/styles/main.css': [ 192 | // '.tmp/styles/{,*/}*.css', 193 | // '<%= yeoman.app %>/styles/{,*/}*.css' 194 | // ] 195 | // } 196 | // } 197 | }, 198 | htmlmin: { 199 | dist: { 200 | options: { 201 | /*removeCommentsFromCDATA: true, 202 | // https://github.com/yeoman/grunt-usemin/issues/44 203 | //collapseWhitespace: true, 204 | collapseBooleanAttributes: true, 205 | removeAttributeQuotes: true, 206 | removeRedundantAttributes: true, 207 | useShortDoctype: true, 208 | removeEmptyAttributes: true, 209 | removeOptionalTags: true*/ 210 | }, 211 | files: [{ 212 | expand: true, 213 | cwd: '<%= yeoman.app %>', 214 | src: ['*.html', 'views/*.html'], 215 | dest: '<%= yeoman.dist %>' 216 | }] 217 | } 218 | }, 219 | // Put files not handled in other tasks here 220 | copy: { 221 | dist: { 222 | files: [{ 223 | expand: true, 224 | dot: true, 225 | cwd: '<%= yeoman.app %>', 226 | dest: '<%= yeoman.dist %>', 227 | src: [ 228 | '*.{ico,png,txt}', 229 | '.htaccess', 230 | 'bower_components/**/*', 231 | 'images/{,*/}*.{gif,webp}', 232 | 'styles/fonts/*' 233 | ] 234 | }, { 235 | expand: true, 236 | cwd: '.tmp/images', 237 | dest: '<%= yeoman.dist %>/images', 238 | src: [ 239 | 'generated/*' 240 | ] 241 | }] 242 | }, 243 | styles: { 244 | expand: true, 245 | cwd: '<%= yeoman.app %>/styles', 246 | dest: '.tmp/styles/', 247 | src: ['{,*/}*.css', '../bower_components/**/*.css'] 248 | } 249 | }, 250 | concurrent: { 251 | server: [ 252 | 'coffee:dist', 253 | 'copy:styles' 254 | ], 255 | test: [ 256 | 'coffee', 257 | 'copy:styles' 258 | ], 259 | dist: [ 260 | 'coffee', 261 | 'copy:styles', 262 | 'imagemin', 263 | 'svgmin', 264 | 'htmlmin' 265 | ] 266 | }, 267 | karma: { 268 | unit: { 269 | configFile: 'karma.conf.js', 270 | singleRun: true 271 | } 272 | }, 273 | cdnify: { 274 | dist: { 275 | html: ['<%= yeoman.dist %>/*.html'] 276 | } 277 | }, 278 | ngmin: { 279 | dist: { 280 | files: [{ 281 | expand: true, 282 | cwd: '<%= yeoman.dist %>/scripts', 283 | src: '*.js', 284 | dest: '<%= yeoman.dist %>/scripts' 285 | }] 286 | } 287 | }, 288 | uglify: { 289 | dist: { 290 | files: { 291 | '<%= yeoman.dist %>/scripts/scripts.js': [ 292 | '<%= yeoman.dist %>/scripts/scripts.js' 293 | ] 294 | } 295 | } 296 | } 297 | }); 298 | 299 | grunt.registerTask('server', function (target) { 300 | if (target === 'dist') { 301 | return grunt.task.run(['build', 'connect:dist:keepalive']); 302 | } 303 | 304 | grunt.task.run([ 305 | 'clean:server', 306 | 'concurrent:server', 307 | 'autoprefixer', 308 | 'connect:livereload', 309 | 'watch' 310 | ]); 311 | }); 312 | 313 | grunt.registerTask('test', [ 314 | 'clean:server', 315 | 'concurrent:test', 316 | 'autoprefixer', 317 | 'connect:test', 318 | 'karma' 319 | ]); 320 | 321 | grunt.registerTask('build', [ 322 | 'clean:dist', 323 | 'useminPrepare', 324 | 'concurrent:dist', 325 | 'autoprefixer', 326 | 'concat', 327 | 'copy:dist', 328 | 'cdnify', 329 | 'ngmin', 330 | 'cssmin', 331 | 'uglify', 332 | 'rev', 333 | 'usemin' 334 | ]); 335 | 336 | grunt.registerTask('default', [ 337 | 'jshint', 338 | 'test', 339 | 'build' 340 | ]); 341 | }; 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-real-time-charts 2 | ======================== 3 | 4 | Real Time Charts with AngularJS. 5 | 6 | ![Real Time Charts](doc/realtimecharts.png "Real Time Charts") 7 | 8 | ## AngularJS directives 9 | 10 | Line Chart with Google Charts 11 | 12 | Gauge with D3.js 13 | 14 | ## Usage 15 | 16 | Install dependencies: 17 | 18 | $ npm install 19 | 20 | Install Bower dependencies: 21 | 22 | $ bower install 23 | 24 | Start Node.js server: 25 | 26 | $ node app 27 | 28 | Application will be available at http://localhost:3000 29 | 30 | ## Links 31 | 32 | [AngularJS](http://angularjs.org/) JavaScript MVC Framework 33 | 34 | [D3](https://github.com/mbostock/d3) JavaScript Data Visualization library 35 | 36 | [Google Charts](https://developers.google.com/chart/) Charts by Google 37 | 38 | [sockjs-node](https://github.com/sockjs/sockjs-node) Node.js WebSocket server 39 | 40 | [sockjs-client](https://github.com/sockjs/sockjs-client) SockJS client 41 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var sockjs = require('sockjs'); 3 | var http = require('http'); 4 | 5 | var app = express(); 6 | 7 | // all environments 8 | app.set('port', process.env.PORT || 3000); 9 | app.use(express.favicon()); 10 | app.use(express.logger('dev')); 11 | app.use(express.bodyParser()); 12 | app.use(express.methodOverride()); 13 | app.use(app.router); 14 | 15 | console.log('environment: ' + app.get('env')); 16 | 17 | if ('production' == app.get('env')) { 18 | app.use(express.static(__dirname + '/dist')); 19 | } else if ('development' == app.get('env')) { 20 | app.use(express.static(__dirname + '/app')); 21 | app.use(express.errorHandler()); 22 | } 23 | 24 | var clients = {}; 25 | var clientCount = 0; 26 | var interval; 27 | 28 | var gaugeValue = 50; 29 | 30 | function broadcast() { 31 | gaugeValue += Math.random() * 40 - 20; 32 | gaugeValue = gaugeValue < 0 ? 0 : gaugeValue > 100 ? 100 : gaugeValue; 33 | var time = Date.now(); 34 | 35 | var message = JSON.stringify({ value: Math.floor(gaugeValue), timestamp: time }); 36 | 37 | for (var key in clients) { 38 | if(clients.hasOwnProperty(key)) { 39 | clients[key].write(message); 40 | } 41 | } 42 | 43 | //setTimeout(broadcast, 1000); 44 | } 45 | 46 | function startBroadcast () { 47 | interval = setInterval(broadcast, 1000); 48 | //broadcast(); 49 | } 50 | 51 | var sockjsServer = sockjs.createServer(); 52 | 53 | sockjsServer.on('connection', function(conn) { 54 | clientCount++; 55 | if (clientCount === 1) { 56 | startBroadcast(); 57 | } 58 | 59 | clients[conn.id] = conn; 60 | 61 | conn.on('close', function() { 62 | clientCount--; 63 | delete clients[conn.id]; 64 | if (clientCount === 0) { 65 | clearInterval(interval); 66 | } 67 | }); 68 | }); 69 | 70 | var server = http.createServer(app).listen(app.get('port'), function(){ 71 | console.log('Express server listening on port ' + app.get('port')); 72 | }); 73 | 74 | sockjsServer.installHandlers(server, { prefix: '/sockjs' }); -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickholub/angular-real-time-charts/e800ea9be8e3802c20808468c02db503a50af3c6/app/favicon.ico -------------------------------------------------------------------------------- /app/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickholub/angular-real-time-charts/e800ea9be8e3802c20808468c02db503a50af3c6/app/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /app/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickholub/angular-real-time-charts/e800ea9be8e3802c20808468c02db503a50af3c6/app/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 32 | 33 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app', ['ngRoute', 'app.service', 'app.directive', 'app.controller']); 4 | 5 | angular.module('app').config(function ($routeProvider, webSocketProvider) { 6 | webSocketProvider.setWebSocketURL('ws://' + window.location.host + '/sockjs/websocket'); 7 | 8 | $routeProvider 9 | .when('/', { 10 | templateUrl: 'views/main.html', 11 | controller: 'MainCtrl' 12 | }) 13 | .otherwise({ 14 | redirectTo: '/' 15 | }); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.controller', []); 4 | 5 | angular.module('app.controller') 6 | .controller('MainCtrl', function ($scope, webSocket) { 7 | $scope.gaugeValue = 0; 8 | 9 | var items = []; 10 | 11 | webSocket.subscribe(function (item) { 12 | items.push(item); 13 | 14 | if (items.length > 40) { 15 | items.shift(); 16 | } 17 | 18 | $scope.chart = { 19 | data: items, 20 | max: 30 21 | }; 22 | 23 | $scope.gaugeValue = item.value; 24 | $scope.$apply(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/scripts/directives/gauge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modified copy of https://github.com/lithiumtech/angular_and_d3/blob/master/step5/custom/gauges.js 3 | */ 4 | 5 | 'use strict'; 6 | 7 | angular.module('app.directive', []); 8 | 9 | angular.module('app.directive') 10 | .directive('gauge', function () { 11 | return { 12 | restrict: 'E', 13 | replace: true, 14 | scope: { 15 | label: '@', 16 | min: '=', 17 | max: '=', 18 | value: '=' 19 | }, 20 | link: function postLink(scope, element, attrs) { 21 | var config = { 22 | size: 250, 23 | label: attrs.label, 24 | min: undefined !== scope.min ? scope.min : 0, 25 | max: undefined !== scope.max ? scope.max : 100, 26 | minorTicks: 5 27 | }; 28 | 29 | var range = config.max - config.min; 30 | config.yellowZones = [ 31 | { from: config.min + range * 0.75, to: config.min + range * 0.9 } 32 | ]; 33 | config.redZones = [ 34 | { from: config.min + range * 0.9, to: config.max } 35 | ]; 36 | 37 | scope.gauge = new Gauge(element[0], config); 38 | scope.gauge.render(); 39 | scope.gauge.redraw(scope.value); 40 | 41 | scope.$watch('value', function () { 42 | if (scope.gauge) { 43 | scope.gauge.redraw(scope.value); 44 | } 45 | }); 46 | } 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /app/scripts/directives/lineChart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.directive') 4 | .directive('lineChart', function () { 5 | return { 6 | template: '
', 7 | scope: { 8 | chart: '=' 9 | }, 10 | restrict: 'E', 11 | replace: true, 12 | link: function postLink(scope, element) { 13 | var lineChart = new google.visualization.LineChart(element[0]); 14 | 15 | function draw(chart) { 16 | var data = chart.data; 17 | 18 | var table = new google.visualization.DataTable(); 19 | table.addColumn('datetime'); 20 | table.addColumn('number'); 21 | table.addRows(data.length); 22 | 23 | var view = new google.visualization.DataView(table); 24 | 25 | for (var i = 0; i < data.length; i++) { 26 | var item = data[i]; 27 | table.setCell(i, 0, new Date(item.timestamp)); 28 | var value = parseFloat(item.value); 29 | table.setCell(i, 1, value); 30 | } 31 | 32 | var last = data[data.length - 1]; 33 | var max = new Date(last.timestamp); 34 | var min = new Date(last.timestamp - chart.max * 1000); 35 | 36 | var chartOptions = { 37 | legend: 'none', 38 | vAxis: { minValue: 0, maxValue: 100 }, 39 | hAxis: { viewWindow: { min: min, max: max }} 40 | }; 41 | 42 | lineChart.draw(view, chartOptions); 43 | } 44 | 45 | scope.$watch('chart', function (chart) { 46 | if (chart && chart.data && chart.max) { 47 | draw(chart); 48 | } 49 | }); 50 | } 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /app/scripts/services/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.service', ['ng']); 4 | 5 | angular.module('app.service') 6 | .provider('webSocket', function () { 7 | 8 | var webSocketURL; 9 | var webSocketObject; // for testing only 10 | 11 | return { 12 | $get: function($q) { 13 | if (!webSocketURL && !webSocketObject) { 14 | throw 'WebSocket URL is not defined'; 15 | } 16 | 17 | var socket = !webSocketObject ? new WebSocket(webSocketURL) : webSocketObject; 18 | 19 | var deferred = $q.defer(); 20 | 21 | socket.onopen = function() { 22 | deferred.resolve(); 23 | }; 24 | 25 | var callbacks = jQuery.Callbacks(); 26 | 27 | socket.onmessage = function(e) { 28 | var data = JSON.parse(e.data); 29 | callbacks.fire(data); 30 | }; 31 | 32 | return { 33 | send: function (message) { 34 | var msg = JSON.stringify(message); 35 | 36 | deferred.promise.then(function () { 37 | socket.send(msg); 38 | }); 39 | }, 40 | 41 | subscribe: function(callback) { 42 | callbacks.add(callback); 43 | } 44 | }; 45 | }, 46 | 47 | setWebSocketURL: function(wsURL) { 48 | webSocketURL = wsURL; 49 | }, 50 | 51 | setWebSocketObject: function(wsObject) { 52 | webSocketObject = wsObject; 53 | } 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /app/scripts/vendor/gauge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from https://github.com/lithiumtech/angular_and_d3 3 | */ 4 | 5 | function Gauge(element, configuration) 6 | { 7 | this.element = element; 8 | 9 | var self = this; // for internal d3 functions 10 | 11 | this.configure = function(configuration) 12 | { 13 | this.config = configuration; 14 | 15 | this.config.size = this.config.size * 0.9; 16 | 17 | this.config.raduis = this.config.size * 0.97 / 2; 18 | this.config.cx = this.config.size / 2; 19 | this.config.cy = this.config.size / 2; 20 | 21 | this.config.min = undefined != configuration.min ? configuration.min : 0; 22 | this.config.max = undefined != configuration.max ? configuration.max : 100; 23 | this.config.range = this.config.max - this.config.min; 24 | 25 | this.config.majorTicks = configuration.majorTicks || 5; 26 | this.config.minorTicks = configuration.minorTicks || 2; 27 | 28 | this.config.greenColor = configuration.greenColor || "#109618"; 29 | this.config.yellowColor = configuration.yellowColor || "#FF9900"; 30 | this.config.redColor = configuration.redColor || "#DC3912"; 31 | 32 | this.config.transitionDuration = configuration.transitionDuration || 500; 33 | } 34 | 35 | this.render = function() 36 | { 37 | this.body = d3.select( this.element ) 38 | .append("svg:svg") 39 | .attr("class", "gauge") 40 | .attr("width", this.config.size) 41 | .attr("height", this.config.size); 42 | 43 | this.body.append("svg:circle") 44 | .attr("cx", this.config.cx) 45 | .attr("cy", this.config.cy) 46 | .attr("r", this.config.raduis) 47 | .style("fill", "#ccc") 48 | .style("stroke", "#000") 49 | .style("stroke-width", "0.5px"); 50 | 51 | this.body.append("svg:circle") 52 | .attr("cx", this.config.cx) 53 | .attr("cy", this.config.cy) 54 | .attr("r", 0.9 * this.config.raduis) 55 | .style("fill", "#fff") 56 | .style("stroke", "#e0e0e0") 57 | .style("stroke-width", "2px"); 58 | 59 | for (var index in this.config.greenZones) 60 | { 61 | this.drawBand(this.config.greenZones[index].from, this.config.greenZones[index].to, self.config.greenColor); 62 | } 63 | 64 | for (var index in this.config.yellowZones) 65 | { 66 | this.drawBand(this.config.yellowZones[index].from, this.config.yellowZones[index].to, self.config.yellowColor); 67 | } 68 | 69 | for (var index in this.config.redZones) 70 | { 71 | this.drawBand(this.config.redZones[index].from, this.config.redZones[index].to, self.config.redColor); 72 | } 73 | 74 | if (undefined != this.config.label) 75 | { 76 | var fontSize = Math.round(this.config.size / 9); 77 | this.body.append("svg:text") 78 | .attr("x", this.config.cx) 79 | .attr("y", this.config.cy / 2 + fontSize / 2) 80 | .attr("dy", fontSize / 2) 81 | .attr("text-anchor", "middle") 82 | .text(this.config.label) 83 | .style("font-size", fontSize + "px") 84 | .style("fill", "#333") 85 | .style("stroke-width", "0px"); 86 | } 87 | 88 | var fontSize = Math.round(this.config.size / 16); 89 | var majorDelta = this.config.range / (this.config.majorTicks - 1); 90 | for (var major = this.config.min; major <= this.config.max; major += majorDelta) 91 | { 92 | var minorDelta = majorDelta / this.config.minorTicks; 93 | for (var minor = major + minorDelta; minor < Math.min(major + majorDelta, this.config.max); minor += minorDelta) 94 | { 95 | var point1 = this.valueToPoint(minor, 0.75); 96 | var point2 = this.valueToPoint(minor, 0.85); 97 | 98 | this.body.append("svg:line") 99 | .attr("x1", point1.x) 100 | .attr("y1", point1.y) 101 | .attr("x2", point2.x) 102 | .attr("y2", point2.y) 103 | .style("stroke", "#666") 104 | .style("stroke-width", "1px"); 105 | } 106 | 107 | var point1 = this.valueToPoint(major, 0.7); 108 | var point2 = this.valueToPoint(major, 0.85); 109 | 110 | this.body.append("svg:line") 111 | .attr("x1", point1.x) 112 | .attr("y1", point1.y) 113 | .attr("x2", point2.x) 114 | .attr("y2", point2.y) 115 | .style("stroke", "#333") 116 | .style("stroke-width", "2px"); 117 | 118 | if (major == this.config.min || major == this.config.max) 119 | { 120 | var point = this.valueToPoint(major, 0.63); 121 | 122 | this.body.append("svg:text") 123 | .attr("x", point.x) 124 | .attr("y", point.y) 125 | .attr("dy", fontSize / 3) 126 | .attr("text-anchor", major == this.config.min ? "start" : "end") 127 | .text(major) 128 | .style("font-size", fontSize + "px") 129 | .style("fill", "#333") 130 | .style("stroke-width", "0px"); 131 | } 132 | } 133 | 134 | var pointerContainer = this.body.append("svg:g").attr("class", "pointerContainer"); 135 | 136 | var midValue = (this.config.min + this.config.max) / 2; 137 | 138 | var pointerPath = this.buildPointerPath(midValue); 139 | 140 | var pointerLine = d3.svg.line() 141 | .x(function(d) { return d.x }) 142 | .y(function(d) { return d.y }) 143 | .interpolate("basis"); 144 | 145 | pointerContainer.selectAll("path") 146 | .data([pointerPath]) 147 | .enter() 148 | .append("svg:path") 149 | .attr("d", pointerLine) 150 | .style("fill", "#dc3912") 151 | .style("stroke", "#c63310") 152 | .style("fill-opacity", 0.7) 153 | 154 | pointerContainer.append("svg:circle") 155 | .attr("cx", this.config.cx) 156 | .attr("cy", this.config.cy) 157 | .attr("r", 0.12 * this.config.raduis) 158 | .style("fill", "#4684EE") 159 | .style("stroke", "#666") 160 | .style("opacity", 1); 161 | 162 | var fontSize = Math.round(this.config.size / 10); 163 | pointerContainer.selectAll("text") 164 | .data([midValue]) 165 | .enter() 166 | .append("svg:text") 167 | .attr("x", this.config.cx) 168 | .attr("y", this.config.size - this.config.cy / 4 - fontSize) 169 | .attr("dy", fontSize / 2) 170 | .attr("text-anchor", "middle") 171 | .style("font-size", fontSize + "px") 172 | .style("fill", "#000") 173 | .style("stroke-width", "0px"); 174 | 175 | this.redraw(this.config.min, 0); 176 | } 177 | 178 | this.buildPointerPath = function(value) 179 | { 180 | var delta = this.config.range / 13; 181 | 182 | var head = valueToPoint(value, 0.85); 183 | var head1 = valueToPoint(value - delta, 0.12); 184 | var head2 = valueToPoint(value + delta, 0.12); 185 | 186 | var tailValue = value - (this.config.range * (1/(270/360)) / 2); 187 | var tail = valueToPoint(tailValue, 0.28); 188 | var tail1 = valueToPoint(tailValue - delta, 0.12); 189 | var tail2 = valueToPoint(tailValue + delta, 0.12); 190 | 191 | return [head, head1, tail2, tail, tail1, head2, head]; 192 | 193 | function valueToPoint(value, factor) 194 | { 195 | var point = self.valueToPoint(value, factor); 196 | point.x -= self.config.cx; 197 | point.y -= self.config.cy; 198 | return point; 199 | } 200 | } 201 | 202 | this.drawBand = function(start, end, color) 203 | { 204 | if (0 >= end - start) return; 205 | 206 | this.body.append("svg:path") 207 | .style("fill", color) 208 | .attr("d", d3.svg.arc() 209 | .startAngle(this.valueToRadians(start)) 210 | .endAngle(this.valueToRadians(end)) 211 | .innerRadius(0.65 * this.config.raduis) 212 | .outerRadius(0.85 * this.config.raduis)) 213 | .attr("transform", function() { return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(270)" }); 214 | } 215 | 216 | this.redraw = function(value, transitionDuration) 217 | { 218 | var pointerContainer = this.body.select(".pointerContainer"); 219 | 220 | pointerContainer.selectAll("text").text(Math.round(value)); 221 | 222 | var pointer = pointerContainer.selectAll("path"); 223 | pointer.transition() 224 | .duration(undefined != transitionDuration ? transitionDuration : this.config.transitionDuration) 225 | //.delay(0) 226 | //.ease("linear") 227 | //.attr("transform", function(d) 228 | .attrTween("transform", function() 229 | { 230 | var pointerValue = value; 231 | if (value > self.config.max) pointerValue = self.config.max + 0.02*self.config.range; 232 | else if (value < self.config.min) pointerValue = self.config.min - 0.02*self.config.range; 233 | var targetRotation = (self.valueToDegrees(pointerValue) - 90); 234 | var currentRotation = self._currentRotation || targetRotation; 235 | self._currentRotation = targetRotation; 236 | 237 | return function(step) 238 | { 239 | var rotation = currentRotation + (targetRotation-currentRotation)*step; 240 | return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(" + rotation + ")"; 241 | } 242 | }); 243 | } 244 | 245 | this.valueToDegrees = function(value) 246 | { 247 | // thanks @closealert 248 | //return value / this.config.range * 270 - 45; 249 | return value / this.config.range * 270 - (this.config.min / this.config.range * 270 + 45); 250 | } 251 | 252 | this.valueToRadians = function(value) 253 | { 254 | return this.valueToDegrees(value) * Math.PI / 180; 255 | } 256 | 257 | this.valueToPoint = function(value, factor) 258 | { 259 | return { x: this.config.cx - this.config.raduis * factor * Math.cos(this.valueToRadians(value)), 260 | y: this.config.cy - this.config.raduis * factor * Math.sin(this.valueToRadians(value)) }; 261 | } 262 | 263 | // initialization 264 | this.configure(configuration); 265 | } -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | padding-bottom: 40px; 4 | } 5 | .sidebar-nav { 6 | padding: 9px 0; 7 | } 8 | 9 | @media (max-width: 980px) { 10 | /* Enable use of floated navbar text */ 11 | .navbar-text.pull-right { 12 | float: none; 13 | padding-left: 5px; 14 | padding-right: 5px; 15 | } 16 | } 17 | 18 | body { 19 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 20 | color: #333; 21 | } 22 | 23 | .hero-unit { 24 | margin: 50px auto 0 auto; 25 | width: 300px; 26 | font-size: 18px; 27 | font-weight: 200; 28 | line-height: 30px; 29 | background-color: #eee; 30 | border-radius: 6px; 31 | padding: 60px; 32 | } 33 | 34 | .hero-unit h1 { 35 | font-size: 60px; 36 | line-height: 1; 37 | letter-spacing: -1px; 38 | } 39 | 40 | .line-chart { 41 | height: 400px; 42 | } 43 | 44 | .gauge { 45 | margin-top: 80px; 46 | } 47 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Google Line Chart

4 | 5 |
6 |
7 |

D3.js Gauge

8 | 9 |
10 |
-------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularRealTimeCharts", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "~1.2.23", 6 | "angular-route": "~1.2.23", 7 | "json3": "~3.2.4", 8 | "jquery": "~1.9.1", 9 | "bootstrap-sass": "~2.3.1", 10 | "es5-shim": "~2.0.8", 11 | "angular-resource": "~1.2.23", 12 | "angular-cookies": "~1.2.23", 13 | "angular-sanitize": "~1.2.23", 14 | "d3": "~3.3.9" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "~1.2.23", 18 | "angular-scenario": "~1.2.23" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /doc/realtimecharts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickholub/angular-real-time-charts/e800ea9be8e3802c20808468c02db503a50af3c6/doc/realtimecharts.png -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'app/bower_components/jquery/jquery.js', 15 | 'app/bower_components/angular/angular.js', 16 | 'app/bower_components/angular-route/angular-route.js', 17 | 'app/bower_components/angular-mocks/angular-mocks.js', 18 | 'app/bower_components/angular-resource/angular-resource.js', 19 | 'app/bower_components/angular-cookies/angular-cookies.js', 20 | 'app/bower_components/angular-sanitize/angular-sanitize.js', 21 | 'app/scripts/*.js', 22 | 'app/scripts/**/*.js', 23 | 'test/mock/**/*.js', 24 | 'test/spec/**/*.js' 25 | ], 26 | 27 | // list of files / patterns to exclude 28 | exclude: [], 29 | 30 | // web server port 31 | port: 8080, 32 | 33 | // level of logging 34 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 35 | logLevel: config.LOG_INFO, 36 | 37 | 38 | // enable / disable watching file and executing tests whenever any file changes 39 | autoWatch: false, 40 | 41 | 42 | // Start these browsers, currently available: 43 | // - Chrome 44 | // - ChromeCanary 45 | // - Firefox 46 | // - Opera 47 | // - Safari (only Mac) 48 | // - PhantomJS 49 | // - IE (only Windows) 50 | browsers: ['PhantomJS'], 51 | 52 | 53 | // Continuous Integration mode 54 | // if true, it capture browsers, run tests and exit 55 | singleRun: false 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-real-time-charts", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "express": "~3.4.3", 6 | "sockjs": "~0.3.8", 7 | "karma-jasmine": "^0.1.5", 8 | "karma-phantomjs-launcher": "^0.1.4" 9 | }, 10 | "devDependencies": { 11 | "grunt": "~0.4.1", 12 | "grunt-contrib-copy": "~0.4.1", 13 | "grunt-contrib-concat": "~0.3.0", 14 | "grunt-contrib-coffee": "~0.7.0", 15 | "grunt-contrib-uglify": "~0.2.0", 16 | "grunt-contrib-compass": "~0.5.0", 17 | "grunt-contrib-jshint": "~0.6.0", 18 | "grunt-contrib-cssmin": "~0.6.0", 19 | "grunt-contrib-connect": "~0.5.0", 20 | "grunt-contrib-clean": "~0.5.0", 21 | "grunt-contrib-htmlmin": "~0.1.3", 22 | "grunt-contrib-imagemin": "~0.2.0", 23 | "grunt-contrib-watch": "~0.5.2", 24 | "grunt-autoprefixer": "~0.2.0", 25 | "grunt-usemin": "~0.1.11", 26 | "grunt-svgmin": "~0.2.0", 27 | "grunt-rev": "~0.1.0", 28 | "grunt-concurrent": "~0.3.0", 29 | "load-grunt-tasks": "~0.1.0", 30 | "grunt-google-cdn": "~0.2.0", 31 | "grunt-ngmin": "~0.0.2", 32 | "time-grunt": "~0.1.0", 33 | "karma-ng-scenario": "^0.1.0", 34 | "grunt-karma": "^0.8.3", 35 | "karma": "^0.12.17", 36 | "karma-ng-html2js-preprocessor": "^0.1.0" 37 | }, 38 | "engines": { 39 | "node": ">=0.8.0" 40 | }, 41 | "scripts": { 42 | "test": "grunt test" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "spyOn": false 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('app')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | //MainCtrl = $controller('MainCtrl', { 15 | // $scope: scope 16 | //}); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | //expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/directives/gauge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: gauge', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('app')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | //element = angular.element(''); 17 | //element = $compile(element)(scope); 18 | //expect(element.text()).toBe('this is the gauge directive'); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/lineChart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: lineChart', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('app')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | //element = angular.element(''); 17 | //element = $compile(element)(scope); 18 | //expect(element.text()).toBe('this is the gLineChart directive'); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/services/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: webSocket', function () { 4 | 5 | var webSocketObject; 6 | var webSocket; 7 | 8 | beforeEach(module('app.service', function(webSocketProvider) { 9 | webSocketObject = { 10 | send: function () {} 11 | }; 12 | 13 | webSocketProvider.setWebSocketObject(webSocketObject); 14 | })); 15 | 16 | beforeEach(inject(function (_webSocket_) { 17 | webSocket = _webSocket_; 18 | })); 19 | 20 | it('should notify subscribers', function () { 21 | expect(webSocketObject.onmessage).toBeDefined(); 22 | 23 | var listener1 = jasmine.createSpy(); 24 | var listener2 = jasmine.createSpy(); 25 | 26 | webSocket.subscribe(listener1); 27 | webSocket.subscribe(listener2); 28 | 29 | webSocketObject.onmessage({ data: '{ "value": 100 }' }); 30 | 31 | expect(listener1).toHaveBeenCalledWith({ value: 100 }); 32 | expect(listener2).toHaveBeenCalledWith({ value: 100 }); 33 | 34 | webSocketObject.onmessage({ data: '{ "value": 50 }' }); 35 | 36 | expect(listener1).toHaveBeenCalledWith({ value: 50 }); 37 | expect(listener2).toHaveBeenCalledWith({ value: 50 }); 38 | }); 39 | 40 | it('should send message when WebSocket connection is opened', inject(function ($rootScope) { 41 | expect(webSocketObject.onopen).toBeDefined(); 42 | 43 | spyOn(webSocketObject, 'send'); 44 | 45 | webSocket.send({ value: 100 }); 46 | 47 | expect(webSocketObject.send).not.toHaveBeenCalled(); // no connection yet 48 | 49 | webSocketObject.onopen(); 50 | $rootScope.$apply(); // required for AngularJS promise resolution 51 | 52 | expect(webSocketObject.send).toHaveBeenCalled(); 53 | })); 54 | 55 | }); 56 | --------------------------------------------------------------------------------