├── .gitignore ├── README.md ├── datasource.html ├── datasource.js ├── datasources.js ├── package.json ├── plugins.js ├── plugins ├── plugin_alert.html ├── plugin_circleGauge.html ├── plugin_dygraph.html ├── plugin_example.html ├── plugin_justgage.html ├── plugin_nvd3bar.html ├── plugin_nvd3pie.html └── plugin_nvd3scatter.html ├── server.js ├── static ├── css │ ├── bootstrap-theme.min.css │ ├── bootstrap.min.css │ ├── jquery-ui.structure.min.css │ └── main.css ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ ├── connected.png │ └── disconnected.png ├── js │ ├── controller │ │ ├── dashboard.js │ │ └── dashboardlist.js │ ├── gridlist │ │ ├── gridList.js │ │ └── jquery.gridList.js │ ├── lib │ │ ├── bootstrap.min.js │ │ ├── d3.min.js │ │ ├── jquery │ │ │ ├── jquery-2.1.4.min.js │ │ │ ├── jquery-ui.min.js │ │ │ └── jquery.ui.touch-punch.min.js │ │ ├── jsrender.min.js │ │ └── simple_statistics.min.js │ ├── main.js │ ├── modal.js │ ├── model │ │ ├── chart.js │ │ ├── dashboard.js │ │ └── datasource.js │ ├── net.js │ ├── page.js │ ├── plugins.js │ ├── settings.js │ └── view │ │ ├── dashboard.js │ │ ├── dashboardlist.js │ │ └── statusbar.js └── plugins │ ├── alert │ ├── alert_bg.png │ └── alert_glow.png │ └── lib │ ├── circleGauge.js │ ├── dygraph │ └── dygraph-combined.js │ ├── justgage │ ├── justgage-1.1.0.min.js │ └── raphael-2.1.4.min.js │ └── nvd3 │ ├── nv.d3.css │ └── nv.d3.min.js ├── template └── index.mst └── users.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .dash/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-graphs 2 | 3 | A Node-RED graphing package. Contains a datasource node which handles historical data and live data streams, and a hackable visualization application designed to connect to the datasource nodes. 4 | 5 | # Install 6 | 7 | Within your local installation of Node-RED run: 8 | 9 | `npm install node-red-contrib-graphs` 10 | 11 | Once installed, the datasource node will be available in Node-RED (*iot-datasource*) under the *GatewayKit* category. The dashboard web application will also start being served at http://localhost:1880/dash/ (by default) 12 | 13 | # Node-RED Datasource node 14 | 15 | This node is designed to accept data (live or historical) in a certain format and send it to the dashboard application included in this package. 16 | 17 | The node expects each datapoint to be a JSON Object that contains a UNIX timestamp field (default: `msg.payload.tstamp`) and a data field (default: `msg.payload.data`). 18 | 19 | The datasource node can be configured, however, to look for these values anywhere within `msg.payload`. 20 | 21 | For example, if the incoming JSON looks something like this... 22 | ``` 23 | msg.payload = { 24 | myData: { 25 | myTimestamp: 1438637044000, 26 | myInnerData: { 27 | x: 3.14 28 | } 29 | } 30 | } 31 | ``` 32 | ...the node will be able to access it as long as you configure its timestamp field to `msg.payload.myData.myTimestamp` and its data field to `msg.payload.myData.myInnerData.x` 33 | 34 | The node is also able to parse JSON Object data. For example, if your data looks something like this... 35 | ``` 36 | msg.payload = { 37 | tstamp: 1438637044000, 38 | data: { 39 | x: 3.14, 40 | y: 1.41, 41 | z: 6.02 42 | } 43 | } 44 | ``` 45 | ...and the node is configured to look for data in `msg.payload.data`, the node will automatically go inside the object, and find `x`, `y`, and `z`, and present them to the dashboard application as "subcomponents." The dashboard application can then choose which subcomponents to graph. If you need this behavior disabled, if you need the JSON Object kept intact, check the *Disable subcomponent discovery* option in the node's configuration. 46 | 47 | *Note: The subcomponent discovery process happens when the first data point is received. If the format of the data changes after that, the node won't register the change.* 48 | 49 | *Note: Until the node receives its first data point, the node is considered uninitialized. Charts referencing this datasource won't load.* 50 | 51 | There is one key different between historical and live data, without which the node wouldn't be able to tell them apart (for now). 52 | Live data is expected to come in as single data points... 53 | ``` 54 | msg.payload = { 55 | tstamp: 1438637044000, 56 | data: 20.0 57 | } 58 | ``` 59 | ...while historical data is expected as an array of data points, ordered by timestamp, in ascending order. 60 | ``` 61 | msg.payload = [ ... 62 | { 63 | tstamp: 1438637044000, 64 | data: 20.0 65 | }, 66 | { 67 | tstamp: 1438637045000, 68 | data: 25.0 69 | } 70 | ... ] 71 | ``` 72 | In order to allow the dashboard applications to make requests for historical data, the datasource node has the ability to output a JSON object containing start and end timestamps on request. 73 | ``` 74 | msg.payload = { 75 | start: 1438637044000, 76 | end: 1438638044000 77 | } 78 | ``` 79 | This output can then be sent through a flow designed to retrieve historical information within the requested timestamps, and feed it back into the datasource node through a loop. 80 | 81 | *Note: It is important that the flow retrieving historical data does not re-create the message. It must only modify it! Otherwise, the request will fail.* 82 | 83 | ***Examples*** *(Node-RED flows)* 84 | 85 | *Live data* 86 | ``` 87 | [{"id":"891b3e25.76e4c","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":313,"y":132,"z":"5b17c53d.a4e83c","wires":[["64a9bc70.9b5644"]]},{"id":"64a9bc70.9b5644","type":"function","name":"Random Data","func":"var now = ( new Date() ).getTime();\nvar value = Math.floor( Math.random() * 100 );\nmsg.payload = {\n tstamp: now,\n data: value\n};\nreturn msg;","outputs":1,"noerr":0,"x":555,"y":132,"z":"5b17c53d.a4e83c","wires":[["9c566cfe.63a99"]]},{"id":"9c566cfe.63a99","type":"iot-datasource","name":"Random Datasource","tstampField":"","dataField":"","disableDiscover":false,"x":843,"y":134,"z":"5b17c53d.a4e83c","wires":[[]]}] 88 | ``` 89 | 90 | *Live & historical data* 91 | ``` 92 | [{"id":"cca5fe7a.335a","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":287,"y":175,"z":"5b17c53d.a4e83c","wires":[["37e83d85.c817c2"]]},{"id":"37e83d85.c817c2","type":"function","name":"Random Data","func":"var now = ( new Date() ).getTime();\nvar value = Math.floor( Math.random() * 100 );\nmsg.payload = {\n tstamp: now,\n data: value\n};\nreturn msg;","outputs":1,"noerr":0,"x":529,"y":175,"z":"5b17c53d.a4e83c","wires":[["adfd9b1a.520268"]]},{"id":"adfd9b1a.520268","type":"iot-datasource","name":"Random Datasource","tstampField":"","dataField":"","disableDiscover":false,"x":818,"y":175,"z":"5b17c53d.a4e83c","wires":[["17605e.ffe89fa2"]]},{"id":"17605e.ffe89fa2","type":"function","name":"Random History","func":"// Get request timestamps\nvar start = msg.payload.start;\nvar end = msg.payload.end;\n\nvar data = [];\nfor( var ts = start; ts < end; ts += 1000 )\n{\n var value = Math.floor( Math.random() * 100 );\n data.push( {\n tstamp: ts,\n data: value\n } );\n}\nmsg.payload = data;\nreturn msg;","outputs":1,"noerr":0,"x":814,"y":278,"z":"5b17c53d.a4e83c","wires":[["adfd9b1a.520268"]]}] 93 | ``` 94 | 95 | *Multiple datasources* 96 | ``` 97 | [{"id":"83c0be0c.7c3f4","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":341,"y":157,"z":"5b17c53d.a4e83c","wires":[["1c86e948.e37917"]]},{"id":"1c86e948.e37917","type":"function","name":"Random Data","func":"var now = ( new Date() ).getTime();\nvar value = Math.floor( Math.random() * 100 );\nmsg.payload = {\n tstamp: now,\n data: value\n};\nreturn msg;","outputs":1,"noerr":0,"x":583,"y":157,"z":"5b17c53d.a4e83c","wires":[["9d399bc.f62c668"]]},{"id":"9d399bc.f62c668","type":"iot-datasource","name":"Random Datasource","tstampField":"","dataField":"","disableDiscover":false,"x":872,"y":157,"z":"5b17c53d.a4e83c","wires":[[]]},{"id":"f28351b4.0d7cb","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":340,"y":204,"z":"5b17c53d.a4e83c","wires":[["8fd198d.f702e68"]]},{"id":"8fd198d.f702e68","type":"function","name":"Random Data","func":"var now = ( new Date() ).getTime();\nvar value = Math.floor( Math.random() * 50 ) + 25;\nmsg.payload = {\n tstamp: now,\n data: value\n};\nreturn msg;","outputs":1,"noerr":0,"x":582,"y":204,"z":"5b17c53d.a4e83c","wires":[["4d6ef8ec.b29108"]]},{"id":"4d6ef8ec.b29108","type":"iot-datasource","name":"Random Datasource 2","tstampField":"","dataField":"","disableDiscover":false,"x":871,"y":204,"z":"5b17c53d.a4e83c","wires":[[]]}] 98 | ``` 99 | 100 | *Custom JSON Object & Subcomponent Discovery* 101 | 102 | (Best viewed with a Line/Area Graph) 103 | ``` 104 | [{"id":"5bbde735.a44218","type":"inject","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":206,"y":198,"z":"5b17c53d.a4e83c","wires":[["2e833127.d17cce"]]},{"id":"2e833127.d17cce","type":"function","name":"Random Data","func":"var now = ( new Date() ).getTime();\nvar value = Math.floor( Math.random() * 100 );\nmsg.payload = {\n myTimestamp: now,\n myInnerData: {\n x : value,\n y : value + 100,\n z : value - 100\n }\n};\nreturn msg;","outputs":1,"noerr":0,"x":448,"y":198,"z":"5b17c53d.a4e83c","wires":[["e7879b6c.187868"]]},{"id":"e7879b6c.187868","type":"iot-datasource","name":"Random Datasource","tstampField":"myTimestamp","dataField":"myInnerData","disableDiscover":false,"x":736,"y":200,"z":"5b17c53d.a4e83c","wires":[[]]}] 105 | ``` 106 | 107 | # Dashboard 108 | 109 | The dashboard application packaged with the node can be accessed at http://localhost:1880/dash (default) 110 | 111 | On the main screen, you can create a new dashboard, or access/remove existing dashboards. Each dashboard contains its own set of charts. Once you create a new dashboard or open an existing one, you can create/edit/remove charts within that dashboard. 112 | 113 | All chart types in this application are plugins. These plugins are located in the `plugins/` folder. Any `.html` files inside the plugins folder, or any subfolders, will be automatically loaded. 114 | 115 | When creating a new chart, any datasource nodes deployed in Node-RED will be available to select. 116 | For example, if you've tried out one of the example flows included above, when creating a new chart, the datasource "Random Datasource" will be available. If not, make sure the flow was deployed or try refreshing the dashboard page. 117 | 118 | 119 | # Additional Information 120 | 121 | The node-red-contrib-graphs node is also part of IBM's Iot Gateway Kit at . The graph node is used to create a dashboard and graph data for the Informix database, although the dash node has no dependencies on Informix. 122 | -------------------------------------------------------------------------------- /datasource.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 49 | 50 | 71 | -------------------------------------------------------------------------------- /datasource.js: -------------------------------------------------------------------------------- 1 | 2 | var util = require( "util" ); 3 | var dashServer = require( "./server" ); 4 | var datasourceManager = require( "./datasources" ); 5 | 6 | module.exports = function(RED) 7 | { 8 | dashServer.init( RED ); 9 | 10 | function Datasource( config ) 11 | { 12 | RED.nodes.createNode( this, config ); 13 | 14 | var self = this; 15 | 16 | this.tstampField = config.tstampField.trim() || "tstamp"; 17 | this.dataField = config.dataField.trim() || "data"; 18 | this.dataComponents = undefined; 19 | if( config.disableDiscover ) this.dataComponents = null; 20 | 21 | this.clients = []; 22 | this.currentHistoryRequest = null; 23 | this.historyRequests = {}; 24 | 25 | this.nodeID = this._alias || this.id; 26 | datasourceManager.addNode(this.nodeID, this); 27 | 28 | this.on( "input" , function( msg ) { 29 | 30 | if( !msg.hasOwnProperty( "payload" ) ) return; 31 | 32 | if( typeof msg.payload == "string" && msg.payload == "reset" ) 33 | { 34 | self.dataComponents = undefined; 35 | self.sendToAll( JSON.stringify( { 36 | type : "config", 37 | id : self.nodeID, 38 | config : self.getDatasourceConfig() 39 | } ) ); 40 | return; 41 | } 42 | 43 | // Deduce(?) data components 44 | if( self.dataComponents === undefined ) 45 | { 46 | var dataPoint = util.isArray( msg.payload ) ? msg.payload[0] : msg.payload; 47 | if( dataPoint.hasOwnProperty( self.dataField ) ) 48 | { 49 | if( typeof dataPoint[ self.dataField ] === "object" ) 50 | { 51 | self.dataComponents = []; 52 | var dataObj = dataPoint[ self.dataField ]; 53 | for( var key in dataObj ) 54 | { 55 | if( !dataObj.hasOwnProperty( key ) ) continue; 56 | self.dataComponents.push( key ); 57 | } 58 | } 59 | else self.dataComponents = null; 60 | } 61 | 62 | var configMsg = { 63 | type : "config", 64 | id : self.nodeID, 65 | config : self.getDatasourceConfig() 66 | }; 67 | 68 | self.sendToAll( JSON.stringify( configMsg ) ); 69 | } 70 | 71 | var newData; 72 | 73 | // Historic data request 74 | if( !self.currentHistoryRequest && self.historyRequests.hasOwnProperty( msg._msgid ) ) 75 | { 76 | self.currentHistoryRequest = self.historyRequests[ msg._msgid ]; 77 | delete self.historyRequests[ msg._msgid ]; 78 | } 79 | 80 | if( self.currentHistoryRequest ) 81 | { 82 | newData = { 83 | type : "history", 84 | id : self.nodeID, 85 | cid : self.currentHistoryRequest.cid, 86 | data : msg.payload 87 | }; 88 | self.currentHistoryRequest.ws.send( JSON.stringify( newData ) ); 89 | self.currentHistoryRequest = null; 90 | } 91 | else 92 | { 93 | newData = { 94 | type : "live", 95 | id : self.nodeID, 96 | data : msg.payload 97 | }; 98 | newData = JSON.stringify( newData ); 99 | 100 | // Send live data to all connected clients 101 | this.sendToAll( newData ); 102 | } 103 | 104 | } ); 105 | 106 | this.on( "close" , function() { 107 | for( var i = 0; i < self.clients.length; i++ ) 108 | { 109 | self.clients[i].ws.close(); 110 | } 111 | 112 | datasourceManager.removeNode(self.nodeID); 113 | } ); 114 | 115 | // Finds the index of a data point inside an array of data points sorted by unique timestamp 116 | // If not found, will return the index of the closest data point with timestamp < queried timestamp 117 | this.findData = function( data , timestamp ) 118 | { 119 | var min = 0, max = data.length - 1, mid = 0; 120 | 121 | while( max >= min ) 122 | { 123 | mid = Math.floor( ( min + max ) / 2 ); 124 | if( data[ mid ][ this.tstampField ] == timestamp ) return mid; 125 | else if( data[ mid ][ this.tstampField ] > timestamp ) max = mid - 1; 126 | else min = mid + 1; 127 | } 128 | 129 | return data[ mid ][ this.tstampField ] < timestamp ? mid : mid - 1; 130 | }; 131 | 132 | this.handleHistoryRequest = function( ws , cid , start , end ) 133 | { 134 | var msg = { 135 | payload : { 136 | start : start, 137 | end : end 138 | } 139 | }; 140 | 141 | var request = { 142 | ws : ws, 143 | cid : cid 144 | }; 145 | 146 | self.currentHistoryRequest = request; 147 | this.send( msg ); 148 | self.currentHistoryRequest = null; 149 | this.historyRequests[ msg._msgid ] = request; 150 | }; 151 | 152 | this.addClient = function( client ) 153 | { 154 | for( var i = 0; i < this.clients.length; i++ ) 155 | { 156 | if( client.ws == this.clients[i].ws ) return; 157 | } 158 | 159 | this.clients.push( client ); 160 | var configMsg = { 161 | type : "config", 162 | id : this.nodeID, 163 | config : this.getDatasourceConfig() 164 | }; 165 | 166 | client.ws.send( JSON.stringify( configMsg ) ); 167 | }; 168 | 169 | this.removeClient = function( ws ) 170 | { 171 | for( var i = 0; i < this.clients.length; i++ ) 172 | { 173 | if( this.clients[i].ws == ws ) 174 | { 175 | this.clients.splice( i , 1 ); 176 | return; 177 | } 178 | } 179 | }; 180 | 181 | this.sendToAll = function( msg ) 182 | { 183 | for( i = 0; i < this.clients.length; i++ ) 184 | { 185 | if( this.clients[i].ws.readyState == this.clients[i].ws.CLOSED ) 186 | { 187 | this.clients.splice( i-- , 1 ); 188 | continue; 189 | } 190 | this.clients[i].ws.send( msg ); 191 | } 192 | }; 193 | 194 | this.getDatasourceConfig = function() 195 | { 196 | return { 197 | name : this.name, 198 | tstampField : this.tstampField, 199 | dataField : this.dataField, 200 | dataComponents : this.dataComponents 201 | }; 202 | }; 203 | } 204 | 205 | RED.nodes.registerType( "iot-datasource", Datasource ); 206 | }; 207 | -------------------------------------------------------------------------------- /datasources.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require( "express" ); 3 | var WebSocketServer = require( "ws" ).Server; 4 | var util = require( "util" ); 5 | var RED = null; 6 | 7 | var wsServer = null; 8 | 9 | var app = express(); 10 | 11 | var datasourceNodes = {}; 12 | 13 | function init( _RED ) 14 | { 15 | RED = _RED; 16 | 17 | app.get( "/" , function( request , response ) { 18 | 19 | var data = {}; 20 | for( var key in datasourceNodes ) { 21 | data[ key ] = datasourceNodes[ key ].getDatasourceConfig(); 22 | } 23 | 24 | response.setHeader( "Content-Type" , "application/json" ); 25 | response.end( JSON.stringify( data ) ); 26 | 27 | } ); 28 | 29 | app.get( "/history" , function( request , response ) { 30 | 31 | var error = false; 32 | 33 | try 34 | { 35 | if( !request.query.hasOwnProperty( "start" ) || 36 | !request.query.hasOwnProperty( "end" ) || 37 | !request.query.hasOwnProperty( "id" ) ) 38 | { 39 | throw 1; 40 | } 41 | 42 | var start = parseInt( request.query.start ); 43 | var end = parseInt( request.query.end ); 44 | if( isNaN( start ) || isNaN( end ) ) throw 1; 45 | 46 | var node = getNode( request.query.id ); 47 | if( !node ) throw 1; 48 | 49 | node.handleHistoryRequest( response , start , end ); 50 | } 51 | catch( e ) 52 | { 53 | error = true; 54 | } 55 | 56 | if( error ) response.status( 400 ).end(); 57 | 58 | } ); 59 | 60 | var wsPath = RED.settings.httpNodeRoot || "/"; 61 | wsPath += ( wsPath[ wsPath.length - 1 ] === "/" ? "" : "/" ) + "dash/dsws"; 62 | 63 | wsServer = new WebSocketServer( { 64 | server : RED.server, 65 | path : wsPath 66 | } ); 67 | 68 | wsServer.on( "connection" , handleWSConnection ); 69 | } 70 | 71 | function handleWSConnection( ws ) 72 | { 73 | ws.on( "message" , function( msg ) { 74 | try 75 | { 76 | msg = JSON.parse( msg ); 77 | } 78 | catch( e ) 79 | { 80 | console.log( e.message ); 81 | return; 82 | } 83 | 84 | if( !msg.hasOwnProperty( "m" ) ) return; 85 | 86 | var node, i; 87 | if( msg.m == "sub" ) 88 | { 89 | if( !util.isArray( msg.id ) ) msg.id = [ msg.id ]; 90 | for( i = 0; i < msg.id.length; i++ ) 91 | { 92 | node = getNode( msg.id[i] ); 93 | if( node ) 94 | { 95 | node.addClient( { ws : ws } ); 96 | } 97 | } 98 | } 99 | else if( msg.m == "unsub" ) 100 | { 101 | if( !util.isArray( msg.id ) ) msg.id = [ msg.id ]; 102 | for( i = 0; i < msg.id.length; i++ ) 103 | { 104 | node = getNode( msg.id[i] ); 105 | if( node ) 106 | { 107 | node.removeClient( ws ); 108 | } 109 | } 110 | } 111 | else if( msg.m == "hist" ) 112 | { 113 | node = getNode( msg.dsid ); 114 | if( node ) 115 | { 116 | node.handleHistoryRequest( ws , msg.cid , msg.start , msg.end ); 117 | } 118 | } 119 | 120 | } ); 121 | 122 | ws.on( "close" , function( code , message ) { 123 | if( code != 1000 && code != 1001 ) 124 | { 125 | console.log( "WS Connection closed (" + code + ( message ? ", " + message : "" ) + ")" ); 126 | } 127 | } ); 128 | 129 | ws.on( "error" , function( err ) { 130 | console.log( "WS Error:", err ); 131 | } ); 132 | } 133 | 134 | function addNode( id, node ) { 135 | datasourceNodes[ id ] = node; 136 | } 137 | 138 | function removeNode( id ) { 139 | delete datasourceNodes[ id ]; 140 | } 141 | 142 | function getNode( id ) { 143 | return datasourceNodes[ id ]; 144 | } 145 | 146 | module.exports = { 147 | app : app, 148 | 149 | init : init, 150 | addNode : addNode, 151 | removeNode : removeNode 152 | }; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-graphs", 3 | "version": "0.3.5", 4 | "description": "A Node-RED graphing package. Contains a datasource node which handles historical data and live data streams, and a hackable visualization application designed to connect to the datasource nodes.", 5 | "main": "datasource.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/IBM-IoT/node-red-contrib-graphs" 12 | }, 13 | "keywords": [ 14 | "node-red", 15 | "graphs", 16 | "gateway", 17 | "kit", 18 | "IBM", 19 | "Informix", 20 | "iot", 21 | "Internet of Things" 22 | ], 23 | "author": "Cristian Traistaru ", 24 | "license": "Apache", 25 | "bugs": { 26 | "url": "https://github.com/IBM-IoT/node-red-contrib-graphs/issues" 27 | }, 28 | "homepage": "https://github.com/IBM-IoT/node-red-contrib-graphs", 29 | "node-red": { 30 | "nodes": { 31 | "datasource": "datasource.js" 32 | } 33 | }, 34 | "dependencies": { 35 | "express": "^4.13.1", 36 | "mustache": "^2.1.3", 37 | "ws": "^0.8.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugins.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require( "express" ); 3 | var fs = require( "fs" ); 4 | 5 | var app = express(); 6 | 7 | var pluginFiles = []; 8 | var dashPluginData = ""; 9 | 10 | function init() 11 | { 12 | app.get( "/" , function( request , response ) { 13 | dashPluginData = loadPlugins( pluginFiles ); 14 | response.setHeader( "Content-Type" , "text/html" ); 15 | response.end( dashPluginData ); 16 | } ); 17 | 18 | pluginFiles = loadPluginFiles(); 19 | } 20 | 21 | function loadPluginFiles() 22 | { 23 | var pluginFiles = []; 24 | 25 | searchDirs = [ __dirname + "/plugins/" ]; 26 | while( searchDirs.length > 0 ) 27 | { 28 | var dir = searchDirs.shift(); 29 | // console.log( "Searching: " + dir ); 30 | 31 | var files = fs.readdirSync( dir ); 32 | for( var i in files ) 33 | { 34 | var file = dir + files[i]; 35 | if( fs.statSync( file ).isDirectory() ) 36 | { 37 | searchDirs.push( file + "/" ); 38 | continue; 39 | } 40 | 41 | if( file.substring( file.length - 5 ) == ".html" ) 42 | { 43 | pluginFiles.push( file ); 44 | // console.log( "Added plugin: " + file ); 45 | } 46 | } 47 | } 48 | 49 | return pluginFiles; 50 | } 51 | 52 | function loadPlugins( fileList ) 53 | { 54 | var data = ""; 55 | 56 | for( var i = 0; i < fileList.length; i++ ) 57 | { 58 | try 59 | { 60 | data += fs.readFileSync( fileList[i] ); 61 | } 62 | catch( e ) 63 | { 64 | console.log( "Unable to read " + fileList[i] + ": " + e.message ); 65 | } 66 | } 67 | 68 | return data; 69 | } 70 | 71 | module.exports = { 72 | app : app, 73 | 74 | init : init 75 | }; 76 | -------------------------------------------------------------------------------- /plugins/plugin_alert.html: -------------------------------------------------------------------------------- 1 | 31 | 32 | 65 | -------------------------------------------------------------------------------- /plugins/plugin_circleGauge.html: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /plugins/plugin_dygraph.html: -------------------------------------------------------------------------------- 1 | 220 | 221 | 252 | 253 | 258 | -------------------------------------------------------------------------------- /plugins/plugin_example.html: -------------------------------------------------------------------------------- 1 | 2 | 91 | 92 | 97 | 103 | 104 | 109 | 114 | -------------------------------------------------------------------------------- /plugins/plugin_justgage.html: -------------------------------------------------------------------------------- 1 | 60 | 61 | 75 | -------------------------------------------------------------------------------- /plugins/plugin_nvd3bar.html: -------------------------------------------------------------------------------- 1 | 35 | 36 | 94 | 95 | 105 | -------------------------------------------------------------------------------- /plugins/plugin_nvd3pie.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | -------------------------------------------------------------------------------- /plugins/plugin_nvd3scatter.html: -------------------------------------------------------------------------------- 1 | 30 | 31 | 102 | 103 | 113 | 114 | 119 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require( "fs" ); 3 | var express = require( "express" ); 4 | var Mustache = require( "mustache" ); 5 | 6 | var plugins = require( "./plugins" ); 7 | var users = require( "./users" ); 8 | var datasources = require( "./datasources" ); 9 | 10 | var app = express(); 11 | var renderedIndex = ""; 12 | 13 | function init( RED ) 14 | { 15 | fs.readFile( __dirname + "/template/index.mst" , function( err , data ) { 16 | if( err ) throw err; 17 | renderedIndex = Mustache.render( data.toString() , { baseUrl : RED.settings.get( "httpNodeRoot" ) } ); 18 | } ); 19 | 20 | RED.log.info( "Dashboard up and running" ); 21 | app.use( "/" , express.static( __dirname + "/static" ) ); 22 | 23 | plugins.init(); 24 | app.use( "/api/plugins" , plugins.app ); 25 | 26 | users.init( RED ); 27 | app.use( "/api/user" , users.app ); 28 | 29 | datasources.init( RED ); 30 | app.use( "/api/datasources" , datasources.app ); 31 | 32 | app.get( "*" , function( request , response ) { 33 | response.send( renderedIndex ); 34 | } ); 35 | 36 | RED.httpNode.use( "/dash/" , app ); 37 | } 38 | 39 | module.exports = { 40 | init : init 41 | }; 42 | -------------------------------------------------------------------------------- /static/css/jquery-ui.structure.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.4 - 2015-08-17 2 | * http://jqueryui.com 3 | * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ 4 | 5 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px} -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | background-color: #202325; 4 | } 5 | 6 | .container { 7 | display : none; 8 | } 9 | 10 | .statusBar { 11 | position: fixed; 12 | bottom: 0px; 13 | width: 100%; 14 | background-color: #34393C; 15 | border-top: solid 1px black; 16 | padding: 2px 10px; 17 | z-index: 1001; 18 | height: 24px; 19 | } 20 | 21 | .statusBarMessage { 22 | width: 100%; 23 | white-space: nowrap; 24 | text-overflow: ellipsis; 25 | } 26 | 27 | .statusBarConnect { 28 | width: 16px; 29 | height: 16px; 30 | background-image: url( '../images/disconnected.png' ); 31 | float: right; 32 | margin-top: -18px; 33 | } 34 | 35 | .statusBarConnect.connected { 36 | background-image: url( '../images/connected.png' ); 37 | } 38 | 39 | #navbar-collapsed ul { 40 | display : none; 41 | } 42 | 43 | .form-control[disabled] { 44 | cursor: default; 45 | } 46 | 47 | .graphContainer { 48 | width: 100%; 49 | } 50 | 51 | #dashboardPage { 52 | width: 100%; 53 | position: absolute; 54 | top: 60px; 55 | left: 0; 56 | right: 0; 57 | bottom: 0; 58 | } 59 | 60 | #gridList { 61 | position: relative; 62 | height: 100%; 63 | list-style: none; 64 | padding: 0; 65 | } 66 | 67 | #gridList li { 68 | display: -webkit-flex; 69 | display: flex; 70 | -webkit-flex-direction: column; 71 | flex-direction: column; 72 | padding: 0; 73 | position: absolute; 74 | -moz-transition: top 0.2s, 75 | left 0.2s; 76 | -webkit-transition: top 0.2s, 77 | left 0.2s; 78 | } 79 | 80 | #gridList li.ui-draggable-dragging, #gridList li.ui-resizable-resizing { 81 | -moz-transition: none; 82 | -webkit-transition: none; 83 | } 84 | 85 | .gridItemHeader { 86 | -webkit-user-select: none; 87 | -moz-user-select: none; 88 | user-select: none; 89 | height: 30px; 90 | padding: 2px; 91 | display: block; 92 | background-color: #353A40; 93 | text-align: center; 94 | cursor: pointer; 95 | } 96 | 97 | .gridItemContent { 98 | -webkit-flex: 1; 99 | flex: 1; 100 | display: -webkit-flex; 101 | display: flex; 102 | -webkit-flex-direction: column; 103 | flex-direction: column; 104 | padding: 5px; 105 | background-color: #292C2F; 106 | overflow: hidden; 107 | } 108 | 109 | .position-highlight { 110 | background-color: #1d1f21; 111 | } 112 | 113 | .gridItemOverlay { 114 | position: absolute; 115 | top: 0; 116 | left: 0; 117 | width: 100%; 118 | height: 100%; 119 | background-color: rgba( 0 , 0 , 0 , 0.5 ); 120 | z-index: 100; 121 | } 122 | 123 | .gridItemOverlayContent { 124 | position: relative; 125 | top: 35%; 126 | width: 50%; 127 | margin: 0 auto 0 auto; 128 | text-align: center; 129 | color: #FFF; 130 | } 131 | 132 | .panel.panel-small { 133 | margin-bottom: 5px; 134 | } 135 | 136 | .panel.panel-small .panel-heading { 137 | font-size: 80%; 138 | padding: 1px 1px 1px 10px; 139 | height: 24px; 140 | line-height: 22px; 141 | } 142 | 143 | .panel-heading.panel-clickable { 144 | cursor: pointer; 145 | } 146 | 147 | .panel-heading.panel-clickable:hover { 148 | background-image: none; 149 | background-color: #3C414A; 150 | } 151 | 152 | .datasourceDataComponents { 153 | margin-bottom: 15px; 154 | } 155 | 156 | .datasourceComponent { 157 | display: -webkit-flex; 158 | display: flex; 159 | -webkit-direction: row; 160 | flex-direction: row; 161 | height: 24px; 162 | padding: 1px; 163 | } 164 | 165 | .datasourceComponentBtn { 166 | width: 24px; 167 | } 168 | 169 | .datasourceComponentInputCont { 170 | -webkit-flex: 1; 171 | flex: 1; 172 | padding-left: 10px; 173 | } 174 | 175 | .datasourceComponentInput { 176 | background-color: #25282D; 177 | width: 100%; 178 | border: none; 179 | padding-left: 5px; 180 | border-radius: 5px; 181 | } 182 | 183 | .datasourceComponentInput::-webkit-input-placeholder 184 | { 185 | color: #6a7482; 186 | } 187 | 188 | .datasourceComponentInput:-moz-placeholder 189 | { 190 | color: #6a7482; 191 | } 192 | 193 | .datasourceComponentInput::-moz-placeholder 194 | { 195 | color: #6a7482; 196 | } 197 | 198 | .datasourceComponentInput:-ms-input-placeholder 199 | { 200 | color: #6a7482; 201 | } 202 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/favicon.ico -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/images/connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/images/connected.png -------------------------------------------------------------------------------- /static/images/disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/images/disconnected.png -------------------------------------------------------------------------------- /static/js/controller/dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.Controller = App.Controller || {}; 4 | 5 | App.Controller.Dashboard = ( function() { 6 | 7 | var selectedPlugin = null; 8 | var currentDashboard = null; 9 | var editingChart = null; 10 | 11 | function init() 12 | { 13 | $( "#createNewChart" ).on( "click" , createNewChartClick ); 14 | $( "#chartPlugins" ).on( "click" , "a" , chartPluginClick ); 15 | $( "#chartDatasources" ).on( "click" , "a" , chartDatasourceClick ); 16 | $( "#chartDone" ).on( "click" , chartDoneClick ); 17 | 18 | $( document ).on( "click" , ".gridItemHeader button" , gridHeaderButtonClick ); 19 | $( document ).on( "click" , ".gridItemOverlay button" , removeOverlayButtonClick ); 20 | $( document ).on( "click" , ".datasourceComponentBtn button" , componentEnableClick ); 21 | 22 | App.Page.onPageChange( onPageChange ); 23 | } 24 | 25 | function onPageChange( page , data ) 26 | { 27 | if( page == "#dashboardPage" ) 28 | { 29 | var dashboard = App.Model.Dashboard.getDashboard( data ); 30 | if( dashboard ) 31 | { 32 | App.Net.createConnection().done( function() { 33 | App.Model.Datasource.getDatasources().done( function() { 34 | loadDashboard( dashboard ); 35 | } ); 36 | } ); 37 | } 38 | else 39 | { 40 | // Quick-fix: Wait to make sure page fade completes before changing pages 41 | // TODO: Add navigation queue 42 | console.log( "Dashboard not found: " + data ); 43 | setTimeout( function() { 44 | App.Page.navigateTo( "" ); 45 | } , 500 ); 46 | } 47 | } 48 | else 49 | { 50 | if( currentDashboard ) 51 | { 52 | App.Net.closeConnection(); 53 | currentDashboard.unload(); 54 | currentDashboard = null; 55 | } 56 | 57 | App.View.Dashboard.Modal.close(); 58 | } 59 | } 60 | 61 | function createNewChartClick( event ) 62 | { 63 | event.preventDefault(); 64 | selectedPlugin = null; 65 | editingChart = null; 66 | App.View.Dashboard.Modal.open(); 67 | } 68 | 69 | function chartPluginClick( event ) 70 | { 71 | event.preventDefault(); 72 | 73 | var id = $( this ).attr( "data-pluginid" ); 74 | if( selectedPlugin !== null && id == selectedPlugin.id ) return; 75 | selectedPlugin = App.Plugins.getPlugin( id ); 76 | 77 | App.View.Dashboard.Modal.loadPluginConfig( selectedPlugin , editingChart ); 78 | } 79 | 80 | function chartDatasourceClick( event ) 81 | { 82 | event.stopPropagation(); 83 | event.preventDefault(); 84 | 85 | var id = $( this ).attr( "data-dsid" ); 86 | $( this ).remove(); 87 | 88 | App.View.Dashboard.Modal.addDatasource( App.Model.Datasource.getDatasource( id ) , selectedPlugin ); 89 | } 90 | 91 | function chartDatasourceHeaderClick( event ) 92 | { 93 | $( this ).siblings( ".panel-body" ).toggle(); 94 | } 95 | 96 | function chartDatasourceRemoveClick( event ) 97 | { 98 | event.stopPropagation(); 99 | App.View.Dashboard.Modal.removeDatasource( $( this ).parents( ".panel" ) ); 100 | } 101 | 102 | function chartDoneClick() 103 | { 104 | var chart, 105 | i, key, 106 | $input; 107 | 108 | // Validate 109 | var errors = []; 110 | 111 | var chartName = $( "#chartName" ).val().trim(); 112 | var $datasources = $( "#chartDatasourceList > div" ); 113 | 114 | if( chartName.length < 1 ) errors.push( "Please enter a name." ); 115 | if( selectedPlugin === null ) errors.push( "Please select a plugin." ); 116 | if( $datasources.length < 1 ) errors.push( "Please add at least one datasource." ); 117 | 118 | if( errors.length > 0 ) 119 | { 120 | App.View.Dashboard.Modal.showErrors( errors ); 121 | return; 122 | } 123 | 124 | if( !editingChart ) 125 | { 126 | chart = new App.Model.Chart(); 127 | chart.id = genRandomID(); 128 | } 129 | else 130 | { 131 | chart = editingChart; 132 | chart.resetDatasources(); 133 | } 134 | 135 | chart.name = chartName; 136 | chart.plugin = selectedPlugin; 137 | chart.config = {}; 138 | 139 | for( i = 0; i < $datasources.length; i++ ) 140 | { 141 | var $datasource = $( $datasources[i] ); 142 | 143 | var datasource = { 144 | datasource : App.Model.Datasource.getDatasource( $datasource.attr( "data-dsid" ) ), 145 | config : {} 146 | }; 147 | 148 | var uid = $datasource.attr( "data-uid" ); 149 | 150 | var $dsConfig = $datasource.find( ".datasourcePluginConfig" ); 151 | for( key in selectedPlugin.datasourceConfig ) 152 | { 153 | $input = $dsConfig.find( '[data-prop="' + key + '"]' ); 154 | if( $input.length > 0 ) 155 | { 156 | datasource.config[ key ] = App.View.Dashboard.getInputValue( $input ); 157 | } 158 | else 159 | { 160 | datasource.config[ key ] = selectedPlugin.datasourceConfig[ key ].default; 161 | } 162 | } 163 | 164 | datasource.config.label = $( "#nds" + uid + "-label" ).val().trim(); 165 | if( !datasource.config.label ) 166 | { 167 | datasource.config.label = $datasource.attr( "data-dsname" ); 168 | } 169 | 170 | var componentConfig = {}; 171 | var $components = $datasource.find( ".datasourceComponent" ); 172 | for( var k = 0; k < $components.length; k++ ) 173 | { 174 | var $component = $( $components[k] ); 175 | var componentName = $component.attr( "data-component" ); 176 | componentConfig[ componentName ] = { 177 | enabled : $component.find( "button" ).hasClass( "btn-success" ), 178 | label : $component.find( "input" ).val().trim() 179 | }; 180 | } 181 | 182 | datasource.config.components = componentConfig; 183 | 184 | chart.addDatasource( datasource.datasource , datasource.config ); 185 | } 186 | 187 | var $chartConfig = $( "#chartPluginConfig" ); 188 | for( key in selectedPlugin.chartConfig ) 189 | { 190 | $input = $chartConfig.find( '[data-prop="' + key + '"]' ); 191 | if( $input.length > 0 ) 192 | { 193 | chart.config[ key ] = App.View.Dashboard.getInputValue( $input ); 194 | } 195 | else 196 | { 197 | chart.config[ key ] = selectedPlugin.chartConfig[ key ].default; 198 | } 199 | } 200 | 201 | var $container; 202 | if( !editingChart ) 203 | { 204 | $container = App.View.Dashboard.createChartContainer( chart ); 205 | } 206 | else 207 | { 208 | // For now, re-create the chart to change settings... 209 | // TODO: Allow chart plugins to handle settings changes 210 | 211 | currentDashboard.removeChart( chart.id ); 212 | $container = App.View.Dashboard.updateChartContainer( chart ); 213 | } 214 | 215 | editingChart = null; 216 | currentDashboard.addChart( chart ); 217 | chart.load( $container ); 218 | App.Settings.saveSettings(); 219 | 220 | App.View.Dashboard.Modal.close(); 221 | } 222 | 223 | function gridHeaderButtonClick() 224 | { 225 | var action = $( this ).attr( "data-act" ); 226 | var $parent = $( this ).parents( "li" ); 227 | 228 | if( action == "remove" ) 229 | { 230 | App.View.Dashboard.showRemoveOverlay( $parent ); 231 | 232 | $overlay.on( "click" , "button" , function() { 233 | var $parent = null; 234 | 235 | } ); 236 | 237 | $content.append( $overlay ); 238 | $overlay.fadeIn( 200 ); 239 | } 240 | else if( action == "edit" ) 241 | { 242 | var chart = currentDashboard.getChart( $parent.attr( "data-id" ) ); 243 | editingChart = chart; 244 | selectedPlugin = chart.plugin; 245 | App.View.Dashboard.Modal.open( chart ); 246 | } 247 | } 248 | 249 | function removeOverlayButtonClick() 250 | { 251 | var $parent = $( this ).parents( "li" ); 252 | if( $( this ).hasClass( "btn-danger" ) ) 253 | { 254 | App.View.Dashboard.hideRemoveOverlay( $parent ); 255 | } 256 | else 257 | { 258 | var id = $parent.attr( "data-id" ); 259 | currentDashboard.removeChart( id ); 260 | 261 | $( "#gridList" ).gridList( "remove" , $parent ); 262 | App.Settings.saveSettings(); 263 | } 264 | } 265 | 266 | function componentEnableClick() 267 | { 268 | $( this ).toggleClass( "btn-success btn-default" ); 269 | } 270 | 271 | function gridListOnChange( items ) 272 | { 273 | if( items.length > 0 ) 274 | { 275 | $( "#gridList" ).gridList( "_updateElementData" ); 276 | $( "#gridList > li[data-id]" ).each( function() { 277 | var chart = currentDashboard.getChart( $( this ).attr( "data-id" ) ); 278 | chart.pos = { 279 | x : Number( $( this ).attr( "data-x" ) ), 280 | y : Number( $( this ).attr( "data-y" ) ), 281 | w : Number( $( this ).attr( "data-w" ) ), 282 | h : Number( $( this ).attr( "data-h" ) ) 283 | }; 284 | } ); 285 | 286 | App.Settings.saveSettings(); 287 | } 288 | } 289 | 290 | function onNetworkMessage( data ) 291 | { 292 | if( !currentDashboard ) return; 293 | 294 | currentDashboard.pushData( data ); 295 | } 296 | 297 | function onNetworkDisconnect() 298 | { 299 | if( !currentDashboard ) return; 300 | 301 | App.Net.createConnection().done( function() { 302 | currentDashboard.subscribeToAllDatasources(); 303 | App.Model.Datasource.getDatasources(); 304 | } ); 305 | } 306 | 307 | function loadDashboard( dashboard ) 308 | { 309 | currentDashboard = dashboard; 310 | selectedPlugin = null; 311 | editingChart = null; 312 | App.View.Dashboard.setPageTitle( dashboard.name ); 313 | App.View.Dashboard.createNewGridList(); 314 | 315 | for( var i in dashboard.charts ) 316 | { 317 | var chart = dashboard.charts[i]; 318 | var $container = App.View.Dashboard.createChartContainer( chart ); 319 | chart.load( $container ); 320 | } 321 | 322 | App.View.Dashboard.initGridList(); 323 | dashboard.load(); 324 | } 325 | 326 | function genRandomID( len ) 327 | { 328 | len = len || 16; 329 | var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 330 | var id = ""; 331 | for( var i = 0; i < len; i++ ) id += chars[ Math.floor( Math.random() * 62 ) ]; 332 | return id; 333 | } 334 | 335 | return { 336 | init : init, 337 | chartDatasourceHeaderClick : chartDatasourceHeaderClick, 338 | chartDatasourceRemoveClick : chartDatasourceRemoveClick, 339 | gridListOnChange : gridListOnChange, 340 | onNetworkMessage : onNetworkMessage, 341 | onNetworkDisconnect : onNetworkDisconnect, 342 | loadDashboard : loadDashboard 343 | }; 344 | 345 | } )(); 346 | -------------------------------------------------------------------------------- /static/js/controller/dashboardlist.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.Controller = App.Controller || {}; 4 | 5 | App.Controller.DashboardList = ( function() { 6 | 7 | function init() 8 | { 9 | $( "#createNewDashboard" ).on( "click" , createNewDashboardClick ); 10 | $( "#dashboardDone" ).on( "click" , dashboardDoneClick ); 11 | $( "#dashboardListPage" ).on( "click" , "button" , dashboardButtonClick ); 12 | 13 | App.Page.onPageChange( onPageChange ); 14 | } 15 | 16 | function onPageChange( page , data ) 17 | { 18 | if( page == "#dashboardListPage" ) 19 | { 20 | $( "#titleDashboard" ).text( "" ); 21 | App.View.DashboardList.render( App.Model.Dashboard.dashboards ); 22 | } 23 | else 24 | { 25 | App.View.DashboardList.Modal.close(); 26 | } 27 | } 28 | 29 | function createNewDashboardClick( event ) 30 | { 31 | event.preventDefault(); 32 | App.View.DashboardList.Modal.open(); 33 | } 34 | 35 | function dashboardDoneClick( event ) 36 | { 37 | var errors = []; 38 | var dashboardName = $( "#dashboardName" ).val().trim(); 39 | if( dashboardName.length < 1 ) errors.push( "Please enter a name." ); 40 | 41 | if( errors.length > 0 ) 42 | { 43 | App.View.DashboardList.Modal.showErrors( errors ); 44 | return; 45 | } 46 | 47 | var dashboard = new App.Model.Dashboard( dashboardName ); 48 | App.Model.Dashboard.addDashboard( dashboard ); 49 | 50 | App.Settings.saveSettings(); 51 | App.View.DashboardList.Modal.close(); 52 | 53 | // HACK: Dashboards don't really have an ID yet, so we can use the dashboards array's length 54 | // to get an "ID" 55 | openDashboard( App.Model.Dashboard.dashboards.length - 1 ); 56 | } 57 | 58 | function dashboardButtonClick() 59 | { 60 | var id = $( this ).attr( "data-open" ); 61 | if( id !== undefined ) 62 | { 63 | openDashboard( id ); 64 | } 65 | else 66 | { 67 | id = $( this ).attr( "data-remove" ); 68 | var name = $( this ).siblings( "button" ).text(); 69 | App.Modal.show( "Remove " + name + "?" , "" , function() { 70 | App.Model.Dashboard.removeDashboard( id ); 71 | App.Settings.saveSettings(); 72 | App.View.DashboardList.render( App.Model.Dashboard.dashboards ); 73 | } ); 74 | } 75 | } 76 | 77 | function openDashboard( id ) 78 | { 79 | App.Page.navigateTo( "board/" + id ); 80 | } 81 | 82 | return { 83 | init : init 84 | }; 85 | 86 | } )(); 87 | -------------------------------------------------------------------------------- /static/js/gridlist/gridList.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define([], factory); 5 | } else if (typeof exports === 'object') { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like environments that support module.exports, 8 | // like Node. 9 | module.exports = factory(); 10 | } else { 11 | // Browser globals (root is window) 12 | root.GridList = factory(); 13 | } 14 | }(this, function() { 15 | 16 | var GridList = function(items, options) { 17 | /** 18 | * A GridList manages the two-dimensional positions from a list of items, 19 | * within a virtual matrix. 20 | * 21 | * The GridList's main function is to convert the item positions from one 22 | * grid size to another, maintaining as much of their order as possible. 23 | * 24 | * The GridList's second function is to handle collisions when moving an item 25 | * over another. 26 | * 27 | * The positioning algorithm places items in columns. Starting from left to 28 | * right, going through each column top to bottom. 29 | * 30 | * The size of an item is expressed using the number of cols and rows it 31 | * takes up within the grid (w and h) 32 | * 33 | * The position of an item is express using the col and row position within 34 | * the grid (x and y) 35 | * 36 | * An item is an object of structure: 37 | * { 38 | * w: 3, h: 1, 39 | * x: 0, y: 1 40 | * } 41 | */ 42 | this.options = options; 43 | for (var k in this.defaults) { 44 | if (!this.options.hasOwnProperty(k)) { 45 | this.options[k] = this.defaults[k]; 46 | } 47 | } 48 | this.items = items; 49 | this._adjustHeightOfItems(); 50 | this.generateGrid(); 51 | }; 52 | 53 | GridList.cloneItems = function(items, _items) { 54 | /** 55 | * Clone items with a deep level of one. Items are not referenced but their 56 | * properties are 57 | */ 58 | var _item, 59 | i, 60 | k; 61 | if (_items === undefined) { 62 | _items = []; 63 | } 64 | for (i = 0; i < items.length; i++) { 65 | // XXX: this is good because we don't want to lose item reference, but 66 | // maybe we should clear their properties since some might be optional 67 | if (!_items[i]) { 68 | _items[i] = {}; 69 | } 70 | for (k in items[i]) { 71 | _items[i][k] = items[i][k]; 72 | } 73 | } 74 | return _items; 75 | }; 76 | 77 | GridList.prototype = { 78 | 79 | defaults: { 80 | rows: 5 81 | }, 82 | 83 | /** 84 | * Illustates grid as text-based table, using a number identifier for each 85 | * item. E.g. 86 | * 87 | * #| 0 1 2 3 4 5 6 7 8 9 10 11 12 13 88 | * -------------------------------------------- 89 | * 0| 00 02 03 04 04 06 08 08 08 12 12 13 14 16 90 | * 1| 01 -- 03 05 05 07 09 10 11 11 -- 13 15 -- 91 | * 92 | * Warn: Does not work if items don't have a width or height specified 93 | * besides their position in the grid. 94 | */ 95 | toString: function() { 96 | var widthOfGrid = this.grid.length, 97 | output = '\n #|', 98 | border = '\n --', 99 | item, 100 | i, 101 | j; 102 | 103 | // Render the table header 104 | for (i = 0; i < widthOfGrid; i++) { 105 | output += ' ' + this._padNumber(i, ' '); 106 | border += '---'; 107 | }; 108 | output += border; 109 | 110 | // Render table contents row by row, as we go on the y axis 111 | for (i = 0; i < this.options.rows; i++) { 112 | output += '\n' + this._padNumber(i, ' ') + '|'; 113 | for (j = 0; j < widthOfGrid; j++) { 114 | output += ' '; 115 | item = this.grid[j][i]; 116 | output += item ? this._padNumber(this.items.indexOf(item), '0') : '--'; 117 | } 118 | }; 119 | output += '\n'; 120 | return output; 121 | }, 122 | 123 | generateGrid: function() { 124 | /** 125 | * Build the grid structure from scratch, with the current item positions 126 | */ 127 | var i; 128 | this._resetGrid(); 129 | for (i = 0; i < this.items.length; i++) { 130 | this._markItemPositionToGrid(this.items[i]); 131 | } 132 | }, 133 | 134 | resizeGrid: function(rows) { 135 | var currentColumn = 0, 136 | item, 137 | i; 138 | 139 | this.options.rows = rows; 140 | this._adjustHeightOfItems(); 141 | 142 | this._sortItemsByPosition(); 143 | this._resetGrid(); 144 | // The items will be sorted based on their index within the this.items array, 145 | // that is their "1d position" 146 | for (i = 0; i < this.items.length; i++) { 147 | item = this.items[i]; 148 | this._updateItemPosition( 149 | item, this.findPositionForItem(item, {x: currentColumn, y: 0})); 150 | // New items should never be placed to the left of previous items 151 | currentColumn = Math.max(currentColumn, item.x); 152 | } 153 | }, 154 | 155 | findPositionForItem: function(item, start, fixedRow) { 156 | /** 157 | * This method has two options for the position we want for the item: 158 | * - Starting from a certain row/column number and only looking for 159 | * positions to its right 160 | * - Accepting positions for a certain row number only (use-case: items 161 | * being shifted to the left/right as a result of collisions) 162 | * 163 | * @param {Object this.options.rows) { 302 | return false; 303 | } 304 | // Make sure the item doesn't overlap with an already positioned item 305 | for (x = position[0]; x < position[0] + item.w; x++) { 306 | col = this.grid[x]; 307 | // Surely a column that hasn't even been created yet is available 308 | if (!col) { 309 | continue; 310 | } 311 | for (y = position[1]; y < position[1] + item.h; y++) { 312 | // Any space occupied by an item can continue to be occupied by the same 313 | // item 314 | if (col[y] && col[y] != item) { 315 | return false; 316 | } 317 | } 318 | } 319 | return true; 320 | }, 321 | 322 | _updateItemPosition: function(item, position) { 323 | if (item.x !== null && item.y !== null) { 324 | this._deleteItemPositionFromGrid(item); 325 | } 326 | item.x = position[0]; 327 | item.y = position[1]; 328 | this._markItemPositionToGrid(item); 329 | }, 330 | 331 | _updateItemSize: function(item, width, height) { 332 | /** 333 | * @param {Object} item A reference to a grid item. 334 | * @param {Number} width The new width. 335 | * @param {Number} height The new height. 336 | */ 337 | 338 | if (item.x !== null && item.y !== null) { 339 | this._deleteItemPositionFromGrid(item); 340 | } 341 | 342 | item.w = width; 343 | item.h = height; 344 | 345 | this._markItemPositionToGrid(item); 346 | }, 347 | 348 | _markItemPositionToGrid: function(item) { 349 | /** 350 | * Mark the grid cells that are occupied by an item. This prevents items 351 | * from overlapping in the grid 352 | */ 353 | var x, y; 354 | // Ensure that the grid has enough columns to accomodate the current item. 355 | this._ensureColumns(item.x + item.w); 356 | 357 | for (x = item.x; x < item.x + item.w; x++) { 358 | for (y = item.y; y < item.y + item.h; y++) { 359 | this.grid[x][y] = item; 360 | } 361 | } 362 | }, 363 | 364 | _deleteItemPositionFromGrid: function(item) { 365 | var x, y; 366 | for (x = item.x; x < item.x + item.w; x++) { 367 | // It can happen to try to remove an item from a position not generated 368 | // in the grid, probably when loading a persisted grid of items. No need 369 | // to create a column to be able to remove something from it, though 370 | if (!this.grid[x]) { 371 | continue; 372 | } 373 | for (y = item.y; y < item.y + item.h; y++) { 374 | // Don't clear the cell if it's been occupied by a different widget in 375 | // the meantime (e.g. when an item has been moved over this one, and 376 | // thus by continuing to clear this item's previous position you would 377 | // cancel the first item's move, leaving it without any position even) 378 | if (this.grid[x][y] == item) { 379 | this.grid[x][y] = null; 380 | } 381 | } 382 | } 383 | }, 384 | 385 | _ensureColumns: function(N) { 386 | /** 387 | * Ensure that the grid has at least N columns available. 388 | */ 389 | var i; 390 | for (i = 0; i < N; i++) { 391 | if (!this.grid[i]) { 392 | this.grid.push(new GridCol(this.options.rows)); 393 | } 394 | } 395 | }, 396 | 397 | _getItemsCollidingWithItem: function(item) { 398 | var collidingItems = []; 399 | for (var i = 0; i < this.items.length; i++) { 400 | if (item != this.items[i] && 401 | this._itemsAreColliding(item, this.items[i])) { 402 | collidingItems.push(i); 403 | } 404 | } 405 | return collidingItems; 406 | }, 407 | 408 | _itemsAreColliding: function(item1, item2) { 409 | return !(item2.x >= item1.x + item1.w || 410 | item2.x + item2.w <= item1.x || 411 | item2.y >= item1.y + item1.h || 412 | item2.y + item2.h <= item1.y); 413 | }, 414 | 415 | _resolveCollisions: function(item) { 416 | if (!this._tryToResolveCollisionsLocally(item)) { 417 | this._pullItemsToLeft(item); 418 | } 419 | this._pullItemsToLeft(); 420 | }, 421 | 422 | _tryToResolveCollisionsLocally: function(item) { 423 | /** 424 | * Attempt to resolve the collisions after moving a an item over one or more 425 | * other items within the grid, by shifting the position of the colliding 426 | * items around the moving one. This might result in subsequent collisions, 427 | * in which case we will revert all position permutations. To be able to 428 | * revert to the initial item positions, we create a virtual grid in the 429 | * process 430 | */ 431 | var collidingItems = this._getItemsCollidingWithItem(item); 432 | if (!collidingItems.length) { 433 | return true; 434 | } 435 | var _gridList = new GridList([], this.options), 436 | collidingItem, 437 | i, 438 | leftOfItem, 439 | rightOfItem, 440 | aboveOfItem, 441 | belowOfItem; 442 | 443 | GridList.cloneItems(this.items, _gridList.items); 444 | _gridList.generateGrid(); 445 | 446 | for (i = 0; i < collidingItems.length; i++) { 447 | collidingItem = _gridList.items[collidingItems[i]]; 448 | 449 | // We use a simple algorithm for moving items around when collisions occur: 450 | // In this prioritized order, we try to move a colliding item around the 451 | // moving one: 452 | // 1. to its left side 453 | // 2. above it 454 | // 3. under it 455 | // 4. to its right side 456 | leftOfItem = [item.x - collidingItem.w, collidingItem.y]; 457 | rightOfItem = [item.x + item.w, collidingItem.y]; 458 | aboveOfItem = [collidingItem.x, item.y - collidingItem.h]; 459 | belowOfItem = [collidingItem.x, item.y + item.h]; 460 | 461 | if (_gridList._itemFitsAtPosition(collidingItem, leftOfItem)) { 462 | _gridList._updateItemPosition(collidingItem, leftOfItem); 463 | } else if (_gridList._itemFitsAtPosition(collidingItem, aboveOfItem)) { 464 | _gridList._updateItemPosition(collidingItem, aboveOfItem); 465 | } else if (_gridList._itemFitsAtPosition(collidingItem, belowOfItem)) { 466 | _gridList._updateItemPosition(collidingItem, belowOfItem); 467 | } else if (_gridList._itemFitsAtPosition(collidingItem, rightOfItem)) { 468 | _gridList._updateItemPosition(collidingItem, rightOfItem); 469 | } else { 470 | // Collisions failed, we must use the pullItemsToLeft method to arrange 471 | // the other items around this item with fixed position. This is our 472 | // plan B for when local collision resolving fails. 473 | return false; 474 | } 475 | } 476 | // If we reached this point it means we managed to resolve the collisions 477 | // from one single iteration, just by moving the colliding items around. So 478 | // we accept this scenario and marge the brached-out grid instance into the 479 | // original one 480 | GridList.cloneItems(_gridList.items, this.items); 481 | this.generateGrid(); 482 | return true; 483 | }, 484 | 485 | _pullItemsToLeft: function(fixedItem) { 486 | /** 487 | * Build the grid from scratch, by using the current item positions and 488 | * pulling them as much to the left as possible, removing as space between 489 | * them as possible. 490 | * 491 | * If a "fixed item" is provided, its position will be kept intact and the 492 | * rest of the items will be layed around it. 493 | */ 494 | var item, 495 | i; 496 | 497 | // Start a fresh grid with the fixed item already placed inside 498 | this._sortItemsByPosition(); 499 | this._resetGrid(); 500 | 501 | // Start the grid with the fixed item as the first positioned item 502 | if (fixedItem) { 503 | this._updateItemPosition(fixedItem, [fixedItem.x, fixedItem.y]); 504 | } 505 | for (i = 0; i < this.items.length; i++) { 506 | item = this.items[i]; 507 | // The fixed item keeps its exact position 508 | if (fixedItem && item == fixedItem) { 509 | continue; 510 | } 511 | this._updateItemPosition(item, this.findPositionForItem( 512 | item, 513 | {x: this._findLeftMostPositionForItem(item), y: 0}, 514 | item.y)); 515 | } 516 | }, 517 | 518 | _findLeftMostPositionForItem: function(item) { 519 | /** 520 | * When pulling items to the left, we need to find the leftmost position for 521 | * an item, with two considerations in mind: 522 | * - preserving its current row 523 | * - preserving the previous horizontal order between items 524 | */ 525 | var tail = 0, 526 | otherItem, 527 | i; 528 | for (i = 0; i < this.grid.length; i++) { 529 | otherItem = this.grid[i][item.y]; 530 | if (!otherItem) { 531 | continue; 532 | } 533 | if (this.items.indexOf(otherItem) < this.items.indexOf(item)) { 534 | tail = otherItem.x + otherItem.w; 535 | } 536 | } 537 | return tail; 538 | }, 539 | 540 | _getItemByAttribute: function(key, value) { 541 | for (var i = 0; i < this.items.length; i++) { 542 | if (this.items[i][key] === value) { 543 | return this.items[i]; 544 | } 545 | } 546 | return null; 547 | }, 548 | 549 | _padNumber: function(nr, prefix) { 550 | // Currently works for 2-digit numbers (<100) 551 | return nr >= 10 ? nr : prefix + nr; 552 | } 553 | }; 554 | 555 | var GridCol = function(rows) { 556 | for (var i = 0; i < rows; i++) { 557 | this.push(null); 558 | } 559 | }; 560 | 561 | // Extend the Array prototype 562 | GridCol.prototype = []; 563 | 564 | // This module will have direct access to the GridList class 565 | return GridList; 566 | 567 | })); 568 | -------------------------------------------------------------------------------- /static/js/lib/jquery/jquery.ui.touch-punch.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Touch Punch 0.2.3 3 | * 4 | * Copyright 2011–2014, Dave Furfero 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * Depends: 8 | * jquery.ui.widget.js 9 | * jquery.ui.mouse.js 10 | */ 11 | !function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); -------------------------------------------------------------------------------- /static/js/lib/jsrender.min.js: -------------------------------------------------------------------------------- 1 | /*! JsRender v1.0.0-beta: http://www.jsviews.com/#jsrender 2 | informal pre V1.0 commit counter: 64*/ 3 | !function(e){if("function"==typeof define&&define.amd)define(e);else if("object"==typeof exports){var t=module.exports=e(!0,require("fs"));t.renderFile=t.__express=function(e,n,r){var i=t.templates("@"+e).render(n);return r&&r(null,i),i}}else e(!1)}(function(e,t){"use strict";function n(e,t){return function(){var n,r=this,i=r.base;return r.base=e,n=t.apply(r,arguments),r.base=i,n}}function r(e,t){return jt(t)&&(t=n(e?e._d?e:n(s,e):s,t),t._d=1),t}function i(e,t){for(var n in t.props)ct.test(n)&&(e[n]=r(e[n],t.props[n]))}function a(e){return e}function s(){return""}function o(e){try{throw"dbg breakpoint"}catch(t){}return this.base?this.baseApply(arguments):e}function l(e){Nt._dbgMode=e!==!1}function p(e){this.name=(G.link?"JsViews":"JsRender")+" Error",this.message=e||this.name}function d(e,t){var n;for(n in t)e[n]=t[n];return e}function u(e,t,n){return(0!==this||e)&&(W=e?e.charAt(0):W,X=e?e.charAt(1):X,Y=t?t.charAt(0):Y,et=t?t.charAt(1):et,tt=n||tt,e="\\"+W+"(\\"+tt+")?\\"+X,t="\\"+Y+"\\"+et,q="(?:(?:(\\w+(?=[\\/\\s\\"+Y+"]))|(?:(\\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\\*)))\\s*((?:[^\\"+Y+"]|\\"+Y+"(?!\\"+et+"))*?)",$t.rTag=q+")",q=new RegExp(e+q+"(\\/)?|(?:\\/(\\w+)))"+t,"g"),H=new RegExp("<.*>|([^\\\\]|^)[{}]|"+e+".*"+t)),[W,X,Y,et,tt]}function c(e,t){t||(t=e,e=void 0);var n,r,i,a,s=this,o=!t||"root"===t;if(e){if(a=s.type===t?s:void 0,!a)if(n=s.views,s._.useKey){for(r in n)if(a=n[r].get(e,t))break}else for(r=0,i=n.length;!a&&i>r;r++)a=n[r].get(e,t)}else if(o)for(;s.parent.parent;)a=s=s.parent;else for(;s&&!a;)a=s.type===t?s:void 0,s=s.parent;return a}function f(){var e=this.get("item");return e?e.index:void 0}function g(){return this.index}function v(e){var t,n=this,r=n.linkCtx,i=(n.ctx||{})[e];return void 0===i&&r&&r.ctx&&(i=r.ctx[e]),void 0===i&&(i=Mt[e]),i&&jt(i)&&!i._wrp&&(t=function(){return i.apply(this&&this!==z?this:n,arguments)},t._wrp=!0,d(t,i)),t||i}function m(e,t,n,r){var a,s,o="number"==typeof n&&t.tmpl.bnds[n-1],l=t.linkCtx;return void 0!==r?n=r={props:{},args:[r]}:o&&(n=o(t.data,t,kt)),s=n.args[0],(e||o)&&(a=l&&l.tag,a||(a=d(new $t._tg,{_:{inline:!l,bnd:o,unlinked:!0},tagName:":",cvt:e,flow:!0,tagCtx:n}),l&&(l.tag=a,a.linkCtx=l),n.ctx=J(n.ctx,(l?l.view:t).ctx)),a._er=r&&s,i(a,n),n.view=t,a.ctx=n.ctx||{},n.ctx=void 0,t._.tag=a,s=a.cvtArgs(a.convert||"true"!==e&&e)[0],s=o&&t._.onRender?t._.onRender(s,t,o):s,t._.tag=void 0),void 0!=s?s:""}function h(e){var t=this,n=t.tagCtx,r=n.view,i=n.args;return e=t.convert||e,e=e&&(""+e===e?r.getRsc("converters",e)||V("Unknown converter: '"+e+"'"):e),i=i.length||n.index?e?i.slice():i:[r.data],e&&(e.depends&&(t.depends=$t.getDeps(t.depends,t,e.depends,e)),i[0]=e.apply(t,i)),i}function w(e,t){for(var n,r,i=this;void 0===n&&i;)r=i.tmpl&&i.tmpl[e],n=r&&r[t],i=i.parent;return n||kt[e][t]}function x(e,t,n,r,a,s){t=t||D;var o,l,p,d,u,c,f,g,v,m,h,w,x,b,_,y,k,C,j="",A=t.linkCtx||0,T=t.ctx,M=n||t.tmpl,$="number"==typeof r&&t.tmpl.bnds[r-1];for("tag"===e._is?(o=e,e=o.tagName,r=o.tagCtxs,p=o.template):(l=t.getRsc("tags",e)||V("Unknown tag: {{"+e+"}} "),p=l.template),void 0!==s?(j+=s,r=s=[{props:{},args:[]}]):$&&(r=$(t.data,t,kt)),g=r.length,f=0;g>f;f++)m=r[f],(!A||!A.tag||f&&!A.tag._.inline||o._er)&&((w=m.tmpl)&&(w=m.content=M.tmpls[w-1]),m.index=f,m.tmpl=p||w,m.render=R,m.view=t,m.ctx=J(m.ctx,T)),(n=m.props.tmpl)&&(n=""+n===n?t.getRsc("templates",n)||Tt(n):n,m.tmpl=n),o||(o=new l._ctr,x=!!o.init,o.parent=c=T&&T.tag,o.tagCtxs=r,A&&(o._.inline=!1,A.tag=o,o.linkCtx=A),(o._.bnd=$||A.fn)?o._.arrVws={}:o.dataBoundOnly&&V("{^{"+e+"}} tag must be data-bound")),m.tag=o,o.dataMap&&o.tagCtxs&&(m.map=o.tagCtxs[f].map),o.flow||(h=m.ctx=m.ctx||{},d=o.parents=h.parentTags=T&&J(h.parentTags,T.parentTags)||{},c&&(d[c.tagName]=c),d[o.tagName]=h.tag=o);if(($||A)&&(t._.tag=o),!(o._er=s)){for(i(o,r[0]),o.rendering={},f=0;g>f;f++)m=o.tagCtx=o.tagCtxs[f],k=m.props,y=o.cvtArgs(),(b=k.dataMap||o.dataMap)&&(y.length||k.dataMap)&&(_=m.map,(!_||_.src!==y[0]||a)&&(_&&_.src&&_.unmap(),_=m.map=b.map(y[0],k)),y=[_.tgt]),o.ctx=m.ctx,f||(x&&(C=o.template,o.init(m,A,o.ctx),x=void 0,o.template!==C&&(o._.tmpl=o.template)),A&&(A.attr=o.attr=A.attr||o.attr),u=o.attr,o._.noVws=u&&u!==ht),v=void 0,o.render&&(v=o.render.apply(o,y)),y.length||(y=[t]),void 0===v&&(v=m.render(y.length?y[0]:t,!0)||(a?void 0:"")),j=j?j+(v||""):v;o.rendering=void 0}return o.tagCtx=o.tagCtxs[0],o.ctx=o.tagCtx.ctx,o._.noVws&&o._.inline&&(j="text"===u?Rt.html(j):""),$&&t._.onRender?t._.onRender(j,t,$):j}function b(e,t,n,r,i,a,s,o){var l,p,d,u,c=this,g="array"===t;c.content=s,c.views=g?[]:{},c.parent=n,c.type=t||"top",c.data=r,c.tmpl=i,u=c._={key:0,useKey:g?0:1,id:""+vt++,onRender:o,bnds:{}},c.linked=!!o,n?(l=n.views,p=n._,p.useKey?(l[u.key="_"+p.useKey++]=c,c.index=bt,c.getIndex=f,d=p.tag,u.bnd=g&&(!d||!!d._.bnd&&d)):l.length===(u.key=c.index=a)?l.push(c):l.splice(a,0,c),c.ctx=e||n.ctx):c.ctx=e}function _(e){var t,n,r,i,a,s,o;for(t in yt)if(a=yt[t],(s=a.compile)&&(n=e[t+"s"]))for(r in n)i=n[r]=s(r,n[r],e,0),i._is=t,i&&(o=$t.onStore[t])&&o(r,i,s)}function y(e,t,n){function i(){var t=this;t._={inline:!0,unlinked:!0},t.tagName=e}var a,s,o,l=new $t._tg;if(jt(t)?t={depends:t.depends,render:t}:""+t===t&&(t={template:t}),s=t.baseTag){t.flow=!!t.flow,t.baseTag=s=""+s===s?n&&n.tags[s]||Vt[s]:s,l=d(l,s);for(o in t)l[o]=r(s[o],t[o])}else l=d(l,t);return void 0!==(a=l.template)&&(l.template=""+a===a?Tt[a]||Tt(a):a),l.init!==!1&&((i.prototype=l).constructor=l._ctr=i),n&&(l._parentTmpl=n),l}function k(e){return this.base.apply(this,e)}function C(e,n,r,i){function a(n){var a;if(""+n===n||n.nodeType>0&&(s=n)){if(!s)if("@"===n.charAt(0))t?n=Tt[e=e||(n=t.realpathSync(n.slice(1)))]=Tt[e]||C(e,t.readFileSync(n,"utf8"),r,i):s=P.getElementById(n);else if(G.fn&&!H.test(n))try{s=G(P).find(n)[0]}catch(o){}s&&(i?n=s.innerHTML:((a=s.getAttribute(xt))&&(n=Tt[a])&&e!==a&&delete Tt[a],e=e||a||"_"+gt++,a||(n=C(e,s.innerHTML,r,i)),s.setAttribute(xt,e),Tt[n.tmplName=e]=n),s=void 0)}else n.fn||(n=void 0);return n}var s,o,l=n=n||"";return 0===i&&(i=void 0,l=a(l)),i=i||(n.markup?n:{}),i.tmplName=e,r&&(i._parentTmpl=r),!l&&n.markup&&(l=a(n.markup))&&l.fn&&(l=l.markup),void 0!==l?(l.fn||n.fn?l.fn&&(o=l):(n=A(l,i),N(l.replace(st,"\\$&"),n)),o||(_(i),o=d(function(){return n.render.apply(n,arguments)},n)),e&&!r&&(_t[e]=o),o):void 0}function j(e){function t(t,n){this.tgt=e.getTgt(t,n)}return jt(e)&&(e={getTgt:e}),e.baseMap&&(e=d(d({},e.baseMap),e)),e.map=function(e,n){return new t(e,n)},e}function A(e,t){var n,r=Nt.wrapMap||{},i=d({tmpls:[],links:{},bnds:[],_is:"template",render:R},t);return i.markup=e,t.htmlTag||(n=pt.exec(e),i.htmlTag=n?n[1].toLowerCase():""),n=r[i.htmlTag],n&&n!==r.div&&(i.markup=G.trim(i.markup)),i}function T(e,t){function n(i,a,s){var o,l,p,d;if(i&&typeof i===wt&&!i.nodeType&&!i.markup&&!i.getTgt){for(p in i)n(p,i[p],a);return kt}return void 0===a&&(a=i,i=void 0),i&&""+i!==i&&(s=a,a=i,i=void 0),d=s?s[r]=s[r]||{}:n,l=t.compile,null===a?i&&delete d[i]:(a=l?l(i,a,s,0):a,i&&(d[i]=a)),l&&a&&(a._is=e),a&&(o=$t.onStore[e])&&o(i,a,l),a}var r=e+"s";kt[r]=n}function R(e,t,n,r,i,a){var s,o,l,p,d,u,c,f,g=r,v="";if(t===!0?(n=t,t=void 0):typeof t!==wt&&(t=void 0),(l=this.tag)?(d=this,p=l._.tmpl||d.tmpl,g=g||d.view,arguments.length||(e=g)):p=this,p){if(!g&&e&&"view"===e._is&&(g=e),g&&e===g&&(e=g.data),p.fn||(p=l._.tmpl=Tt[p]||Tt(p)),Q=Q||(u=!g),g||((t=t||{}).root=e),!Q||p.useViews)v=M(p,e,t,n,g,i,a,l);else{if(g?(c=g.data,f=g.index,g.index=bt):(g=D,g.data=e,g.ctx=t),At(e)&&!n)for(s=0,o=e.length;o>s;s++)g.index=s,g.data=e[s],v+=p.fn(e[s],g,kt);else v+=p.fn(e,g,kt);g.data=c,g.index=f}u&&(Q=void 0)}return v}function M(e,t,n,r,i,a,s,o){function l(e){_=d({},n),_[x]=e}var p,u,c,f,g,v,m,h,w,x,_,y,k="";if(o&&(w=o.tagName,y=o.tagCtx,n=n?J(n,o.ctx):o.ctx,m=y.content,y.props.link===!1&&(n=n||{},n.link=!1),(x=y.props.itemVar)&&("~"!==x.charAt(0)&&$("Use itemVar='~myItem'"),x=x.slice(1))),i&&(m=m||i.content,s=s||i._.onRender,n=n||i.ctx),a===!0&&(v=!0,a=0),s&&(n&&n.link===!1||o&&o._.noVws)&&(s=void 0),h=s,s===!0&&(h=void 0,s=i._.onRender),n=e.helpers?J(e.helpers,n):n,_=n,At(t)&&!r)for(c=v?i:void 0!==a&&i||new b(n,"array",i,t,e,a,m,s),x&&(c.it=x),x=c.it,p=0,u=t.length;u>p;p++)x&&l(t[p]),f=new b(_,"item",c,t[p],e,(a||0)+p,m,s),g=e.fn(t[p],f,kt),k+=c._.onRender?c._.onRender(g,f):g;else x&&l(t),c=v?i:new b(_,w||"data",i,t,e,a,m,s),o&&!o.flow&&(c.tag=o),k+=e.fn(t,c,kt);return h?h(k,c):k}function V(e,t,n){var r=Nt.onError(e,t,n);if(""+e===e)throw new $t.Err(r);return!t.linkCtx&&t.linked?Rt.html(r):r}function $(e){V("Syntax error\n"+e)}function N(e,t,n,r,i){function a(t){t-=f,t&&v.push(e.substr(f,t).replace(it,"\\n"))}function s(t,n){t&&(t+="}}",$((n?"{{"+n+"}} block has {{/"+t+" without {{"+t:"Unmatched or missing {{/"+t)+", in template:\n"+e))}function o(o,l,c,h,w,x,b,_,y,k,C,j){x&&(w=":",h=ht),k=k||n&&!i;var A=(l||n)&&[[]],T="",R="",M="",V="",N="",E="",S="",U="",J=!k&&!w&&!b;c=c||(y=y||"#data",w),a(j),f=j+o.length,_?u&&v.push(["*","\n"+y.replace(/^:/,"ret+= ").replace(at,"$1")+";\n"]):c?("else"===c&&(lt.test(y)&&$('for "{{else if expr}}" use "{{else expr}}"'),A=m[7]&&[[]],m[8]=e.substring(m[8],j),m=g.pop(),v=m[2],J=!0),y&&I(y.replace(it," "),A,t).replace(ot,function(e,t,n,r,i,a,s,o){return r="'"+i+"':",s?(R+=a+",",V+="'"+o+"',"):n?(M+=r+a+",",E+=r+"'"+o+"',"):t?S+=a:("trigger"===i&&(U+=a),T+=r+a+",",N+=r+"'"+o+"',",d=d||ct.test(i)),""}).slice(0,-1),A&&A[0]&&A.pop(),p=[c,h||!!r||d||"",J&&[],F(V,N,E),F(R,T,M),S,U,A||0],v.push(p),J&&(g.push(m),m=p,m[8]=f)):C&&(s(C!==m[0]&&"else"!==m[0]&&C,m[0]),m[8]=e.substring(m[8],j),m=g.pop()),s(!m&&C),v=m[2]}var l,p,d,u=Nt.allowCode||t&&t.allowCode,c=[],f=0,g=[],v=c,m=[,,c];return u&&(t.allowCode=u),n&&(e=W+e+et),s(g[0]&&g[0][2].pop()[0]),e.replace(q,o),a(e.length),(f=c[c.length-1])&&s(""+f!==f&&+f[8]===f[8]&&f[0]),n?(l=U(c,e,n),E(l,[c[0][7]])):l=U(c,t),l}function E(e,t){var n,r,i=0,a=t.length;for(e.deps=[];a>i;i++){r=t[i];for(n in r)"_jsvto"!==n&&r[n].length&&(e.deps=e.deps.concat(r[n]))}e.paths=r}function F(e,t,n){return[e.slice(0,-1),t.slice(0,-1),n.slice(0,-1)]}function S(e,t){return"\n "+(t?t+":{":"")+"args:["+e[0]+"]"+(e[1]||!t?",\n props:{"+e[1]+"}":"")+(e[2]?",\n ctx:{"+e[2]+"}":"")}function I(e,t,n){function r(r,h,w,x,b,_,y,k,C,j,A,T,R,M,V,E,F,S,I,U){function J(e,n,r,s,o,l,u,c){var f="."===r;if(r&&(b=b.slice(n.length),f||(e=(s?'view.hlp("'+s+'")':o?"view":"data")+(c?(l?"."+l:s?"":o?"":"."+r)+(u||""):(c=s?"":o?l||"":r,"")),e+=c?"."+c:"",e=n+("view.data"===e.slice(0,9)?e.slice(5):e)),p)){if(B="linkTo"===i?a=t._jsvto=t._jsvto||[]:d.bd,L=f&&B[B.length-1]){if(L._jsv){for(;L.sb;)L=L.sb;L.bnd&&(b="^"+b.slice(1)),L.sb=b,L.bnd=L.bnd||"^"===b.charAt(0)}}else B.push(b);m[g]=I+(f?1:0)}return e}x=p&&x,x&&!k&&(b=x+b),_=_||"",w=w||h||T,b=b||C,j=j||F||"";var K,O,B,L,q;if(!y||l||o){if(p&&E&&!l&&!o&&(!i||s||a)&&(K=m[g-1],U.length-1>I-(K||0))){if(K=U.slice(K,I+r.length),O!==!0)if(B=a||u[g-1].bd,L=B[B.length-1],L&&L.prm){for(;L.sb&&L.sb.prm;)L=L.sb;q=L.sb={path:L.sb,bnd:L.bnd}}else B.push(q={path:B.pop()});E=X+":"+K+" onerror=''"+Y,O=f[E],O||(f[E]=!0,f[E]=O=N(E,n,!0)),O!==!0&&q&&(q._jsv=O,q.prm=d.bd,q.bnd=q.bnd||q.path&&q.path.indexOf("^")>=0)}return l?(l=!R,l?r:T+'"'):o?(o=!M,o?r:T+'"'):(w?(m[g]=I++,d=u[++g]={bd:[]},w):"")+(S?g?"":(c=U.slice(c,I),(i?(i=s=a=!1,"\b"):"\b,")+c+(c=I+r.length,p&&t.push(d.bd=[]),"\b")):k?(g&&$(e),p&&t.pop(),i=b,s=x,c=I+r.length,x&&(p=d.bd=t[i]=[]),b+":"):b?b.split("^").join(".").replace(nt,J)+(j?(d=u[++g]={bd:[]},v[g]=!0,j):_):_?_:V?(v[g]=!1,d=u[--g],V+(j?(d=u[++g],v[g]=!0,j):"")):A?(v[g]||$(e),","):h?"":(l=R,o=M,'"'))}$(e)}var i,a,s,o,l,p=t&&t[0],d={bd:p},u={0:d},c=0,f=n?n.links:p&&(p.links=p.links||{}),g=0,v={},m={};return(e+(n?" ":"")).replace(rt,r)}function U(e,t,n){var r,i,a,s,o,l,p,d,u,c,f,g,v,m,h,w,x,b,_,y,k,C,j,T,R,M,V,N,F,I,J=0,K=t.useViews||t.tags||t.templates||t.helpers||t.converters,O="",B={},L=e.length;for(""+t===t?(b=n?'data-link="'+t.replace(it," ").slice(1,-1)+'"':t,t=0):(b=t.tmplName||"unnamed",t.allowCode&&(B.allowCode=!0),t.debug&&(B.debug=!0),f=t.bnds,x=t.tmpls),r=0;L>r;r++)if(i=e[r],""+i===i)O+='\n+"'+i+'"';else if(a=i[0],"*"===a)O+=";\n"+i[1]+"\nret=ret";else{if(s=i[1],k=!n&&i[2],o=S(i[3],"params")+"},"+S(v=i[4]),N=i[5],I=i[6],C=i[8]&&i[8].replace(at,"$1"),(R="else"===a)?g&&g.push(i[7]):(J=0,f&&(g=i[7])&&(g=[g],J=f.push(1))),K=K||v[1]||v[2]||g||/view.(?!index)/.test(v[0]),(M=":"===a)?s&&(a=s===ht?">":s+a):(k&&(_=A(C,B),_.tmplName=b+"/"+a,_.useViews=_.useViews||K,U(k,_),K=_.useViews,x.push(_)),R||(y=a,K=K||a&&(!Vt[a]||!Vt[a].flow),T=O,O=""),j=e[r+1],j=j&&"else"===j[0]),F=N?";\ntry{\nret+=":"\n+",m="",h="",M&&(g||I||s&&s!==ht)){if(V="return {"+o+"};",w='c("'+s+'",view,',V=new Function("data,view,j,u"," // "+b+" "+J+" "+a+"\n"+V),V._er=N,m=w+J+",",h=")",V._tag=a,n)return V;E(V,g),c=!0}if(O+=M?(n?(N?"\ntry{\n":"")+"return ":F)+(c?(c=void 0,K=u=!0,w+(g?(f[J-1]=V,J):"{"+o+"}")+")"):">"===a?(p=!0,"h("+v[0]+")"):(d=!0,"((v="+(v[0]||"data")+')!=null?v:"")')):(l=!0,"\n{view:view,tmpl:"+(k?x.length:"0")+","+o+"},"),y&&!j){if(O="["+O.slice(0,-1)+"]",w='t("'+y+'",view,this,',n||g){if(O=new Function("data,view,j,u"," // "+b+" "+J+" "+y+"\nreturn "+O+";"),O._er=N,O._tag=y,g&&E(f[J-1]=O,g),n)return O;m=w+J+",undefined,",h=")"}O=T+F+w+(J||O)+")",g=0,y=0}N&&(K=!0,O+=";\n}catch(e){ret"+(n?"urn ":"+=")+m+"j._err(e,view,"+N+")"+h+";}\n"+(n?"":"ret=ret"))}O="// "+b+"\nvar v"+(l?",t=j._tag":"")+(u?",c=j._cnvt":"")+(p?",h=j.converters.html":"")+(n?";\n":',ret=""\n')+(B.debug?"debugger;":"")+O+(n?"\n":";\nreturn ret;"),Nt._dbgMode&&(O="try {\n"+O+"\n}catch(e){\nreturn j._err(e, view);\n}");try{O=new Function("data,view,j,u",O)}catch(q){$("Compiled template code:\n\n"+O+'\n: "'+q.message+'"')}return t&&(t.fn=O,t.useViews=!!K),O}function J(e,t){return e&&e!==t?t?d(d({},t),e):e:t&&d({},t)}function K(e){return mt[e]||(mt[e]="&#"+e.charCodeAt(0)+";")}function O(e){var t,n,r=[];if(typeof e===wt)for(t in e)n=e[t],n&&n.toJSON&&!n.toJSON()||jt(n)||r.push({key:t,prop:n});return r}function B(e){return void 0!=e?ut.test(e)&&(""+e).replace(ft,K)||e:""}e=e===!0;var L,q,H,D,Q,Z="v1.0.0-beta",z=(0,eval)("this"),G=z.jQuery,P=z.document,W="{",X="{",Y="}",et="}",tt="^",nt=/^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,rt=/(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^\(\[])|[)\]])([([]?))|(\s+)/g,it=/[ \t]*(\r\n|\n|\r)/g,at=/\\(['"])/g,st=/['"\\]/g,ot=/(?:\x08|^)(onerror:)?(?:(~?)(([\w$_\.]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,lt=/^if\s/,pt=/<(\w+)[>\s]/,dt=/[\x00`><"'&]/g,ut=/[\x00`><\"'&]/,ct=/^on[A-Z]|^convert(Back)?$/,ft=dt,gt=0,vt=0,mt={"&":"&","<":"<",">":">","\x00":"�","'":"'",'"':""","`":"`"},ht="html",wt="object",xt="data-jsv-tmpl",bt="For #index in nested block use #getIndex().",_t={},yt={template:{compile:C},tag:{compile:y},helper:{},converter:{}},kt={jsviews:Z,settings:function(e){d(Nt,e),l(Nt._dbgMode),Nt.jsv&&Nt.jsv()},sub:{View:b,Err:p,tmplFn:N,parse:I,extend:d,syntaxErr:$,onStore:{},_ths:i,_tg:function(){}},map:j,_cnvt:m,_tag:x,_err:V},Ct=z.jsviews;(p.prototype=new Error).constructor=p,f.depends=function(){return[this.get("item"),"index"]},g.depends="index",b.prototype={get:c,getIndex:g,getRsc:w,hlp:v,_is:"view"};for(L in yt)T(L,yt[L]);var jt,At,Tt=kt.templates,Rt=kt.converters,Mt=kt.helpers,Vt=kt.tags,$t=kt.sub,Nt=kt.settings;return $t._tg.prototype={baseApply:k,cvtArgs:h},D=$t.topView=new b,G?(G.fn.render=function(e,t,n){var r=this.jquery&&(this[0]||V('Unknown template: "'+this.selector+'"')),i=r.getAttribute(xt);return R.call(i?Tt[i]:Tt(r),e,t,n)},G.observable&&(d($t,G.views.sub),kt.map=G.views.map)):(G={},e||(z.jsviews=G),G.isFunction=function(e){return"function"==typeof e},G.isArray=Array.isArray||function(e){return"[object Array]"===G.toString.call(e)},G.noConflict=function(){return z.jsviews===G&&(z.jsviews=Ct),G}),jt=G.isFunction,At=G.isArray,G.render=_t,G.views=kt,G.templates=Tt=kt.templates,kt.compile=function(e,t){return t=t||{},t.markup=e,Tt(t)},Nt({debugMode:l,delimiters:u,onError:function(e,t,n){return t&&(e=void 0===n?"{Error: "+(e.message||e)+"}":jt(n)?n(e,t):n),void 0==e?"":e},_dbgMode:!1}),Vt({"if":{render:function(e){var t=this,n=t.tagCtx,r=t.rendering.done||!e&&(arguments.length||!n.index)?"":(t.rendering.done=!0,t.selected=n.index,n.render(n.view,!0));return r},flow:!0},"for":{render:function(e){var t,n=!arguments.length,r=this,i=r.tagCtx,a="",s=0;return r.rendering.done||(t=n?i.view.data:e,void 0!==t&&(a+=i.render(t,n),s+=At(t)?t.length:1),(r.rendering.done=s)&&(r.selected=i.index)),a},flow:!0},props:{baseTag:"for",dataMap:j(O),flow:!0},include:{flow:!0},"*":{render:a,flow:!0},":*":{render:a,flow:!0},dbg:Mt.dbg=Rt.dbg=o}),Rt({html:B,attr:B,url:function(e){return void 0!=e?encodeURI(""+e):null===e?e:""}}),u(),kt}); 4 | //# sourceMappingURL=jsrender.min.js.map -------------------------------------------------------------------------------- /static/js/lib/simple_statistics.min.js: -------------------------------------------------------------------------------- 1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n;n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,n.ss=t()}}(function(){return function t(n,r,e){function i(u,s){if(!r[u]){if(!n[u]){var a="function"==typeof require&&require;if(!s&&a)return a(u,!0);if(o)return o(u,!0);var f=new Error("Cannot find module '"+u+"'");throw f.code="MODULE_NOT_FOUND",f}var l=r[u]={exports:{}};n[u][0].call(l.exports,function(t){var r=n[u][1][t];return i(r?r:t)},l,l.exports,t,n,r,e)}return r[u].exports}for(var o="function"==typeof require&&require,u=0;ut||t>1?null:i(1,t)}var i=t(4);n.exports=e},{4:4}],4:[function(t,n,r){"use strict";function e(t,n){if(0>n||n>1||0>=t||t%1!==0)return null;var r=0,e=0,u={};do u[r]=o(t)/(o(r)*o(t-r))*(Math.pow(n,r)*Math.pow(1-n,t-r)),e+=u[r],r++;while(1-i>e);return u}var i=t(10),o=t(12);n.exports=e},{10:10,12:12}],5:[function(t,n,r){"use strict";var e={1:{.995:0,.99:0,.975:0,.95:0,.9:.02,.5:.45,.1:2.71,.05:3.84,.025:5.02,.01:6.63,.005:7.88},2:{.995:.01,.99:.02,.975:.05,.95:.1,.9:.21,.5:1.39,.1:4.61,.05:5.99,.025:7.38,.01:9.21,.005:10.6},3:{.995:.07,.99:.11,.975:.22,.95:.35,.9:.58,.5:2.37,.1:6.25,.05:7.81,.025:9.35,.01:11.34,.005:12.84},4:{.995:.21,.99:.3,.975:.48,.95:.71,.9:1.06,.5:3.36,.1:7.78,.05:9.49,.025:11.14,.01:13.28,.005:14.86},5:{.995:.41,.99:.55,.975:.83,.95:1.15,.9:1.61,.5:4.35,.1:9.24,.05:11.07,.025:12.83,.01:15.09,.005:16.75},6:{.995:.68,.99:.87,.975:1.24,.95:1.64,.9:2.2,.5:5.35,.1:10.65,.05:12.59,.025:14.45,.01:16.81,.005:18.55},7:{.995:.99,.99:1.25,.975:1.69,.95:2.17,.9:2.83,.5:6.35,.1:12.02,.05:14.07,.025:16.01,.01:18.48,.005:20.28},8:{.995:1.34,.99:1.65,.975:2.18,.95:2.73,.9:3.49,.5:7.34,.1:13.36,.05:15.51,.025:17.53,.01:20.09,.005:21.96},9:{.995:1.73,.99:2.09,.975:2.7,.95:3.33,.9:4.17,.5:8.34,.1:14.68,.05:16.92,.025:19.02,.01:21.67,.005:23.59},10:{.995:2.16,.99:2.56,.975:3.25,.95:3.94,.9:4.87,.5:9.34,.1:15.99,.05:18.31,.025:20.48,.01:23.21,.005:25.19},11:{.995:2.6,.99:3.05,.975:3.82,.95:4.57,.9:5.58,.5:10.34,.1:17.28,.05:19.68,.025:21.92,.01:24.72,.005:26.76},12:{.995:3.07,.99:3.57,.975:4.4,.95:5.23,.9:6.3,.5:11.34,.1:18.55,.05:21.03,.025:23.34,.01:26.22,.005:28.3},13:{.995:3.57,.99:4.11,.975:5.01,.95:5.89,.9:7.04,.5:12.34,.1:19.81,.05:22.36,.025:24.74,.01:27.69,.005:29.82},14:{.995:4.07,.99:4.66,.975:5.63,.95:6.57,.9:7.79,.5:13.34,.1:21.06,.05:23.68,.025:26.12,.01:29.14,.005:31.32},15:{.995:4.6,.99:5.23,.975:6.27,.95:7.26,.9:8.55,.5:14.34,.1:22.31,.05:25,.025:27.49,.01:30.58,.005:32.8},16:{.995:5.14,.99:5.81,.975:6.91,.95:7.96,.9:9.31,.5:15.34,.1:23.54,.05:26.3,.025:28.85,.01:32,.005:34.27},17:{.995:5.7,.99:6.41,.975:7.56,.95:8.67,.9:10.09,.5:16.34,.1:24.77,.05:27.59,.025:30.19,.01:33.41,.005:35.72},18:{.995:6.26,.99:7.01,.975:8.23,.95:9.39,.9:10.87,.5:17.34,.1:25.99,.05:28.87,.025:31.53,.01:34.81,.005:37.16},19:{.995:6.84,.99:7.63,.975:8.91,.95:10.12,.9:11.65,.5:18.34,.1:27.2,.05:30.14,.025:32.85,.01:36.19,.005:38.58},20:{.995:7.43,.99:8.26,.975:9.59,.95:10.85,.9:12.44,.5:19.34,.1:28.41,.05:31.41,.025:34.17,.01:37.57,.005:40},21:{.995:8.03,.99:8.9,.975:10.28,.95:11.59,.9:13.24,.5:20.34,.1:29.62,.05:32.67,.025:35.48,.01:38.93,.005:41.4},22:{.995:8.64,.99:9.54,.975:10.98,.95:12.34,.9:14.04,.5:21.34,.1:30.81,.05:33.92,.025:36.78,.01:40.29,.005:42.8},23:{.995:9.26,.99:10.2,.975:11.69,.95:13.09,.9:14.85,.5:22.34,.1:32.01,.05:35.17,.025:38.08,.01:41.64,.005:44.18},24:{.995:9.89,.99:10.86,.975:12.4,.95:13.85,.9:15.66,.5:23.34,.1:33.2,.05:36.42,.025:39.36,.01:42.98,.005:45.56},25:{.995:10.52,.99:11.52,.975:13.12,.95:14.61,.9:16.47,.5:24.34,.1:34.28,.05:37.65,.025:40.65,.01:44.31,.005:46.93},26:{.995:11.16,.99:12.2,.975:13.84,.95:15.38,.9:17.29,.5:25.34,.1:35.56,.05:38.89,.025:41.92,.01:45.64,.005:48.29},27:{.995:11.81,.99:12.88,.975:14.57,.95:16.15,.9:18.11,.5:26.34,.1:36.74,.05:40.11,.025:43.19,.01:46.96,.005:49.65},28:{.995:12.46,.99:13.57,.975:15.31,.95:16.93,.9:18.94,.5:27.34,.1:37.92,.05:41.34,.025:44.46,.01:48.28,.005:50.99},29:{.995:13.12,.99:14.26,.975:16.05,.95:17.71,.9:19.77,.5:28.34,.1:39.09,.05:42.56,.025:45.72,.01:49.59,.005:52.34},30:{.995:13.79,.99:14.95,.975:16.79,.95:18.49,.9:20.6,.5:29.34,.1:40.26,.05:43.77,.025:46.98,.01:50.89,.005:53.67},40:{.995:20.71,.99:22.16,.975:24.43,.95:26.51,.9:29.05,.5:39.34,.1:51.81,.05:55.76,.025:59.34,.01:63.69,.005:66.77},50:{.995:27.99,.99:29.71,.975:32.36,.95:34.76,.9:37.69,.5:49.33,.1:63.17,.05:67.5,.025:71.42,.01:76.15,.005:79.49},60:{.995:35.53,.99:37.48,.975:40.48,.95:43.19,.9:46.46,.5:59.33,.1:74.4,.05:79.08,.025:83.3,.01:88.38,.005:91.95},70:{.995:43.28,.99:45.44,.975:48.76,.95:51.74,.9:55.33,.5:69.33,.1:85.53,.05:90.53,.025:95.02,.01:100.42,.005:104.22},80:{.995:51.17,.99:53.54,.975:57.15,.95:60.39,.9:64.28,.5:79.33,.1:96.58,.05:101.88,.025:106.63,.01:112.33,.005:116.32},90:{.995:59.2,.99:61.75,.975:65.65,.95:69.13,.9:73.29,.5:89.33,.1:107.57,.05:113.14,.025:118.14,.01:124.12,.005:128.3},100:{.995:67.33,.99:70.06,.975:74.22,.95:77.93,.9:82.36,.5:99.33,.1:118.5,.05:124.34,.025:129.56,.01:135.81,.005:140.17}};n.exports=e},{}],6:[function(t,n,r){"use strict";function e(t,n,r){for(var e,u,s=i(t),a=0,f=1,l=n(s),c=[],h=[],p=0;p=0;u--)h[u]<3&&(h[u-1]+=h[u],h.pop(),c[u-1]+=c[u],c.pop());for(u=0;u=n)return null;for(var e=0;ee;e++){for(var i=[],o=0;n>o;o++)i.push(0);r.push(i)}return r}function i(t,n){if(n>t.length)throw new Error("Cannot generate more classes than there are data values");var r=u(t),i=o(r);if(1===i)return[r];for(var s=e(n,r.length),a=e(n,r.length),f=0;n>f;f++)for(var l=r[0],c=Math.max(f,1);c=f;d--)v+=(c-d)/(c-d+1)*Math.pow(r[d]-g,2),g=(r[d]+(c-d)*g)/(c-d+1),d===c?(s[f][c]=v,a[f][c]=d,d>0&&(s[f][c]+=s[f-1][d-1])):0===d?v<=s[f][c]&&(s[f][c]=v,a[f][c]=d):v+s[f-1][d-1]=0;f--){var w=a[f][M];x[f]=r.slice(w,M+1),f>0&&(M=w-1)}return x}var o=t(42),u=t(26);n.exports=i},{26:26,42:42}],9:[function(t,n,r){"use strict";function e(t){var n=Math.abs(t),r=Math.min(Math.round(100*n),i.length-1);return t>=0?i[r]:+(1-i[r]).toFixed(4)}var i=t(44);n.exports=e},{44:44}],10:[function(t,n,r){"use strict";var e=1e-4;n.exports=e},{}],11:[function(t,n,r){"use strict";function e(t){var n=1/(1+.5*Math.abs(t)),r=n*Math.exp(-Math.pow(t,2)-1.26551223+1.00002368*n+.37409196*Math.pow(n,2)+.09678418*Math.pow(n,3)-.18628806*Math.pow(n,4)+.27886807*Math.pow(n,5)-1.13520398*Math.pow(n,6)+1.48851587*Math.pow(n,7)-.82215223*Math.pow(n,8)+.17087277*Math.pow(n,9));return t>=0?1-r:r-1}n.exports=e},{}],12:[function(t,n,r){"use strict";function e(t){if(0>t)return null;for(var n=1,r=2;t>=r;r++)n*=r;return n}n.exports=e},{}],13:[function(t,n,r){"use strict";function e(t){if(0===t.length)return null;for(var n=1,r=0;r=0?r:-r}n.exports=e},{}],17:[function(t,n,r){"use strict";function e(t){var n,r,e=t.length;if(1===e)n=0,r=t[0][1];else{for(var i,o,u,s=0,a=0,f=0,l=0,c=0;e>c;c++)i=t[c],o=i[0],u=i[1],s+=o,a+=u,f+=o*o,l+=o*u;n=(e*l-s*a)/(e*f-s*s),r=a/e-n*s/e}return{m:n,b:r}}n.exports=e},{}],18:[function(t,n,r){"use strict";function e(t){return function(n){return t.b+t.m*n}}n.exports=e},{}],19:[function(t,n,r){"use strict";function e(t){if(!t||0===t.length)return null;for(var n=i(t),r=[],e=0;en||void 0===n)&&(n=t[r]);return n}n.exports=e},{}],21:[function(t,n,r){"use strict";function e(t){return 0===t.length?null:i(t)/t.length}var i=t(45);n.exports=e},{45:45}],22:[function(t,n,r){"use strict";function e(t){if(0===t.length)return null;var n=i(t);if(n.length%2===1)return n[(n.length-1)/2];var r=n[n.length/2-1],e=n[n.length/2];return(r+e)/2}var i=t(26);n.exports=e},{26:26}],23:[function(t,n,r){"use strict";function e(t){for(var n,r=0;ro&&(o=u,n=e),u=1,e=r[s]):u++;return n}var i=t(26);n.exports=e},{26:26}],26:[function(t,n,r){"use strict";function e(t){return t.slice().sort(function(t,n){return t-n})}n.exports=e},{}],27:[function(t,n,r){"use strict";function e(){this.weights=[],this.bias=0}e.prototype.predict=function(t){if(t.length!==this.weights.length)return null;for(var n=0,r=0;r0?1:0},e.prototype.train=function(t,n){if(0!==n&&1!==n)return null;t.length!==this.weights.length&&(this.weights=t,this.bias=1);var r=this.predict(t);if(r!==n){for(var e=n-r,i=0;i=t)return null;var n=0,r=0,e={};do e[n]=Math.pow(Math.E,-t)*Math.pow(t,n)/o(n),r+=e[n],n++;while(1-i>r);return e}var i=t(10),o=t(12);n.exports=e},{10:10,12:12}],29:[function(t,n,r){"use strict";function e(t){return 0===t?t=i:t>=1&&(t=1-i),Math.sqrt(2)*o(2*t-1)}var i=t(10),o=t(16);n.exports=e},{10:10,16:16}],30:[function(t,n,r){"use strict";function e(t,n){if(0===t.length)return null;var r=o(t);if(n.length){for(var e=[],u=0;un||n>1?null:1===n?t[t.length-1]:0===n?t[0]:r%1!==0?t[Math.ceil(r)-1]:t.length%2===0?(t[r-1]+t[r])/2:t[r]}n.exports=e},{}],32:[function(t,n,r){"use strict";function e(t,n){if(t.length<2)return 1;for(var r,e=0,i=0;i0;)e=Math.floor(n()*i--),r=t[i],t[i]=t[e],t[e]=r;return t}n.exports=e},{}],42:[function(t,n,r){"use strict";function e(t){for(var n,r=0,e=0;ee;e++)r*=t*t/(2*e+1),n+=r;return Math.round(1e4*(.5+n/i*Math.exp(-t*t/2)))/1e4}for(var i=Math.sqrt(2*Math.PI),o=[],u=0;3.09>=u;u+=.01)o.push(e(u));n.exports=o},{}],45:[function(t,n,r){"use strict";function e(t){for(var n=0,r=0;rError loading plugins.' ); 25 | dfd.resolve(); 26 | } ); 27 | 28 | return dfd.promise(); 29 | } 30 | 31 | return { 32 | init : init 33 | }; 34 | 35 | } )(); 36 | 37 | $( window ).on( "resize" , function( event ) { 38 | if( event.target === window ) $( "#gridList" ).gridList( "resize" ); 39 | } ); 40 | 41 | $( document ).on( "ready" , function() { 42 | 43 | App.Main.init().done( function() { 44 | App.Page.init(); 45 | } ); 46 | 47 | } ); 48 | -------------------------------------------------------------------------------- /static/js/modal.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | 4 | App.Modal = ( function() { 5 | 6 | function show( title , content , yesCallback , cancelCallback ) 7 | { 8 | var template = $.templates( "#tmpl_Modal" ); 9 | var $modal = $( template.render( { 10 | title : title, 11 | content : content 12 | } ) ); 13 | 14 | $( "body" ).append( $modal ); 15 | 16 | $modal.on( "hidden.bs.modal" , function() { 17 | if( typeof cancelCallback === "function" ) 18 | { 19 | cancelCallback(); 20 | } 21 | $( this ).remove(); 22 | } ); 23 | 24 | $( "#dynamicModalYes" ).on( "click" , function() { 25 | if( typeof yesCallback === "function" ) 26 | { 27 | yesCallback(); 28 | } 29 | $modal.modal( "hide" ); 30 | } ); 31 | 32 | $modal.modal(); 33 | } 34 | 35 | return { 36 | show : show 37 | }; 38 | 39 | } )(); 40 | -------------------------------------------------------------------------------- /static/js/model/chart.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.Model = App.Model || {}; 4 | 5 | App.Model.Chart = ( function() { 6 | 7 | var ChartDatasource = function( parent , datasource , config ) 8 | { 9 | this.parent = parent; 10 | this.datasource = datasource; 11 | this.config = config; 12 | this.components = []; 13 | }; 14 | 15 | ChartDatasource.prototype.loadComponents = function() 16 | { 17 | this.components = []; 18 | var cconfig, i; 19 | if( this.datasource.dataComponents && !this.parent.plugin.disableComponentDiscovery ) 20 | { 21 | for( i = 0; i < this.datasource.dataComponents.length; i++ ) 22 | { 23 | if( this.config.components.hasOwnProperty( this.datasource.dataComponents[i] ) ) 24 | { 25 | cconfig = this.config.components[ this.datasource.dataComponents[i] ]; 26 | } 27 | else 28 | { 29 | cconfig = { 30 | enabled : true 31 | }; 32 | } 33 | 34 | if( !cconfig.enabled ) continue; 35 | this.components.push( new ChartDatasourceComponent( this , this.datasource.dataComponents[i] , cconfig ) ); 36 | } 37 | } 38 | else 39 | { 40 | cconfig = { 41 | enabled : true, 42 | label : this.config.label 43 | }; 44 | this.components.push( new ChartDatasourceComponent( this , null , cconfig ) ); 45 | } 46 | }; 47 | 48 | ChartDatasource.prototype.requestHistoryData = function( start , end , callback ) 49 | { 50 | this.datasource.requestHistoryData( this.parent , start , end , callback ); 51 | }; 52 | 53 | var ChartDatasourceComponent = function( datasource , component , config ) 54 | { 55 | this.datasource = datasource; 56 | this.component = component; 57 | this.config = { 58 | enabled : config.enabled, 59 | label : config.label 60 | }; 61 | 62 | if( !this.config.label ) this.config.label = this.component; 63 | }; 64 | 65 | ChartDatasourceComponent.prototype.getData = function( data ) 66 | { 67 | return this.component ? data[ this.component ] : data; 68 | }; 69 | 70 | var Chart = function( data ) 71 | { 72 | this.$container = null; 73 | 74 | this.pluginInstance = null; 75 | this.resetDatasources(); 76 | 77 | if( data ) this.unserialize( data ); 78 | }; 79 | 80 | Chart.prototype.serialize = function() 81 | { 82 | var data = { 83 | id : this.id, 84 | name : this.name, 85 | config : this.config, 86 | datasources : [], 87 | pos : this.pos 88 | }; 89 | 90 | if( this.plugin ) data.plugin = this.plugin.id; 91 | 92 | for( var i in this.datasources ) 93 | { 94 | data.datasources.push( { 95 | id : this.datasources[i].datasource.id, 96 | config : this.datasources[i].config 97 | } ); 98 | } 99 | 100 | return data; 101 | }; 102 | 103 | Chart.prototype.unserialize = function( data ) 104 | { 105 | this.id = data.id; 106 | this.name = data.name; 107 | this.config = data.config; 108 | this.pos = data.pos; 109 | 110 | // A bit of backwards compatibility 111 | if( data.chartPlugin ) data.plugin = data.chartPlugin; 112 | 113 | this.plugin_id = data.plugin; 114 | this.plugin = App.Plugins.getPlugin( this.plugin_id ); 115 | 116 | this.datasources = []; 117 | for( var i in data.datasources ) 118 | { 119 | var datasource = App.Model.Datasource.getDatasource( data.datasources[i].id ); 120 | if( !datasource ) continue; 121 | 122 | this.addDatasource( datasource , data.datasources[i].config ); 123 | } 124 | }; 125 | 126 | Chart.prototype.resetDatasources = function() { 127 | this.datasources = []; 128 | this.datasourceMap = {}; 129 | this.unreadyDatasources = []; 130 | }; 131 | 132 | Chart.prototype.addDatasource = function( datasource , config ) 133 | { 134 | var index = this.datasources.length; 135 | 136 | if( !datasource.isReady() && this.plugin && !this.plugin.disableComponentDiscovery ) this.unreadyDatasources.push( datasource ); 137 | 138 | var chartDatasource = new ChartDatasource( this , datasource , config ); 139 | this.datasources.push( chartDatasource ); 140 | this.datasourceMap[ datasource.id ] = this.datasources[ index ]; 141 | }; 142 | 143 | Chart.prototype.loadDatasourceComponents = function() 144 | { 145 | this.labelConflicts = { 146 | labels : [], 147 | conflicts : [], 148 | counts : [] 149 | }; 150 | 151 | this.components = []; 152 | for( var j = 0; j < this.datasources.length; j++ ) 153 | { 154 | var chartDatasource = this.datasources[j]; 155 | chartDatasource.componentsIndex = this.components.length; 156 | chartDatasource.loadComponents(); 157 | 158 | for( var i = 0; i < chartDatasource.components.length; i++ ) 159 | { 160 | var label = chartDatasource.components[i].config.label; 161 | 162 | var cindex = this.labelConflicts.conflicts.indexOf( label ); 163 | if( cindex != -1 ) 164 | { 165 | this.labelConflicts.counts[ cindex ]++; 166 | } 167 | else if( this.labelConflicts.labels.indexOf( label ) != -1 ) 168 | { 169 | cindex = this.labelConflicts.counts.length; 170 | this.labelConflicts.conflicts.push( label ); 171 | this.labelConflicts.counts.push( 2 ); 172 | } 173 | 174 | if( cindex != -1 ) 175 | { 176 | chartDatasource.components[i].config.label = label + "_" + this.labelConflicts.counts[ cindex ]; 177 | } 178 | 179 | this.components.push( chartDatasource.components[i] ); 180 | this.labelConflicts.labels.push( chartDatasource.components[i].config.label ); 181 | } 182 | } 183 | }; 184 | 185 | Chart.prototype.datasourceConfigChanged = function( datasource ) 186 | { 187 | var index = this.unreadyDatasources.indexOf( datasource ); 188 | if( datasource.isReady() && index != -1 ) 189 | { 190 | this.unreadyDatasources.splice( index , 1 ); 191 | if( this.$container ) this.load( this.$container ); 192 | } 193 | else if( !datasource.isReady() && index == -1 && this.plugin && !this.plugin.disableComponentDiscovery ) 194 | { 195 | this.unreadyDatasources.push( datasource ); 196 | if( this.$container ) 197 | { 198 | if( this.pluginInstance ) 199 | { 200 | this.pluginInstance = null; 201 | this.$container.empty(); 202 | } 203 | this.load( this.$container ); 204 | } 205 | } 206 | }; 207 | 208 | Chart.prototype.load = function( $container ) 209 | { 210 | this.$container = $container; 211 | 212 | if( !this.plugin ) 213 | { 214 | App.View.Dashboard.showMissingPlugin( $container , this.plugin_id ); 215 | return; 216 | } 217 | 218 | if( this.unreadyDatasources.length ) 219 | { 220 | App.View.Dashboard.showPendingDatasources( $container , this.unreadyDatasources ); 221 | return; 222 | } 223 | 224 | this.loadDatasourceComponents(); 225 | 226 | $container.empty(); 227 | this.pluginInstance = new this.plugin.plugin( $container[0] , this.datasources , this.components , this.config ); 228 | }; 229 | 230 | Chart.prototype.unload = function() 231 | { 232 | this.$container = null; 233 | this.pluginInstance = null; 234 | }; 235 | 236 | Chart.prototype.pushData = function( datasource , data , callback ) 237 | { 238 | if( this.pluginInstance ) 239 | { 240 | if( typeof callback != "function" ) callback = this.pluginInstance.pushData.bind( this.pluginInstance ); 241 | datasource = this.datasourceMap[ datasource.id ]; 242 | for( var i = 0; i < datasource.components.length; i++ ) 243 | { 244 | callback( datasource.componentsIndex + i , data ); 245 | } 246 | } 247 | }; 248 | 249 | return Chart; 250 | 251 | } )(); 252 | -------------------------------------------------------------------------------- /static/js/model/dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.Model = App.Model || {}; 4 | 5 | App.Model.Dashboard = ( function() { 6 | 7 | var Dashboard = function( name ) { 8 | this.name = name; 9 | this.datasources = {}; 10 | this.charts = {}; 11 | 12 | this.active = false; 13 | }; 14 | 15 | Dashboard.prototype.serialize = function() 16 | { 17 | var data = { 18 | name : this.name, 19 | charts : [] 20 | }; 21 | 22 | for( var i in this.charts ) 23 | data.charts.push( this.charts[i].serialize() ); 24 | 25 | return data; 26 | }; 27 | 28 | Dashboard.prototype.unserialize = function( data ) 29 | { 30 | this.name = data.name; 31 | for( var i in data.charts ) 32 | { 33 | var chart = new App.Model.Chart( data.charts[i] ); 34 | this.addChart( chart ); 35 | } 36 | }; 37 | 38 | Dashboard.prototype.load = function() 39 | { 40 | this.active = true; 41 | this.subscribeToAllDatasources(); 42 | }; 43 | 44 | Dashboard.prototype.unload = function() 45 | { 46 | this.active = false; 47 | for( var id in this.charts ) 48 | this.charts[id].unload(); 49 | }; 50 | 51 | Dashboard.prototype.getChart = function( id ) 52 | { 53 | return this.charts.hasOwnProperty( id ) ? this.charts[id] : null; 54 | }; 55 | 56 | Dashboard.prototype.addChart = function( chart ) 57 | { 58 | this.charts[ chart.id ] = chart; 59 | 60 | var sub = []; 61 | for( var i = 0; i < chart.datasources.length; i++ ) 62 | { 63 | var datasource = chart.datasources[i].datasource; 64 | if( !this.datasources.hasOwnProperty( datasource.id ) ) 65 | { 66 | this.datasources[ datasource.id ] = datasource; 67 | sub.push( datasource.id ); 68 | } 69 | 70 | datasource.addChart( chart , i ); 71 | } 72 | 73 | if( this.active && sub.length ) 74 | App.Net.subscribeToDatasources( sub ); 75 | }; 76 | 77 | Dashboard.prototype.removeChart = function( id ) { 78 | if( this.active ) 79 | { 80 | var unsub = []; 81 | for( var dsid in this.datasources ) 82 | { 83 | this.datasources[ dsid ].removeChart( id ); 84 | if( this.datasources[ dsid ].isEmpty() ) 85 | { 86 | unsub.push( dsid ); 87 | delete this.datasources[ dsid ]; 88 | } 89 | } 90 | 91 | if( unsub.length ) 92 | App.Net.unsubscribeFromDatasources( unsub ); 93 | } 94 | 95 | // chart.unload() 96 | delete this.charts[ id ]; 97 | }; 98 | 99 | Dashboard.prototype.subscribeToAllDatasources = function() { 100 | var sub = []; 101 | for( var id in this.datasources ) sub.push( id ); 102 | App.Net.subscribeToDatasources( sub ); 103 | }; 104 | 105 | Dashboard.prototype.pushData = function( data ) { 106 | if( !this.datasources.hasOwnProperty( data.id ) ) return; 107 | 108 | var datasource = this.datasources[ data.id ]; 109 | if( data.type == "live" ) datasource.pushData( data.data ); 110 | else if( data.type == "history" ) datasource.pushHistoryData( data.cid , data.data ); 111 | else if( data.type == "config" ) datasource.updateConfig( data.config ); 112 | }; 113 | 114 | Dashboard.dashboards = []; 115 | 116 | Dashboard.getDashboard = function( id ) { 117 | return Dashboard.dashboards.hasOwnProperty( id ) ? Dashboard.dashboards[ id ] : null; 118 | }; 119 | 120 | Dashboard.addDashboard = function( dashboard ) { 121 | Dashboard.dashboards.push( dashboard ); 122 | }; 123 | 124 | Dashboard.removeDashboard = function( id ) { 125 | Dashboard.dashboards.splice( id , 1 ); 126 | }; 127 | 128 | Dashboard.serializeAll = function() 129 | { 130 | var data = []; 131 | for( var i in Dashboard.dashboards ) 132 | data.push( Dashboard.dashboards[i].serialize() ); 133 | 134 | return data; 135 | }; 136 | 137 | Dashboard.unserializeAll = function( data ) 138 | { 139 | for( var i in data ) 140 | { 141 | var dashboard = new Dashboard(); 142 | dashboard.unserialize( data[i] ); 143 | Dashboard.addDashboard( dashboard ); 144 | } 145 | }; 146 | 147 | return Dashboard; 148 | 149 | } )(); 150 | -------------------------------------------------------------------------------- /static/js/model/datasource.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.Model = App.Model || {}; 4 | 5 | App.Model.Datasource = ( function() { 6 | 7 | var Datasource = function( id , config ) { 8 | if( config ) this.unserialize( config ); 9 | 10 | this.id = id; 11 | this.chartCount = 0; 12 | this.charts = {}; 13 | this.end = null; 14 | 15 | this.historyRequests = {}; 16 | }; 17 | 18 | Datasource.prototype.unserialize = function( data ) 19 | { 20 | this.name = data.name; 21 | this.tstampField = data.tstampField; 22 | this.dataField = data.dataField; 23 | this.dataComponents = data.dataComponents; 24 | 25 | if( this.tstampField !== "tstamp" ) this.tstampField = this.tstampField.split("."); 26 | if( this.dataField !== "data" ) this.dataField = this.dataField.split("."); 27 | }; 28 | 29 | Datasource.prototype.getNestedValue = function( obj , keyArr ) { 30 | try 31 | { 32 | return keyArr.reduce( function( reduceObj , i ) { 33 | return reduceObj[i]; 34 | } , obj ); 35 | } 36 | catch( e ) 37 | { 38 | return undefined; 39 | } 40 | }; 41 | 42 | Datasource.prototype.addChart = function( chart ) { 43 | if( this.charts.hasOwnProperty( chart.id ) ) 44 | { 45 | console.error( "Datasource already has chart: " + chart.id ); 46 | return; 47 | } 48 | 49 | this.charts[ chart.id ] = chart; 50 | this.chartCount++; 51 | }; 52 | 53 | Datasource.prototype.removeChart = function( id ) { 54 | if( this.charts.hasOwnProperty( id ) ) 55 | { 56 | delete this.charts[ id ]; 57 | delete this.historyRequests[ id ]; 58 | this.chartCount--; 59 | } 60 | }; 61 | 62 | Datasource.prototype.updateConfig = function( config ) 63 | { 64 | this.unserialize( config ); 65 | for( var id in this.charts ) 66 | this.charts[ id ].datasourceConfigChanged( this ); 67 | }; 68 | 69 | Datasource.prototype.isEmpty = function() { 70 | return this.chartCount === 0; 71 | }; 72 | 73 | Datasource.prototype.isReady = function() { 74 | return this.dataComponents !== undefined; 75 | }; 76 | 77 | Datasource.prototype.convertData = function( data ) { 78 | if( $.isArray( data ) ) 79 | { 80 | for( var i = 0; i < data.length; i++ ) 81 | data[i] = this.convertDataPoint( data[i] ); 82 | 83 | return data; 84 | } 85 | else return this.convertDataPoint( data ); 86 | }; 87 | 88 | Datasource.prototype.convertDataPoint = function( data ) { 89 | var converted = { 90 | tstamp : this.tstampField == "tstamp" ? data.tstamp : this.getNestedValue( data , this.tstampField ), 91 | data : this.dataField == "data" ? data.data : this.getNestedValue( data , this.dataField ) 92 | }; 93 | 94 | return converted; 95 | }; 96 | 97 | Datasource.prototype.pushData = function( data ) { 98 | data = this.convertData( data ); 99 | 100 | for( var id in this.charts ) 101 | { 102 | this.charts[id].pushData( this , data ); 103 | } 104 | }; 105 | 106 | Datasource.prototype.pushHistoryData = function( chartID , data ) { 107 | if( !this.charts.hasOwnProperty( chartID ) || !this.historyRequests.hasOwnProperty( chartID ) ) return; 108 | 109 | if( this.tstampField !== "tstamp" || this.dataField !== "data" ) 110 | { 111 | data = this.convertData( data ); 112 | } 113 | 114 | this.charts[ chartID ].pushData( this , data , this.historyRequests[ chartID ] ); 115 | delete this.historyRequests[ chartID ]; 116 | }; 117 | 118 | Datasource.prototype.requestHistoryData = function( chart , start , end , callback ) { 119 | if( !this.charts.hasOwnProperty( chart.id ) || this.historyRequests.hasOwnProperty( chart.id ) ) return; 120 | if( typeof callback != "function" ) return; 121 | 122 | this.historyRequests[ chart.id ] = callback; 123 | App.Net.requestHistoryData( this.id , chart.id , start , end ); 124 | }; 125 | 126 | Datasource.getDatasources = function() { 127 | var dfd = $.Deferred(); 128 | 129 | $.getJSON( "api/datasources" ).done( function( datasources ) { 130 | if( !Datasource.datasources ) Datasource.datasources = {}; 131 | 132 | for( var id in datasources ) 133 | { 134 | if( Datasource.datasources.hasOwnProperty( id ) ) 135 | Datasource.datasources[ id ].updateConfig( datasources[id] ); 136 | else 137 | Datasource.datasources[ id ] = new Datasource( id , datasources[id] ); 138 | } 139 | 140 | dfd.resolve(); 141 | } ); 142 | 143 | return dfd.promise(); 144 | }; 145 | 146 | Datasource.getDatasource = function( id ) 147 | { 148 | return Datasource.datasources.hasOwnProperty( id ) ? Datasource.datasources[id] : null; 149 | }; 150 | 151 | Datasource.datasources = null; 152 | 153 | return Datasource; 154 | 155 | } )(); 156 | -------------------------------------------------------------------------------- /static/js/net.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | 4 | App.Net = ( function() { 5 | 6 | var ws = null; 7 | 8 | function createConnection() 9 | { 10 | var dfd = $.Deferred(); 11 | 12 | var basePath = $( "base" ).attr( "href" ); 13 | ws = new WebSocket( ( location.protocol == "https:" ? "wss" : "ws" ) + "://" + location.host + basePath + "dsws" ); 14 | 15 | ws.onopen = function( event ) { 16 | App.View.Status.setConnected( true ); 17 | dfd.resolve(); 18 | }; 19 | 20 | ws.onmessage = function( event ) { 21 | var data = event.data; 22 | try 23 | { 24 | data = JSON.parse( data ); 25 | } 26 | catch( e ) {} // No worries, treat data as String 27 | 28 | if( typeof data === "string" ) 29 | { 30 | 31 | } 32 | else 33 | { 34 | App.Controller.Dashboard.onNetworkMessage( data ); 35 | } 36 | }; 37 | 38 | ws.onclose = function( event ) { 39 | console.log( "Close" , event ); 40 | ws = null; 41 | if( event.code === 1000 ) 42 | { 43 | App.Controller.Dashboard.onNetworkDisconnect(); 44 | } 45 | 46 | App.View.Status.setConnected( false ); 47 | }; 48 | 49 | ws.onerror = function( event ) { 50 | console.log( "Error" , event ); 51 | ws = null; 52 | App.View.Status.setConnected( false ); 53 | }; 54 | 55 | return dfd.promise(); 56 | } 57 | 58 | function closeConnection() 59 | { 60 | ws.onclose = null; 61 | ws.close(); 62 | ws = null; 63 | App.View.Status.setConnected( false ); 64 | } 65 | 66 | function subscribeToDatasources( datasources ) 67 | { 68 | if( ws === null ) return; 69 | ws.send( JSON.stringify( { m : "sub" , id : datasources } ) ); 70 | } 71 | 72 | function unsubscribeFromDatasources( datasources ) 73 | { 74 | if( ws === null ) return; 75 | ws.send( JSON.stringify( { m : "unsub" , id : datasources } ) ); 76 | } 77 | 78 | function requestHistoryData( dsid , cid , start , end ) 79 | { 80 | if( ws === null ) return; 81 | ws.send( JSON.stringify( { m : "hist" , dsid : dsid , cid : cid , start : start , end : end } ) ); 82 | } 83 | 84 | return { 85 | createConnection : createConnection, 86 | closeConnection : closeConnection, 87 | subscribeToDatasources : subscribeToDatasources, 88 | unsubscribeFromDatasources : unsubscribeFromDatasources, 89 | requestHistoryData : requestHistoryData 90 | }; 91 | 92 | } )(); 93 | -------------------------------------------------------------------------------- /static/js/page.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | 4 | App.Page = ( function() { 5 | 6 | var pageChangeCallbacks = []; 7 | 8 | var currentPage = null, currentPageNav = null; 9 | var isNavigating = false; 10 | var baseUrl = "/dash/"; 11 | 12 | function init() 13 | { 14 | baseUrl = $( "base" ).attr( "href" ) || "/dash/"; 15 | navigateTo( location.pathname.replace( baseUrl , "" ) , false ); 16 | 17 | window.onpopstate = onPopState; 18 | } 19 | 20 | function onPopState( event ) 21 | { 22 | navigateTo( location.pathname.replace( baseUrl , "" ) , false ); 23 | } 24 | 25 | function onPageChange( func ) 26 | { 27 | pageChangeCallbacks.push( func ); 28 | } 29 | 30 | function finishNavigateTo( newPage , data ) 31 | { 32 | currentPage = $( newPage ); 33 | currentPageNav = $( newPage + "Nav" ); 34 | 35 | currentPage.fadeIn( 200 , function() { 36 | isNavigating = false; 37 | currentPageNav.show(); 38 | } ); 39 | 40 | for( var i = 0; i < pageChangeCallbacks.length; i++ ) 41 | pageChangeCallbacks[i].call( null , newPage , data ); 42 | } 43 | 44 | function changePage( newPage , data ) 45 | { 46 | if( isNavigating === true ) return; 47 | isNavigating = true; 48 | 49 | if( currentPageNav !== null ) 50 | { 51 | currentPageNav.hide(); 52 | } 53 | 54 | if( currentPage === null ) 55 | { 56 | finishNavigateTo( newPage , data ); 57 | } 58 | else 59 | { 60 | currentPage.fadeOut( 200 , function() { 61 | finishNavigateTo( newPage , data ); 62 | } ); 63 | } 64 | } 65 | 66 | function navigateTo( path , pushState ) 67 | { 68 | if( pushState || pushState === undefined ) history.pushState( null , "" , path ); 69 | var pathData = path.split( "/" ); 70 | 71 | if( pathData[0] == "board" ) changePage( "#dashboardPage" , pathData[1] ); 72 | else changePage( "#dashboardListPage" ); 73 | } 74 | 75 | return { 76 | init : init, 77 | onPageChange : onPageChange, 78 | navigateTo : navigateTo 79 | }; 80 | 81 | } )(); 82 | -------------------------------------------------------------------------------- /static/js/plugins.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | 4 | App.Plugins = ( function() { 5 | 6 | var chartTypes = {}; 7 | var chartDependencies = []; 8 | 9 | function loadPlugins() 10 | { 11 | return $.get( "api/plugins" ).then( function( data ) { 12 | $( "body" ).append( data ); 13 | return loadDependencies(); 14 | } ); 15 | } 16 | 17 | function loadDependencies() 18 | { 19 | var dependencyDeferreds = []; 20 | 21 | for( var i = 0; i < chartDependencies.length; i++ ) 22 | { 23 | var ext = chartDependencies[i].substring( chartDependencies[i].lastIndexOf( "." ) + 1 ); 24 | if( ext == "js" ) 25 | { 26 | dependencyDeferreds.push( $.getScript( chartDependencies[i] ) ); 27 | } 28 | else if( ext == "css" ) 29 | { 30 | $( "head" ).append( '' ); 31 | } 32 | else 33 | { 34 | console.log( "Unknown dependency type: " + ext ); 35 | } 36 | } 37 | 38 | return $.when.apply( $ , dependencyDeferreds ); 39 | } 40 | 41 | function registerChartType( id , chart , options ) 42 | { 43 | if( chartTypes.hasOwnProperty( id ) ) 44 | { 45 | console.error( "Chart type " + id + " already registered." ); 46 | return; 47 | } 48 | 49 | options = options || {}; 50 | 51 | if( options.hasOwnProperty( "dependencies" ) && 52 | $.isArray( options.dependencies ) && 53 | options.dependencies.length > 0 ) 54 | { 55 | // TODO: Watch out for duplicate entries 56 | chartDependencies = chartDependencies.concat( options.dependencies ); 57 | } 58 | 59 | var plugin = { 60 | id : id, 61 | display_name : options.display_name || id, 62 | plugin : chart, 63 | chartConfig : options.chartConfig || {}, 64 | datasourceConfig : options.datasourceConfig || {}, 65 | disableComponentDiscovery : options.disableComponentDiscovery 66 | }; 67 | 68 | chartTypes[ id ] = plugin; 69 | } 70 | 71 | function getPlugin( id ) 72 | { 73 | return chartTypes.hasOwnProperty( id ) ? chartTypes[ id ] : null; 74 | } 75 | 76 | function getAllPlugins() 77 | { 78 | return chartTypes; 79 | } 80 | 81 | return { 82 | loadPlugins : loadPlugins, 83 | registerChartType : registerChartType, 84 | getPlugin : getPlugin, 85 | getAllPlugins : getAllPlugins 86 | }; 87 | 88 | } )(); 89 | -------------------------------------------------------------------------------- /static/js/settings.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | 4 | App.Settings = ( function() { 5 | 6 | function loadSettings() 7 | { 8 | var dfd = $.Deferred(); 9 | 10 | $.getJSON( "api/user/settings" ).done( function( data , status , xhr ) { 11 | if( data.hasOwnProperty( "dashboards" ) ) App.Model.Dashboard.unserializeAll( data.dashboards ); 12 | dfd.resolve(); 13 | } ); 14 | 15 | return dfd.promise(); 16 | } 17 | 18 | function saveSettings() 19 | { 20 | var settings = {}; 21 | settings.dashboards = App.Model.Dashboard.serializeAll(); 22 | 23 | $.ajax( { 24 | method : "POST", 25 | url : "api/user/settings", 26 | data : JSON.stringify( settings ), 27 | contentType : "text/plain" 28 | } ).done( function( data ) { 29 | if( data != "ok" ) 30 | { 31 | console.error( "Error saving settings: " + data ); 32 | } 33 | } ); 34 | } 35 | 36 | return { 37 | loadSettings : loadSettings, 38 | saveSettings : saveSettings 39 | }; 40 | 41 | } )(); 42 | -------------------------------------------------------------------------------- /static/js/view/dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.View = App.View || {}; 4 | 5 | App.View.Dashboard = ( function() { 6 | 7 | var DEFAULT_CHART_WIDTH = 2; 8 | var DEFAULT_CHART_HEIGHT = 1; 9 | 10 | var $gridList = null; 11 | 12 | var Modal = ( function() { 13 | 14 | var datasourceNextID = 0; 15 | 16 | function open( chart ) 17 | { 18 | datasourceNextID = 0; 19 | 20 | var key, i; 21 | 22 | var $modal = $( "#chartModal" ); 23 | var $modalTitle = $modal.find( ".modal-title" ); 24 | var $modalBody = $modal.find( ".modal-body" ); 25 | 26 | $( "#chartError" ).hide(); 27 | 28 | // Build plugins dropdown 29 | setSelectedPlugin(); 30 | var $pluginDropdown = $( "#chartPlugins" ); 31 | $pluginDropdown.empty(); 32 | var chartPlugins = App.Plugins.getAllPlugins(); 33 | for( key in chartPlugins ) 34 | { 35 | $pluginDropdown.append( '
  • ' + chartPlugins[ key ].display_name + '
  • ' ); 36 | } 37 | 38 | // Build datasources dropdown 39 | var $datasourceDropdown = $( "#chartDatasources" ); 40 | $datasourceDropdown.empty(); 41 | 42 | var datasources = App.Model.Datasource.datasources; 43 | for( key in datasources ) 44 | { 45 | $datasourceDropdown.append( '
  • ' + datasources[key].name + '
  • ' ); 46 | } 47 | 48 | $( "#chartDatasourceList" ).empty(); 49 | $( "#chartPluginConfig" ).empty(); 50 | 51 | if( !chart ) 52 | { 53 | $modalTitle.text( "Create New Chart" ); 54 | 55 | $( "#chartName" ).val( "" ); 56 | } 57 | else 58 | { 59 | $modalTitle.text( "Edit Chart" ); 60 | 61 | $( "#chartName" ).val( chart.name ); 62 | 63 | for( i = 0; i < chart.datasources.length; i++ ) 64 | { 65 | addDatasource( chart.datasources[i].datasource , chart.plugin , chart.datasources[i].config ); 66 | 67 | // TODO: Oh God fix this! 68 | $datasourceDropdown.find( 'a[data-dsid="' + chart.datasources[i].datasource.id + '"]' ).parent().remove(); 69 | } 70 | 71 | if( chart.plugin ) 72 | { 73 | loadPluginConfig( chart.plugin , chart ); 74 | } 75 | } 76 | 77 | $modal.one( "shown.bs.modal" , function() { 78 | $( "#chartName" ).focus(); 79 | } ); 80 | $modal.modal( "show" ); 81 | } 82 | 83 | function close() 84 | { 85 | $( "#chartModal" ).modal( "hide" ); 86 | } 87 | 88 | function showErrors( errors ) 89 | { 90 | $alertBox = $( "#chartError" ); 91 | $alertBox.html( errors.join( "
    " ) ); 92 | $alertBox.show(); 93 | } 94 | 95 | function setSelectedPlugin( plugin ) 96 | { 97 | var text = plugin ? plugin.display_name : "Select plugin"; 98 | $( "#chartPluginsButton" ).html( text + ' ' ); 99 | } 100 | 101 | function addDatasource( datasource , plugin , config ) 102 | { 103 | dsData = { 104 | id : datasource.id, 105 | name : datasource.name, 106 | uid : datasourceNextID++ 107 | }; 108 | 109 | var template = $.templates( "#tmpl_ChartDatasource" ); 110 | var $datasource = $( template.render( dsData ) ); 111 | $( "#chartDatasourceList" ).append( $datasource ); 112 | 113 | if( config ) 114 | { 115 | $datasource.find( "#nds" + dsData.uid + "-label" ).val( config.label ); 116 | } 117 | 118 | if( datasource.dataComponents && !plugin.disableComponentDiscovery ) 119 | { 120 | $datasource.find( "#nds" + dsData.uid + "-label" ).hide(); 121 | 122 | var dcTemplate = $.templates( "#tmpl_ChartDatasourceComponent" ); 123 | var $componentContainer = $( '
    ' ); 124 | $datasource.find( ".panel-body" ).prepend( $componentContainer ); 125 | for( var i = 0; i < datasource.dataComponents.length; i++ ) 126 | { 127 | var dcData = { name : datasource.dataComponents[i] }; 128 | $componentContainer.append( dcTemplate.render( dcData ) ); 129 | } 130 | 131 | if( config && config.components ) 132 | { 133 | for( var key in config.components ) 134 | { 135 | var $component = $componentContainer.find( 'div[data-component="' + key + '"]' ); 136 | if( $component.length ) 137 | { 138 | if( !config.components[key].enabled ) $component.find( "button" ).toggleClass( "btn-default btn-success" ); 139 | $component.find( "input" ).val( config.components[key].label ); 140 | } 141 | } 142 | } 143 | } 144 | 145 | if( plugin ) 146 | { 147 | $datasourceConfig = $datasource.find( ".datasourcePluginConfig" ); 148 | loadPluginDatasourceConfig( $datasourceConfig , plugin , config ); 149 | } 150 | 151 | var $panelHeading = $datasource.find( ".panel-heading" ); 152 | var $removeButton = $panelHeading.find( "button" ); 153 | $panelHeading.on( "click" , App.Controller.Dashboard.chartDatasourceHeaderClick ); 154 | $removeButton.on( "click" , App.Controller.Dashboard.chartDatasourceRemoveClick ); 155 | } 156 | 157 | function removeDatasource( $panel ) 158 | { 159 | var id = $panel.attr( "data-dsid" ); 160 | var name = $panel.attr( "data-dsname" ); 161 | 162 | var $datasourceDropdown = $( "#chartDatasources" ); 163 | $datasourceDropdown.append( '
  • ' + name + '
  • ' ); 164 | 165 | $panel.remove(); 166 | } 167 | 168 | function loadPluginConfig( plugin , chart ) 169 | { 170 | setSelectedPlugin( plugin ); 171 | 172 | var $container = $( "#chartPluginConfig" ); 173 | $container.empty(); 174 | 175 | $( "#chartDatasourceList > div" ).each( function() { 176 | var dsConfig = null; 177 | if( chart ) dsConfig = chart.datasourceMap[ $( this ).attr( "data-dsid" ) ].config; 178 | loadPluginDatasourceConfig( $( this ).find( ".datasourcePluginConfig" ) , plugin , dsConfig ); 179 | } ); 180 | 181 | var $template = $( 'script[data-chart-config="' + plugin.id + '"]' ); 182 | if( !$template.length ) return; 183 | 184 | $container.html( $template.text() ); 185 | 186 | var key; 187 | for( key in plugin.chartConfig ) 188 | { 189 | var $input = $container.find( '[data-prop="' + key + '"]' ); 190 | if( chart && chart.plugin === plugin && chart.config.hasOwnProperty( key ) ) 191 | { 192 | setInputValue( $input , chart.config[ key ] ); 193 | } 194 | else 195 | { 196 | setInputValue( $input , plugin.chartConfig[ key ].default ); 197 | } 198 | } 199 | } 200 | 201 | function loadPluginDatasourceConfig( $container , plugin , config ) 202 | { 203 | $container.empty(); 204 | 205 | $template = $( 'script[data-datasource-config="' + plugin.id + '"]' ); 206 | if( !$template.length ) return; 207 | 208 | $container.html( $template.text() ); 209 | 210 | for( var key in plugin.datasourceConfig ) 211 | { 212 | var $input = $container.find( '[data-prop="' + key + '"]' ); 213 | if( config && config.hasOwnProperty( key ) ) 214 | { 215 | setInputValue( $input , config[ key ] ); 216 | } 217 | else 218 | { 219 | setInputValue( $input , plugin.datasourceConfig[ key ].default ); 220 | } 221 | } 222 | } 223 | 224 | return { 225 | open : open, 226 | close : close, 227 | showErrors : showErrors, 228 | addDatasource : addDatasource, 229 | removeDatasource : removeDatasource, 230 | loadPluginConfig : loadPluginConfig 231 | }; 232 | 233 | } )(); 234 | 235 | function setPageTitle( title ) 236 | { 237 | $( "#titleDashboard" ).text( " : " + title ); 238 | } 239 | 240 | function createNewGridList() 241 | { 242 | $gridList = null; 243 | $( "#gridList" ).remove(); 244 | $( "#dashboardPage" ).append( '
    ' ); 245 | } 246 | 247 | function initGridList() 248 | { 249 | $gridList = $( "#gridList" ); 250 | $gridList.gridList( { 251 | rows : 4, 252 | vertical : true, 253 | widthHeightRatio : 0.62, 254 | onChange : App.Controller.Dashboard.gridListOnChange 255 | } , { 256 | handle : ".gridItemHeader", 257 | zIndex : 1000 258 | } ); 259 | } 260 | 261 | function createChartContainer( chart ) 262 | { 263 | var template = $.templates( "#tmpl_GridContainer" ); 264 | var $container = $( template.render( chart ) ); 265 | 266 | if( chart.pos ) 267 | { 268 | $container.attr( { 269 | 'data-x' : chart.pos.x, 270 | 'data-y' : chart.pos.y, 271 | 'data-w' : chart.pos.w, 272 | 'data-h' : chart.pos.h 273 | } ); 274 | 275 | if( !$gridList ) $( "#gridList" ).append( $container ); 276 | } 277 | else if( $gridList ) 278 | { 279 | var newPos = $gridList.gridList( "add" , $container , DEFAULT_CHART_WIDTH , DEFAULT_CHART_HEIGHT ); 280 | chart.pos = { 281 | x : newPos.x, 282 | y : newPos.y, 283 | w : DEFAULT_CHART_WIDTH, 284 | h : DEFAULT_CHART_HEIGHT 285 | }; 286 | } 287 | 288 | return $container.find( ".gridItemContent" ); 289 | } 290 | 291 | function updateChartContainer( chart ) 292 | { 293 | var $listItem = $( '#gridList li[data-id="' + chart.id + '"]' ); 294 | $listItem.find( ".gridItemHeader > span" ).text( chart.name ); 295 | 296 | var $content = $listItem.find( ".gridItemContent" ); 297 | $content.empty(); 298 | return $content; 299 | } 300 | 301 | function showRemoveOverlay( $container ) 302 | { 303 | $content = $container.find( ".gridItemContent" ); 304 | if( $content.find( ".gridItemOverlay" ).length > 0 ) return; 305 | 306 | $overlay = $( 307 | '' 311 | ); 312 | } 313 | 314 | function hideRemoveOverlay( $container ) 315 | { 316 | var $overlay = $container.find( ".gridItemOverlay" ); 317 | $overlay.fadeOut( 200 , ( function() { 318 | this.remove(); 319 | } ).bind( $overlay ) ); 320 | } 321 | 322 | function showMissingPlugin( $container , plugin_id ) 323 | { 324 | $container.append( '
    Missing plugin: ' + plugin_id + '
    ' ); 325 | } 326 | 327 | function showPendingDatasources( $container , datasources ) 328 | { 329 | var $alert = $container.find( ".alert" ); 330 | if( !$alert.length ) 331 | { 332 | $alert = $( '
    ' ); 333 | $container.append( $alert ); 334 | } 335 | 336 | var namesHtml = datasources.map( function( d ) { 337 | return d.name; 338 | } ).join( ', ' ); 339 | 340 | $alert.html( 'Waiting on datasources: ' + namesHtml ); 341 | } 342 | 343 | function getInputValue( $input ) 344 | { 345 | if( $input.attr( "type" ) === "checkbox" ) 346 | { 347 | return $input.prop( "checked" ); 348 | } 349 | return $input.val().trim(); 350 | } 351 | 352 | function setInputValue( $input , value ) 353 | { 354 | if( $input.attr( "type" ) === "checkbox" ) 355 | { 356 | $input.prop( "checked" , value ); 357 | } 358 | else 359 | { 360 | $input.val( value ); 361 | } 362 | } 363 | 364 | return { 365 | Modal : Modal, 366 | 367 | setPageTitle : setPageTitle, 368 | createNewGridList : createNewGridList, 369 | initGridList : initGridList, 370 | createChartContainer : createChartContainer, 371 | updateChartContainer : updateChartContainer, 372 | showRemoveOverlay : showRemoveOverlay, 373 | hideRemoveOverlay : hideRemoveOverlay, 374 | showMissingPlugin : showMissingPlugin, 375 | showPendingDatasources : showPendingDatasources, 376 | getInputValue : getInputValue 377 | }; 378 | 379 | } )(); 380 | -------------------------------------------------------------------------------- /static/js/view/dashboardlist.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.View = App.View || {}; 4 | 5 | App.View.DashboardList = ( function() { 6 | 7 | var Modal = ( function() { 8 | 9 | function open() 10 | { 11 | var $modal = $( "#dashboardModal" ); 12 | 13 | $( "#dashboardError" ).hide(); 14 | $( "#dashboardName" ).val( "" ); 15 | 16 | $modal.one( "shown.bs.modal" , function() { 17 | $( "#dashboardName" ).focus(); 18 | } ); 19 | $modal.modal( "show" ); 20 | } 21 | 22 | function close() 23 | { 24 | $( "#dashboardModal" ).modal( "hide" ); 25 | } 26 | 27 | function showErrors( errors ) 28 | { 29 | $alertBox = $( "#dashboardError" ); 30 | $alertBox.html( errors.join( "
    " ) ); 31 | $alertBox.show(); 32 | } 33 | 34 | return { 35 | open : open, 36 | close : close, 37 | showErrors : showErrors 38 | }; 39 | 40 | } )(); 41 | 42 | function render( dashboards ) 43 | { 44 | var $container = $( "#dashboardListPage" ); 45 | $container.empty(); 46 | 47 | if( dashboards.length > 0 ) 48 | { 49 | var template = $.templates( "#tmpl_dashboardItems" ); 50 | $container.append( template.render( dashboards ) ); 51 | } 52 | else 53 | { 54 | $container.append( '
    No dashboards available.
    ' ); 55 | } 56 | } 57 | 58 | return { 59 | Modal : Modal, 60 | 61 | render : render 62 | }; 63 | 64 | } )(); 65 | -------------------------------------------------------------------------------- /static/js/view/statusbar.js: -------------------------------------------------------------------------------- 1 | 2 | var App = App || {}; 3 | App.View = App.View || {}; 4 | 5 | App.View.Status = ( function() { 6 | 7 | function set( msg ) 8 | { 9 | var time = ( new Date() ).toLocaleTimeString(); 10 | $( "#statusBarMessage" ).text( "[" + time + "] " + msg ); 11 | } 12 | 13 | function clear() 14 | { 15 | $( "#statusBarMessage" ).html( " " ); 16 | } 17 | 18 | function setConnected( connected ) 19 | { 20 | var $target = $( ".statusBarConnect" ); 21 | if( connected ) $target.addClass( "connected" ); 22 | else $target.removeClass( "connected" ); 23 | } 24 | 25 | return { 26 | set : set, 27 | clear : clear, 28 | setConnected : setConnected 29 | }; 30 | 31 | } )(); 32 | -------------------------------------------------------------------------------- /static/plugins/alert/alert_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/plugins/alert/alert_bg.png -------------------------------------------------------------------------------- /static/plugins/alert/alert_glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM-IoT/node-red-contrib-graphs/95f10eb61b17d4306654cc70ca806391ed5ab255/static/plugins/alert/alert_glow.png -------------------------------------------------------------------------------- /static/plugins/lib/circleGauge.js: -------------------------------------------------------------------------------- 1 | 2 | var CG = CG || {}; 3 | 4 | CG.CircleGauge = function( container , options ) { 5 | this.container = container; 6 | 7 | var defaults = { 8 | innerColor : "#305683", 9 | outerColor : "#5178a6", 10 | textColor : "#ffffff", 11 | 12 | valueMin : 0, 13 | valueMax : 100, 14 | 15 | angleStart : 90, 16 | 17 | valueFormatter : function( v ) { return v.toFixed( 1 ) + "%"; } 18 | }; 19 | 20 | this.options = {}; 21 | 22 | var key; 23 | for( key in defaults ) 24 | { 25 | this.options[ key ] = options.hasOwnProperty( key ) ? options[ key ] : defaults[ key ]; 26 | } 27 | 28 | this.options.angleStart = this.options.angleStart * Math.PI / 180; 29 | 30 | this.width = this.height = 0; 31 | this.animationValue = this.value = this.options.valueMin; 32 | this.isAnimating = false; 33 | 34 | this.canvas = document.createElement( "canvas" ); 35 | this.container.appendChild( this.canvas ); 36 | 37 | this.ctx = this.canvas.getContext( "2d" ); 38 | 39 | this.resize(); 40 | }; 41 | 42 | CG.CircleGauge.prototype.resize = function() { 43 | this.width = this.container.clientWidth; 44 | this.height = this.container.clientHeight; 45 | 46 | if( this.width > this.height ) 47 | { 48 | this.canvas.style.marginLeft = Math.floor( ( this.width - this.height ) / 2 ) + "px"; 49 | this.canvas.style.marginTop = "0"; 50 | this.width = this.height; 51 | } 52 | else if( this.height > this.width ) 53 | { 54 | this.canvas.style.marginTop = Math.floor( ( this.height - this.width ) / 2 ) + "px"; 55 | this.canvas.style.marginLeft = "0"; 56 | this.height = this.width; 57 | } 58 | 59 | this.canvas.setAttribute( "width" , this.width ); 60 | this.canvas.setAttribute( "height" , this.height ); 61 | 62 | this.redraw(); 63 | }; 64 | 65 | CG.CircleGauge.prototype.redraw = function() { 66 | if( this.width === 0 || this.height === 0 ) return; 67 | 68 | var midx = Math.floor( this.width / 2 ); 69 | var midy = Math.floor( this.height / 2 ); 70 | 71 | this.ctx.clearRect( 0 , 0 , this.width , this.height ); 72 | 73 | this.ctx.beginPath(); 74 | this.ctx.fillStyle = this.options.innerColor; 75 | this.ctx.arc( midx , midy , midx * 0.6 , 0 , Math.PI * 2 ); 76 | this.ctx.fill(); 77 | 78 | if( this.animationValue !== this.options.valueMin ) 79 | { 80 | var currentAngle = ( this.animationValue - this.options.valueMin ) / ( this.options.valueMax - this.options.valueMin ); 81 | currentAngle = currentAngle * Math.PI * 2 + this.options.angleStart; 82 | 83 | this.ctx.beginPath(); 84 | this.ctx.fillStyle = this.options.outerColor; 85 | this.ctx.arc( midx , midy , midx * 0.67 , this.options.angleStart , currentAngle ); 86 | this.ctx.arc( midx , midy , midx * 0.8 , currentAngle , this.options.angleStart , true ); 87 | this.ctx.closePath(); 88 | this.ctx.fill(); 89 | } 90 | 91 | this.ctx.beginPath(); 92 | this.ctx.fillStyle = this.options.textColor; 93 | this.ctx.font = ( this.width / 10 ) + "px sans-serif"; 94 | this.ctx.textAlign = "center"; 95 | this.ctx.textBaseline = "middle"; 96 | this.ctx.fillText( this.options.valueFormatter( this.animationValue ) , midx , midy ); 97 | }; 98 | 99 | CG.CircleGauge.prototype.updateValue = function( newValue ) { 100 | this.value = Math.min( this.options.valueMax , Math.max( this.options.valueMin , newValue ) ); 101 | 102 | if( !this.isAnimating ) 103 | { 104 | this.isAnimating = true; 105 | setTimeout( this.updateAnimation.bind( this ) , 50 ); 106 | } 107 | }; 108 | 109 | CG.CircleGauge.prototype.updateAnimation = function() { 110 | this.animationValue += ( this.value - this.animationValue ) / 2; 111 | if( Math.abs( this.value - this.animationValue ) / ( this.options.valueMax - this.options.valueMin ) < 0.001 ) 112 | { 113 | this.animationValue = this.value; 114 | this.isAnimating = false; 115 | } 116 | else 117 | { 118 | setTimeout( this.updateAnimation.bind( this ) , 50 ); 119 | } 120 | this.redraw(); 121 | }; 122 | -------------------------------------------------------------------------------- /static/plugins/lib/justgage/justgage-1.1.0.min.js: -------------------------------------------------------------------------------- 1 | function getColor(e,t,i,o,n){var a,l,r,u,c,m,f,g,s,d,p,h,v,x,o=o||n.length>0;if(n.length>0)for(var b=0;bn[b].lo&&e<=n[b].hi)return n[b].color;if(a=i.length,1===a)return i[0];for(l=o?1/a:1/(a-1),r=[],b=0;b9)&&e.node.firstChild.attributes.dy&&(e.node.firstChild.attributes.dy.value=0)}function getRandomInt(e,t){return Math.floor(Math.random()*(t-e+1))+e}function cutHex(e){return"#"==e.charAt(0)?e.substring(1,7):e}function humanFriendlyNumber(e,t){var i,o,n,a;for(i=Math.pow,o=i(10,t),n=7;n;)a=i(10,3*n--),e>=a&&(e=Math.round(e*o/a)/o+"KMGTPE"[n]);return e}function formatNumber(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")}function getStyle(e,t){var i="";return document.defaultView&&document.defaultView.getComputedStyle?i=document.defaultView.getComputedStyle(e,"").getPropertyValue(t):e.currentStyle&&(t=t.replace(/\-(\w)/g,function(e,t){return t.toUpperCase()}),i=e.currentStyle[t]),i}function onCreateElementNsReady(e){void 0!==document.createElementNS?e():setTimeout(function(){onCreateElementNsReady(e)},100)}JustGage=function(e){var t=this;if(null===e||void 0===e)return console.log("* justgage: Make sure to pass options to the constructor!"),!1;var i;if(null!==e.id&&void 0!==e.id){if(i=document.getElementById(e.id),!i)return console.log("* justgage: No element with id : %s found",e.id),!1}else{if(null===e.parentNode||void 0===e.parentNode)return console.log("* justgage: Make sure to pass the existing element id or parentNode to the constructor."),!1;i=e.parentNode}var o=i.dataset?i.dataset:{};t.config={id:e.id,parentNode:t.kvLookup("parentNode",e,o,null),width:t.kvLookup("width",e,o,null),height:t.kvLookup("height",e,o,null),title:t.kvLookup("title",e,o,""),titleFontColor:t.kvLookup("titleFontColor",e,o,"#999999"),value:t.kvLookup("value",e,o,0,"float"),valueFontColor:t.kvLookup("valueFontColor",e,o,"#010101"),symbol:t.kvLookup("symbol",e,o,""),min:t.kvLookup("min",e,o,0,"float"),max:t.kvLookup("max",e,o,100,"float"),humanFriendlyDecimal:t.kvLookup("humanFriendlyDecimal",e,o,0),textRenderer:t.kvLookup("textRenderer",e,o,null),gaugeWidthScale:t.kvLookup("gaugeWidthScale",e,o,1),gaugeColor:t.kvLookup("gaugeColor",e,o,"#edebeb"),label:t.kvLookup("label",e,o,""),labelFontColor:t.kvLookup("labelFontColor",e,o,"#b3b3b3"),shadowOpacity:t.kvLookup("shadowOpacity",e,o,.2),shadowSize:t.kvLookup("shadowSize",e,o,5),shadowVerticalOffset:t.kvLookup("shadowVerticalOffset",e,o,3),levelColors:t.kvLookup("levelColors",e,o,["#a9d70b","#f9c802","#ff0000"],"array",","),startAnimationTime:t.kvLookup("startAnimationTime",e,o,700),startAnimationType:t.kvLookup("startAnimationType",e,o,">"),refreshAnimationTime:t.kvLookup("refreshAnimationTime",e,o,700),refreshAnimationType:t.kvLookup("refreshAnimationType",e,o,">"),donutStartAngle:t.kvLookup("donutStartAngle",e,o,90),valueMinFontSize:t.kvLookup("valueMinFontSize",e,o,16),titleMinFontSize:t.kvLookup("titleMinFontSize",e,o,10),labelMinFontSize:t.kvLookup("labelMinFontSize",e,o,10),minLabelMinFontSize:t.kvLookup("minLabelMinFontSize",e,o,10),maxLabelMinFontSize:t.kvLookup("maxLabelMinFontSize",e,o,10),hideValue:t.kvLookup("hideValue",e,o,!1),hideMinMax:t.kvLookup("hideMinMax",e,o,!1),hideInnerShadow:t.kvLookup("hideInnerShadow",e,o,!1),humanFriendly:t.kvLookup("humanFriendly",e,o,!1),noGradient:t.kvLookup("noGradient",e,o,!1),donut:t.kvLookup("donut",e,o,!1),relativeGaugeSize:t.kvLookup("relativeGaugeSize",e,o,!1),counter:t.kvLookup("counter",e,o,!1),decimals:t.kvLookup("decimals",e,o,0),customSectors:t.kvLookup("customSectors",e,o,[]),formatNumber:t.kvLookup("formatNumber",e,o,!1)};var n,a,l,r,u,c,m,f,g,s,d,p,h,v,x,b,y,k,S,F,M,L;t.config.value>t.config.max&&(t.config.value=t.config.max),t.config.valuea?(r=a,l=r):a>n?(l=n,r=l,r>a&&(u=r/a,r/=u,l=r/u)):(l=n,r=l),c=(n-l)/2,m=(a-r)/2,f=r/8>10?r/10:10,g=c+l/2,s=m+r/11,d=r/6.4>16?r/5.4:18,p=c+l/2,h=""!==t.config.label?m+r/1.85:m+r/1.7,v=r/16>10?r/16:10,x=c+l/2,b=h+v,y=r/16>10?r/16:10,k=c+l/10+l/6.666666666666667*t.config.gaugeWidthScale/2,S=b,F=r/16>10?r/16:10,M=c+l-l/10-l/6.666666666666667*t.config.gaugeWidthScale/2,L=b):(n>a?(r=a,l=1.25*r,l>n&&(u=l/n,l/=u,r/=u)):a>n?(l=n,r=l/1.25,r>a&&(u=r/a,r/=u,l=r/u)):(l=n,r=.75*l),c=(n-l)/2,m=(a-r)/2,f=r/8>t.config.titleMinFontSize?r/10:t.config.titleMinFontSize,g=c+l/2,s=m+r/6.4,d=r/6.5>t.config.valueMinFontSize?r/6.5:t.config.valueMinFontSize,p=c+l/2,h=m+r/1.275,v=r/16>t.config.labelMinFontSize?r/16:t.config.labelMinFontSize,x=c+l/2,b=h+d/2+5,y=r/16>t.config.minLabelMinFontSize?r/16:t.config.minLabelMinFontSize,k=c+l/10+l/6.666666666666667*t.config.gaugeWidthScale/2,S=b,F=r/16>t.config.maxLabelMinFontSize?r/16:t.config.maxLabelMinFontSize,M=c+l-l/10-l/6.666666666666667*t.config.gaugeWidthScale/2,L=b),t.params={canvasW:n,canvasH:a,widgetW:l,widgetH:r,dx:c,dy:m,titleFontSize:f,titleX:g,titleY:s,valueFontSize:d,valueX:p,valueY:h,labelFontSize:v,labelX:x,labelY:b,minFontSize:y,minX:k,minY:S,maxFontSize:F,maxX:M,maxY:L},L=null,t.canvas.customAttributes.pki=function(e,t,i,o,n,a,l,r,u){var c,m,f,g,s,d,p,h,v,x;return u?(c=(1-2*(e-t)/(i-t))*Math.PI,m=o/2-o/7,f=m-o/6.666666666666667*r,g=o/2+a,s=n/1.95+l,d=o/2+a+m*Math.cos(c),p=n-(n-s)-m*Math.sin(c),h=o/2+a+f*Math.cos(c),v=n-(n-s)-f*Math.sin(c),x="M"+(g-f)+","+s+" ",x+="L"+(g-m)+","+s+" ",e>(i-t)/2&&(x+="A"+m+","+m+" 0 0 1 "+(g+m)+","+s+" "),x+="A"+m+","+m+" 0 0 1 "+d+","+p+" ",x+="L"+h+","+v+" ",e>(i-t)/2&&(x+="A"+f+","+f+" 0 0 0 "+(g+f)+","+s+" "),x+="A"+f+","+f+" 0 0 0 "+(g-f)+","+s+" ",x+="Z ",{path:x}):(c=(1-(e-t)/(i-t))*Math.PI,m=o/2-o/10,f=m-o/6.666666666666667*r,g=o/2+a,s=n/1.25+l,d=o/2+a+m*Math.cos(c),p=n-(n-s)-m*Math.sin(c),h=o/2+a+f*Math.cos(c),v=n-(n-s)-f*Math.sin(c),x="M"+(g-f)+","+s+" ",x+="L"+(g-m)+","+s+" ",x+="A"+m+","+m+" 0 0 1 "+d+","+p+" ",x+="L"+h+","+v+" ",x+="A"+f+","+f+" 0 0 0 "+(g-f)+","+s+" ",x+="Z ",{path:x})},t.gauge=t.canvas.path().attr({stroke:"none",fill:t.config.gaugeColor,pki:[t.config.max,t.config.min,t.config.max,t.params.widgetW,t.params.widgetH,t.params.dx,t.params.dy,t.config.gaugeWidthScale,t.config.donut]}),t.level=t.canvas.path().attr({stroke:"none",fill:getColor(t.config.value,(t.config.value-t.config.min)/(t.config.max-t.config.min),t.config.levelColors,t.config.noGradient,t.config.customSectors),pki:[t.config.min,t.config.min,t.config.max,t.params.widgetW,t.params.widgetH,t.params.dx,t.params.dy,t.config.gaugeWidthScale,t.config.donut]}),t.config.donut&&t.level.transform("r"+t.config.donutStartAngle+", "+(t.params.widgetW/2+t.params.dx)+", "+(t.params.widgetH/1.95+t.params.dy)),t.txtTitle=t.canvas.text(t.params.titleX,t.params.titleY,t.config.title),t.txtTitle.attr({"font-size":t.params.titleFontSize,"font-weight":"bold","font-family":"Arial",fill:t.config.titleFontColor,"fill-opacity":"1"}),setDy(t.txtTitle,t.params.titleFontSize,t.params.titleY),t.txtValue=t.canvas.text(t.params.valueX,t.params.valueY,0),t.txtValue.attr({"font-size":t.params.valueFontSize,"font-weight":"bold","font-family":"Arial",fill:t.config.valueFontColor,"fill-opacity":"0"}),setDy(t.txtValue,t.params.valueFontSize,t.params.valueY),t.txtLabel=t.canvas.text(t.params.labelX,t.params.labelY,t.config.label),t.txtLabel.attr({"font-size":t.params.labelFontSize,"font-weight":"normal","font-family":"Arial",fill:t.config.labelFontColor,"fill-opacity":"0"}),setDy(t.txtLabel,t.params.labelFontSize,t.params.labelY),t.txtMinimum=t.config.min,t.config.humanFriendly?t.txtMinimum=humanFriendlyNumber(t.config.min,t.config.humanFriendlyDecimal):t.config.formatNumber&&(t.txtMinimum=formatNumber(t.config.min)),t.txtMin=t.canvas.text(t.params.minX,t.params.minY,t.txtMinimum),t.txtMin.attr({"font-size":t.params.minFontSize,"font-weight":"normal","font-family":"Arial",fill:t.config.labelFontColor,"fill-opacity":t.config.hideMinMax||t.config.donut?"0":"1"}),setDy(t.txtMin,t.params.minFontSize,t.params.minY),t.txtMaximum=t.config.max,t.config.formatNumber?t.txtMaximum=formatNumber(t.txtMaximum):t.config.humanFriendly&&(t.txtMaximum=humanFriendlyNumber(t.config.max,t.config.humanFriendlyDecimal)),t.txtMax=t.canvas.text(t.params.maxX,t.params.maxY,t.txtMaximum),t.txtMax.attr({"font-size":t.params.maxFontSize,"font-weight":"normal","font-family":"Arial",fill:t.config.labelFontColor,"fill-opacity":t.config.hideMinMax||t.config.donut?"0":"1"}),setDy(t.txtMax,t.params.maxFontSize,t.params.maxY);var w=t.canvas.canvas.childNodes[1],A="http://www.w3.org/2000/svg";"undefined"!==ie&&9>ie||("undefined"!==ie?onCreateElementNsReady(function(){t.generateShadow(A,w)}):t.generateShadow(A,w)),A=null,t.config.textRenderer?t.originalValue=t.config.textRenderer(t.originalValue):t.config.humanFriendly?t.originalValue=humanFriendlyNumber(t.originalValue,t.config.humanFriendlyDecimal)+t.config.symbol:t.config.formatNumber?t.originalValue=formatNumber(t.originalValue)+t.config.symbol:t.originalValue=(1*t.originalValue).toFixed(t.config.decimals)+t.config.symbol,t.config.counter===!0?(eve.on("raphael.anim.frame."+t.level.id,function(){var e=t.level.attr("pki");t.config.textRenderer?t.txtValue.attr("text",t.config.textRenderer(Math.floor(e[0]))):t.config.humanFriendly?t.txtValue.attr("text",humanFriendlyNumber(Math.floor(e[0]),t.config.humanFriendlyDecimal)+t.config.symbol):t.config.formatNumber?t.txtValue.attr("text",formatNumber(Math.floor(e[0]))+t.config.symbol):t.txtValue.attr("text",(1*e[0]).toFixed(t.config.decimals)+t.config.symbol),setDy(t.txtValue,t.params.valueFontSize,t.params.valueY),e=null}),eve.on("raphael.anim.finish."+t.level.id,function(){t.txtValue.attr({text:t.originalValue}),setDy(t.txtValue,t.params.valueFontSize,t.params.valueY)})):eve.on("raphael.anim.start."+t.level.id,function(){t.txtValue.attr({text:t.originalValue}),setDy(t.txtValue,t.params.valueFontSize,t.params.valueY)}),t.level.animate({pki:[t.config.value,t.config.min,t.config.max,t.params.widgetW,t.params.widgetH,t.params.dx,t.params.dy,t.config.gaugeWidthScale,t.config.donut]},t.config.startAnimationTime,t.config.startAnimationType),t.txtValue.animate({"fill-opacity":t.config.hideValue?"0":"1"},t.config.startAnimationTime,t.config.startAnimationType),t.txtLabel.animate({"fill-opacity":"1"},t.config.startAnimationTime,t.config.startAnimationType)},JustGage.prototype.kvLookup=function(e,t,i,o,n,a){var l=o,r=!1;if(null!==e&&void 0!==e&&(null!==i&&void 0!==i&&"object"==typeof i&&e in i?(l=i[e],r=!0):null!==t&&void 0!==t&&"object"==typeof t&&e in t?(l=t[e],r=!0):l=o,r===!0&&null!==n&&void 0!==n))switch(n){case"int":l=parseInt(l,10);break;case"float":l=parseFloat(l)}return l},JustGage.prototype.refresh=function(e,t){var i,o,n=this,t=t||null;null!==t&&(n.config.max=t,n.txtMaximum=n.config.max,n.config.humanFriendly?n.txtMaximum=humanFriendlyNumber(n.config.max,n.config.humanFriendlyDecimal):n.config.formatNumber&&(n.txtMaximum=formatNumber(n.config.max)),n.txtMax.attr({text:n.txtMaximum}),setDy(n.txtMax,n.params.maxFontSize,n.params.maxY)),i=e,1*e>1*n.config.max&&(e=1*n.config.max),1*e<1*n.config.min&&(e=1*n.config.min),o=getColor(e,(e-n.config.min)/(n.config.max-n.config.min),n.config.levelColors,n.config.noGradient,n.config.customSectors),i=n.config.textRenderer?n.config.textRenderer(i):n.config.humanFriendly?humanFriendlyNumber(i,n.config.humanFriendlyDecimal)+n.config.symbol:n.config.formatNumber?formatNumber((1*i).toFixed(n.config.decimals))+n.config.symbol:(1*i).toFixed(n.config.decimals)+n.config.symbol,n.originalValue=i,n.config.value=1*e,n.config.counter||(n.txtValue.attr({text:i}),setDy(n.txtValue,n.params.valueFontSize,n.params.valueY)),n.level.animate({pki:[n.config.value,n.config.min,n.config.max,n.params.widgetW,n.params.widgetH,n.params.dx,n.params.dy,n.config.gaugeWidthScale,n.config.donut],fill:o},n.config.refreshAnimationTime,n.config.refreshAnimationType),t=null},JustGage.prototype.generateShadow=function(e,t){var i,o,n,a,l,r,u,c=this;i=document.createElementNS(e,"filter"),i.setAttribute("id","inner-shadow"),t.appendChild(i),o=document.createElementNS(e,"feOffset"),o.setAttribute("dx",0),o.setAttribute("dy",c.config.shadowVerticalOffset),i.appendChild(o),n=document.createElementNS(e,"feGaussianBlur"),n.setAttribute("result","offset-blur"),n.setAttribute("stdDeviation",c.config.shadowSize),i.appendChild(n),a=document.createElementNS(e,"feComposite"),a.setAttribute("operator","out"),a.setAttribute("in","SourceGraphic"),a.setAttribute("in2","offset-blur"),a.setAttribute("result","inverse"),i.appendChild(a),l=document.createElementNS(e,"feFlood"),l.setAttribute("flood-color","black"),l.setAttribute("flood-opacity",c.config.shadowOpacity),l.setAttribute("result","color"),i.appendChild(l),r=document.createElementNS(e,"feComposite"),r.setAttribute("operator","in"),r.setAttribute("in","color"),r.setAttribute("in2","inverse"),r.setAttribute("result","shadow"),i.appendChild(r),u=document.createElementNS(e,"feComposite"),u.setAttribute("operator","over"),u.setAttribute("in","shadow"),u.setAttribute("in2","SourceGraphic"),i.appendChild(u),c.config.hideInnerShadow||(c.canvas.canvas.childNodes[2].setAttribute("filter","url(#inner-shadow)"),c.canvas.canvas.childNodes[3].setAttribute("filter","url(#inner-shadow)")),u=null};var ie=function(){for(var e,t=3,i=document.createElement("div"),o=i.getElementsByTagName("i");i.innerHTML="",o[0];);return t>4?t:e}(); -------------------------------------------------------------------------------- /static/plugins/lib/nvd3/nv.d3.css: -------------------------------------------------------------------------------- 1 | /* nvd3 version 1.8.1 (https://github.com/novus/nvd3) 2015-06-15 */ 2 | .nvd3 .nv-axis { 3 | pointer-events:none; 4 | opacity: 1; 5 | } 6 | 7 | .nvd3 .nv-axis path { 8 | fill: none; 9 | stroke: #000; 10 | stroke-opacity: .75; 11 | shape-rendering: crispEdges; 12 | } 13 | 14 | .nvd3 .nv-axis path.domain { 15 | stroke-opacity: .75; 16 | } 17 | 18 | .nvd3 .nv-axis.nv-x path.domain { 19 | stroke-opacity: 0; 20 | } 21 | 22 | .nvd3 .nv-axis line { 23 | fill: none; 24 | stroke: #e5e5e5; 25 | shape-rendering: crispEdges; 26 | } 27 | 28 | .nvd3 .nv-axis .zero line, 29 | /*this selector may not be necessary*/ .nvd3 .nv-axis line.zero { 30 | stroke-opacity: .75; 31 | } 32 | 33 | .nvd3 .nv-axis .nv-axisMaxMin text { 34 | font-weight: bold; 35 | } 36 | 37 | .nvd3 .x .nv-axis .nv-axisMaxMin text, 38 | .nvd3 .x2 .nv-axis .nv-axisMaxMin text, 39 | .nvd3 .x3 .nv-axis .nv-axisMaxMin text { 40 | text-anchor: middle 41 | } 42 | 43 | .nvd3 .nv-axis.nv-disabled { 44 | opacity: 0; 45 | } 46 | 47 | .nvd3 .nv-bars rect { 48 | fill-opacity: .75; 49 | 50 | transition: fill-opacity 250ms linear; 51 | -moz-transition: fill-opacity 250ms linear; 52 | -webkit-transition: fill-opacity 250ms linear; 53 | } 54 | 55 | .nvd3 .nv-bars rect.hover { 56 | fill-opacity: 1; 57 | } 58 | 59 | .nvd3 .nv-bars .hover rect { 60 | fill: lightblue; 61 | } 62 | 63 | .nvd3 .nv-bars text { 64 | fill: rgba(0,0,0,0); 65 | } 66 | 67 | .nvd3 .nv-bars .hover text { 68 | fill: rgba(0,0,0,1); 69 | } 70 | 71 | .nvd3 .nv-multibar .nv-groups rect, 72 | .nvd3 .nv-multibarHorizontal .nv-groups rect, 73 | .nvd3 .nv-discretebar .nv-groups rect { 74 | stroke-opacity: 0; 75 | 76 | transition: fill-opacity 250ms linear; 77 | -moz-transition: fill-opacity 250ms linear; 78 | -webkit-transition: fill-opacity 250ms linear; 79 | } 80 | 81 | .nvd3 .nv-multibar .nv-groups rect:hover, 82 | .nvd3 .nv-multibarHorizontal .nv-groups rect:hover, 83 | .nvd3 .nv-candlestickBar .nv-ticks rect:hover, 84 | .nvd3 .nv-discretebar .nv-groups rect:hover { 85 | fill-opacity: 1; 86 | } 87 | 88 | .nvd3 .nv-discretebar .nv-groups text, 89 | .nvd3 .nv-multibarHorizontal .nv-groups text { 90 | font-weight: bold; 91 | fill: rgba(0,0,0,1); 92 | stroke: rgba(0,0,0,0); 93 | } 94 | 95 | /* boxplot CSS */ 96 | .nvd3 .nv-boxplot circle { 97 | fill-opacity: 0.5; 98 | } 99 | 100 | .nvd3 .nv-boxplot circle:hover { 101 | fill-opacity: 1; 102 | } 103 | 104 | .nvd3 .nv-boxplot rect:hover { 105 | fill-opacity: 1; 106 | } 107 | 108 | .nvd3 line.nv-boxplot-median { 109 | stroke: black; 110 | } 111 | 112 | .nv-boxplot-tick:hover { 113 | stroke-width: 2.5px; 114 | } 115 | /* bullet */ 116 | .nvd3.nv-bullet { font: 10px sans-serif; } 117 | .nvd3.nv-bullet .nv-measure { fill-opacity: .8; } 118 | .nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; } 119 | .nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; } 120 | .nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; } 121 | .nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; } 122 | .nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; } 123 | .nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; } 124 | .nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; } 125 | .nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; } 126 | .nvd3.nv-bullet .nv-subtitle { fill: #999; } 127 | 128 | 129 | .nvd3.nv-bullet .nv-range { 130 | fill: #bababa; 131 | fill-opacity: .4; 132 | } 133 | .nvd3.nv-bullet .nv-range:hover { 134 | fill-opacity: .7; 135 | } 136 | 137 | .nvd3.nv-candlestickBar .nv-ticks .nv-tick { 138 | stroke-width: 1px; 139 | } 140 | 141 | .nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover { 142 | stroke-width: 2px; 143 | } 144 | 145 | .nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect { 146 | stroke: #2ca02c; 147 | fill: #2ca02c; 148 | } 149 | 150 | .nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect { 151 | stroke: #d62728; 152 | fill: #d62728; 153 | } 154 | 155 | .with-transitions .nv-candlestickBar .nv-ticks .nv-tick { 156 | transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 157 | -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 158 | -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 159 | 160 | } 161 | 162 | .nvd3.nv-candlestickBar .nv-ticks line { 163 | stroke: #333; 164 | } 165 | 166 | 167 | .nvd3 .nv-legend .nv-disabled rect { 168 | /*fill-opacity: 0;*/ 169 | } 170 | 171 | .nvd3 .nv-check-box .nv-box { 172 | fill-opacity:0; 173 | stroke-width:2; 174 | } 175 | 176 | .nvd3 .nv-check-box .nv-check { 177 | fill-opacity:0; 178 | stroke-width:4; 179 | } 180 | 181 | .nvd3 .nv-series.nv-disabled .nv-check-box .nv-check { 182 | fill-opacity:0; 183 | stroke-opacity:0; 184 | } 185 | 186 | .nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check { 187 | opacity: 0; 188 | } 189 | 190 | /* line plus bar */ 191 | .nvd3.nv-linePlusBar .nv-bar rect { 192 | fill-opacity: .75; 193 | } 194 | 195 | .nvd3.nv-linePlusBar .nv-bar rect:hover { 196 | fill-opacity: 1; 197 | } 198 | .nvd3 .nv-groups path.nv-line { 199 | fill: none; 200 | } 201 | 202 | .nvd3 .nv-groups path.nv-area { 203 | stroke: none; 204 | } 205 | 206 | .nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point { 207 | fill-opacity: 0; 208 | stroke-opacity: 0; 209 | } 210 | 211 | .nvd3.nv-scatter.nv-single-point .nv-groups .nv-point { 212 | fill-opacity: .5 !important; 213 | stroke-opacity: .5 !important; 214 | } 215 | 216 | 217 | .with-transitions .nvd3 .nv-groups .nv-point { 218 | transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 219 | -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 220 | -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; 221 | 222 | } 223 | 224 | .nvd3.nv-scatter .nv-groups .nv-point.hover, 225 | .nvd3 .nv-groups .nv-point.hover { 226 | stroke-width: 7px; 227 | fill-opacity: .95 !important; 228 | stroke-opacity: .95 !important; 229 | } 230 | 231 | 232 | .nvd3 .nv-point-paths path { 233 | stroke: #aaa; 234 | stroke-opacity: 0; 235 | fill: #eee; 236 | fill-opacity: 0; 237 | } 238 | 239 | 240 | 241 | .nvd3 .nv-indexLine { 242 | cursor: ew-resize; 243 | } 244 | 245 | /******************** 246 | * SVG CSS 247 | */ 248 | 249 | /******************** 250 | Default CSS for an svg element nvd3 used 251 | */ 252 | svg.nvd3-svg { 253 | -webkit-touch-callout: none; 254 | -webkit-user-select: none; 255 | -khtml-user-select: none; 256 | -ms-user-select: none; 257 | -moz-user-select: none; 258 | user-select: none; 259 | display: block; 260 | width:100%; 261 | height:100%; 262 | } 263 | 264 | /******************** 265 | Box shadow and border radius styling 266 | */ 267 | .nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip { 268 | -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); 269 | -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); 270 | box-shadow: 0 5px 10px rgba(0,0,0,.2); 271 | 272 | -webkit-border-radius: 5px; 273 | -moz-border-radius: 5px; 274 | border-radius: 5px; 275 | } 276 | 277 | 278 | .nvd3 text { 279 | font: normal 12px Arial; 280 | } 281 | 282 | .nvd3 .title { 283 | font: bold 14px Arial; 284 | } 285 | 286 | .nvd3 .nv-background { 287 | fill: white; 288 | fill-opacity: 0; 289 | } 290 | 291 | .nvd3.nv-noData { 292 | font-size: 18px; 293 | font-weight: bold; 294 | } 295 | 296 | 297 | /********** 298 | * Brush 299 | */ 300 | 301 | .nv-brush .extent { 302 | fill-opacity: .125; 303 | shape-rendering: crispEdges; 304 | } 305 | 306 | .nv-brush .resize path { 307 | fill: #eee; 308 | stroke: #666; 309 | } 310 | 311 | 312 | /********** 313 | * Legend 314 | */ 315 | 316 | .nvd3 .nv-legend .nv-series { 317 | cursor: pointer; 318 | } 319 | 320 | .nvd3 .nv-legend .nv-disabled circle { 321 | fill-opacity: 0; 322 | } 323 | 324 | /* focus */ 325 | .nvd3 .nv-brush .extent { 326 | fill-opacity: 0 !important; 327 | } 328 | 329 | .nvd3 .nv-brushBackground rect { 330 | stroke: #000; 331 | stroke-width: .4; 332 | fill: #fff; 333 | fill-opacity: .7; 334 | } 335 | 336 | 337 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick { 338 | stroke-width: 1px; 339 | } 340 | 341 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover { 342 | stroke-width: 2px; 343 | } 344 | 345 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive { 346 | stroke: #2ca02c; 347 | } 348 | 349 | .nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative { 350 | stroke: #d62728; 351 | } 352 | 353 | 354 | .nvd3 .background path { 355 | fill: none; 356 | stroke: #EEE; 357 | stroke-opacity: .4; 358 | shape-rendering: crispEdges; 359 | } 360 | 361 | .nvd3 .foreground path { 362 | fill: none; 363 | stroke-opacity: .7; 364 | } 365 | 366 | .nvd3 .nv-parallelCoordinates-brush .extent 367 | { 368 | fill: #fff; 369 | fill-opacity: .6; 370 | stroke: gray; 371 | shape-rendering: crispEdges; 372 | } 373 | 374 | .nvd3 .nv-parallelCoordinates .hover { 375 | fill-opacity: 1; 376 | stroke-width: 3px; 377 | } 378 | 379 | 380 | .nvd3 .missingValuesline line { 381 | fill: none; 382 | stroke: black; 383 | stroke-width: 1; 384 | stroke-opacity: 1; 385 | stroke-dasharray: 5, 5; 386 | } 387 | .nvd3.nv-pie path { 388 | stroke-opacity: 0; 389 | transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 390 | -moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 391 | -webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; 392 | 393 | } 394 | 395 | .nvd3.nv-pie .nv-pie-title { 396 | font-size: 24px; 397 | fill: rgba(19, 196, 249, 0.59); 398 | } 399 | 400 | .nvd3.nv-pie .nv-slice text { 401 | stroke: #000; 402 | stroke-width: 0; 403 | } 404 | 405 | .nvd3.nv-pie path { 406 | stroke: #fff; 407 | stroke-width: 1px; 408 | stroke-opacity: 1; 409 | } 410 | 411 | .nvd3.nv-pie .hover path { 412 | fill-opacity: .7; 413 | } 414 | .nvd3.nv-pie .nv-label { 415 | pointer-events: none; 416 | } 417 | .nvd3.nv-pie .nv-label rect { 418 | fill-opacity: 0; 419 | stroke-opacity: 0; 420 | } 421 | 422 | /* scatter */ 423 | .nvd3 .nv-groups .nv-point.hover { 424 | stroke-width: 20px; 425 | stroke-opacity: .5; 426 | } 427 | 428 | .nvd3 .nv-scatter .nv-point.hover { 429 | fill-opacity: 1; 430 | } 431 | .nv-noninteractive { 432 | pointer-events: none; 433 | } 434 | 435 | .nv-distx, .nv-disty { 436 | pointer-events: none; 437 | } 438 | 439 | /* sparkline */ 440 | .nvd3.nv-sparkline path { 441 | fill: none; 442 | } 443 | 444 | .nvd3.nv-sparklineplus g.nv-hoverValue { 445 | pointer-events: none; 446 | } 447 | 448 | .nvd3.nv-sparklineplus .nv-hoverValue line { 449 | stroke: #333; 450 | stroke-width: 1.5px; 451 | } 452 | 453 | .nvd3.nv-sparklineplus, 454 | .nvd3.nv-sparklineplus g { 455 | pointer-events: all; 456 | } 457 | 458 | .nvd3 .nv-hoverArea { 459 | fill-opacity: 0; 460 | stroke-opacity: 0; 461 | } 462 | 463 | .nvd3.nv-sparklineplus .nv-xValue, 464 | .nvd3.nv-sparklineplus .nv-yValue { 465 | stroke-width: 0; 466 | font-size: .9em; 467 | font-weight: normal; 468 | } 469 | 470 | .nvd3.nv-sparklineplus .nv-yValue { 471 | stroke: #f66; 472 | } 473 | 474 | .nvd3.nv-sparklineplus .nv-maxValue { 475 | stroke: #2ca02c; 476 | fill: #2ca02c; 477 | } 478 | 479 | .nvd3.nv-sparklineplus .nv-minValue { 480 | stroke: #d62728; 481 | fill: #d62728; 482 | } 483 | 484 | .nvd3.nv-sparklineplus .nv-currentValue { 485 | font-weight: bold; 486 | font-size: 1.1em; 487 | } 488 | /* stacked area */ 489 | .nvd3.nv-stackedarea path.nv-area { 490 | fill-opacity: .7; 491 | stroke-opacity: 0; 492 | transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 493 | -moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 494 | -webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; 495 | } 496 | 497 | .nvd3.nv-stackedarea path.nv-area.hover { 498 | fill-opacity: .9; 499 | } 500 | 501 | 502 | .nvd3.nv-stackedarea .nv-groups .nv-point { 503 | stroke-opacity: 0; 504 | fill-opacity: 0; 505 | } 506 | 507 | 508 | .nvtooltip { 509 | position: absolute; 510 | background-color: rgba(255,255,255,1.0); 511 | color: rgba(0,0,0,1.0); 512 | padding: 1px; 513 | border: 1px solid rgba(0,0,0,.2); 514 | z-index: 10000; 515 | display: block; 516 | 517 | font-family: Arial; 518 | font-size: 13px; 519 | text-align: left; 520 | pointer-events: none; 521 | 522 | white-space: nowrap; 523 | 524 | -webkit-touch-callout: none; 525 | -webkit-user-select: none; 526 | -khtml-user-select: none; 527 | -moz-user-select: none; 528 | -ms-user-select: none; 529 | user-select: none; 530 | } 531 | 532 | .nvtooltip { 533 | background: rgba(255,255,255, 0.8); 534 | border: 1px solid rgba(0,0,0,0.5); 535 | border-radius: 4px; 536 | } 537 | 538 | /*Give tooltips that old fade in transition by 539 | putting a "with-transitions" class on the container div. 540 | */ 541 | .nvtooltip.with-transitions, .with-transitions .nvtooltip { 542 | transition: opacity 50ms linear; 543 | -moz-transition: opacity 50ms linear; 544 | -webkit-transition: opacity 50ms linear; 545 | 546 | transition-delay: 200ms; 547 | -moz-transition-delay: 200ms; 548 | -webkit-transition-delay: 200ms; 549 | } 550 | 551 | .nvtooltip.x-nvtooltip, 552 | .nvtooltip.y-nvtooltip { 553 | padding: 8px; 554 | } 555 | 556 | .nvtooltip h3 { 557 | margin: 0; 558 | padding: 4px 14px; 559 | line-height: 18px; 560 | font-weight: normal; 561 | background-color: rgba(247,247,247,0.75); 562 | color: rgba(0,0,0,1.0); 563 | text-align: center; 564 | 565 | border-bottom: 1px solid #ebebeb; 566 | 567 | -webkit-border-radius: 5px 5px 0 0; 568 | -moz-border-radius: 5px 5px 0 0; 569 | border-radius: 5px 5px 0 0; 570 | } 571 | 572 | .nvtooltip p { 573 | margin: 0; 574 | padding: 5px 14px; 575 | text-align: center; 576 | } 577 | 578 | .nvtooltip span { 579 | display: inline-block; 580 | margin: 2px 0; 581 | } 582 | 583 | .nvtooltip table { 584 | margin: 6px; 585 | border-spacing:0; 586 | } 587 | 588 | 589 | .nvtooltip table td { 590 | padding: 2px 9px 2px 0; 591 | vertical-align: middle; 592 | } 593 | 594 | .nvtooltip table td.key { 595 | font-weight:normal; 596 | } 597 | .nvtooltip table td.value { 598 | text-align: right; 599 | font-weight: bold; 600 | } 601 | 602 | .nvtooltip table tr.highlight td { 603 | padding: 1px 9px 1px 0; 604 | border-bottom-style: solid; 605 | border-bottom-width: 1px; 606 | border-top-style: solid; 607 | border-top-width: 1px; 608 | } 609 | 610 | .nvtooltip table td.legend-color-guide div { 611 | width: 8px; 612 | height: 8px; 613 | vertical-align: middle; 614 | } 615 | 616 | .nvtooltip table td.legend-color-guide div { 617 | width: 12px; 618 | height: 12px; 619 | border: 1px solid #999; 620 | } 621 | 622 | .nvtooltip .footer { 623 | padding: 3px; 624 | text-align: center; 625 | } 626 | 627 | .nvtooltip-pending-removal { 628 | pointer-events: none; 629 | display: none; 630 | } 631 | 632 | 633 | /**** 634 | Interactive Layer 635 | */ 636 | .nvd3 .nv-interactiveGuideLine { 637 | pointer-events:none; 638 | } 639 | .nvd3 line.nv-guideline { 640 | stroke: #ccc; 641 | } -------------------------------------------------------------------------------- /template/index.mst: -------------------------------------------------------------------------------- 1 | {{=<% %>=}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Dashboard 36 | 37 | 38 | 58 | 59 |
    60 |
     
    61 |
    62 |
    63 | 64 |
    65 | 66 |
    67 | 68 |
    69 |
    70 | 71 | 92 | 93 | 138 | 139 | 145 | 146 | 158 | 159 | 174 | 175 | 185 | 186 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /users.js: -------------------------------------------------------------------------------- 1 | 2 | var libURL = require( "url" ); 3 | var fs = require( "fs" ); 4 | var path = require( "path" ); 5 | var express = require( "express" ); 6 | 7 | var app = express(); 8 | 9 | var settings = {}; 10 | var cfgDir; 11 | 12 | function init( RED ) 13 | { 14 | var oldCfgDir = __dirname + "/.dash"; 15 | cfgDir = path.join( RED.settings.userDir , ".dash" ); 16 | 17 | try 18 | { 19 | fs.mkdirSync( cfgDir ); 20 | } 21 | catch( e ) 22 | { 23 | if( e.code != "EEXIST" ) 24 | { 25 | console.error( "Unable to create .dash dir in " + __dirname ); 26 | return; 27 | } 28 | } 29 | 30 | var oldCfgData = null; 31 | try 32 | { 33 | oldCfgData = fs.readFileSync( path.join( oldCfgDir , "config_default.json" ) ); 34 | } 35 | catch( e ) {} 36 | 37 | if( oldCfgData ) 38 | { 39 | try 40 | { 41 | fs.unlinkSync( path.join( oldCfgDir , "config_default.json" ) ); 42 | fs.rmdirSync( oldCfgDir ); 43 | fs.writeFileSync( getSettingsFilename( "default" ) , oldCfgData ); 44 | RED.log.info( "Moved dashboard config to " + cfgDir ); 45 | } 46 | catch( e ) 47 | { 48 | console.error( "Unable to move old config file: " + e.message ); 49 | } 50 | } 51 | 52 | app.get( "/settings" , function( request , response ) { 53 | 54 | response.setHeader( "Content-Type" , "application/json" ); 55 | response.end( JSON.stringify( getSettings( "default" ) ) ); 56 | 57 | } ); 58 | 59 | // TODO: Not do this... 60 | app.post( "/settings" , function( request , response ) { 61 | 62 | var data = ""; 63 | 64 | request.on( "data" , function( chunk ) { 65 | data += chunk; 66 | } ); 67 | 68 | request.on( "end" , function() { 69 | try 70 | { 71 | settings.default = JSON.parse( data ); 72 | saveSettings( "default" ); 73 | response.end( "ok" ); 74 | } 75 | catch( e ) 76 | { 77 | response.end( e.message ); 78 | } 79 | } ); 80 | 81 | } ); 82 | } 83 | 84 | function getSettings( id ) 85 | { 86 | if( !settings.hasOwnProperty( id ) ) 87 | { 88 | var loadedSettings = loadSettings( id ); 89 | settings[ id ] = ( loadedSettings === null ? {} : loadedSettings ); 90 | } 91 | return settings[ id ]; 92 | } 93 | 94 | function loadSettings( id ) 95 | { 96 | try 97 | { 98 | return JSON.parse( fs.readFileSync( getSettingsFilename( id ) ) ); 99 | } 100 | catch( e ) 101 | { 102 | console.log( "Unable to load settings file '" + id + "': " + e.message ); 103 | } 104 | 105 | return null; 106 | } 107 | 108 | function saveSettings( id ) 109 | { 110 | if( !settings.hasOwnProperty( id ) ) return; 111 | 112 | try 113 | { 114 | fs.writeFileSync( getSettingsFilename( id ) , JSON.stringify( settings[ id ] , null , '\t' ) ); 115 | } 116 | catch( e ) 117 | { 118 | console.log( "Unable to save settings '" + id + "': " + e.message ); 119 | } 120 | } 121 | 122 | function getSettingsFilename( id ) 123 | { 124 | return cfgDir + "/config_" + id + ".json"; 125 | } 126 | 127 | module.exports = { 128 | app : app, 129 | 130 | init : init 131 | }; 132 | --------------------------------------------------------------------------------