├── .cfignore ├── .gitignore ├── public ├── images │ ├── monogram-wdmk.png │ ├── wind-turbine-2.png │ └── predix_logo.svg ├── scripts │ ├── learningpaths.js │ ├── Asset.js │ ├── Timeseries.js │ └── Chart.js ├── index.html └── stylesheets │ └── style.css ├── COPYRIGHT.md ├── server ├── routes │ ├── index.js │ ├── predix-asset-routes.js │ ├── time-series-routes.js │ ├── view-service-routes.js │ └── proxy.js ├── learningpaths │ └── learningpaths.js ├── localConfig.json ├── passport-config.js ├── predix-config.js └── app.js ├── .eslintrc.json ├── workshop.yml ├── version.json ├── manifest.yml ├── manifest.yml.template ├── package.json ├── test └── predix-config-test.js ├── README.md ├── scripts ├── quickstart-front-end-template.sh ├── quickstart-front-end-basic-node-express.sh ├── quickstart-front-end-template.bat └── quickstart-front-end-basic-node-express.bat ├── secure └── secure.html └── LICENSE.md /.cfignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | predix-scripts/ 3 | ./manifest-vpc.yml 4 | -------------------------------------------------------------------------------- /public/images/monogram-wdmk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredixDev/predix-nodejs-starter/HEAD/public/images/monogram-wdmk.png -------------------------------------------------------------------------------- /public/images/wind-turbine-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredixDev/predix-nodejs-starter/HEAD/public/images/wind-turbine-2.png -------------------------------------------------------------------------------- /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | The dependencies in this project are resolved by Maven pom.xml. Copyrights from those projects are included [here](http://predixdev.github.io/rmd-ref-app-copyright/). This list may be a superset of projects actually referenced by this project. 2 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET index page. */ 5 | 6 | router.use(function(req,res,next){ 7 | // console.log('index.html from router - index.js'); 8 | next(); 9 | }); 10 | 11 | router.get('/', function(req, res, next) { 12 | res.sendFile('index.html'); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /server/learningpaths/learningpaths.js: -------------------------------------------------------------------------------- 1 | var getLearningPaths = function(predixConfig) { 2 | if (predixConfig.isUaaConfigured()) { 3 | return { 4 | "cloudbasics" : false, 5 | "authorization" : true 6 | }; 7 | } else { 8 | return { 9 | "cloudbasics" : true, 10 | "authorization" : false 11 | }; 12 | } 13 | }; 14 | 15 | module.exports = { 16 | getLearningPaths: getLearningPaths 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "warn", 10 | "tab" 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "semi": [ 17 | "error", 18 | "always" 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /workshop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: -predix-nodejs-starter 4 | memory: 128M 5 | buildpack: nodejs_buildpack 6 | #command: DEBUG=express:* node app.js 7 | command: node app.js 8 | services: 9 | - workshop-secure-uaa-instance 10 | env: 11 | node_env: cloud 12 | uaa_service_label : predix-uaa 13 | clientId: app_client_id 14 | base64ClientCredential: YXBwLWNsaWVudC1pZDpzZWNyZXQ= 15 | # Following properties configured only for Timeseries WindData service Integration 16 | windServiceUrl: https://machine-workshop-training4.run.aws-usw02-pr.ice.predix.io 17 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Predix Front End Web App.html", 3 | "version": "1.1.169", 4 | "private": true, 5 | "dependencies": { 6 | "local-setup": "https://github.com/PredixDev/local-setup#1.0.106", 7 | "predix-scripts": "https://github.com/PredixDev/predix-scripts#1.1.218", 8 | "Predix-HelloWorld-WebApp": "https://github.com/PredixDev/Predix-HelloWorld-WebApp#1.1.151", 9 | "predix-nodejs-starter": "https://github.com/PredixDev/predix-nodejs-starter#1.1.169", 10 | "winddata-timeseries-service": "https://github.com/PredixDev/winddata-timeseries-service#2.0.61" 11 | }, 12 | "author": "turnerth" 13 | } 14 | -------------------------------------------------------------------------------- /public/scripts/learningpaths.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function learningPaths(body) { 4 | var resultJSON; 5 | var getData = new XMLHttpRequest(); 6 | getData.open('GET', "/learning-paths", true); 7 | getData.onload = function() { 8 | resultJSON = JSON.parse(getData.response); 9 | if ( resultJSON["learningPathsConfig"].authorization == true ) { 10 | var cloudbasics = document.getElementById('learningpaths.authentication'); 11 | cloudbasics.style.display="list-item"; 12 | //cloudbasics.style="display"; 13 | } 14 | else { 15 | var cloudbasics = document.getElementById('learningpaths.cloudbasics'); 16 | cloudbasics.style.display="list-item"; 17 | } 18 | }; 19 | getData.send(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /server/localConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "clientId": "{UAA Client ID created}", 4 | "uaaURL": "{The UAA URI end point to get auth token }", 5 | "base64ClientCredential": "{Get clientID:clientSecret then base64 encode and place it here}", 6 | "appURL": "http://localhost:5000", 7 | "timeseriesURL": "{Time Series URL from VCAPS}", 8 | "assetZoneId": "{The Zone ID for the Asset Service Created}", 9 | "timeseriesZoneId": "{The Zone ID for the Timeseries Service Created}", 10 | "assetURL": "{Asset URL from VCAPS}", 11 | "windServiceURL": "{URL of the microservice -winddata-timeseries-service}, e.g. https://your-name-winddata-timeseries-service.run.asw-usw02-pr.predix.io" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: -predix-nodejs-starter Change 4 | memory: 128M 5 | buildpack: nodejs_buildpack 6 | #command: DEBUG=express:* node app.js 7 | command: node server/app.js 8 | #services: 9 | # - your-name-uaa 10 | # - your-name-time-series 11 | # - your-name-asset 12 | env: 13 | node_env: cloud 14 | uaa_service_label : predix-uaa 15 | #clientId: {Enter client ID, e.g. app_client_id, and place it here} 16 | #base64ClientCredential: {Get clientID:clientSecret then base64 encode and place it here} 17 | # Following properties configured only for Timeseries WindData service Integration 18 | #windServiceUrl: "{URL of the microservice -winddata-timeseries-service}, e.g. https://your-name-winddata-timeseries-service.run.asw-usw02-pr.predix.io" 19 | -------------------------------------------------------------------------------- /manifest.yml.template: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: -predix-nodejs-starter Change 4 | memory: 128M 5 | buildpack: nodejs_buildpack 6 | #command: DEBUG=express:* node app.js 7 | command: node server/app.js 8 | #services: 9 | # - your-name-uaa 10 | # - your-name-time-series 11 | # - your-name-asset 12 | env: 13 | node_env: cloud 14 | uaa_service_label : predix-uaa 15 | #clientId: {Enter client ID, e.g. app_client_id, and place it here} 16 | #base64ClientCredential: {Get clientID:clientSecret then base64 encode and place it here} 17 | # Following properties configured only for Timeseries WindData service Integration 18 | #windServiceUrl: "{URL of the microservice -winddata-timeseries-service}, e.g. https://your-name-winddata-timeseries-service.run.asw-usw02-pr.predix.io" 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "predix-nodejs-starter", 3 | "version": "1.1.169", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server/app.js", 7 | "test": "mocha" 8 | }, 9 | "dependencies": { 10 | "body-parser": "~1.13.2", 11 | "chart.js": "^1.0.2", 12 | "cookie-parser": "~1.3.5", 13 | "debug": "~2.2.0", 14 | "express": "~4.13.1", 15 | "express-http-proxy": "^0.6.0", 16 | "express-session": "^1.12.1", 17 | "http-proxy-middleware": "^0.9.1", 18 | "https-proxy-agent": "^1.0.0", 19 | "passport": ">= 0.0.0", 20 | "passport-oauth2-middleware": "*", 21 | "passport-predix-oauth": "0.1.55", 22 | "request": "~2.67.0", 23 | "url": "~0.11.0" 24 | }, 25 | "author": "swapnavad,gstroup", 26 | "devDependencies": { 27 | "chai": "^3.5.0", 28 | "mocha": "^2.5.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/routes/predix-asset-routes.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob"); 2 | var path = require("path"); 3 | var _ = require("lodash"); 4 | 5 | // for each asset level, collect the json data to configure the json-server router 6 | // example structure: { 'assets': [ {...}, {...} ], 'enterprises': [...], ... } 7 | var getRoutes = function() { 8 | var levels = ['assets', 'enterprises', 'meters', 'plants', 'root', 'sites']; 9 | var routes = {}; 10 | _.each(levels, function(level) { 11 | routes[level] = getLevelData(level); 12 | }); 13 | return routes; 14 | }; 15 | 16 | // Pass in the name of the level 17 | // returns an array of objects based on data in .json files in level folder 18 | // example structure: [ { ... json file data ... }, { ... json file data ...} ] 19 | var getLevelData = function(level) { 20 | var fullPath = './sample-data/predix-asset/' + level + '/**/*.json'; 21 | var resolvedPath = path.resolve(__dirname, fullPath); 22 | var jsonObjects = []; 23 | var files = glob.sync(resolvedPath, {}); // all JSON files in path 24 | _.each(files, function(file) { 25 | var json = require(file); // import json data 26 | jsonObjects.push(json); 27 | }); 28 | return jsonObjects; 29 | }; 30 | 31 | // export the routes to be used in express/json-server in app.js 32 | module.exports = function() { 33 | return getRoutes(); 34 | }; 35 | -------------------------------------------------------------------------------- /server/routes/time-series-routes.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob"); 2 | var path = require("path"); 3 | var _ = require("lodash"); 4 | 5 | // for each asset level, collect the json data to configure the json-server router 6 | // example structure: { 'assets': [ {...}, {...} ], 'enterprises': [...], ... } 7 | var getRoutes = function() { 8 | var levels = ['core-vibe', 'datagrid', 'delta-egt', 'fan-vibration', 'simple-data', 'scatter']; 9 | var routes = {}; 10 | _.each(levels, function(level) { 11 | routes[level] = getLevelData(level); 12 | }); 13 | return routes; 14 | }; 15 | 16 | // Pass in the name of the level 17 | // returns an array of objects based on data in .json files in level folder 18 | // example structure: [ { ... json file data ... }, { ... json file data ...} ] 19 | var getLevelData = function(level) { 20 | var fullPath = './sample-data/time-series/' + level + '/**/*.json'; 21 | var resolvedPath = path.resolve(__dirname, fullPath); 22 | var jsonObjects = []; 23 | var files = glob.sync(resolvedPath, {}); // all JSON files in path 24 | _.each(files, function(file) { 25 | var json = require(file); // import json data 26 | jsonObjects.push(json); 27 | }); 28 | return jsonObjects; 29 | }; 30 | 31 | // export the routes to be used in express/json-server in app.js 32 | module.exports = function() { 33 | return getRoutes(); 34 | }; 35 | -------------------------------------------------------------------------------- /server/routes/view-service-routes.js: -------------------------------------------------------------------------------- 1 | var glob = require("glob"); 2 | var path = require("path"); 3 | var _ = require("lodash"); 4 | 5 | // options is optional 6 | var options = {}; 7 | 8 | var decksPath = path.resolve(__dirname, "./sample-data/view-service/decks/*.json"); 9 | var cardsPath = path.resolve(__dirname, "./sample-data/view-service/cards/*.json"); 10 | var joinsPath = path.resolve(__dirname, "./sample-data/view-service/joins/*.json"); 11 | 12 | var decks = []; 13 | var cards = []; 14 | var joins = []; 15 | 16 | var joinFiles = glob.sync(joinsPath, options); 17 | _.each(joinFiles, function(joinFile) { 18 | var joinJSON = require(joinFile); 19 | joins.push(joinJSON); 20 | }); 21 | 22 | var cardFiles = glob.sync(cardsPath, options); 23 | _.each(cardFiles, function(cardFile) { 24 | var cardJSON = require(cardFile); 25 | cards.push(cardJSON); 26 | }); 27 | 28 | var deckFiles = glob.sync(decksPath, options); 29 | _.each(deckFiles, function(deckFile) { 30 | var deckJSON = require(deckFile); 31 | var matchingJoins = _.filter(joins, { 32 | "deckId": deckJSON["id"] 33 | }); 34 | deckJSON.cards = []; 35 | _.each(matchingJoins, function(join) { 36 | var joinedCard = _.find(cards, { 37 | "id": join["cardId"] 38 | }); 39 | deckJSON.cards.push(joinedCard); 40 | }); 41 | var newTags = []; 42 | // flatten tags to be searchable with query string filter e.g.: "?tags=tagName" 43 | _.each(deckJSON.tags, function(tagObject) { 44 | return newTags.push(tagObject.value); 45 | }, ''); 46 | deckJSON.tag = newTags.join(', '); 47 | decks.push(deckJSON); 48 | }); 49 | 50 | module.exports = function() { 51 | return { 52 | decks: decks, 53 | cards: cards 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /public/images/predix_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 16 | 17 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/predix-config-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('chai').assert; 4 | var predixConfig = require('../server/predix-config.js'); 5 | 6 | describe('predix-config initialization in development mode', function() { 7 | var devConfig = require('../server/localConfig.json').development; 8 | it('loads values from localConfig.json', function() { 9 | assert.equal(predixConfig.assetURL, devConfig.assetURL); 10 | assert.equal(predixConfig.assetZoneId, devConfig.assetZoneId); 11 | assert.equal(predixConfig.timeseriesZoneId, devConfig.timeseriesZoneId); 12 | assert.equal(predixConfig.timeseriesURL, devConfig.timeseriesURL); 13 | assert.equal(predixConfig.uaaURL, devConfig.uaaURL); 14 | assert.equal(predixConfig.clientId, devConfig.clientId); 15 | assert.equal(predixConfig.base64ClientCredential, devConfig.base64ClientCredential); 16 | assert.equal(predixConfig.appURL, devConfig.appURL); 17 | }); 18 | 19 | describe('builds a VCAP object from localConfig', function() { 20 | var vcap = predixConfig.buildVcapObjectFromLocalConfig(devConfig); 21 | 22 | it('with UAA info', function() { 23 | assert.equal(vcap['predix-uaa'][0].credentials.uri, devConfig.uaaURL); 24 | }); 25 | 26 | it('with asset info', function() { 27 | assert.equal(vcap['predix-asset'][0].credentials.uri, devConfig.assetURL); 28 | assert.equal(vcap['predix-asset'][0].credentials.zone['http-header-value'], devConfig.assetZoneId); 29 | }); 30 | 31 | it('with time series info', function() { 32 | assert.equal(vcap['predix-timeseries'][0].credentials.query.uri, devConfig.timeseriesURL); 33 | assert.equal(vcap['predix-timeseries'][0].credentials.query['zone-http-header-value'], devConfig.timeseriesZoneId); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Predix Development Kit - NodeJs Starter Application 2 | ========================================================== 3 | 4 | This is simple starter Node application that demonstrates user authentication with Predix UAA, 5 | and integration with microservices. 6 | 7 | ## Running locally 8 | Edit the config.json to run the application locally for your UAA client. 9 | 10 | Sample : 11 | ``` 12 | "development":{ 13 | "clientId": "${clientId}", 14 | "uaaUri" : "${UAA URL}", 15 | "base64ClientCredential": "${base 64 encoding of clientId:secret}", 16 | "appUrl": "http://localhost:3000", 17 | "assetZoneId": "${asset zone id for Asset service instantiated}", 18 | "tagname": "${tag name list to query. Separated by comma}", 19 | "assetURL": "${The asset url to query the tags from. https:///}", 20 | "timeseries_zone": "${timeseries zone id for Timeseries service instantiated}", 21 | "timeseriesURL": "${Timeseries to query for data. /v1/datapoints}", 22 | "uaaURL": "${The UAA URI. /predix.io", 23 | } 24 | ``` 25 | *Note:* You can encode your clientId:secret combination using or the base64 command on Unix / Mac OSX. 26 | 27 | `echo -n clientId:clientSecret | base64` 28 | 29 | #### Install and start local web server 30 | ``` 31 | npm install 32 | node app.js or npm start 33 | ``` 34 | Navigate to in your web browser. 35 | 36 | Debugging 37 | ``` 38 | DEBUG=predix-boot-node-app:* npm start 39 | DEBUG=express:* npm start 40 | ``` 41 | 42 | #### Run sample tests 43 | A sample unit test is included, which you can run with npm: 44 | `npm test` 45 | 46 | #### Running locally behind a corporate firewall 47 | 48 | If you are behind a corporate firewall, make sure you have the `http_proxy` environment variable set in the same terminal window where you start the server. 49 | 50 | ## Running in the cloud 51 | 52 | Set up the manifest file for Cloud deployment 53 | 54 | 1. Copy manifest.yml.template to my-app-manifest.yml. 55 | 2. Edit the my-app-manifest.yml 56 | ``` 57 | --- 58 | applications: 59 | - name: 60 | memory: 128M 61 | buildpack: nodejs_buildpack 62 | #command: DEBUG=express:* node app.js 63 | command: node app.js 64 | services: 65 | - 66 | - 67 | - 68 | env: 69 | node_env: cloud 70 | uaa_service_label : predix-uaa 71 | clientId: 72 | base64ClientCredential: 73 | # Following properties configured only for Timeseries WindData service Integration 74 | assetMachine: 75 | tagname: 76 | ``` 77 | 3. `predix push -f my-app-manifest.yml` 78 | 79 | [![Analytics](https://predix-beacon.appspot.com/UA-82773213-1/predix-nodejs-starter/readme?pixel)](https://github.com/PredixDev) 80 | 81 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Predix Node JS Starter App 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

My First Predix Sample Application

13 |

Predix Developer Kit

14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | 55 | 64 |
65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /server/passport-config.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var CloudFoundryStrategy = require('passport-predix-oauth').Strategy; 3 | var OAuth2RefreshTokenStrategy = require('passport-oauth2-middleware').Strategy; 4 | var cfStrategy; 5 | 6 | /********************************************************************* 7 | PASSPORT PREDIX STRATEGY SETUP 8 | **********************************************************************/ 9 | function configurePassportStrategy(predixConfig) { 10 | 'use strict'; 11 | var refreshStrategy = new OAuth2RefreshTokenStrategy({ 12 | refreshWindow: 10, // Time in seconds to perform a token refresh before it expires 13 | userProperty: 'ticket', // Active user property name to store OAuth tokens 14 | authenticationURL: '/', // URL to redirect unathorized users to 15 | callbackParameter: 'callback' //URL query parameter name to pass a return URL 16 | }); 17 | 18 | passport.use('main', refreshStrategy); //Main authorization strategy that authenticates 19 | //user with stored OAuth access token 20 | //and performs a token refresh if needed 21 | 22 | // Passport session setup. 23 | // To support persistent login sessions, Passport needs to be able to 24 | // serialize users into and deserialize users out of the session. Typically, 25 | // this will be as simple as storing the user ID when serializing, and finding 26 | // the user by ID when deserializing. However, since this example does not 27 | // have a database of user records, the complete CloudFoundry profile is 28 | // serialized and deserialized. 29 | passport.serializeUser(function(user, done) { 30 | // console.log("From USER-->"+JSON.stringify(user)); 31 | done(null, user); 32 | }); 33 | passport.deserializeUser(function(obj, done) { 34 | done(null, obj); 35 | }); 36 | 37 | function getSecretFromEncodedString(encoded) { 38 | if (!encoded) { 39 | return ''; 40 | } 41 | var decoded = new Buffer(encoded, 'base64').toString(); 42 | // console.log('DECODED: ' + decoded); 43 | var values = decoded.split(':'); 44 | if (values.length !== 2) { 45 | throw "base64ClientCredential is not correct. \n It should be the base64 encoded value of: 'client:secret' \n Set in localConfig.json for local dev, or environment variable in the cloud."; 46 | } 47 | return values[1]; 48 | } 49 | 50 | cfStrategy = new CloudFoundryStrategy({ 51 | clientID: predixConfig.clientId, 52 | clientSecret: getSecretFromEncodedString(predixConfig.base64ClientCredential), 53 | callbackURL: predixConfig.callbackURL, 54 | authorizationURL: predixConfig.uaaURL, 55 | tokenURL: predixConfig.tokenURL 56 | },refreshStrategy.getOAuth2StrategyCallback() //Create a callback for OAuth2Strategy 57 | /* TODO: implement if needed. 58 | function(accessToken, refreshToken, profile, done) { 59 | token = accessToken; 60 | done(null, profile); 61 | }*/); 62 | 63 | passport.use(cfStrategy); 64 | //Register the OAuth strategy to perform OAuth2 refresh token workflow 65 | refreshStrategy.useOAuth2Strategy(cfStrategy); 66 | 67 | return passport; 68 | } 69 | 70 | function reset() { 71 | 'use strict'; 72 | cfStrategy.reset(); 73 | } 74 | 75 | module.exports = { 76 | configurePassportStrategy: configurePassportStrategy, 77 | reset: reset 78 | }; 79 | -------------------------------------------------------------------------------- /server/predix-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | This module reads config settings from localConfig.json when running locally, 3 | or from the VCAPS environment variables when running in Cloud Foundry. 4 | */ 5 | 6 | var settings = {}; 7 | 8 | // checking NODE_ENV to load cloud properties from VCAPS 9 | // or development properties from config.json. 10 | // These properties are not needed for fetching mock data. 11 | // Only needed if you want to connect to real Predix services. 12 | var node_env = process.env.node_env || 'development'; 13 | if(node_env === 'development') { 14 | // use localConfig file 15 | var devConfig = require('./localConfig.json')[node_env]; 16 | // console.log(devConfig); 17 | settings.base64ClientCredential = devConfig.base64ClientCredential; 18 | settings.clientId = devConfig.clientId; 19 | settings.uaaURL = devConfig.uaaURL; 20 | settings.tokenURL = devConfig.uaaURL; 21 | settings.appURL = devConfig.appURL; 22 | settings.callbackURL = devConfig.appURL + '/callback'; 23 | 24 | settings.assetURL = devConfig.assetURL; 25 | settings.assetZoneId = devConfig.assetZoneId; 26 | settings.timeseriesZoneId = devConfig.timeseriesZoneId; 27 | settings.timeseriesURL = devConfig.timeseriesURL; 28 | 29 | } else { 30 | // read VCAP_SERVICES 31 | var vcapsServices = JSON.parse(process.env.VCAP_SERVICES); 32 | var uaaService = vcapsServices[process.env.uaa_service_label]; 33 | var assetService = vcapsServices['predix-asset']; 34 | var timeseriesService = vcapsServices['predix-timeseries']; 35 | 36 | if(uaaService) { 37 | settings.uaaURL = uaaService[0].credentials.uri; 38 | settings.tokenURL = uaaService[0].credentials.uri; 39 | } 40 | if(assetService) { 41 | settings.assetURL = assetService[0].credentials.uri + '/' + process.env.assetMachine; 42 | settings.assetZoneId = assetService[0].credentials.zone['http-header-value']; 43 | } 44 | if(timeseriesService) { 45 | settings.timeseriesZoneId = timeseriesService[0].credentials.query['zone-http-header-value']; 46 | settings.timeseriesURL = timeseriesService[0].credentials.query.uri; 47 | } 48 | 49 | // read VCAP_APPLICATION 50 | var vcapsApplication = JSON.parse(process.env.VCAP_APPLICATION); 51 | settings.appURL = 'https://' + vcapsApplication.uris[0]; 52 | settings.callbackURL = settings.appURL + '/callback'; 53 | settings.base64ClientCredential = process.env.base64ClientCredential; 54 | settings.clientId = process.env.clientId; 55 | } 56 | // console.log('config settings: ' + JSON.stringify(settings)); 57 | 58 | // This vcap object is used by the proxy module. 59 | settings.buildVcapObjectFromLocalConfig = function(config) { 60 | 'use strict'; 61 | // console.log('local config: ' + JSON.stringify(config)); 62 | var vcapObj = {}; 63 | if (config.uaaURL) { 64 | vcapObj['predix-uaa'] = [{ 65 | credentials: { 66 | uri: config.uaaURL 67 | } 68 | }]; 69 | } 70 | if (config.timeseriesURL) { 71 | vcapObj['predix-timeseries'] = [{ 72 | credentials: { 73 | query: { 74 | uri: config.timeseriesURL, 75 | 'zone-http-header-value': config.timeseriesZoneId 76 | } 77 | } 78 | }]; 79 | } 80 | if (config.assetURL) { 81 | vcapObj['predix-asset'] = [{ 82 | credentials: { 83 | uri: config.assetURL, 84 | zone: { 85 | 'http-header-value': config.assetZoneId 86 | } 87 | } 88 | }]; 89 | } 90 | return vcapObj; 91 | }; 92 | 93 | settings.isUaaConfigured = function() { 94 | return settings.clientId && 95 | settings.uaaURL && 96 | settings.uaaURL.indexOf('https') === 0 && 97 | settings.base64ClientCredential; 98 | }; 99 | 100 | module.exports = settings; 101 | -------------------------------------------------------------------------------- /scripts/quickstart-front-end-template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function local_read_args() { 5 | while (( "$#" )); do 6 | opt="$1" 7 | case $opt in 8 | -h|-\?|--\?--help) 9 | PRINT_USAGE=1 10 | QUICKSTART_ARGS=" $1" 11 | break 12 | ;; 13 | -b|--branch) 14 | BRANCH="$2" 15 | QUICKSTART_ARGS+=" $1 $2" 16 | shift 17 | ;; 18 | -o|--override) 19 | QUICKSTART_ARGS=" $SCRIPT" 20 | ;; 21 | --skip-setup) 22 | SKIP_SETUP=true 23 | ;; 24 | *) 25 | QUICKSTART_ARGS+=" $1" 26 | #echo $1 27 | ;; 28 | esac 29 | shift 30 | done 31 | 32 | if [[ -z $BRANCH ]]; then 33 | echo "Usage: $0 -b/--branch [--skip-setup]" 34 | exit 1 35 | fi 36 | } 37 | 38 | IZON_SH="https://raw.githubusercontent.com/PredixDev/izon/1.2.0/izon2.sh" 39 | BRANCH="master" 40 | PRINT_USAGE=0 41 | SKIP_SETUP=false 42 | #ASSET_MODEL="-amrmd predix-ui-seed/server/sample-data/predix-asset/asset-model-metadata.json predix-ui-seed/server/sample-data/predix-asset/asset-model.json" 43 | SCRIPT="-script build-basic-app.sh -script-readargs build-basic-app-readargs.sh" 44 | QUICKSTART_ARGS="-ns $SCRIPT" 45 | VERSION_JSON="version.json" 46 | PREDIX_SCRIPTS=predix-scripts 47 | REPO_NAME=predix-nodejs-starter 48 | SCRIPT_NAME="quickstart-front-end-template.sh" 49 | APP_DIR="frontend-microservice-template" 50 | APP_NAME="Predix Front End WebApp Microservice Template" 51 | GITHUB_RAW="https://raw.githubusercontent.com/PredixDev" 52 | TOOLS="Cloud Foundry CLI, Git, Node.js, Predix CLI" 53 | TOOLS_SWITCHES="--cf --git --nodejs --predixcli" 54 | 55 | # Process switches 56 | local_read_args $@ 57 | 58 | #variables after processing switches 59 | echo "branch=$BRANCH" 60 | SCRIPT_LOC="$GITHUB_RAW/$REPO_NAME/$BRANCH/scripts/$SCRIPT_NAME" 61 | VERSION_JSON_URL="$GITHUB_RAW/$REPO_NAME/$BRANCH/version.json" 62 | echo "VERSION_JSON_URL=$VERSION_JSON_URL" 63 | 64 | function check_internet() { 65 | set +e 66 | echo "" 67 | echo "Checking internet connection..." 68 | curl "http://github.com" > /dev/null 2>&1 69 | if [ $? -ne 0 ]; then 70 | echo "Unable to connect to internet, make sure you are connected to a network and check your proxy settings if behind a corporate proxy" 71 | echo "If you are behind a corporate proxy, set the 'http_proxy' and 'https_proxy' environment variables." 72 | exit 1 73 | fi 74 | echo "OK" 75 | echo "" 76 | set -e 77 | } 78 | 79 | function init() { 80 | currentDir=$(pwd) 81 | if [[ $currentDir == *"scripts" ]]; then 82 | echo 'Please launch the script from the root dir of the project' 83 | exit 1 84 | fi 85 | 86 | check_internet 87 | #get the script that reads version.json 88 | eval "$(curl -s -L $IZON_SH)" 89 | 90 | #download the script and cd 91 | getUsingCurl $SCRIPT_LOC 92 | chmod 755 $SCRIPT_NAME 93 | if [[ ! $currentDir == *"$REPO_NAME" ]]; then 94 | mkdir -p $APP_DIR 95 | cd $APP_DIR 96 | fi 97 | 98 | 99 | getVersionFile 100 | getLocalSetupFuncs $GITHUB_RAW 101 | } 102 | 103 | if [[ $PRINT_USAGE == 1 ]]; then 104 | __print_out_usage 105 | init 106 | else 107 | if $SKIP_SETUP; then 108 | init 109 | else 110 | init 111 | __standard_mac_initialization 112 | fi 113 | fi 114 | 115 | getPredixScripts 116 | #clone the repo itself if running from oneclick script 117 | getCurrentRepo 118 | 119 | echo "quickstart_args=$QUICKSTART_ARGS" 120 | source $PREDIX_SCRIPTS/bash/quickstart.sh $QUICKSTART_ARGS 121 | 122 | __append_new_line_log "Successfully completed $APP_NAME installation!" "$quickstartLogDir" 123 | __append_new_line_log "" "$quickstartLogDir" 124 | 125 | -------------------------------------------------------------------------------- /scripts/quickstart-front-end-basic-node-express.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function local_read_args() { 5 | while (( "$#" )); do 6 | opt="$1" 7 | case $opt in 8 | -h|-\?|--\?--help) 9 | PRINT_USAGE=1 10 | QUICKSTART_ARGS="$SCRIPT $1" 11 | break 12 | ;; 13 | -b|--branch) 14 | BRANCH="$2" 15 | QUICKSTART_ARGS+=" $1 $2" 16 | shift 17 | ;; 18 | -o|--override) 19 | QUICKSTART_ARGS=" $SCRIPT" 20 | ;; 21 | --skip-setup) 22 | SKIP_SETUP=true 23 | ;; 24 | *) 25 | QUICKSTART_ARGS+=" $1" 26 | #echo $1 27 | ;; 28 | esac 29 | shift 30 | done 31 | 32 | if [[ -z $BRANCH ]]; then 33 | echo "Usage: $0 -b/--branch [--skip-setup]" 34 | exit 1 35 | fi 36 | } 37 | 38 | # default settings 39 | BRANCH="master" 40 | PRINT_USAGE=0 41 | SKIP_SETUP=false 42 | 43 | IZON_SH="https://raw.githubusercontent.com/PredixDev/izon/1.2.0/izon2.sh" 44 | #ASSET_MODEL="-amrmd predix-ui-seed/server/sample-data/predix-asset/asset-model-metadata.json predix-ui-seed/server/sample-data/predix-asset/asset-model.json" 45 | SCRIPT="-script build-basic-app.sh -script-readargs build-basic-app-readargs.sh" 46 | QUICKSTART_ARGS="-ba -uaa -asset -ts -wd -nsts $SCRIPT" 47 | VERSION_JSON="version.json" 48 | PREDIX_SCRIPTS=predix-scripts 49 | REPO_NAME=predix-nodejs-starter 50 | SCRIPT_NAME="quickstart-front-end-basic-node-express.sh" 51 | GITHUB_RAW="https://raw.githubusercontent.com/PredixDev" 52 | APP_DIR="build-basic-app" 53 | APP_NAME="Predix Front End Basic App - Node.js Express with UAA, Asset, Time Series" 54 | TOOLS="Cloud Foundry CLI, Git, Node.js, Maven, Predix CLI" 55 | TOOLS_SWITCHES="--cf --git --nodejs --maven --predixcli" 56 | 57 | # Process switches 58 | local_read_args $@ 59 | 60 | #variables after processing switches 61 | SCRIPT_LOC="$GITHUB_RAW/$REPO_NAME/$BRANCH/$SCRIPT_NAME" 62 | VERSION_JSON_URL="$GITHUB_RAW/$REPO_NAME/$BRANCH/version.json" 63 | 64 | function check_internet() { 65 | set +e 66 | echo "" 67 | echo "Checking internet connection..." 68 | curl "http://google.com" > /dev/null 2>&1 69 | if [ $? -ne 0 ]; then 70 | echo "Unable to connect to internet, make sure you are connected to a network and check your proxy settings if behind a corporate proxy" 71 | echo "If you are behind a corporate proxy, set the 'http_proxy' and 'https_proxy' environment variables. Please read this tutorial for detailed info about setting your proxy https://www.predix.io/resources/tutorials/tutorial-details.html?tutorial_id=1565" 72 | exit 1 73 | fi 74 | echo "OK" 75 | echo "" 76 | set -e 77 | } 78 | 79 | function init() { 80 | currentDir=$(pwd) 81 | if [[ $currentDir == *"scripts" ]]; then 82 | echo 'Please launch the script from the root dir of the project' 83 | exit 1 84 | fi 85 | if [[ ! $currentDir == *"$REPO_NAME" ]]; then 86 | mkdir -p $APP_DIR 87 | cd $APP_DIR 88 | fi 89 | 90 | check_internet 91 | 92 | 93 | #get the script that reads version.json 94 | eval "$(curl -s -L $IZON_SH)" 95 | getUsingCurl $SCRIPT_LOC 96 | chmod 755 $SCRIPT_NAME 97 | getVersionFile 98 | getLocalSetupFuncs $GITHUB_RAW 99 | } 100 | 101 | if [[ $PRINT_USAGE == 1 ]]; then 102 | init 103 | __print_out_standard_usage 104 | else 105 | if $SKIP_SETUP; then 106 | init 107 | else 108 | init 109 | __standard_mac_initialization 110 | fi 111 | fi 112 | 113 | getPredixScripts 114 | #clone the repo itself if running from oneclick script 115 | getCurrentRepo 116 | 117 | echo "quickstart_args=$QUICKSTART_ARGS" 118 | source $PREDIX_SCRIPTS/bash/quickstart.sh $QUICKSTART_ARGS 119 | 120 | __append_new_line_log "Successfully completed $APP_NAME installation!" "$quickstartLogDir" 121 | __append_new_line_log "" "$quickstartLogDir" 122 | -------------------------------------------------------------------------------- /public/scripts/Asset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gets Asset Data for the tags 3 | **/ 4 | function getAssetData(isConnectedAssetEnabled ,tagString) { 5 | 6 | // Call the Asset service if applicable 7 | if (isConnectedAssetEnabled) { 8 | // Get the Asset data if the Asset URI is enabled 9 | var table = document.getElementById("aTable"); 10 | var assetGetData = new XMLHttpRequest(); 11 | // Assumption that tag are defined as : 12 | var assetId = tagString.split(":"); 13 | var assetGetDataURL = "/predix-api/predix-asset/asset/" + assetId[0]; 14 | 15 | assetGetData.open('GET', assetGetDataURL, true); 16 | assetGetData.onload = function() { 17 | if (assetGetData.status >= 200 && assetGetData.status < 400) { 18 | document.getElementById("predix_asset_table").innerHTML = ''; 19 | var resultJSON = JSON.parse(assetGetData.response)[0]; 20 | var resultString = JSON.stringify(resultJSON); 21 | if (resultJSON) { 22 | var nameOfTable = document.getElementById("predix_asset_table"); 23 | nameOfTable.innerHTML = "Asset Information"; 24 | while (table.firstChild) { 25 | table.removeChild(table.firstChild); 26 | } 27 | var keys = Object.keys(resultJSON); 28 | var assetRowIndex = 0; 29 | for(var i = 0; i element and add it to the 1st position of the table: 31 | if(keys[i] == 'uri' || keys[i] == 'description' || keys[i] == 'assetId' ){ 32 | console.log("inside keys"+keys[i]); 33 | var row = table.insertRow(assetRowIndex++); 34 | // Insert new cells ( elements) at the 1st and 2nd position of the "new" element: 35 | var cell1 = row.insertCell(0); 36 | var cell2 = row.insertCell(1); 37 | cell1.style.borderWidth = "1px"; 38 | cell1.style.borderStyle = "solid"; 39 | cell1.style.borderColor = "black"; 40 | 41 | cell2.style.borderWidth = "1px"; 42 | cell2.style.borderStyle = "solid"; 43 | cell2.style.borderColor = "black"; 44 | // Add some text to the new cells: 45 | cell1.innerHTML = keys[i]; 46 | cell2.innerHTML = resultJSON[keys[i]]; 47 | }else{console.log("outside keys"+keys[i]);} 48 | 49 | } 50 | table.style.borderCollapse= "collapse"; 51 | document.getElementById("asset_detail_model").innerHTML = JSON.stringify(resultJSON, undefined, 4); 52 | // enable Asset detail button 53 | var asssetLinkElement = document.getElementById('assset_detail_link'); 54 | asssetLinkElement.style.display = "block"; 55 | } else { 56 | //document.getElementById("predix_asset_table").innerHTML = "Asset Model Information is not available for:" + tagString; 57 | console.log("Asset Model Information is not available for:" + tagString); 58 | while (table.firstChild) { 59 | table.removeChild(table.firstChild); 60 | } 61 | } 62 | }else if(assetGetData.status >= 404 ) { 63 | console.log("Asset Model Information is not available for:" + tagString); 64 | } 65 | else { 66 | console.log("Error: Error Acceesing Asset Service : " + tagString); 67 | //document.getElementById("predix_asset_table").innerHTML = "Error fetching asset model info for tag: " + tagString; 68 | } 69 | }; 70 | assetGetData.onerror = function() { 71 | console.log("Error: Accessing Asset Service for : " + tagString); 72 | //document.getElementById("predix_asset_table").innerHTML = "Error fetching asset model info for tag: " + tagString; 73 | }; 74 | if (tagString !== undefined) 75 | { 76 | assetGetData.send(); 77 | } 78 | } 79 | } 80 | 81 | 82 | function detailAssetModel() { 83 | var modal = document.getElementById('myModal'); 84 | modal.style.display = "block"; 85 | } 86 | 87 | function assetDetailCLose(){ 88 | var modal = document.getElementById('myModal'); 89 | modal.style.display = "none"; 90 | } 91 | -------------------------------------------------------------------------------- /secure/secure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Predix Node JS Starter App 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

My First Predix Sample Application

14 |

Predix Developer Kit

15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |

Welcome to a Secure Page.

24 |

This page is gated by the check_token call of UAA.

25 | 26 | 34 | 35 |

Wind Data Microservices Integration

36 |
37 | 38 | 39 | 55 |
40 |

41 |
42 |
43 | 44 |
45 | 46 | 54 |
56 |

Select the tags

57 | 59 |
60 |

Select start time

61 | 84 | 85 |
86 | 87 |

88 |
89 |

90 |
91 |

92 |
93 |
94 |
95 | 96 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | /*padding: 50px;*/ 3 | font: 14px Roboto,'Helvetica Neue',Helvetica,Arial,sans-serif; 4 | margin: 0; 5 | height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | a { 11 | color: #00B7FF; 12 | } 13 | 14 | h2 { 15 | font-weight: 300; 16 | font-size: 40px; 17 | margin: 15px 0; 18 | } 19 | 20 | h4 { 21 | font-weight: normal; 22 | font-size: 16px; 23 | } 24 | 25 | td { 26 | vertical-align: top; 27 | padding-right: 20px; 28 | } 29 | 30 | textarea { 31 | margin: 0px; 32 | width: 1138px; 33 | height: 150px; 34 | } 35 | 36 | .header { 37 | color: #ffffff; 38 | padding: 20px 20px 0 20px; 39 | min-height: 124px; 40 | 41 | background: rgba(40,134,175,1); 42 | background: -moz-linear-gradient(left, rgba(40,134,175,1) 0%, rgba(9,35,40,1) 66%, rgba(9,35,40,1) 100%); 43 | background: -webkit-gradient(left top, right top, color-stop(0%, rgba(40,134,175,1)), color-stop(66%, rgba(9,35,40,1)), color-stop(100%, rgba(9,35,40,1))); 44 | background: -webkit-linear-gradient(left, rgba(40,134,175,1) 0%, rgba(9,35,40,1) 66%, rgba(9,35,40,1) 100%); 45 | background: -o-linear-gradient(left, rgba(40,134,175,1) 0%, rgba(9,35,40,1) 66%, rgba(9,35,40,1) 100%); 46 | background: -ms-linear-gradient(left, rgba(40,134,175,1) 0%, rgba(9,35,40,1) 66%, rgba(9,35,40,1) 100%); 47 | background: linear-gradient(to right, rgba(40,134,175,1) 0%, rgba(9,35,40,1) 66%, rgba(9,35,40,1) 100%); 48 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2886af', endColorstr='#092328', GradientType=1 ); 49 | display: flex; 50 | justify-content: space-between; 51 | } 52 | 53 | .header-logo { 54 | height: 50px; 55 | float: right; 56 | } 57 | 58 | .header-logo-container { 59 | padding: 20px 20px 20px 20px; 60 | position: relative; 61 | } 62 | 63 | .container { 64 | padding: 0 20px; 65 | min-height: 200px; 66 | border: 0px solid blue; 67 | } 68 | 69 | .section { 70 | width: 80%; 71 | height: 80%; 72 | background: none; 73 | padding: 15px 0px 0px 0px; 74 | border: 0px solid red; 75 | 76 | } 77 | 78 | .body-img-container { 79 | height: 420px; 80 | width: 250px; 81 | padding: 20px 20px 20px 20px; 82 | float: left; 83 | border: 0px solid blue; 84 | } 85 | 86 | .body-img { 87 | height: 210px; 88 | float: left; 89 | } 90 | 91 | .body-container { 92 | padding: 0px 20px 0px 0px; 93 | height: 460px; 94 | border: 0px solid blue; 95 | } 96 | 97 | .footer { 98 | width: 100%; 99 | position: fixed; 100 | bottom: 0; 101 | z-index: -99; 102 | } 103 | 104 | .footer-logo-container { 105 | position: relative; 106 | } 107 | 108 | .footer-logo { 109 | height: 30px; 110 | padding: 20px; 111 | float: right; 112 | } 113 | 114 | li { 115 | list-style: none; 116 | padding-bottom: 10px; 117 | } 118 | 119 | .tag-multiple{ 120 | font-size:15px; 121 | text-indent:-1.5px; 122 | padding-left:2.5px; 123 | height: 100%; 124 | min-height: 200px; 125 | } 126 | 127 | .timeseries_button { 128 | background-color: #4CAF50; /* Green */ 129 | border: none; 130 | color: white; 131 | padding: 15px 32px; 132 | text-align: center; 133 | text-decoration: none; 134 | display: inline-block; 135 | font-size: 16px; 136 | border-radius: 8px; 137 | cursor:pointer; 138 | margin-top: 20px; 139 | } 140 | 141 | .start-time{ 142 | font-size:15px; 143 | text-indent:-1.5px; 144 | padding-left:2.5px; 145 | } 146 | 147 | .windyservice_chart_div{ 148 | display: inline-block; 149 | } 150 | 151 | .windyservice_chart_canvas{ 152 | height:500px; 153 | width:400px; 154 | } 155 | /* The Modal (background) */ 156 | .modal { 157 | display: none; /* Hidden by default */ 158 | position: fixed; /* Stay in place */ 159 | z-index: 1; /* Sit on top */ 160 | left: 0; 161 | top: 0; 162 | width: 100%; /* Full width */ 163 | height: 100%; /* Full height */ 164 | overflow: auto; /* Enable scroll if needed */ 165 | background-color: rgb(0,0,0); /* Fallback color */ 166 | background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ 167 | } 168 | 169 | /* Modal Content/Box */ 170 | .modal-content { 171 | background-color: #fefefe; 172 | margin: 15% auto; /* 15% from the top and centered */ 173 | padding: 20px; 174 | border: 1px solid #888; 175 | width: 80%; /* Could be more or less, depending on screen size */ 176 | } 177 | 178 | /* The Close Button */ 179 | .close { 180 | color:#ff3333; 181 | float: right; 182 | font-size: 28px; 183 | font-weight: bold; 184 | } 185 | 186 | .close:hover, 187 | .close:focus { 188 | color: black; 189 | text-decoration: none; 190 | cursor: pointer; 191 | } 192 | -------------------------------------------------------------------------------- /scripts/quickstart-front-end-template.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION 3 | set CURRENTDIR=%cd% 4 | echo "currentdir=!CURRENTDIR!" 5 | SET FILE_NAME=%0 6 | SET BRANCH=master 7 | SET SKIP_SETUP=FALSE 8 | SET CF_URL="" 9 | SET SKIP_SETUP=FALSE 10 | 11 | :GETOPTS 12 | IF /I [%1] == [--skip-setup] SET SKIP_SETUP=TRUE 13 | rem Here we call SHIFT twice to remove the switch and value, 14 | rem since these are not needed by the .sh script. 15 | IF /I [%1] == [-b] SET BRANCH=%2& SHIFT & SHIFT 16 | IF /I [%1] == [--branch] SET BRANCH=%2& SHIFT & SHIFT 17 | IF /I [%1] == [--cf-url] SET CF_URL=%2& SHIFT & SHIFT 18 | IF /I [%1] == [--cf-user] SET CF_USER=%2& SHIFT & SHIFT 19 | IF /I [%1] == [--cf-password] SET CF_PASSWORD=%2& SHIFT & SHIFT 20 | IF /I [%1] == [--cf-org] SET CF_ORG=%2& SHIFT & SHIFT 21 | IF /I [%1] == [--cf-space] SET CF_SPACE=%2& SHIFT & SHIFT 22 | SET QUICKSTART_ARGS=!QUICKSTART_ARGS! %1 23 | rem echo "#### QUICKSTART_ARGS: !QUICKSTART_ARGS!" 24 | SHIFT & IF NOT [%1]==[] GOTO :GETOPTS 25 | GOTO :AFTERGETOPTS 26 | 27 | CALL :GETOPTS %* 28 | :AFTERGETOPTS 29 | 30 | IF [!BRANCH!]==[] ( 31 | ECHO "Usage: %FILE_NAME% -b/--branch " 32 | EXIT /b 1 33 | ) 34 | 35 | SET IZON_BAT=https://raw.githubusercontent.com/PredixDev/izon/1.2.0/izon.bat 36 | SET TUTORIAL="https://www.predix.io/resources/tutorials/tutorial-details.html?tutorial_id=1569&tag=1719&journey=Hello%20World&resources=1844,1475,1569,1523" 37 | SET REPO_NAME=predix-nodejs-starter 38 | SET SHELL_SCRIPT_NAME=quickstart-front-end-template.sh 39 | SET APP_DIR="frontend-microservice-template" 40 | SET APP_NAME=Front End Microservice Template 41 | SET TOOLS=Cloud Foundry CLI, Git, Node.js, Predix CLI 42 | SET TOOLS_SWITCHES=/cf /git /nodejs /predixcli 43 | 44 | SET SHELL_SCRIPT_URL=https://raw.githubusercontent.com/PredixDev/!REPO_NAME!/!BRANCH!/scripts/!SHELL_SCRIPT_NAME! 45 | SET VERSION_JSON_URL=https://raw.githubusercontent.com/PredixDev/!REPO_NAME!/!BRANCH!/version.json 46 | 47 | GOTO START 48 | 49 | :CHECK_DIR 50 | IF not "!CURRENTDIR!"=="!CURRENTDIR:System32=!" ( 51 | ECHO. 52 | ECHO. 53 | ECHO Exiting tutorial. Looks like you are in the system32 directory, please change directories, e.g. \Users\your-login-name 54 | EXIT /b 1 55 | ) 56 | GOTO :eof 57 | 58 | :CHECK_FAIL 59 | IF NOT !errorlevel! EQU 0 ( 60 | CALL :MANUAL 61 | ) 62 | GOTO :eof 63 | 64 | :MANUAL 65 | ECHO. 66 | ECHO. 67 | ECHO Exiting tutorial. You can manually go through the tutorial steps here 68 | ECHO !TUTORIAL! 69 | GOTO :eof 70 | 71 | :CHECK_PERMISSIONS 72 | echo Administrative permissions required. Detecting permissions... 73 | 74 | net session >nul 2>&1 75 | if %errorLevel% == 0 ( 76 | echo Success: Administrative permissions confirmed. 77 | ) else ( 78 | echo Failure: Current permissions inadequate. This script installs tools, ensure you are launching Windows Command window by Right clicking and choosing 'Run as Administrator'. 79 | EXIT /b 1 80 | ) 81 | GOTO :eof 82 | 83 | :INIT 84 | IF not "!CURRENTDIR!"=="!CURRENTDIR:System32=!" ( 85 | ECHO. 86 | ECHO. 87 | ECHO Exiting tutorial. Looks like you are in the system32 directory, please change directories, e.g. \Users\your-login-name 88 | EXIT /b 1 89 | ) 90 | IF not "!CURRENTDIR!"=="!CURRENTDIR:\scripts=!" ( 91 | ECHO. 92 | ECHO. 93 | ECHO Exiting tutorial. Please launch the script from the root dir of the project 94 | EXIT /b 1 95 | ) 96 | 97 | mkdir !APP_DIR! 98 | PUSHD !APP_DIR! 99 | cd 100 | 101 | ECHO Let's start by verifying that you have the required tools installed. 102 | SET /p answer=Should we install the required tools if not already installed (!TOOLS!)? 103 | IF "!answer!"=="" ( 104 | SET /p answer=Specify yes/no - 105 | ) 106 | IF "!answer:~0,1!"=="y" SET doInstall=Y 107 | IF "!answer:~0,1!"=="Y" echo doInstall=Y 108 | 109 | if "!doInstall!"=="Y" ( 110 | CALL :CHECK_PERMISSIONS 111 | IF NOT !errorlevel! EQU 0 EXIT /b !errorlevel! 112 | 113 | CALL :GET_DEPENDENCIES 114 | 115 | ECHO Calling setup-windows.bat 116 | CALL "setup-windows.bat" !TOOLS_SWITCHES! 117 | IF NOT !errorlevel! EQU 0 ( 118 | ECHO. 119 | ECHO "Unable to install tools. Is there a proxy server? Perhaps if you go on a regular internet connection (turning off any proxy variables), the tools portion of the install will succeed. Please read this tutorial for detailed info about setting your proxy https://www.predix.io/resources/tutorials/tutorial-details.html?tutorial_id=1565" 120 | EXIT /b !errorlevel! 121 | ) 122 | ECHO. 123 | ECHO The required tools have been installed or you have chosen to not install them. Now you can proceed with the tutorial. 124 | pause 125 | ) 126 | 127 | GOTO :eof 128 | 129 | :GET_DEPENDENCIES 130 | ECHO Getting Dependencies 131 | 132 | powershell -Command "(new-object net.webclient).DownloadFile('!IZON_BAT!','izon.bat')" 133 | powershell -Command "(new-object net.webclient).DownloadFile('!VERSION_JSON_URL!','version.json')" 134 | CALL izon.bat READ_DEPENDENCY local-setup LOCAL_SETUP_URL LOCAL_SETUP_BRANCH %cd% 135 | ECHO "LOCAL_SETUP_BRANCH=!LOCAL_SETUP_BRANCH!" 136 | SET SETUP_WINDOWS=https://raw.githubusercontent.com/PredixDev/local-setup/!LOCAL_SETUP_BRANCH!/setup-windows.bat 137 | rem SET SETUP_WINDOWS=https://raw.githubusercontent.com/PredixDev/local-setup/!LOCAL_SETUP_BRANCH!/setup-windows.bat 138 | 139 | ECHO !SETUP_WINDOWS! 140 | powershell -Command "(new-object net.webclient).DownloadFile('!SETUP_WINDOWS!','setup-windows.bat')" 141 | 142 | GOTO :eof 143 | 144 | :START 145 | 146 | CALL :CHECK_DIR 147 | 148 | ECHO. 149 | ECHO Welcome to the !APP_NAME! Quickstart. 150 | ECHO -------------------------------------------------------------- 151 | ECHO. 152 | ECHO This is an automated script which will guide you through the tutorial. 153 | ECHO. 154 | 155 | if "!SKIP_SETUP!"=="FALSE" ( 156 | CALL :INIT 157 | ) 158 | CALL :CHECK_FAIL 159 | IF NOT !errorlevel! EQU 0 EXIT /b !errorlevel! 160 | 161 | 162 | if !CF_URL!=="" ( 163 | ECHO CF_URL=!CF_URL! 164 | ) else ( 165 | rem this is here so jenkins can non-interactively log in to the cloud 166 | cf login -a !CF_URL! -u !CF_USER! -p !CF_PASSWORD! -o !CF_ORG! -s !CF_SPACE! 167 | ) 168 | 169 | powershell -Command "(new-object net.webclient).DownloadFile('!SHELL_SCRIPT_URL!','!CURRENTDIR!\!SHELL_SCRIPT_NAME!')" 170 | ECHO Running the !CURRENTDIR!\%SHELL_SCRIPT_NAME% script using Git-Bash 171 | cd !CURRENTDIR! 172 | ECHO. 173 | "%PROGRAMFILES%\Git\bin\bash" --login -i -- "!CURRENTDIR!\%SHELL_SCRIPT_NAME%" -b !BRANCH! --skip-setup !QUICKSTART_ARGS! 174 | -------------------------------------------------------------------------------- /scripts/quickstart-front-end-basic-node-express.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION 3 | set CURRENTDIR=%cd% 4 | echo "currentdir=!CURRENTDIR!" 5 | SET FILE_NAME=%0 6 | SET BRANCH=master 7 | SET SKIP_SETUP=FALSE 8 | SET CF_URL="" 9 | 10 | :GETOPTS 11 | IF /I [%1] == [--skip-setup] SET SKIP_SETUP=TRUE 12 | rem Here we call SHIFT twice to remove the switch and value, 13 | rem since these are not needed by the .sh script. 14 | IF /I [%1] == [-b] SET BRANCH=%2& SHIFT & SHIFT 15 | IF /I [%1] == [--branch] SET BRANCH=%2& SHIFT & SHIFT 16 | IF /I [%1] == [--cf-url] SET CF_URL=%2& SHIFT & SHIFT 17 | IF /I [%1] == [--cf-user] SET CF_USER=%2& SHIFT & SHIFT 18 | IF /I [%1] == [--cf-password] SET CF_PASSWORD=%2& SHIFT & SHIFT 19 | IF /I [%1] == [--cf-org] SET CF_ORG=%2& SHIFT & SHIFT 20 | IF /I [%1] == [--cf-space] SET CF_SPACE=%2& SHIFT & SHIFT 21 | SET QUICKSTART_ARGS=!QUICKSTART_ARGS! %1 22 | rem echo "#### QUICKSTART_ARGS: !QUICKSTART_ARGS!" 23 | SHIFT & IF NOT [%1]==[] GOTO :GETOPTS 24 | GOTO :AFTERGETOPTS 25 | 26 | CALL :GETOPTS %* 27 | :AFTERGETOPTS 28 | 29 | IF [!BRANCH!]==[] ( 30 | ECHO "Usage: %FILE_NAME% -b/--branch " 31 | EXIT /b 1 32 | ) 33 | 34 | SET IZON_BAT=https://raw.githubusercontent.com/PredixDev/izon/1.2.0/izon.bat 35 | SET TUTORIAL="https://www.predix.io/resources/tutorials/tutorial-details.html?tutorial_id=1569&tag=1719&journey=Hello%20World&resources=1844,1475,1569,1523" 36 | SET REPO_NAME=predix-nodejs-starter 37 | SET SHELL_SCRIPT_NAME=quickstart-front-end-basic-node-express.sh 38 | SET APP_DIR="build-basic-app" 39 | SET APP_NAME=Front End Basic App - Node.js Express with UAA, Asset, Time Series 40 | SET TOOLS=Cloud Foundry CLI, Git, Node.js, Maven, Predix CLI 41 | SET TOOLS_SWITCHES=/cf /git /nodejs /maven /predixcli 42 | 43 | SET SHELL_SCRIPT_URL=https://raw.githubusercontent.com/PredixDev/!REPO_NAME!/!BRANCH!/scripts/!SHELL_SCRIPT_NAME! 44 | SET VERSION_JSON_URL=https://raw.githubusercontent.com/PredixDev/!REPO_NAME!/!BRANCH!/version.json 45 | 46 | GOTO START 47 | 48 | :CHECK_DIR 49 | IF not "!CURRENTDIR!"=="!CURRENTDIR:System32=!" ( 50 | ECHO. 51 | ECHO. 52 | ECHO Exiting tutorial. Looks like you are in the system32 directory, please change directories, e.g. \Users\your-login-name 53 | EXIT /b 1 54 | ) 55 | GOTO :eof 56 | 57 | :CHECK_FAIL 58 | IF NOT !errorlevel! EQU 0 ( 59 | CALL :MANUAL 60 | ) 61 | GOTO :eof 62 | 63 | :MANUAL 64 | ECHO. 65 | ECHO. 66 | ECHO Exiting tutorial. You can manually go through the tutorial steps here 67 | ECHO !TUTORIAL! 68 | GOTO :eof 69 | 70 | :CHECK_PERMISSIONS 71 | echo Administrative permissions required. Detecting permissions... 72 | 73 | net session >nul 2>&1 74 | if %errorLevel% == 0 ( 75 | echo Success: Administrative permissions confirmed. 76 | ) else ( 77 | echo Failure: Current permissions inadequate. This script installs tools, ensure you are launching Windows Command window by Right clicking and choosing 'Run as Administrator'. 78 | EXIT /b 1 79 | ) 80 | GOTO :eof 81 | 82 | :INIT 83 | IF not "!CURRENTDIR!"=="!CURRENTDIR:System32=!" ( 84 | ECHO. 85 | ECHO. 86 | ECHO Exiting tutorial. Looks like you are in the system32 directory, please change directories, e.g. \Users\your-login-name 87 | EXIT /b 1 88 | ) 89 | IF not "!CURRENTDIR!"=="!CURRENTDIR:\scripts=!" ( 90 | ECHO. 91 | ECHO. 92 | ECHO Exiting tutorial. Please launch the script from the root dir of the project 93 | EXIT /b 1 94 | ) 95 | 96 | mkdir !APP_DIR! 97 | PUSHD !APP_DIR! 98 | cd 99 | 100 | ECHO Let's start by verifying that you have the required tools installed. 101 | SET /p answer=Should we install the required tools if not already installed (!TOOLS!)? 102 | IF "!answer!"=="" ( 103 | SET /p answer=Specify yes/no - 104 | ) 105 | IF "!answer:~0,1!"=="y" SET doInstall=Y 106 | IF "!answer:~0,1!"=="Y" echo doInstall=Y 107 | 108 | if "!doInstall!"=="Y" ( 109 | CALL :CHECK_PERMISSIONS 110 | IF NOT !errorlevel! EQU 0 EXIT /b !errorlevel! 111 | 112 | CALL :GET_DEPENDENCIES 113 | 114 | ECHO Calling setup-windows.bat 115 | CALL "setup-windows.bat" !TOOLS_SWITCHES! 116 | IF NOT !errorlevel! EQU 0 ( 117 | ECHO. 118 | ECHO "Unable to install tools. Is there a proxy server? Perhaps if you go on a regular internet connection (turning off any proxy variables), the tools portion of the install will succeed. Please read this tutorial for detailed info about setting your proxy https://www.predix.io/resources/tutorials/tutorial-details.html?tutorial_id=1565" 119 | EXIT /b !errorlevel! 120 | ) 121 | ECHO. 122 | ECHO The required tools have been installed or you have chosen to not install them. Now you can proceed with the tutorial. 123 | pause 124 | ) 125 | 126 | GOTO :eof 127 | 128 | :GET_DEPENDENCIES 129 | ECHO Getting Dependencies 130 | 131 | powershell -Command "(new-object net.webclient).DownloadFile('!IZON_BAT!','izon.bat')" 132 | powershell -Command "(new-object net.webclient).DownloadFile('!VERSION_JSON_URL!','version.json')" 133 | CALL izon.bat READ_DEPENDENCY local-setup LOCAL_SETUP_URL LOCAL_SETUP_BRANCH %cd% 134 | ECHO "LOCAL_SETUP_BRANCH=!LOCAL_SETUP_BRANCH!" 135 | SET SETUP_WINDOWS=https://raw.githubusercontent.com/PredixDev/local-setup/!LOCAL_SETUP_BRANCH!/setup-windows.bat 136 | rem SET SETUP_WINDOWS=https://raw.githubusercontent.com/PredixDev/local-setup/!LOCAL_SETUP_BRANCH!/setup-windows.bat 137 | 138 | ECHO !SETUP_WINDOWS! 139 | powershell -Command "(new-object net.webclient).DownloadFile('!SETUP_WINDOWS!','setup-windows.bat')" 140 | 141 | GOTO :eof 142 | 143 | :START 144 | 145 | CALL :CHECK_DIR 146 | 147 | ECHO. 148 | ECHO Welcome to the !APP_NAME! Quickstart. 149 | ECHO -------------------------------------------------------------- 150 | ECHO. 151 | ECHO This is an automated script which will guide you through the tutorial. 152 | ECHO. 153 | 154 | if "!SKIP_SETUP!"=="FALSE" ( 155 | CALL :INIT 156 | ) 157 | CALL :CHECK_FAIL 158 | IF NOT !errorlevel! EQU 0 EXIT /b !errorlevel! 159 | 160 | 161 | if !CF_URL!=="" ( 162 | ECHO CF_URL=!CF_URL! 163 | ) else ( 164 | rem this is here so jenkins can non-interactively log in to the cloud 165 | cf login -a !CF_URL! -u !CF_USER! -p !CF_PASSWORD! -o !CF_ORG! -s !CF_SPACE! 166 | ) 167 | 168 | powershell -Command "(new-object net.webclient).DownloadFile('!SHELL_SCRIPT_URL!','!CURRENTDIR!\!SHELL_SCRIPT_NAME!')" 169 | ECHO Running the !CURRENTDIR!\%SHELL_SCRIPT_NAME% script using Git-Bash 170 | cd !CURRENTDIR! 171 | ECHO. 172 | "%PROGRAMFILES%\Git\bin\bash" --login -i -- "!CURRENTDIR!\%SHELL_SCRIPT_NAME%" -b !BRANCH! --skip-setup !QUICKSTART_ARGS! 173 | -------------------------------------------------------------------------------- /server/routes/proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module can be used to set up reverse proxying from client to Predix services. 3 | * It assumes only one UAA instance, one UAA client, and one instance of each service. 4 | * Use setUaaConfig() and setServiceConfig() for local development. 5 | * In cloud foundry, set the following environment vars: clientId, base64ClientCredential 6 | * Info for bound services is read from VCAP environment variables. 7 | */ 8 | 9 | var url = require('url'); 10 | var express = require('express'); 11 | var expressProxy = require('express-http-proxy'); 12 | var HttpsProxyAgent = require('https-proxy-agent'); 13 | var router = express.Router(); 14 | var vcapServices = {}; 15 | 16 | var corporateProxyServer = process.env.http_proxy || process.env.HTTP_PROXY || process.env.https_proxy || process.env.HTTPS_PROXY; 17 | var corporateProxyAgent; 18 | if (corporateProxyServer) { 19 | corporateProxyAgent = new HttpsProxyAgent(corporateProxyServer); 20 | } 21 | 22 | var clientId = process.env.clientId; 23 | var base64ClientCredential = process.env.base64ClientCredential; 24 | var uaaURL = (function() { 25 | var vcapsServices = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : {}; 26 | var uaaService = vcapsServices['predix-uaa']; 27 | var uaaURL; 28 | 29 | if(uaaService) { 30 | uaaURL = uaaService[0].credentials.uri; 31 | } 32 | return uaaURL; 33 | }) (); 34 | 35 | // Pass a VCAPS object here if desired, for local config. 36 | // Otherwise, this module reads from VCAP_SERVICES environment variable. 37 | var setServiceConfig = function(vcaps) { 38 | vcapServices = vcaps; 39 | setProxyRoutes(); 40 | }; 41 | 42 | var setUaaConfig = function(options) { 43 | clientId = options.clientId || clientId; 44 | uaaURL = options.uaaURL || uaaURL; 45 | base64ClientCredential = options.base64ClientCredential || base64ClientCredential; 46 | }; 47 | 48 | var getClientToken = function(successCallback, errorCallback) { 49 | var request = require('request'); 50 | var options = { 51 | method: 'POST', 52 | url: uaaURL + '/oauth/token', 53 | form: { 54 | 'grant_type': 'client_credentials', 55 | 'client_id': clientId 56 | }, 57 | headers: { 58 | 'Authorization': 'Basic ' + base64ClientCredential 59 | } 60 | }; 61 | 62 | request(options, function(err, response, body) { 63 | if (!err && response.statusCode == 200) { 64 | console.log('response from getClientToken: ' + body); 65 | var clientTokenResponse = JSON.parse(body); 66 | successCallback(clientTokenResponse['token_type'] + ' ' + clientTokenResponse['access_token']); 67 | } else if (errorCallback) { 68 | errorCallback(body); 69 | } else { 70 | console.log('ERROR fetching client token: ' + body); 71 | } 72 | }); 73 | }; 74 | 75 | function cleanResponseHeaders (rsp, data, req, res, cb) { 76 | res.removeHeader('Access-Control-Allow-Origin'); 77 | cb(null, data); 78 | } 79 | 80 | function buildDecorator(zoneId) { 81 | var decorator = function(req) { 82 | if (corporateProxyAgent) { 83 | req.agent = corporateProxyAgent; 84 | } 85 | req.headers['Content-Type'] = 'application/json'; 86 | if (zoneId) { 87 | req.headers['Predix-Zone-Id'] = zoneId; 88 | } 89 | return req; 90 | }; 91 | return decorator; 92 | } 93 | 94 | function getEndpointAndZone(key, credentials) { 95 | var out = {}; 96 | // ugly code needed since vcap service variables are not consistent across services 97 | // TODO: all the other predix services 98 | if (key === 'predix-asset') { 99 | out.serviceEndpoint = credentials.uri; 100 | out.zoneId = credentials.zone['http-header-value']; 101 | } else if (key === 'predix-timeseries') { 102 | var urlObj = url.parse(credentials.query.uri); 103 | out.serviceEndpoint = urlObj.protocol + '//' + urlObj.host; 104 | out.zoneId = credentials.query['zone-http-header-value']; 105 | } 106 | if (!out.serviceEndpoint) { 107 | console.log('no proxy set for service: ' + key); 108 | } 109 | return out; 110 | } 111 | 112 | var setProxyRoute = function(key, credentials) { 113 | console.log(JSON.stringify(credentials)); 114 | var routeOptions = getEndpointAndZone(key, credentials); 115 | if (!routeOptions.serviceEndpoint) { 116 | return; 117 | } 118 | console.log('setting proxy route for key: ' + key); 119 | console.log('serviceEndpoint: ' + routeOptions.serviceEndpoint); 120 | // console.log('zone id: ' + routeOptions.zoneId); 121 | var decorator = buildDecorator(routeOptions.zoneId); 122 | 123 | router.use('/' + key, expressProxy(routeOptions.serviceEndpoint, { 124 | https: true, 125 | forwardPath: function (req) { 126 | console.log('req.headers'+ req.headers['Authorization']); 127 | console.log('req.url: ' + req.url); 128 | return req.url; 129 | }, 130 | intercept: cleanResponseHeaders, 131 | decorateRequest: decorator 132 | })); 133 | }; 134 | 135 | // Fetches client token, adds to request headers, and stores in session. 136 | // Returns 403 if no session. 137 | // Use this middleware to proxy a request to a secure service, using a client token. 138 | var addClientTokenMiddleware = function(req, res, next) { 139 | function errorHandler(errorString) { 140 | // TODO: fix, so it doesn't return a status 200. 141 | // Tried sendStatus, but headers were already set. 142 | res.send(errorString); 143 | } 144 | console.log('proxy root route'); 145 | if (req.session) { 146 | console.log('session found.'); 147 | if (!req.session.clientToken) { 148 | console.log('fetching client token'); 149 | getClientToken(function(token) { 150 | req.session.clientToken = token; 151 | req.headers['Authorization'] = req.session.clientToken; 152 | next(); 153 | }, errorHandler); 154 | } else { 155 | console.log('client token found in session'); 156 | req.headers['Authorization'] = req.session.clientToken; 157 | next(); 158 | } 159 | } else { 160 | next(res.sendStatus(403).send('Forbidden')); 161 | } 162 | }; 163 | 164 | router.use('/', addClientTokenMiddleware); 165 | 166 | // TODO: Support for multiple instances of the same service. 167 | var setProxyRoutes = function() { 168 | var vcapString = process.env.VCAP_SERVICES; 169 | var serviceKeys = []; 170 | vcapServices = vcapString ? JSON.parse(vcapString) : vcapServices; 171 | console.log('vcaps: ' + JSON.stringify(vcapServices)); 172 | 173 | serviceKeys = Object.keys(vcapServices); 174 | serviceKeys.forEach(function(key) { 175 | setProxyRoute(key, vcapServices[key][0].credentials); 176 | }); 177 | }; 178 | // TODO: only call this, if we find a vcapstring in environment? 179 | setProxyRoutes(); 180 | 181 | // Use this to set up your own proxy route to your custom microservice. 182 | // Path and arguments after the pathPrefix will be passed on to the target endpoint. 183 | // pathPrefix: the path that clients will call in your express app. 184 | // endpoint: the URL of your custom microservice. 185 | // example usage: 186 | // customProxyMiddleware('/my-custom-api', 'https://my-custom-service.run.aws-usw02-pr.ice.predix.io') 187 | var customProxyMiddleware = function(pathPrefix, endpoint) { 188 | console.log('custom endpoint: ' + endpoint); 189 | return expressProxy(endpoint, { 190 | https: true, 191 | forwardPath: function (req) { 192 | var path = req.url.replace(pathPrefix, ''); 193 | console.log('proxying to:', path); 194 | return path; 195 | }, 196 | intercept: cleanResponseHeaders, 197 | decorateRequest: buildDecorator() 198 | }); 199 | }; 200 | 201 | module.exports = { 202 | router: router, 203 | setServiceConfig: setServiceConfig, 204 | setUaaConfig: setUaaConfig, 205 | customProxyMiddleware: customProxyMiddleware, 206 | addClientTokenMiddleware: addClientTokenMiddleware, 207 | expressProxy: expressProxy 208 | }; 209 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var cookieParser = require('cookie-parser'); // used for session cookie 4 | var bodyParser = require('body-parser'); 5 | // simple in-memory session is used here. use connect-redis for production!! 6 | var session = require('express-session'); 7 | var proxy = require('./routes/proxy'); // used when requesting data from real services. 8 | 9 | var index = require('./routes/index'); 10 | 11 | // get config settings from local file or VCAPS env var in the cloud 12 | var config = require('./predix-config'); 13 | 14 | var passport; // only used if you have configured properties for UAA 15 | // configure passport for oauth authentication with UAA 16 | var passportConfig = require('./passport-config'); 17 | 18 | // if running locally, we need to set up the proxy from local config file: 19 | var node_env = process.env.node_env || 'development'; 20 | if (node_env === 'development') { 21 | var devConfig = require('./localConfig.json')[node_env]; 22 | proxy.setServiceConfig(config.buildVcapObjectFromLocalConfig(devConfig)); 23 | proxy.setUaaConfig(devConfig); 24 | } 25 | 26 | //a back-end java microservice used in the Build A Basic App learningpath 27 | var windServiceURL = devConfig ? devConfig.windServiceURL : process.env.windServiceURL; 28 | 29 | console.log('************'+node_env+'******************'); 30 | 31 | if (config.isUaaConfigured()) { 32 | passport = passportConfig.configurePassportStrategy(config); 33 | } 34 | 35 | //turns on or off text or links depending on which tutorial you are in, guides you to the next tutorial 36 | var learningpaths = require('./learningpaths/learningpaths.js'); 37 | 38 | /********************************************************************** 39 | SETTING UP EXRESS SERVER 40 | ***********************************************************************/ 41 | var app = express(); 42 | 43 | app.set('trust proxy', 1); 44 | app.use(cookieParser('predixsample')); 45 | // Initializing default session store 46 | // *** Use this in-memory session store for development only. Use redis for prod. ** 47 | app.use(session({ 48 | secret: 'predixsample', 49 | name: 'cookie_name', 50 | proxy: true, 51 | resave: true, 52 | saveUninitialized: true})); 53 | 54 | if (config.isUaaConfigured()) { 55 | app.use(passport.initialize()); 56 | // Also use passport.session() middleware, to support persistent login sessions (recommended). 57 | app.use(passport.session()); 58 | } 59 | 60 | //Initializing application modules 61 | app.use(bodyParser.json()); 62 | app.use(bodyParser.urlencoded({ extended: false })); 63 | 64 | var server = app.listen(process.env.VCAP_APP_PORT || 5000, function () { 65 | console.log ('Server started on port: ' + server.address().port); 66 | }); 67 | 68 | /******************************************************* 69 | SET UP MOCK API ROUTES 70 | *******************************************************/ 71 | // Import route modules 72 | // var viewServiceRoutes = require('./routes/view-service-routes.js')(); 73 | // var assetRoutes = require('./routes/predix-asset-routes.js')(); 74 | // var timeSeriesRoutes = require('./routes/time-series-routes.js')(); 75 | 76 | // add mock API routes. (Remove these before deploying to production.) 77 | //app.use('/api/view-service', jsonServer.router(viewServiceRoutes)); 78 | //app.use('/api/predix-asset', jsonServer.router(assetRoutes)); 79 | //app.use('/api/time-series', jsonServer.router(timeSeriesRoutes)); 80 | 81 | /**************************************************************************** 82 | SET UP EXPRESS ROUTES 83 | *****************************************************************************/ 84 | 85 | //route to retrieve learningpath info which drives what is displayed 86 | app.get('/learning-paths', function(req, res) { 87 | //console.log(learningpaths); 88 | res.json({"learningPathsConfig": learningpaths.getLearningPaths(config)}); 89 | }); 90 | 91 | app.use(express.static(path.join(__dirname, process.env['base-dir'] ? process.env['base-dir'] : '../public'))); 92 | 93 | if (config.isUaaConfigured()) { 94 | //Use this route to make the entire app secure. This forces login for any path in the entire app. 95 | app.use('/', index); 96 | //login route redirect to predix uaa login page 97 | app.get('/login',passport.authenticate('predix', {'scope': ''}), function(req, res) { 98 | // The request will be redirected to Predix for authentication, so this 99 | // function will not be called. 100 | }); 101 | 102 | // access real Predix services using this route. 103 | // the proxy will add UAA token and Predix Zone ID. 104 | app.use('/predix-api', 105 | passport.authenticate('main', { 106 | noredirect: true 107 | }), 108 | proxy.router); 109 | 110 | //callback route redirects to secure route after login 111 | app.get('/callback', passport.authenticate('predix', { 112 | failureRedirect: '/' 113 | }), function(req, res) { 114 | console.log('Redirecting to secure route...'); 115 | res.redirect('/secure'); 116 | }); 117 | 118 | // example of calling a custom microservice. 119 | if (windServiceURL && windServiceURL.indexOf('https') === 0) { 120 | app.get('/api/services/windservices/*', passport.authenticate('main', { noredirect: true}), 121 | // if calling a secure microservice, you can use this middleware to add a client token. 122 | // proxy.addClientTokenMiddleware, 123 | proxy.customProxyMiddleware('/api', windServiceURL) 124 | ); 125 | } 126 | 127 | /** 128 | ** this endpoint is required by Timeseries.js, for winddata is switch 129 | **/ 130 | app.get('/config-details', passport.authenticate('main', { 131 | noredirect: true //Don't redirect a user to the authentication page, just show an error 132 | }), function(req, res) { 133 | console.log('Accessing the secure route data'); 134 | res.setHeader('Content-Type', 'application/json'); 135 | var configuration = {}; 136 | if(!windServiceURL) { 137 | configuration.connectToTimeseries = "true"; 138 | } 139 | if(config.assetURL && config.assetZoneId) { 140 | configuration.isConnectedAssetEnabled = "true"; 141 | } 142 | res.send(JSON.stringify(configuration)); 143 | 144 | }); 145 | 146 | //Or you can follow this pattern to create secure routes, 147 | // if only some portions of the app are secure. 148 | app.get('/secure', passport.authenticate('main', { 149 | noredirect: true //Don't redirect a user to the authentication page, just show an error 150 | }), function(req, res) { 151 | console.log('Accessing the secure route'); 152 | // modify this to send a secure.html file if desired. 153 | res.sendFile(path.join(__dirname + '/../secure/secure.html')); 154 | //res.send('

This is a sample secure route.

'); 155 | }); 156 | 157 | 158 | 159 | } 160 | 161 | //logout route 162 | app.get('/logout', function(req, res) { 163 | req.session.destroy(); 164 | req.logout(); 165 | passportConfig.reset(); //reset auth tokens 166 | res.redirect(config.uaaURL + '/logout?redirect=' + config.appURL); 167 | }); 168 | 169 | app.get('/favicon.ico', function (req, res) { 170 | res.send('favicon.ico'); 171 | }); 172 | 173 | // Sample route middleware to ensure user is authenticated. 174 | // Use this route middleware on any resource that needs to be protected. If 175 | // the request is authenticated (typically via a persistent login session), 176 | // the request will proceed. Otherwise, the user will be redirected to the 177 | // login page. 178 | //currently not being used as we are using passport-oauth2-middleware to check if 179 | //token has expired 180 | /* 181 | function ensureAuthenticated(req, res, next) { 182 | if(req.isAuthenticated()) { 183 | return next(); 184 | } 185 | res.redirect('/'); 186 | } 187 | */ 188 | 189 | ////// error handlers ////// 190 | // catch 404 and forward to error handler 191 | app.use(function(err, req, res, next) { 192 | console.error(err.stack); 193 | var err = new Error('Not Found'); 194 | err.status = 404; 195 | next(err); 196 | }); 197 | 198 | // development error handler - prints stacktrace 199 | if (node_env === 'development') { 200 | app.use(function(err, req, res, next) { 201 | if (!res.headersSent) { 202 | res.status(err.status || 500); 203 | res.send({ 204 | message: err.message, 205 | error: err 206 | }); 207 | } 208 | }); 209 | } 210 | 211 | // production error handler 212 | // no stacktraces leaked to user 213 | app.use(function(err, req, res, next) { 214 | if (!res.headersSent) { 215 | res.status(err.status || 500); 216 | res.send({ 217 | message: err.message, 218 | error: {} 219 | }); 220 | } 221 | }); 222 | 223 | module.exports = app; 224 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GE Software Development License Agreement – General Release 2 | 3 | THIS SOFTWARE LICENSE AGREEMENT (the “License”) describes the rights granted by the General Electric Company, operating through GE Digital (also referred to as “GE Software”), located at 2623 Camino Ramon, San Ramon, CA 94583 (herein referred to as “Licensor”) to any entity (the “Licensee”) receiving a copy of any of the following GE Digital development materials: Predix DevBox; Predix Reference Application (“RefApp”); Predix Dashboard Seed; Predix Px, Predix Security Service redistributable .jar files; Predix Machine redistributable .jar files; and Predix Machine SDK . These materials may include scripts, compiled code, supporting components, and documentation and are collectively referred to as the “Licensed Programs”. Both Licensor and Licensee are referred to hereinafter as a “Party” and collectively as the “Parties” to this License 4 | 5 | ### Section 1 – Conditional Grant. 6 | 7 | No Licensee is required to accept this License for use of the Licensed Programs. In the absence of a signed license agreement between Licensor and Licensee specifying alternate terms, any use of the Licensed Programs by the Licensee shall be considered acceptance of these terms. The Licensed Programs are copyrighted and are licensed, not sold, to you. If you are not willing to be bound by the terms of this License, do not install, copy or use the Licensed Programs. If you received this software from any source other than the Licensor, your access to the Licensed Programs is NOT permitted under this License, and you must delete the software and any copies from your systems. 8 | 9 | ### Section 2 – Warranty Disclaimer. 10 | 11 | NO WARRANTIES. LICENSOR AND OUR AFFILIATES, RESELLERS, DISTRIBUTORS, AND VENDORS, MAKE NO WARRANTIES, EXPRESS OR IMPLIED, GUARANTEES OR CONDITIONS WITH RESPECT TO USE OF THE LICENSED PROGRAMS. LICENSEE’S USE OF ALL SUCH PROGRAMS ARE AT LICENSEE’S AND CUSTOMERS’ OWN RISK. LICENSOR PROVIDES THE LICENSED PROGRAMS ON AN “AS IS” BASIS “WITH ALL FAULTS” AND “AS AVAILABLE.” LICENSOR DOES NOT GUARANTEE THE ACCURACY OR TIMELINESS OF INFORMATION AVAILABLE FROM, OR PROCESSED BY, THE LICENSED PROGRAMS. TO THE EXTENT PERMITTED UNDER LAW, LICENSOR EXCLUDES ANY IMPLIED WARRANTIES, INCLUDING FOR MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, WORKMANLIKE EFFORT, AND NON-INFRINGEMENT. NO GUARANTEE OF UNINTERRUPTED, TIMELY, SECURE, OR ERROR-FREE OPERATION IS MADE. 12 | 13 | THESE LICENSED PROGRAMS MAY BE USED AS PART OF A DEVELOPMENT ENVIRONMENT, AND MAY BE COMBINED WITH OTHER CODE BY END-USERS. LICENSOR IS NOT ABLE TO GUARANTEE THAT THE LICENSED PROGRAMS WILL OPERATE WITHOUT DEFECTS WHEN USED IN COMBINATION WITH END-USER SOFTWARE. LICENSEE IS ADVISED TO SAFEGUARD IMPORTANT DATA, TO USE CAUTION, AND NOT TO RELY IN ANY WAY ON THE CORRECT FUNCTIONING OR PERFORMANCE OF ANY COMBINATION OF END-USER SOFTWARE AND THE LICENSED PROGRAMS AND/OR ACCOMPANYING MATERIALS. LICENSEE IS ADVISED NOT TO USE ANY COMBINATION OF LICENSED PROGRAMS AND END-USER PROVIDED SOFTWARE IN A PRODUCTION ENVIRONMENT WITHOUT PRIOR SUITABILITY AND DEFECT TESTING. 14 | 15 | ### Section 3 – Feedback. 16 | 17 | It is expressly understood, acknowledged and agreed that you may provide GE reasonable suggestions, comments and feedback regarding the Software, including but not limited to usability, bug reports and test results, with respect to Software testing (collectively, "Feedback"). If you provide such Feedback to GE, you shall grant GE the following worldwide, non-exclusive, perpetual, irrevocable, royalty free, fully paid up rights: 18 | 19 | A). to make, use, copy, modify, sell, distribute, sub-license, and create derivative works of, the Feedback as part of any product, technology, service, specification or other documentation developed or offered by GE or any of its affiliates (individually and collectively, "GE Products"); 20 | 21 | B). to publicly perform or display, import, broadcast, transmit, distribute, license, offer to sell, and sell, rent, lease or lend copies of the Feedback (and derivative works thereof) as part of any GE Product; 22 | 23 | C). solely with respect to Licensee's copyright and trade secret rights, to sublicense to third parties the foregoing rights, including the right to sublicense to further third parties; and d. to sublicense to third parties any claims of any patents owned or licensable by Licensee that are necessarily infringed by a third party product, technology or service that uses, interfaces, interoperates or communicates with the Feedback or portion thereof incorporated into a GE Product, technology or service. Further, you represent and warrant that your Feedback is not subject to any license terms that would purport to require GE to comply with any additional obligations with respect to any GE Products that incorporate any Feedback. 24 | 25 | ### Section 4 – Reserved 26 | 27 | ### Section 5 – Limitation of Liability. 28 | 29 | LIABILITY ARISING UNDER THIS LICENSE, WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), SHALL BE LIMITED TO DIRECT, OBJECTIVELY MEASURABLE DAMAGES. LICENSOR SHALL HAVE NO LIABILITY TO THE OTHER PARTY OR TO ANY THIRD PARTY, FOR ANY INCIDENTAL, PUNITIVE, INDIRECT, OR CONSEQUENTIAL DAMAGES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LIABILITY FOR ANY SOFTWARE LICENSED FROM THIRD PARTIES FOR USE WITH THE SERVICES IS EXPLICILTLY DISCLAIMED AND LIMITED TO THE MAXIMUM EXTENT PERMITTED BY LAW. 30 | 31 | Notwithstanding anything to the contrary, the aggregate liability of Licensor and its suppliers under this License shall not exceed the total amounts paid by Licensee to Licensor hereunder during the one-year period immediately preceding the event which gave rise to the claims. 32 | 33 | ### Section 6 – License. 34 | 35 | A). License Grant. Subject to the terms and conditions of this License, Licensor hereby grants Licensee a worldwide, perpetual, royalty-free, non-exclusive license to: 36 | 37 | i) install the Licensed Programs on Licensee’s premises, and permit Licensee’s users to use the Licensed Programs so installed, solely for Licensee’s own development, testing, demonstration, staging, and production of Licensee’s own software that makes use of the Licensed Programs in a way that adds substantial functionality not present in the Licensed Programs (the result, a “Licensee Application”); 38 | 39 | ii) permit Licensee to permit third-party hosts (“Hosts”) to install the Licensee Application on such Hosts’ respective premises on Licensee’s behalf, and permit Licensee’s users to access and use the Licensed Programs so installed, solely for Licensee’s own development, testing, demonstration, staging and production purposes 40 | 41 | iii) install the Licensee Application on Licensee’s own premises and permit its own users to use the Licensee Application so installed on the same terms as sub-sections (i) and (ii) above. 42 | 43 | B). For the purposes of this License, the right to “use” the Licensed Programs shall include the right to utilize, run, access, store, copy, test or display the Licensed Programs. No right or license is granted or agreed to be granted to disassemble or decompile any Licensed Programs furnished in object code form, and Licensee agrees not to engage in any such conduct unless permitted by law. Reverse engineering of Licensed Programs provided in object code form is prohibited, unless such a right is explicitly granted by any explicit license subject to sub-section (d) below or as a matter of law, and then only to the extent explicitly permitted. Licensor shall have no obligation to support any such reverse engineering, any product or derivative of such reverse engineering, or any use of the Licensed Programs with any modified versions of any of their components under this License. 44 | 45 | C). Licensee shall ensure that any Licensee Applications incorporate the Licensed Programs in such a way as to prevent third parties (other than Hosts) from viewing the code of the Licensed Programs or gaining access to any programmatic interface or other hidden aspect of the Licensed Programs. Licensee shall also restrict distribution of the Licensed Programs, including as part of Licensee Applications, to only those parties who are notified of, and subject to, an enforceable obligation to refrain from any of the prohibited activities listed herein, such as reverse engineering or disassembling the Licensed Programs. 46 | 47 | D). Use of some open source and third party software applications or components included in or accessed through the Licensed Programs may be subject to other terms and conditions found in a separate license agreement, terms of use or “Notice” file located at the download page. The Licensed Programs are accompanied by additional software components solely to enable the Licensed Programs to operate as designed. Licensee is not permitted to use such additional software independently of the Licensed Programs unless Licensee secures a separate license for use from the named vendor. Do not use any third party code unless you agree with the applicable license terms for that code. 48 | 49 | E). Title. Title to and ownership of the Licensed Programs shall at all times remain with Licensor. 50 | 51 | ### Section 7 – Termination. 52 | 53 | A). The Licensor reserves the right to cease distribution and grant of further licenses to any or all of the Licensed Programs at any time in its sole discretion. 54 | 55 | B). The Licensor reserves the right to at any time and at its sole discretion provide updated versions of any or all of the Licensed Programs that supercede and replace the prior version of that Licensed Program. 56 | 57 | C). Your license rights under Section 6 are effective until terminated as described below: 58 | 59 | i). This license and all rights under it will terminate or cease to be effective without notice if Licensee breaches the terms of the License and does not correct or remedy such breach promptly. 60 | 61 | ii). Notwithstanding the foregoing, Licensee may terminate this License at any time for any reason or no reason by providing the Licensor written notice thereof. 62 | 63 | D). Upon any expiration or termination of this License, the rights and licenses granted to you under this License shall immediately terminate, and you shall immediately cease using and delete the Licensed Programs. Licensee Applications based upon the Licensed Programs (see Section 6(a) above) are not subject to this limitation. 64 | 65 | In the event of any expiration or termination of this Licensee, any Confidentiality provision, disclaimers of GE’s representations and warranties, choice of applicable law and limitations of GE’s liability shall survive. 66 | 67 | ### Section 8 – Applicable Law. 68 | 69 | The License shall be governed by and interpreted in accordance with the substantive law of the State of California, U.S.A., excluding its conflicts of law provisions, and by the courts of that state. 70 | -------------------------------------------------------------------------------- /public/scripts/Timeseries.js: -------------------------------------------------------------------------------- 1 | /** 2 | Global variables 3 | **/ 4 | var lineChartMap ; 5 | var connectedDeviceConfig = ''; 6 | 7 | /** 8 | This function is called on the submit button of Get timeseries data to fetch 9 | data from TimeSeries. 10 | **/ 11 | function onclick_machineServiceData() { 12 | lineChartMap = getMachineServiceData(); 13 | setInterval(updateChart,3000); 14 | } 15 | 16 | /** 17 | * gets time series data for tags 18 | **/ 19 | function getMachineServiceData() { 20 | var tagString = getTagsSelectedValue(); 21 | 22 | if (connectedDeviceConfig.connectToTimeseries) { 23 | // If the Connected Device attribute exists, then 24 | // update the chart without using the Microservice 25 | getMachineServiceDataWithoutMicroservice(); 26 | } 27 | else { 28 | var request = new XMLHttpRequest(); 29 | var starttime = getStartTimeSelectedValue(); 30 | var datapointsUrl = "/api/services/windservices/yearly_data/sensor_id/"+tagString+"?order=asc"; 31 | if(starttime) { 32 | datapointsUrl = datapointsUrl + "&starttime="+starttime; 33 | } 34 | //console.log(tagString); 35 | request.open('GET', datapointsUrl, true); 36 | request.onload = function() { 37 | if (request.status >= 200 && request.status < 400) { 38 | var data = JSON.parse(request.responseText); 39 | document.getElementById("line_chart_info").innerHTML = 'Chart for Tags'; 40 | lineChartMap = constructMachineChartResponse(data); 41 | document.getElementById("windService_machine_yearly").innerHTML = ''; 42 | return lineChartMap; 43 | } else { 44 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 45 | } 46 | }; 47 | request.onerror = function() { 48 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 49 | }; 50 | request.send(); 51 | } 52 | getAssetData(connectedDeviceConfig.isConnectedAssetEnabled,tagString) ; 53 | } 54 | 55 | /** 56 | This function actually performs the retrieval of TimeSeries tags as well as 57 | the data of those tags chosen by the user. Data is queried directly from Timeseries. 58 | **/ 59 | function getMachineServiceDataWithoutMicroservice() { 60 | 61 | var myTimeSeriesBody = { 62 | tags: [] 63 | }; 64 | var timeSeriesGetData = new XMLHttpRequest(); 65 | var tagString = getTagsSelectedValue(); 66 | var starttime = getStartTimeSelectedValue(); 67 | timeSeriesGetData.open('POST', '/predix-api/predix-timeseries/v1/datapoints', true); 68 | 69 | var tags = tagString.split(","); 70 | for (var i=0; i < tags.length; i++) 71 | { 72 | myTimeSeriesBody.tags.push({ 73 | "name" : tags[i], 74 | "limit": 25, 75 | "order": "desc" 76 | }); 77 | } 78 | if(starttime) { 79 | myTimeSeriesBody.start = starttime; 80 | } 81 | 82 | timeSeriesGetData.onload = function() { 83 | if (timeSeriesGetData.status >= 200 && timeSeriesGetData.status < 400) { 84 | var data = JSON.parse(timeSeriesGetData.responseText); 85 | document.getElementById("line_chart_info").innerHTML = 'Chart for Tags'; 86 | var str = JSON.stringify(timeSeriesGetData.responseText, null, 2); 87 | console.log('First call to Timeseries returned data:'+ str); 88 | lineChartMap = constructMachineChartResponse(data); 89 | document.getElementById("windService_machine_yearly").innerHTML = ''; 90 | return lineChartMap; 91 | } else { 92 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 93 | } 94 | }; 95 | timeSeriesGetData.onerror = function() { 96 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 97 | }; 98 | 99 | if (tagString !== undefined) 100 | { 101 | timeSeriesGetData.send(JSON.stringify(myTimeSeriesBody)); 102 | } 103 | } 104 | 105 | /** 106 | Fetching the selected tags 107 | **/ 108 | function getTagsSelectedValue() { 109 | var tagString = ""; 110 | var tagAppender = ""; 111 | var tagList = document.getElementById('tagList'); 112 | for (var tagCount = 0; tagCount < tagList.options.length; tagCount++) { 113 | if(tagList.options[tagCount].selected === true){ 114 | tagString = tagString+tagAppender+tagList.options[tagCount].value ; 115 | tagAppender = ","; 116 | } 117 | } 118 | return tagString; 119 | } 120 | 121 | /** 122 | Fetching the selected start time value 123 | **/ 124 | function getStartTimeSelectedValue() { 125 | var startTime; 126 | 127 | var startTimeList = document.getElementById('start-time'); 128 | for (var stCount = 0; stCount < startTimeList.options.length; stCount++) { 129 | if(startTimeList.options[stCount].selected === true){ 130 | 131 | startTime = startTimeList.options[stCount].value ; 132 | return startTime; 133 | } 134 | } 135 | return startTime; 136 | } 137 | 138 | /** 139 | Method to draw chart as per tags and construct html for same 140 | **/ 141 | function constructMachineChartResponse(data) { 142 | var lineChartMap = new Map(); 143 | // remove exisitn elements -reset 144 | document.getElementById('add_machine_canvas').innerHTML = ""; 145 | // get the base element 146 | var add_machine_canvas = document.getElementById('add_machine_canvas'); 147 | 148 | for(var i = 0; i < data.tags.length; i++) { 149 | var divTag = document.createElement('div'); 150 | divTag.id="windService_machine_div_"+i; 151 | divTag.setAttribute("class", "windyservice_chart_div"); 152 | 153 | add_machine_canvas.appendChild(divTag); 154 | 155 | var add_machine_div = document.getElementById('windService_machine_div_'+i); 156 | var pTagName = document.createElement('p'); 157 | pTagName.id="windService_machine_tag_"+i; 158 | pTagName.class="windyservice_machine_tag"; 159 | add_machine_div.appendChild(pTagName); 160 | 161 | document.getElementById("windService_machine_tag_"+i).innerHTML = data.tags[i].name; 162 | 163 | var canvas = document.createElement('canvas'); 164 | canvas.id="machine_canvas_"+i; 165 | canvas.setAttribute("class", "windyservice_chart_canvas"); 166 | add_machine_div.appendChild(canvas); 167 | 168 | var ctx = document.getElementById(canvas.id).getContext("2d"); 169 | // console.log('constructing new chart, with points: ' + data.tags[i].length); 170 | var lineChartDemo = new Chart(ctx).Line(getMachineLineChartData_each(data.tags[i]), { 171 | responsive: true, 172 | animation: false 173 | }); 174 | lineChartMap.set(data.tags[i].name,lineChartDemo); 175 | 176 | } 177 | return lineChartMap; 178 | } 179 | 180 | /** 181 | Method to update the Chart with the latest data from the selected tags 182 | This method quries the Microservice created in the 'Build a Basic App Journey' 183 | **/ 184 | function updateChart() { 185 | if (connectedDeviceConfig.connectToTimeseries) { 186 | // If the Connected Device attribute exists, then 187 | // update the chart without using the Microservice 188 | updateChartWithoutMicroservice(); 189 | } 190 | else { 191 | var tagString = getTagsSelectedValue(); 192 | var request = new XMLHttpRequest(); 193 | var datapointsUrl = "/api/services/windservices/yearly_data/sensor_id/"+tagString+"?order=asc&starttime=5mi-ago"; 194 | //console.log(datapointsUrl); 195 | request.open('GET', datapointsUrl, true); 196 | request.onload = function() { 197 | if (request.status >= 200 && request.status < 400) { 198 | var data = JSON.parse(request.responseText); 199 | //console.log('updated data is '+str); 200 | for(var i = 0; i < data.tags.length; i++) { 201 | var datapoints = data.tags[i].results[0].values; 202 | for(var j = 0; j < datapoints.length; j++) { 203 | var lineChartDemo = lineChartMap.get(data.tags[i].name); 204 | var d = new Date(datapoints[j][0]); 205 | var formatDate = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); 206 | lineChartDemo.addData([datapoints[j][1]],formatDate); 207 | lineChartDemo.removeData(); 208 | } 209 | } 210 | document.getElementById("windService_machine_yearly").innerHTML = ''; 211 | } 212 | }; 213 | request.onerror = function() { 214 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 215 | }; 216 | request.send(); 217 | } 218 | } 219 | 220 | 221 | /** 222 | Method to update the Chart with the latest data from the selected tags 223 | This method queries Timeseries directly. (not wind data service) 224 | **/ 225 | function updateChartWithoutMicroservice() { 226 | var myTimeSeriesBody = { 227 | tags: [] 228 | }; 229 | var timeSeriesGetData = new XMLHttpRequest(); 230 | var tagString = getTagsSelectedValue(); 231 | var tags = tagString.split(","); 232 | // Should we use the selected start time? Or just use 5 minutes on each update? 233 | // var starttime = getStartTimeSelectedValue(); 234 | timeSeriesGetData.open('POST', '/predix-api/predix-timeseries/v1/datapoints', true); 235 | 236 | for (var i=0; i < tags.length; i++) 237 | { 238 | myTimeSeriesBody.tags.push({ 239 | "name" : tags[i], 240 | "limit": 25, 241 | "order": "desc" 242 | }); 243 | } 244 | myTimeSeriesBody.start = "5mi-ago"; 245 | 246 | timeSeriesGetData.onload = function() { 247 | if (timeSeriesGetData.status >= 200 && timeSeriesGetData.status < 400) { 248 | var data = JSON.parse(timeSeriesGetData.responseText); 249 | console.log("Updated data: " + JSON.stringify(timeSeriesGetData.responseText, null, 2)); 250 | 251 | for(i = 0; i < data.tags.length; i++) { 252 | var datapoints = data.tags[i].results[0].values; 253 | for(var j = datapoints.length - 1; j >= 0; j--) { 254 | // console.log('j: ' + j); 255 | var lineChartDemo = lineChartMap.get(data.tags[i].name); 256 | var d = new Date(datapoints[j][0]); 257 | var formatDate = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); 258 | // console.log('formatDate: ' + formatDate); 259 | lineChartDemo.removeData(); 260 | lineChartDemo.addData([datapoints[j][1]],formatDate); 261 | } 262 | } 263 | } 264 | else { 265 | console.log("Error on updating the chart..."); 266 | } 267 | }; 268 | timeSeriesGetData.send(JSON.stringify(myTimeSeriesBody)); 269 | } 270 | 271 | /* 272 | Method that get the timeseries data and convert that in Chart format. 273 | */ 274 | function getMachineLineChartData_each(tag){ 275 | var dataset = { 276 | label: tag.name, 277 | fillColor: "rgba(220,220,220,0.2)", 278 | strokeColor: "rgba(220,220,220,1)", 279 | pointColor: "rgba(220,220,220,1)", 280 | pointStrokeColor: "#fff", 281 | pointHighlightFill: "#fff", 282 | pointHighlightStroke: "rgba(220,220,220,1)", 283 | data: [] 284 | }; 285 | 286 | var lineChartData = { 287 | labels : [], 288 | datasets : [dataset] 289 | }; 290 | var datapoints = tag.results[0].values; 291 | for(var j = datapoints.length - 1; j >= 0; j--) { 292 | var d = new Date(datapoints[j][0]); 293 | var formatDate = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); 294 | lineChartData.labels.push(formatDate); 295 | lineChartData.datasets[0].data.push(datapoints[j][1]); 296 | } 297 | document.getElementById('windService_machine_yearly').scrollIntoView(); 298 | return lineChartData; 299 | } 300 | 301 | /** 302 | Method to generate the list of tags to choose from 303 | (Called from secure page on load.) 304 | **/ 305 | function configureTagsTimeseriesData() { 306 | 307 | getConnectedServiceConfig().then( 308 | function(response) { 309 | connectedDeviceConfig = JSON.parse(response); 310 | 311 | if (connectedDeviceConfig.connectToTimeseries) { 312 | var headerTitle = document.getElementById('tag_list_title'); 313 | if (headerTitle) { 314 | headerTitle.innerHTML = 'Connected Devices'; 315 | } 316 | var select = document.getElementById('tagList'); 317 | if (select) { 318 | 319 | var timeSeriesGetAllTags = new XMLHttpRequest(); 320 | // call timeseries through the proxy routes set up in express. 321 | timeSeriesGetAllTags.open('GET', '/predix-api/predix-timeseries/v1/tags', true); 322 | 323 | timeSeriesGetAllTags.onload = function() { 324 | if (timeSeriesGetAllTags.status >= 200 && timeSeriesGetAllTags.status < 400) { 325 | var data = JSON.parse(timeSeriesGetAllTags.responseText); 326 | 327 | // Create all Tags (assuming separated by comma) 328 | var tagsToGenerate = []; 329 | if(connectedDeviceConfig.assetTagname){ 330 | tagsToGenerate = (connectedDeviceConfig.assetTagname).split(","); 331 | } 332 | 333 | // Make call to timeseries to get all Tags 334 | for (var i = 0; i < data.results.length; i++) { 335 | var tagname = data.results[i]; 336 | if (tagsToGenerate.indexOf(tagname) < 0) { 337 | tagsToGenerate.push(tagname); 338 | console.log("Adding timeseries tag: " + tagname); 339 | } 340 | } 341 | var tagListElement = document.getElementById('tagList'); 342 | while (tagListElement.firstChild) { 343 | tagListElement.removeChild(tagListElement.firstChild); 344 | } 345 | 346 | for (i=0; i < tagsToGenerate.length; i++) { 347 | if(tagsToGenerate[i]) { 348 | var opt = document.createElement('option'); 349 | opt.value = tagsToGenerate[i].trim(); 350 | opt.innerHTML = tagsToGenerate[i].trim(); 351 | tagListElement.appendChild(opt); 352 | } 353 | } 354 | } 355 | else { 356 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting tags from Timeseries"; 357 | } 358 | }; 359 | timeSeriesGetAllTags.onerror = function() { 360 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting tags from Timeseries"; 361 | }; 362 | timeSeriesGetAllTags.send(); 363 | } 364 | } 365 | else { 366 | getTagsFromMicroservice(); 367 | } 368 | 369 | }, 370 | function(error) { 371 | console.error("Failed when getting the connected device configurations.", error); 372 | }); 373 | } 374 | 375 | function getTagsFromMicroservice (){ 376 | var request = new XMLHttpRequest(); 377 | request.open('GET', '/api/services/windservices/tags', true); 378 | request.onload = function() { 379 | if (request.status >= 200 && request.status < 400) { 380 | var data = JSON.parse(request.responseText); 381 | //console.log('tags response is '+JSON.stringify(request.responseText, null, 2)); 382 | var select = document.getElementById('tagList'); 383 | if (select) { 384 | for(var tagCount = 0; tagCount < data.results.length; tagCount++) { 385 | var opt = document.createElement('option'); 386 | opt.value = data.results[tagCount]; 387 | if(tagCount === 0){ 388 | opt.selected = "selected"; 389 | } 390 | opt.innerHTML = data.results[tagCount]; 391 | select.appendChild(opt); 392 | } 393 | } 394 | document.getElementById("windService_machine_yearly").innerHTML = ''; 395 | } else { 396 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 397 | } 398 | }; 399 | request.onerror = function() { 400 | document.getElementById("windService_machine_yearly").innerHTML = "Error getting data for tags"; 401 | }; 402 | request.send(); 403 | } 404 | 405 | /** 406 | Method to make the necessary rest call and get the connected device configurations 407 | from the server 408 | **/ 409 | function getConnectedServiceConfig() { 410 | console.log("Making call to /config-details to get services configurations..."); 411 | return new Promise(function(resolve, reject) { 412 | var request = new XMLHttpRequest(); 413 | request.open('GET', '/config-details'); 414 | request.onload = function() { 415 | if (request.status == 200) { 416 | resolve(request.response); 417 | } 418 | else { 419 | reject(Error(request.statusText)); 420 | } 421 | }; 422 | request.send(); 423 | }); 424 | } 425 | -------------------------------------------------------------------------------- /public/scripts/Chart.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Chart.js 3 | * http://chartjs.org/ 4 | * Version: 1.0.2 5 | * 6 | * Copyright 2015 Nick Downie 7 | * Released under the MIT license 8 | * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md 9 | */ 10 | 11 | 12 | (function(){ 13 | 14 | "use strict"; 15 | 16 | //Declare root variable - window in the browser, global on the server 17 | var root = this, 18 | previous = root.Chart; 19 | 20 | //Occupy the global variable of Chart, and create a simple base class 21 | var Chart = function(context){ 22 | var chart = this; 23 | this.canvas = context.canvas; 24 | 25 | this.ctx = context; 26 | 27 | //Variables global to the chart 28 | var computeDimension = function(element,dimension) 29 | { 30 | if (element['offset'+dimension]) 31 | { 32 | return element['offset'+dimension]; 33 | } 34 | else 35 | { 36 | return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); 37 | } 38 | } 39 | 40 | var width = this.width = computeDimension(context.canvas,'Width'); 41 | var height = this.height = computeDimension(context.canvas,'Height'); 42 | 43 | // Firefox requires this to work correctly 44 | context.canvas.width = width; 45 | context.canvas.height = height; 46 | 47 | var width = this.width = context.canvas.width; 48 | var height = this.height = context.canvas.height; 49 | this.aspectRatio = this.width / this.height; 50 | //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. 51 | helpers.retinaScale(this); 52 | 53 | return this; 54 | }; 55 | //Globally expose the defaults to allow for user updating/changing 56 | Chart.defaults = { 57 | global: { 58 | // Boolean - Whether to animate the chart 59 | animation: true, 60 | 61 | // Number - Number of animation steps 62 | animationSteps: 60, 63 | 64 | // String - Animation easing effect 65 | animationEasing: "easeOutQuart", 66 | 67 | // Boolean - If we should show the scale at all 68 | showScale: true, 69 | 70 | // Boolean - If we want to override with a hard coded scale 71 | scaleOverride: false, 72 | 73 | // ** Required if scaleOverride is true ** 74 | // Number - The number of steps in a hard coded scale 75 | scaleSteps: null, 76 | // Number - The value jump in the hard coded scale 77 | scaleStepWidth: null, 78 | // Number - The scale starting value 79 | scaleStartValue: null, 80 | 81 | // String - Colour of the scale line 82 | scaleLineColor: "rgba(0,0,0,.1)", 83 | 84 | // Number - Pixel width of the scale line 85 | scaleLineWidth: 1, 86 | 87 | // Boolean - Whether to show labels on the scale 88 | scaleShowLabels: true, 89 | 90 | // Interpolated JS string - can access value 91 | scaleLabel: "<%=value%>", 92 | 93 | // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there 94 | scaleIntegersOnly: true, 95 | 96 | // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value 97 | scaleBeginAtZero: false, 98 | 99 | // String - Scale label font declaration for the scale label 100 | scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 101 | 102 | // Number - Scale label font size in pixels 103 | scaleFontSize: 12, 104 | 105 | // String - Scale label font weight style 106 | scaleFontStyle: "normal", 107 | 108 | // String - Scale label font colour 109 | scaleFontColor: "#666", 110 | 111 | // Boolean - whether or not the chart should be responsive and resize when the browser does. 112 | responsive: false, 113 | 114 | // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container 115 | maintainAspectRatio: true, 116 | 117 | // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove 118 | showTooltips: true, 119 | 120 | // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function 121 | customTooltips: false, 122 | 123 | // Array - Array of string names to attach tooltip events 124 | tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], 125 | 126 | // String - Tooltip background colour 127 | tooltipFillColor: "rgba(0,0,0,0.8)", 128 | 129 | // String - Tooltip label font declaration for the scale label 130 | tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 131 | 132 | // Number - Tooltip label font size in pixels 133 | tooltipFontSize: 14, 134 | 135 | // String - Tooltip font weight style 136 | tooltipFontStyle: "normal", 137 | 138 | // String - Tooltip label font colour 139 | tooltipFontColor: "#fff", 140 | 141 | // String - Tooltip title font declaration for the scale label 142 | tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", 143 | 144 | // Number - Tooltip title font size in pixels 145 | tooltipTitleFontSize: 14, 146 | 147 | // String - Tooltip title font weight style 148 | tooltipTitleFontStyle: "bold", 149 | 150 | // String - Tooltip title font colour 151 | tooltipTitleFontColor: "#fff", 152 | 153 | // Number - pixel width of padding around tooltip text 154 | tooltipYPadding: 6, 155 | 156 | // Number - pixel width of padding around tooltip text 157 | tooltipXPadding: 6, 158 | 159 | // Number - Size of the caret on the tooltip 160 | tooltipCaretSize: 8, 161 | 162 | // Number - Pixel radius of the tooltip border 163 | tooltipCornerRadius: 6, 164 | 165 | // Number - Pixel offset from point x to tooltip edge 166 | tooltipXOffset: 10, 167 | 168 | // String - Template string for single tooltips 169 | tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", 170 | 171 | // String - Template string for single tooltips 172 | multiTooltipTemplate: "<%= value %>", 173 | 174 | // String - Colour behind the legend colour block 175 | multiTooltipKeyBackground: '#fff', 176 | 177 | // Function - Will fire on animation progression. 178 | onAnimationProgress: function(){}, 179 | 180 | // Function - Will fire on animation completion. 181 | onAnimationComplete: function(){} 182 | 183 | } 184 | }; 185 | 186 | //Create a dictionary of chart types, to allow for extension of existing types 187 | Chart.types = {}; 188 | 189 | //Global Chart helpers object for utility methods and classes 190 | var helpers = Chart.helpers = {}; 191 | 192 | //-- Basic js utility methods 193 | var each = helpers.each = function(loopable,callback,self){ 194 | var additionalArgs = Array.prototype.slice.call(arguments, 3); 195 | // Check to see if null or undefined firstly. 196 | if (loopable){ 197 | if (loopable.length === +loopable.length){ 198 | var i; 199 | for (i=0; i= 0; i--) { 271 | var currentItem = arrayToSearch[i]; 272 | if (filterCallback(currentItem)){ 273 | return currentItem; 274 | } 275 | } 276 | }, 277 | inherits = helpers.inherits = function(extensions){ 278 | //Basic javascript inheritance based on the model created in Backbone.js 279 | var parent = this; 280 | var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; 281 | 282 | var Surrogate = function(){ this.constructor = ChartElement;}; 283 | Surrogate.prototype = parent.prototype; 284 | ChartElement.prototype = new Surrogate(); 285 | 286 | ChartElement.extend = inherits; 287 | 288 | if (extensions) extend(ChartElement.prototype, extensions); 289 | 290 | ChartElement.__super__ = parent.prototype; 291 | 292 | return ChartElement; 293 | }, 294 | noop = helpers.noop = function(){}, 295 | uid = helpers.uid = (function(){ 296 | var id=0; 297 | return function(){ 298 | return "chart-" + id++; 299 | }; 300 | })(), 301 | warn = helpers.warn = function(str){ 302 | //Method for warning of errors 303 | if (window.console && typeof window.console.warn == "function") console.warn(str); 304 | }, 305 | amd = helpers.amd = (typeof define == 'function' && define.amd), 306 | //-- Math methods 307 | isNumber = helpers.isNumber = function(n){ 308 | return !isNaN(parseFloat(n)) && isFinite(n); 309 | }, 310 | max = helpers.max = function(array){ 311 | return Math.max.apply( Math, array ); 312 | }, 313 | min = helpers.min = function(array){ 314 | return Math.min.apply( Math, array ); 315 | }, 316 | cap = helpers.cap = function(valueToCap,maxValue,minValue){ 317 | if(isNumber(maxValue)) { 318 | if( valueToCap > maxValue ) { 319 | return maxValue; 320 | } 321 | } 322 | else if(isNumber(minValue)){ 323 | if ( valueToCap < minValue ){ 324 | return minValue; 325 | } 326 | } 327 | return valueToCap; 328 | }, 329 | getDecimalPlaces = helpers.getDecimalPlaces = function(num){ 330 | if (num%1!==0 && isNumber(num)){ 331 | return num.toString().split(".")[1].length; 332 | } 333 | else { 334 | return 0; 335 | } 336 | }, 337 | toRadians = helpers.radians = function(degrees){ 338 | return degrees * (Math.PI/180); 339 | }, 340 | // Gets the angle from vertical upright to the point about a centre. 341 | getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ 342 | var distanceFromXCenter = anglePoint.x - centrePoint.x, 343 | distanceFromYCenter = anglePoint.y - centrePoint.y, 344 | radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); 345 | 346 | 347 | var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); 348 | 349 | //If the segment is in the top left quadrant, we need to add another rotation to the angle 350 | if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ 351 | angle += Math.PI*2; 352 | } 353 | 354 | return { 355 | angle: angle, 356 | distance: radialDistanceFromCenter 357 | }; 358 | }, 359 | aliasPixel = helpers.aliasPixel = function(pixelWidth){ 360 | return (pixelWidth % 2 === 0) ? 0 : 0.5; 361 | }, 362 | splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ 363 | //Props to Rob Spencer at scaled innovation for his post on splining between points 364 | //http://scaledinnovation.com/analytics/splines/aboutSplines.html 365 | var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), 366 | d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), 367 | fa=t*d01/(d01+d12),// scaling factor for triangle Ta 368 | fb=t*d12/(d01+d12); 369 | return { 370 | inner : { 371 | x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), 372 | y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) 373 | }, 374 | outer : { 375 | x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), 376 | y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) 377 | } 378 | }; 379 | }, 380 | calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ 381 | return Math.floor(Math.log(val) / Math.LN10); 382 | }, 383 | calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ 384 | 385 | //Set a minimum step of two - a point at the top of the graph, and a point at the base 386 | var minSteps = 2, 387 | maxSteps = Math.floor(drawingSize/(textSize * 1.5)), 388 | skipFitting = (minSteps >= maxSteps); 389 | 390 | var maxValue = max(valuesArray), 391 | minValue = min(valuesArray); 392 | 393 | // We need some degree of seperation here to calculate the scales if all the values are the same 394 | // Adding/minusing 0.5 will give us a range of 1. 395 | if (maxValue === minValue){ 396 | maxValue += 0.5; 397 | // So we don't end up with a graph with a negative start value if we've said always start from zero 398 | if (minValue >= 0.5 && !startFromZero){ 399 | minValue -= 0.5; 400 | } 401 | else{ 402 | // Make up a whole number above the values 403 | maxValue += 0.5; 404 | } 405 | } 406 | 407 | var valueRange = Math.abs(maxValue - minValue), 408 | rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), 409 | graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), 410 | graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), 411 | graphRange = graphMax - graphMin, 412 | stepValue = Math.pow(10, rangeOrderOfMagnitude), 413 | numberOfSteps = Math.round(graphRange / stepValue); 414 | 415 | //If we have more space on the graph we'll use it to give more definition to the data 416 | while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { 417 | if(numberOfSteps > maxSteps){ 418 | stepValue *=2; 419 | numberOfSteps = Math.round(graphRange/stepValue); 420 | // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. 421 | if (numberOfSteps % 1 !== 0){ 422 | skipFitting = true; 423 | } 424 | } 425 | //We can fit in double the amount of scale points on the scale 426 | else{ 427 | //If user has declared ints only, and the step value isn't a decimal 428 | if (integersOnly && rangeOrderOfMagnitude >= 0){ 429 | //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float 430 | if(stepValue/2 % 1 === 0){ 431 | stepValue /=2; 432 | numberOfSteps = Math.round(graphRange/stepValue); 433 | } 434 | //If it would make it a float break out of the loop 435 | else{ 436 | break; 437 | } 438 | } 439 | //If the scale doesn't have to be an int, make the scale more granular anyway. 440 | else{ 441 | stepValue /=2; 442 | numberOfSteps = Math.round(graphRange/stepValue); 443 | } 444 | 445 | } 446 | } 447 | 448 | if (skipFitting){ 449 | numberOfSteps = minSteps; 450 | stepValue = graphRange / numberOfSteps; 451 | } 452 | 453 | return { 454 | steps : numberOfSteps, 455 | stepValue : stepValue, 456 | min : graphMin, 457 | max : graphMin + (numberOfSteps * stepValue) 458 | }; 459 | 460 | }, 461 | /* jshint ignore:start */ 462 | // Blows up jshint errors based on the new Function constructor 463 | //Templating methods 464 | //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ 465 | template = helpers.template = function(templateString, valuesObject){ 466 | 467 | // If templateString is function rather than string-template - call the function for valuesObject 468 | 469 | if(templateString instanceof Function){ 470 | return templateString(valuesObject); 471 | } 472 | 473 | var cache = {}; 474 | function tmpl(str, data){ 475 | // Figure out if we're getting a template, or if we need to 476 | // load the template - and be sure to cache the result. 477 | var fn = !/\W/.test(str) ? 478 | cache[str] = cache[str] : 479 | 480 | // Generate a reusable function that will serve as a template 481 | // generator (and which will be cached). 482 | new Function("obj", 483 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 484 | 485 | // Introduce the data as local variables using with(){} 486 | "with(obj){p.push('" + 487 | 488 | // Convert the template into pure JavaScript 489 | str 490 | .replace(/[\r\t\n]/g, " ") 491 | .split("<%").join("\t") 492 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 493 | .replace(/\t=(.*?)%>/g, "',$1,'") 494 | .split("\t").join("');") 495 | .split("%>").join("p.push('") 496 | .split("\r").join("\\'") + 497 | "');}return p.join('');" 498 | ); 499 | 500 | // Provide some basic currying to the user 501 | return data ? fn( data ) : fn; 502 | } 503 | return tmpl(templateString,valuesObject); 504 | }, 505 | /* jshint ignore:end */ 506 | generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ 507 | var labelsArray = new Array(numberOfSteps); 508 | if (labelTemplateString){ 509 | each(labelsArray,function(val,index){ 510 | labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); 511 | }); 512 | } 513 | return labelsArray; 514 | }, 515 | //--Animation methods 516 | //Easing functions adapted from Robert Penner's easing equations 517 | //http://www.robertpenner.com/easing/ 518 | easingEffects = helpers.easingEffects = { 519 | linear: function (t) { 520 | return t; 521 | }, 522 | easeInQuad: function (t) { 523 | return t * t; 524 | }, 525 | easeOutQuad: function (t) { 526 | return -1 * t * (t - 2); 527 | }, 528 | easeInOutQuad: function (t) { 529 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; 530 | return -1 / 2 * ((--t) * (t - 2) - 1); 531 | }, 532 | easeInCubic: function (t) { 533 | return t * t * t; 534 | }, 535 | easeOutCubic: function (t) { 536 | return 1 * ((t = t / 1 - 1) * t * t + 1); 537 | }, 538 | easeInOutCubic: function (t) { 539 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; 540 | return 1 / 2 * ((t -= 2) * t * t + 2); 541 | }, 542 | easeInQuart: function (t) { 543 | return t * t * t * t; 544 | }, 545 | easeOutQuart: function (t) { 546 | return -1 * ((t = t / 1 - 1) * t * t * t - 1); 547 | }, 548 | easeInOutQuart: function (t) { 549 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; 550 | return -1 / 2 * ((t -= 2) * t * t * t - 2); 551 | }, 552 | easeInQuint: function (t) { 553 | return 1 * (t /= 1) * t * t * t * t; 554 | }, 555 | easeOutQuint: function (t) { 556 | return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); 557 | }, 558 | easeInOutQuint: function (t) { 559 | if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; 560 | return 1 / 2 * ((t -= 2) * t * t * t * t + 2); 561 | }, 562 | easeInSine: function (t) { 563 | return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; 564 | }, 565 | easeOutSine: function (t) { 566 | return 1 * Math.sin(t / 1 * (Math.PI / 2)); 567 | }, 568 | easeInOutSine: function (t) { 569 | return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); 570 | }, 571 | easeInExpo: function (t) { 572 | return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); 573 | }, 574 | easeOutExpo: function (t) { 575 | return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); 576 | }, 577 | easeInOutExpo: function (t) { 578 | if (t === 0) return 0; 579 | if (t === 1) return 1; 580 | if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); 581 | return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); 582 | }, 583 | easeInCirc: function (t) { 584 | if (t >= 1) return t; 585 | return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); 586 | }, 587 | easeOutCirc: function (t) { 588 | return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); 589 | }, 590 | easeInOutCirc: function (t) { 591 | if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); 592 | return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); 593 | }, 594 | easeInElastic: function (t) { 595 | var s = 1.70158; 596 | var p = 0; 597 | var a = 1; 598 | if (t === 0) return 0; 599 | if ((t /= 1) == 1) return 1; 600 | if (!p) p = 1 * 0.3; 601 | if (a < Math.abs(1)) { 602 | a = 1; 603 | s = p / 4; 604 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 605 | return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); 606 | }, 607 | easeOutElastic: function (t) { 608 | var s = 1.70158; 609 | var p = 0; 610 | var a = 1; 611 | if (t === 0) return 0; 612 | if ((t /= 1) == 1) return 1; 613 | if (!p) p = 1 * 0.3; 614 | if (a < Math.abs(1)) { 615 | a = 1; 616 | s = p / 4; 617 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 618 | return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; 619 | }, 620 | easeInOutElastic: function (t) { 621 | var s = 1.70158; 622 | var p = 0; 623 | var a = 1; 624 | if (t === 0) return 0; 625 | if ((t /= 1 / 2) == 2) return 1; 626 | if (!p) p = 1 * (0.3 * 1.5); 627 | if (a < Math.abs(1)) { 628 | a = 1; 629 | s = p / 4; 630 | } else s = p / (2 * Math.PI) * Math.asin(1 / a); 631 | if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); 632 | return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; 633 | }, 634 | easeInBack: function (t) { 635 | var s = 1.70158; 636 | return 1 * (t /= 1) * t * ((s + 1) * t - s); 637 | }, 638 | easeOutBack: function (t) { 639 | var s = 1.70158; 640 | return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); 641 | }, 642 | easeInOutBack: function (t) { 643 | var s = 1.70158; 644 | if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); 645 | return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); 646 | }, 647 | easeInBounce: function (t) { 648 | return 1 - easingEffects.easeOutBounce(1 - t); 649 | }, 650 | easeOutBounce: function (t) { 651 | if ((t /= 1) < (1 / 2.75)) { 652 | return 1 * (7.5625 * t * t); 653 | } else if (t < (2 / 2.75)) { 654 | return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); 655 | } else if (t < (2.5 / 2.75)) { 656 | return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); 657 | } else { 658 | return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); 659 | } 660 | }, 661 | easeInOutBounce: function (t) { 662 | if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; 663 | return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; 664 | } 665 | }, 666 | //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 667 | requestAnimFrame = helpers.requestAnimFrame = (function(){ 668 | return window.requestAnimationFrame || 669 | window.webkitRequestAnimationFrame || 670 | window.mozRequestAnimationFrame || 671 | window.oRequestAnimationFrame || 672 | window.msRequestAnimationFrame || 673 | function(callback) { 674 | return window.setTimeout(callback, 1000 / 60); 675 | }; 676 | })(), 677 | cancelAnimFrame = helpers.cancelAnimFrame = (function(){ 678 | return window.cancelAnimationFrame || 679 | window.webkitCancelAnimationFrame || 680 | window.mozCancelAnimationFrame || 681 | window.oCancelAnimationFrame || 682 | window.msCancelAnimationFrame || 683 | function(callback) { 684 | return window.clearTimeout(callback, 1000 / 60); 685 | }; 686 | })(), 687 | animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ 688 | 689 | var currentStep = 0, 690 | easingFunction = easingEffects[easingString] || easingEffects.linear; 691 | 692 | var animationFrame = function(){ 693 | currentStep++; 694 | var stepDecimal = currentStep/totalSteps; 695 | var easeDecimal = easingFunction(stepDecimal); 696 | 697 | callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); 698 | onProgress.call(chartInstance,easeDecimal,stepDecimal); 699 | if (currentStep < totalSteps){ 700 | chartInstance.animationFrame = requestAnimFrame(animationFrame); 701 | } else{ 702 | onComplete.apply(chartInstance); 703 | } 704 | }; 705 | requestAnimFrame(animationFrame); 706 | }, 707 | //-- DOM methods 708 | getRelativePosition = helpers.getRelativePosition = function(evt){ 709 | var mouseX, mouseY; 710 | var e = evt.originalEvent || evt, 711 | canvas = evt.currentTarget || evt.srcElement, 712 | boundingRect = canvas.getBoundingClientRect(); 713 | 714 | if (e.touches){ 715 | mouseX = e.touches[0].clientX - boundingRect.left; 716 | mouseY = e.touches[0].clientY - boundingRect.top; 717 | 718 | } 719 | else{ 720 | mouseX = e.clientX - boundingRect.left; 721 | mouseY = e.clientY - boundingRect.top; 722 | } 723 | 724 | return { 725 | x : mouseX, 726 | y : mouseY 727 | }; 728 | 729 | }, 730 | addEvent = helpers.addEvent = function(node,eventType,method){ 731 | if (node.addEventListener){ 732 | node.addEventListener(eventType,method); 733 | } else if (node.attachEvent){ 734 | node.attachEvent("on"+eventType, method); 735 | } else { 736 | node["on"+eventType] = method; 737 | } 738 | }, 739 | removeEvent = helpers.removeEvent = function(node, eventType, handler){ 740 | if (node.removeEventListener){ 741 | node.removeEventListener(eventType, handler, false); 742 | } else if (node.detachEvent){ 743 | node.detachEvent("on"+eventType,handler); 744 | } else{ 745 | node["on" + eventType] = noop; 746 | } 747 | }, 748 | bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ 749 | // Create the events object if it's not already present 750 | if (!chartInstance.events) chartInstance.events = {}; 751 | 752 | each(arrayOfEvents,function(eventName){ 753 | chartInstance.events[eventName] = function(){ 754 | handler.apply(chartInstance, arguments); 755 | }; 756 | addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); 757 | }); 758 | }, 759 | unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { 760 | each(arrayOfEvents, function(handler,eventName){ 761 | removeEvent(chartInstance.chart.canvas, eventName, handler); 762 | }); 763 | }, 764 | getMaximumWidth = helpers.getMaximumWidth = function(domNode){ 765 | var container = domNode.parentNode; 766 | // TODO = check cross browser stuff with this. 767 | return container.clientWidth; 768 | }, 769 | getMaximumHeight = helpers.getMaximumHeight = function(domNode){ 770 | var container = domNode.parentNode; 771 | // TODO = check cross browser stuff with this. 772 | return container.clientHeight; 773 | }, 774 | getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support 775 | retinaScale = helpers.retinaScale = function(chart){ 776 | var ctx = chart.ctx, 777 | width = chart.canvas.width, 778 | height = chart.canvas.height; 779 | 780 | if (window.devicePixelRatio) { 781 | ctx.canvas.style.width = width + "px"; 782 | ctx.canvas.style.height = height + "px"; 783 | ctx.canvas.height = height * window.devicePixelRatio; 784 | ctx.canvas.width = width * window.devicePixelRatio; 785 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 786 | } 787 | }, 788 | //-- Canvas methods 789 | clear = helpers.clear = function(chart){ 790 | chart.ctx.clearRect(0,0,chart.width,chart.height); 791 | }, 792 | fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ 793 | return fontStyle + " " + pixelSize+"px " + fontFamily; 794 | }, 795 | longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ 796 | ctx.font = font; 797 | var longest = 0; 798 | each(arrayOfStrings,function(string){ 799 | var textWidth = ctx.measureText(string).width; 800 | longest = (textWidth > longest) ? textWidth : longest; 801 | }); 802 | return longest; 803 | }, 804 | drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ 805 | ctx.beginPath(); 806 | ctx.moveTo(x + radius, y); 807 | ctx.lineTo(x + width - radius, y); 808 | ctx.quadraticCurveTo(x + width, y, x + width, y + radius); 809 | ctx.lineTo(x + width, y + height - radius); 810 | ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); 811 | ctx.lineTo(x + radius, y + height); 812 | ctx.quadraticCurveTo(x, y + height, x, y + height - radius); 813 | ctx.lineTo(x, y + radius); 814 | ctx.quadraticCurveTo(x, y, x + radius, y); 815 | ctx.closePath(); 816 | }; 817 | 818 | 819 | //Store a reference to each instance - allowing us to globally resize chart instances on window resize. 820 | //Destroy method on the chart will remove the instance of the chart from this reference. 821 | Chart.instances = {}; 822 | 823 | Chart.Type = function(data,options,chart){ 824 | this.options = options; 825 | this.chart = chart; 826 | this.id = uid(); 827 | //Add the chart instance to the global namespace 828 | Chart.instances[this.id] = this; 829 | 830 | // Initialize is always called when a chart type is created 831 | // By default it is a no op, but it should be extended 832 | if (options.responsive){ 833 | this.resize(); 834 | } 835 | this.initialize.call(this,data); 836 | }; 837 | 838 | //Core methods that'll be a part of every chart type 839 | extend(Chart.Type.prototype,{ 840 | initialize : function(){return this;}, 841 | clear : function(){ 842 | clear(this.chart); 843 | return this; 844 | }, 845 | stop : function(){ 846 | // Stops any current animation loop occuring 847 | cancelAnimFrame(this.animationFrame); 848 | return this; 849 | }, 850 | resize : function(callback){ 851 | this.stop(); 852 | var canvas = this.chart.canvas, 853 | newWidth = getMaximumWidth(this.chart.canvas), 854 | newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); 855 | 856 | canvas.width = this.chart.width = newWidth; 857 | canvas.height = this.chart.height = newHeight; 858 | 859 | retinaScale(this.chart); 860 | 861 | if (typeof callback === "function"){ 862 | callback.apply(this, Array.prototype.slice.call(arguments, 1)); 863 | } 864 | return this; 865 | }, 866 | reflow : noop, 867 | render : function(reflow){ 868 | if (reflow){ 869 | this.reflow(); 870 | } 871 | if (this.options.animation && !reflow){ 872 | helpers.animationLoop( 873 | this.draw, 874 | this.options.animationSteps, 875 | this.options.animationEasing, 876 | this.options.onAnimationProgress, 877 | this.options.onAnimationComplete, 878 | this 879 | ); 880 | } 881 | else{ 882 | this.draw(); 883 | this.options.onAnimationComplete.call(this); 884 | } 885 | return this; 886 | }, 887 | generateLegend : function(){ 888 | return template(this.options.legendTemplate,this); 889 | }, 890 | destroy : function(){ 891 | this.clear(); 892 | unbindEvents(this, this.events); 893 | var canvas = this.chart.canvas; 894 | 895 | // Reset canvas height/width attributes starts a fresh with the canvas context 896 | canvas.width = this.chart.width; 897 | canvas.height = this.chart.height; 898 | 899 | // < IE9 doesn't support removeProperty 900 | if (canvas.style.removeProperty) { 901 | canvas.style.removeProperty('width'); 902 | canvas.style.removeProperty('height'); 903 | } else { 904 | canvas.style.removeAttribute('width'); 905 | canvas.style.removeAttribute('height'); 906 | } 907 | 908 | delete Chart.instances[this.id]; 909 | }, 910 | showTooltip : function(ChartElements, forceRedraw){ 911 | // Only redraw the chart if we've actually changed what we're hovering on. 912 | if (typeof this.activeElements === 'undefined') this.activeElements = []; 913 | 914 | var isChanged = (function(Elements){ 915 | var changed = false; 916 | 917 | if (Elements.length !== this.activeElements.length){ 918 | changed = true; 919 | return changed; 920 | } 921 | 922 | each(Elements, function(element, index){ 923 | if (element !== this.activeElements[index]){ 924 | changed = true; 925 | } 926 | }, this); 927 | return changed; 928 | }).call(this, ChartElements); 929 | 930 | if (!isChanged && !forceRedraw){ 931 | return; 932 | } 933 | else{ 934 | this.activeElements = ChartElements; 935 | } 936 | this.draw(); 937 | if(this.options.customTooltips){ 938 | this.options.customTooltips(false); 939 | } 940 | if (ChartElements.length > 0){ 941 | // If we have multiple datasets, show a MultiTooltip for all of the data points at that index 942 | if (this.datasets && this.datasets.length > 1) { 943 | var dataArray, 944 | dataIndex; 945 | 946 | for (var i = this.datasets.length - 1; i >= 0; i--) { 947 | dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; 948 | dataIndex = indexOf(dataArray, ChartElements[0]); 949 | if (dataIndex !== -1){ 950 | break; 951 | } 952 | } 953 | var tooltipLabels = [], 954 | tooltipColors = [], 955 | medianPosition = (function(index) { 956 | 957 | // Get all the points at that particular index 958 | var Elements = [], 959 | dataCollection, 960 | xPositions = [], 961 | yPositions = [], 962 | xMax, 963 | yMax, 964 | xMin, 965 | yMin; 966 | helpers.each(this.datasets, function(dataset){ 967 | dataCollection = dataset.points || dataset.bars || dataset.segments; 968 | if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ 969 | Elements.push(dataCollection[dataIndex]); 970 | } 971 | }); 972 | 973 | helpers.each(Elements, function(element) { 974 | xPositions.push(element.x); 975 | yPositions.push(element.y); 976 | 977 | 978 | //Include any colour information about the element 979 | tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); 980 | tooltipColors.push({ 981 | fill: element._saved.fillColor || element.fillColor, 982 | stroke: element._saved.strokeColor || element.strokeColor 983 | }); 984 | 985 | }, this); 986 | 987 | yMin = min(yPositions); 988 | yMax = max(yPositions); 989 | 990 | xMin = min(xPositions); 991 | xMax = max(xPositions); 992 | 993 | return { 994 | x: (xMin > this.chart.width/2) ? xMin : xMax, 995 | y: (yMin + yMax)/2 996 | }; 997 | }).call(this, dataIndex); 998 | 999 | new Chart.MultiTooltip({ 1000 | x: medianPosition.x, 1001 | y: medianPosition.y, 1002 | xPadding: this.options.tooltipXPadding, 1003 | yPadding: this.options.tooltipYPadding, 1004 | xOffset: this.options.tooltipXOffset, 1005 | fillColor: this.options.tooltipFillColor, 1006 | textColor: this.options.tooltipFontColor, 1007 | fontFamily: this.options.tooltipFontFamily, 1008 | fontStyle: this.options.tooltipFontStyle, 1009 | fontSize: this.options.tooltipFontSize, 1010 | titleTextColor: this.options.tooltipTitleFontColor, 1011 | titleFontFamily: this.options.tooltipTitleFontFamily, 1012 | titleFontStyle: this.options.tooltipTitleFontStyle, 1013 | titleFontSize: this.options.tooltipTitleFontSize, 1014 | cornerRadius: this.options.tooltipCornerRadius, 1015 | labels: tooltipLabels, 1016 | legendColors: tooltipColors, 1017 | legendColorBackground : this.options.multiTooltipKeyBackground, 1018 | title: ChartElements[0].label, 1019 | chart: this.chart, 1020 | ctx: this.chart.ctx, 1021 | custom: this.options.customTooltips 1022 | }).draw(); 1023 | 1024 | } else { 1025 | each(ChartElements, function(Element) { 1026 | var tooltipPosition = Element.tooltipPosition(); 1027 | new Chart.Tooltip({ 1028 | x: Math.round(tooltipPosition.x), 1029 | y: Math.round(tooltipPosition.y), 1030 | xPadding: this.options.tooltipXPadding, 1031 | yPadding: this.options.tooltipYPadding, 1032 | fillColor: this.options.tooltipFillColor, 1033 | textColor: this.options.tooltipFontColor, 1034 | fontFamily: this.options.tooltipFontFamily, 1035 | fontStyle: this.options.tooltipFontStyle, 1036 | fontSize: this.options.tooltipFontSize, 1037 | caretHeight: this.options.tooltipCaretSize, 1038 | cornerRadius: this.options.tooltipCornerRadius, 1039 | text: template(this.options.tooltipTemplate, Element), 1040 | chart: this.chart, 1041 | custom: this.options.customTooltips 1042 | }).draw(); 1043 | }, this); 1044 | } 1045 | } 1046 | return this; 1047 | }, 1048 | toBase64Image : function(){ 1049 | return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); 1050 | } 1051 | }); 1052 | 1053 | Chart.Type.extend = function(extensions){ 1054 | 1055 | var parent = this; 1056 | 1057 | var ChartType = function(){ 1058 | return parent.apply(this,arguments); 1059 | }; 1060 | 1061 | //Copy the prototype object of the this class 1062 | ChartType.prototype = clone(parent.prototype); 1063 | //Now overwrite some of the properties in the base class with the new extensions 1064 | extend(ChartType.prototype, extensions); 1065 | 1066 | ChartType.extend = Chart.Type.extend; 1067 | 1068 | if (extensions.name || parent.prototype.name){ 1069 | 1070 | var chartName = extensions.name || parent.prototype.name; 1071 | //Assign any potential default values of the new chart type 1072 | 1073 | //If none are defined, we'll use a clone of the chart type this is being extended from. 1074 | //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart 1075 | //doesn't define some defaults of their own. 1076 | 1077 | var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; 1078 | 1079 | Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); 1080 | 1081 | Chart.types[chartName] = ChartType; 1082 | 1083 | //Register this new chart type in the Chart prototype 1084 | Chart.prototype[chartName] = function(data,options){ 1085 | var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); 1086 | return new ChartType(data,config,this); 1087 | }; 1088 | } else{ 1089 | warn("Name not provided for this chart, so it hasn't been registered"); 1090 | } 1091 | return parent; 1092 | }; 1093 | 1094 | Chart.Element = function(configuration){ 1095 | extend(this,configuration); 1096 | this.initialize.apply(this,arguments); 1097 | this.save(); 1098 | }; 1099 | extend(Chart.Element.prototype,{ 1100 | initialize : function(){}, 1101 | restore : function(props){ 1102 | if (!props){ 1103 | extend(this,this._saved); 1104 | } else { 1105 | each(props,function(key){ 1106 | this[key] = this._saved[key]; 1107 | },this); 1108 | } 1109 | return this; 1110 | }, 1111 | save : function(){ 1112 | this._saved = clone(this); 1113 | delete this._saved._saved; 1114 | return this; 1115 | }, 1116 | update : function(newProps){ 1117 | each(newProps,function(value,key){ 1118 | this._saved[key] = this[key]; 1119 | this[key] = value; 1120 | },this); 1121 | return this; 1122 | }, 1123 | transition : function(props,ease){ 1124 | each(props,function(value,key){ 1125 | this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; 1126 | },this); 1127 | return this; 1128 | }, 1129 | tooltipPosition : function(){ 1130 | return { 1131 | x : this.x, 1132 | y : this.y 1133 | }; 1134 | }, 1135 | hasValue: function(){ 1136 | return isNumber(this.value); 1137 | } 1138 | }); 1139 | 1140 | Chart.Element.extend = inherits; 1141 | 1142 | 1143 | Chart.Point = Chart.Element.extend({ 1144 | display: true, 1145 | inRange: function(chartX,chartY){ 1146 | var hitDetectionRange = this.hitDetectionRadius + this.radius; 1147 | return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); 1148 | }, 1149 | draw : function(){ 1150 | if (this.display){ 1151 | var ctx = this.ctx; 1152 | ctx.beginPath(); 1153 | 1154 | ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); 1155 | ctx.closePath(); 1156 | 1157 | ctx.strokeStyle = this.strokeColor; 1158 | ctx.lineWidth = this.strokeWidth; 1159 | 1160 | ctx.fillStyle = this.fillColor; 1161 | 1162 | ctx.fill(); 1163 | ctx.stroke(); 1164 | } 1165 | 1166 | 1167 | //Quick debug for bezier curve splining 1168 | //Highlights control points and the line between them. 1169 | //Handy for dev - stripped in the min version. 1170 | 1171 | // ctx.save(); 1172 | // ctx.fillStyle = "black"; 1173 | // ctx.strokeStyle = "black" 1174 | // ctx.beginPath(); 1175 | // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); 1176 | // ctx.fill(); 1177 | 1178 | // ctx.beginPath(); 1179 | // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); 1180 | // ctx.fill(); 1181 | 1182 | // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); 1183 | // ctx.lineTo(this.x, this.y); 1184 | // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); 1185 | // ctx.stroke(); 1186 | 1187 | // ctx.restore(); 1188 | 1189 | 1190 | 1191 | } 1192 | }); 1193 | 1194 | Chart.Arc = Chart.Element.extend({ 1195 | inRange : function(chartX,chartY){ 1196 | 1197 | var pointRelativePosition = helpers.getAngleFromPoint(this, { 1198 | x: chartX, 1199 | y: chartY 1200 | }); 1201 | 1202 | //Check if within the range of the open/close angle 1203 | var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), 1204 | withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); 1205 | 1206 | return (betweenAngles && withinRadius); 1207 | //Ensure within the outside of the arc centre, but inside arc outer 1208 | }, 1209 | tooltipPosition : function(){ 1210 | var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), 1211 | rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; 1212 | return { 1213 | x : this.x + (Math.cos(centreAngle) * rangeFromCentre), 1214 | y : this.y + (Math.sin(centreAngle) * rangeFromCentre) 1215 | }; 1216 | }, 1217 | draw : function(animationPercent){ 1218 | 1219 | var easingDecimal = animationPercent || 1; 1220 | 1221 | var ctx = this.ctx; 1222 | 1223 | ctx.beginPath(); 1224 | 1225 | ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); 1226 | 1227 | ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); 1228 | 1229 | ctx.closePath(); 1230 | ctx.strokeStyle = this.strokeColor; 1231 | ctx.lineWidth = this.strokeWidth; 1232 | 1233 | ctx.fillStyle = this.fillColor; 1234 | 1235 | ctx.fill(); 1236 | ctx.lineJoin = 'bevel'; 1237 | 1238 | if (this.showStroke){ 1239 | ctx.stroke(); 1240 | } 1241 | } 1242 | }); 1243 | 1244 | Chart.Rectangle = Chart.Element.extend({ 1245 | draw : function(){ 1246 | var ctx = this.ctx, 1247 | halfWidth = this.width/2, 1248 | leftX = this.x - halfWidth, 1249 | rightX = this.x + halfWidth, 1250 | top = this.base - (this.base - this.y), 1251 | halfStroke = this.strokeWidth / 2; 1252 | 1253 | // Canvas doesn't allow us to stroke inside the width so we can 1254 | // adjust the sizes to fit if we're setting a stroke on the line 1255 | if (this.showStroke){ 1256 | leftX += halfStroke; 1257 | rightX -= halfStroke; 1258 | top += halfStroke; 1259 | } 1260 | 1261 | ctx.beginPath(); 1262 | 1263 | ctx.fillStyle = this.fillColor; 1264 | ctx.strokeStyle = this.strokeColor; 1265 | ctx.lineWidth = this.strokeWidth; 1266 | 1267 | // It'd be nice to keep this class totally generic to any rectangle 1268 | // and simply specify which border to miss out. 1269 | ctx.moveTo(leftX, this.base); 1270 | ctx.lineTo(leftX, top); 1271 | ctx.lineTo(rightX, top); 1272 | ctx.lineTo(rightX, this.base); 1273 | ctx.fill(); 1274 | if (this.showStroke){ 1275 | ctx.stroke(); 1276 | } 1277 | }, 1278 | height : function(){ 1279 | return this.base - this.y; 1280 | }, 1281 | inRange : function(chartX,chartY){ 1282 | return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); 1283 | } 1284 | }); 1285 | 1286 | Chart.Tooltip = Chart.Element.extend({ 1287 | draw : function(){ 1288 | 1289 | var ctx = this.chart.ctx; 1290 | 1291 | ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); 1292 | 1293 | this.xAlign = "center"; 1294 | this.yAlign = "above"; 1295 | 1296 | //Distance between the actual element.y position and the start of the tooltip caret 1297 | var caretPadding = this.caretPadding = 2; 1298 | 1299 | var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, 1300 | tooltipRectHeight = this.fontSize + 2*this.yPadding, 1301 | tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; 1302 | 1303 | if (this.x + tooltipWidth/2 >this.chart.width){ 1304 | this.xAlign = "left"; 1305 | } else if (this.x - tooltipWidth/2 < 0){ 1306 | this.xAlign = "right"; 1307 | } 1308 | 1309 | if (this.y - tooltipHeight < 0){ 1310 | this.yAlign = "below"; 1311 | } 1312 | 1313 | 1314 | var tooltipX = this.x - tooltipWidth/2, 1315 | tooltipY = this.y - tooltipHeight; 1316 | 1317 | ctx.fillStyle = this.fillColor; 1318 | 1319 | // Custom Tooltips 1320 | if(this.custom){ 1321 | this.custom(this); 1322 | } 1323 | else{ 1324 | switch(this.yAlign) 1325 | { 1326 | case "above": 1327 | //Draw a caret above the x/y 1328 | ctx.beginPath(); 1329 | ctx.moveTo(this.x,this.y - caretPadding); 1330 | ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); 1331 | ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); 1332 | ctx.closePath(); 1333 | ctx.fill(); 1334 | break; 1335 | case "below": 1336 | tooltipY = this.y + caretPadding + this.caretHeight; 1337 | //Draw a caret below the x/y 1338 | ctx.beginPath(); 1339 | ctx.moveTo(this.x, this.y + caretPadding); 1340 | ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); 1341 | ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); 1342 | ctx.closePath(); 1343 | ctx.fill(); 1344 | break; 1345 | } 1346 | 1347 | switch(this.xAlign) 1348 | { 1349 | case "left": 1350 | tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); 1351 | break; 1352 | case "right": 1353 | tooltipX = this.x - (this.cornerRadius + this.caretHeight); 1354 | break; 1355 | } 1356 | 1357 | drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); 1358 | 1359 | ctx.fill(); 1360 | 1361 | ctx.fillStyle = this.textColor; 1362 | ctx.textAlign = "center"; 1363 | ctx.textBaseline = "middle"; 1364 | ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); 1365 | } 1366 | } 1367 | }); 1368 | 1369 | Chart.MultiTooltip = Chart.Element.extend({ 1370 | initialize : function(){ 1371 | this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); 1372 | 1373 | this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); 1374 | 1375 | this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; 1376 | 1377 | this.ctx.font = this.titleFont; 1378 | 1379 | var titleWidth = this.ctx.measureText(this.title).width, 1380 | //Label has a legend square as well so account for this. 1381 | labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, 1382 | longestTextWidth = max([labelWidth,titleWidth]); 1383 | 1384 | this.width = longestTextWidth + (this.xPadding*2); 1385 | 1386 | 1387 | var halfHeight = this.height/2; 1388 | 1389 | //Check to ensure the height will fit on the canvas 1390 | if (this.y - halfHeight < 0 ){ 1391 | this.y = halfHeight; 1392 | } else if (this.y + halfHeight > this.chart.height){ 1393 | this.y = this.chart.height - halfHeight; 1394 | } 1395 | 1396 | //Decide whether to align left or right based on position on canvas 1397 | if (this.x > this.chart.width/2){ 1398 | this.x -= this.xOffset + this.width; 1399 | } else { 1400 | this.x += this.xOffset; 1401 | } 1402 | 1403 | 1404 | }, 1405 | getLineHeight : function(index){ 1406 | var baseLineHeight = this.y - (this.height/2) + this.yPadding, 1407 | afterTitleIndex = index-1; 1408 | 1409 | //If the index is zero, we're getting the title 1410 | if (index === 0){ 1411 | return baseLineHeight + this.titleFontSize/2; 1412 | } else{ 1413 | return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; 1414 | } 1415 | 1416 | }, 1417 | draw : function(){ 1418 | // Custom Tooltips 1419 | if(this.custom){ 1420 | this.custom(this); 1421 | } 1422 | else{ 1423 | drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); 1424 | var ctx = this.ctx; 1425 | ctx.fillStyle = this.fillColor; 1426 | ctx.fill(); 1427 | ctx.closePath(); 1428 | 1429 | ctx.textAlign = "left"; 1430 | ctx.textBaseline = "middle"; 1431 | ctx.fillStyle = this.titleTextColor; 1432 | ctx.font = this.titleFont; 1433 | 1434 | ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); 1435 | 1436 | ctx.font = this.font; 1437 | helpers.each(this.labels,function(label,index){ 1438 | ctx.fillStyle = this.textColor; 1439 | ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); 1440 | 1441 | //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) 1442 | //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1443 | //Instead we'll make a white filled block to put the legendColour palette over. 1444 | 1445 | ctx.fillStyle = this.legendColorBackground; 1446 | ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1447 | 1448 | ctx.fillStyle = this.legendColors[index].fill; 1449 | ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); 1450 | 1451 | 1452 | },this); 1453 | } 1454 | } 1455 | }); 1456 | 1457 | Chart.Scale = Chart.Element.extend({ 1458 | initialize : function(){ 1459 | this.fit(); 1460 | }, 1461 | buildYLabels : function(){ 1462 | this.yLabels = []; 1463 | 1464 | var stepDecimalPlaces = getDecimalPlaces(this.stepValue); 1465 | 1466 | for (var i=0; i<=this.steps; i++){ 1467 | this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); 1468 | } 1469 | this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; 1470 | }, 1471 | addXLabel : function(label){ 1472 | this.xLabels.push(label); 1473 | this.valuesCount++; 1474 | this.fit(); 1475 | }, 1476 | removeXLabel : function(){ 1477 | this.xLabels.shift(); 1478 | this.valuesCount--; 1479 | this.fit(); 1480 | }, 1481 | // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use 1482 | fit: function(){ 1483 | // First we need the width of the yLabels, assuming the xLabels aren't rotated 1484 | 1485 | // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation 1486 | this.startPoint = (this.display) ? this.fontSize : 0; 1487 | this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels 1488 | 1489 | // Apply padding settings to the start and end point. 1490 | this.startPoint += this.padding; 1491 | this.endPoint -= this.padding; 1492 | 1493 | // Cache the starting height, so can determine if we need to recalculate the scale yAxis 1494 | var cachedHeight = this.endPoint - this.startPoint, 1495 | cachedYLabelWidth; 1496 | 1497 | // Build the current yLabels so we have an idea of what size they'll be to start 1498 | /* 1499 | * This sets what is returned from calculateScaleRange as static properties of this class: 1500 | * 1501 | this.steps; 1502 | this.stepValue; 1503 | this.min; 1504 | this.max; 1505 | * 1506 | */ 1507 | this.calculateYRange(cachedHeight); 1508 | 1509 | // With these properties set we can now build the array of yLabels 1510 | // and also the width of the largest yLabel 1511 | this.buildYLabels(); 1512 | 1513 | this.calculateXLabelRotation(); 1514 | 1515 | while((cachedHeight > this.endPoint - this.startPoint)){ 1516 | cachedHeight = this.endPoint - this.startPoint; 1517 | cachedYLabelWidth = this.yLabelWidth; 1518 | 1519 | this.calculateYRange(cachedHeight); 1520 | this.buildYLabels(); 1521 | 1522 | // Only go through the xLabel loop again if the yLabel width has changed 1523 | if (cachedYLabelWidth < this.yLabelWidth){ 1524 | this.calculateXLabelRotation(); 1525 | } 1526 | } 1527 | 1528 | }, 1529 | calculateXLabelRotation : function(){ 1530 | //Get the width of each grid by calculating the difference 1531 | //between x offsets between 0 and 1. 1532 | 1533 | this.ctx.font = this.font; 1534 | 1535 | var firstWidth = this.ctx.measureText(this.xLabels[0]).width, 1536 | lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, 1537 | firstRotated, 1538 | lastRotated; 1539 | 1540 | 1541 | this.xScalePaddingRight = lastWidth/2 + 3; 1542 | this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; 1543 | 1544 | this.xLabelRotation = 0; 1545 | if (this.display){ 1546 | var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), 1547 | cosRotation, 1548 | firstRotatedWidth; 1549 | this.xLabelWidth = originalLabelWidth; 1550 | //Allow 3 pixels x2 padding either side for label readability 1551 | var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; 1552 | 1553 | //Max label rotate should be 90 - also act as a loop counter 1554 | while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ 1555 | cosRotation = Math.cos(toRadians(this.xLabelRotation)); 1556 | 1557 | firstRotated = cosRotation * firstWidth; 1558 | lastRotated = cosRotation * lastWidth; 1559 | 1560 | // We're right aligning the text now. 1561 | if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ 1562 | this.xScalePaddingLeft = firstRotated + this.fontSize / 2; 1563 | } 1564 | this.xScalePaddingRight = this.fontSize/2; 1565 | 1566 | 1567 | this.xLabelRotation++; 1568 | this.xLabelWidth = cosRotation * originalLabelWidth; 1569 | 1570 | } 1571 | if (this.xLabelRotation > 0){ 1572 | this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; 1573 | } 1574 | } 1575 | else{ 1576 | this.xLabelWidth = 0; 1577 | this.xScalePaddingRight = this.padding; 1578 | this.xScalePaddingLeft = this.padding; 1579 | } 1580 | 1581 | }, 1582 | // Needs to be overidden in each Chart type 1583 | // Otherwise we need to pass all the data into the scale class 1584 | calculateYRange: noop, 1585 | drawingArea: function(){ 1586 | return this.startPoint - this.endPoint; 1587 | }, 1588 | calculateY : function(value){ 1589 | var scalingFactor = this.drawingArea() / (this.min - this.max); 1590 | return this.endPoint - (scalingFactor * (value - this.min)); 1591 | }, 1592 | calculateX : function(index){ 1593 | var isRotated = (this.xLabelRotation > 0), 1594 | // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, 1595 | innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), 1596 | valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), 1597 | valueOffset = (valueWidth * index) + this.xScalePaddingLeft; 1598 | 1599 | if (this.offsetGridLines){ 1600 | valueOffset += (valueWidth/2); 1601 | } 1602 | 1603 | return Math.round(valueOffset); 1604 | }, 1605 | update : function(newProps){ 1606 | helpers.extend(this, newProps); 1607 | this.fit(); 1608 | }, 1609 | draw : function(){ 1610 | var ctx = this.ctx, 1611 | yLabelGap = (this.endPoint - this.startPoint) / this.steps, 1612 | xStart = Math.round(this.xScalePaddingLeft); 1613 | if (this.display){ 1614 | ctx.fillStyle = this.textColor; 1615 | ctx.font = this.font; 1616 | each(this.yLabels,function(labelString,index){ 1617 | var yLabelCenter = this.endPoint - (yLabelGap * index), 1618 | linePositionY = Math.round(yLabelCenter), 1619 | drawHorizontalLine = this.showHorizontalLines; 1620 | 1621 | ctx.textAlign = "right"; 1622 | ctx.textBaseline = "middle"; 1623 | if (this.showLabels){ 1624 | ctx.fillText(labelString,xStart - 10,yLabelCenter); 1625 | } 1626 | 1627 | // This is X axis, so draw it 1628 | if (index === 0 && !drawHorizontalLine){ 1629 | drawHorizontalLine = true; 1630 | } 1631 | 1632 | if (drawHorizontalLine){ 1633 | ctx.beginPath(); 1634 | } 1635 | 1636 | if (index > 0){ 1637 | // This is a grid line in the centre, so drop that 1638 | ctx.lineWidth = this.gridLineWidth; 1639 | ctx.strokeStyle = this.gridLineColor; 1640 | } else { 1641 | // This is the first line on the scale 1642 | ctx.lineWidth = this.lineWidth; 1643 | ctx.strokeStyle = this.lineColor; 1644 | } 1645 | 1646 | linePositionY += helpers.aliasPixel(ctx.lineWidth); 1647 | 1648 | if(drawHorizontalLine){ 1649 | ctx.moveTo(xStart, linePositionY); 1650 | ctx.lineTo(this.width, linePositionY); 1651 | ctx.stroke(); 1652 | ctx.closePath(); 1653 | } 1654 | 1655 | ctx.lineWidth = this.lineWidth; 1656 | ctx.strokeStyle = this.lineColor; 1657 | ctx.beginPath(); 1658 | ctx.moveTo(xStart - 5, linePositionY); 1659 | ctx.lineTo(xStart, linePositionY); 1660 | ctx.stroke(); 1661 | ctx.closePath(); 1662 | 1663 | },this); 1664 | 1665 | each(this.xLabels,function(label,index){ 1666 | var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), 1667 | // Check to see if line/bar here and decide where to place the line 1668 | linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), 1669 | isRotated = (this.xLabelRotation > 0), 1670 | drawVerticalLine = this.showVerticalLines; 1671 | 1672 | // This is Y axis, so draw it 1673 | if (index === 0 && !drawVerticalLine){ 1674 | drawVerticalLine = true; 1675 | } 1676 | 1677 | if (drawVerticalLine){ 1678 | ctx.beginPath(); 1679 | } 1680 | 1681 | if (index > 0){ 1682 | // This is a grid line in the centre, so drop that 1683 | ctx.lineWidth = this.gridLineWidth; 1684 | ctx.strokeStyle = this.gridLineColor; 1685 | } else { 1686 | // This is the first line on the scale 1687 | ctx.lineWidth = this.lineWidth; 1688 | ctx.strokeStyle = this.lineColor; 1689 | } 1690 | 1691 | if (drawVerticalLine){ 1692 | ctx.moveTo(linePos,this.endPoint); 1693 | ctx.lineTo(linePos,this.startPoint - 3); 1694 | ctx.stroke(); 1695 | ctx.closePath(); 1696 | } 1697 | 1698 | 1699 | ctx.lineWidth = this.lineWidth; 1700 | ctx.strokeStyle = this.lineColor; 1701 | 1702 | 1703 | // Small lines at the bottom of the base grid line 1704 | ctx.beginPath(); 1705 | ctx.moveTo(linePos,this.endPoint); 1706 | ctx.lineTo(linePos,this.endPoint + 5); 1707 | ctx.stroke(); 1708 | ctx.closePath(); 1709 | 1710 | ctx.save(); 1711 | ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); 1712 | ctx.rotate(toRadians(this.xLabelRotation)*-1); 1713 | ctx.font = this.font; 1714 | ctx.textAlign = (isRotated) ? "right" : "center"; 1715 | ctx.textBaseline = (isRotated) ? "middle" : "top"; 1716 | ctx.fillText(label, 0, 0); 1717 | ctx.restore(); 1718 | },this); 1719 | 1720 | } 1721 | } 1722 | 1723 | }); 1724 | 1725 | Chart.RadialScale = Chart.Element.extend({ 1726 | initialize: function(){ 1727 | this.size = min([this.height, this.width]); 1728 | this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); 1729 | }, 1730 | calculateCenterOffset: function(value){ 1731 | // Take into account half font size + the yPadding of the top value 1732 | var scalingFactor = this.drawingArea / (this.max - this.min); 1733 | 1734 | return (value - this.min) * scalingFactor; 1735 | }, 1736 | update : function(){ 1737 | if (!this.lineArc){ 1738 | this.setScaleSize(); 1739 | } else { 1740 | this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); 1741 | } 1742 | this.buildYLabels(); 1743 | }, 1744 | buildYLabels: function(){ 1745 | this.yLabels = []; 1746 | 1747 | var stepDecimalPlaces = getDecimalPlaces(this.stepValue); 1748 | 1749 | for (var i=0; i<=this.steps; i++){ 1750 | this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); 1751 | } 1752 | }, 1753 | getCircumference : function(){ 1754 | return ((Math.PI*2) / this.valuesCount); 1755 | }, 1756 | setScaleSize: function(){ 1757 | /* 1758 | * Right, this is really confusing and there is a lot of maths going on here 1759 | * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 1760 | * 1761 | * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif 1762 | * 1763 | * Solution: 1764 | * 1765 | * We assume the radius of the polygon is half the size of the canvas at first 1766 | * at each index we check if the text overlaps. 1767 | * 1768 | * Where it does, we store that angle and that index. 1769 | * 1770 | * After finding the largest index and angle we calculate how much we need to remove 1771 | * from the shape radius to move the point inwards by that x. 1772 | * 1773 | * We average the left and right distances to get the maximum shape radius that can fit in the box 1774 | * along with labels. 1775 | * 1776 | * Once we have that, we can find the centre point for the chart, by taking the x text protrusion 1777 | * on each side, removing that from the size, halving it and adding the left x protrusion width. 1778 | * 1779 | * This will mean we have a shape fitted to the canvas, as large as it can be with the labels 1780 | * and position it in the most space efficient manner 1781 | * 1782 | * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif 1783 | */ 1784 | 1785 | 1786 | // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. 1787 | // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points 1788 | var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), 1789 | pointPosition, 1790 | i, 1791 | textWidth, 1792 | halfTextWidth, 1793 | furthestRight = this.width, 1794 | furthestRightIndex, 1795 | furthestRightAngle, 1796 | furthestLeft = 0, 1797 | furthestLeftIndex, 1798 | furthestLeftAngle, 1799 | xProtrusionLeft, 1800 | xProtrusionRight, 1801 | radiusReductionRight, 1802 | radiusReductionLeft, 1803 | maxWidthRadius; 1804 | this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); 1805 | for (i=0;i furthestRight) { 1815 | furthestRight = pointPosition.x + halfTextWidth; 1816 | furthestRightIndex = i; 1817 | } 1818 | if (pointPosition.x - halfTextWidth < furthestLeft) { 1819 | furthestLeft = pointPosition.x - halfTextWidth; 1820 | furthestLeftIndex = i; 1821 | } 1822 | } 1823 | else if (i < this.valuesCount/2) { 1824 | // Less than half the values means we'll left align the text 1825 | if (pointPosition.x + textWidth > furthestRight) { 1826 | furthestRight = pointPosition.x + textWidth; 1827 | furthestRightIndex = i; 1828 | } 1829 | } 1830 | else if (i > this.valuesCount/2){ 1831 | // More than half the values means we'll right align the text 1832 | if (pointPosition.x - textWidth < furthestLeft) { 1833 | furthestLeft = pointPosition.x - textWidth; 1834 | furthestLeftIndex = i; 1835 | } 1836 | } 1837 | } 1838 | 1839 | xProtrusionLeft = furthestLeft; 1840 | 1841 | xProtrusionRight = Math.ceil(furthestRight - this.width); 1842 | 1843 | furthestRightAngle = this.getIndexAngle(furthestRightIndex); 1844 | 1845 | furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); 1846 | 1847 | radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); 1848 | 1849 | radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); 1850 | 1851 | // Ensure we actually need to reduce the size of the chart 1852 | radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; 1853 | radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; 1854 | 1855 | this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; 1856 | 1857 | //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) 1858 | this.setCenterPoint(radiusReductionLeft, radiusReductionRight); 1859 | 1860 | }, 1861 | setCenterPoint: function(leftMovement, rightMovement){ 1862 | 1863 | var maxRight = this.width - rightMovement - this.drawingArea, 1864 | maxLeft = leftMovement + this.drawingArea; 1865 | 1866 | this.xCenter = (maxLeft + maxRight)/2; 1867 | // Always vertically in the centre as the text height doesn't change 1868 | this.yCenter = (this.height/2); 1869 | }, 1870 | 1871 | getIndexAngle : function(index){ 1872 | var angleMultiplier = (Math.PI * 2) / this.valuesCount; 1873 | // Start from the top instead of right, so remove a quarter of the circle 1874 | 1875 | return index * angleMultiplier - (Math.PI/2); 1876 | }, 1877 | getPointPosition : function(index, distanceFromCenter){ 1878 | var thisAngle = this.getIndexAngle(index); 1879 | return { 1880 | x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, 1881 | y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter 1882 | }; 1883 | }, 1884 | draw: function(){ 1885 | if (this.display){ 1886 | var ctx = this.ctx; 1887 | each(this.yLabels, function(label, index){ 1888 | // Don't draw a centre value 1889 | if (index > 0){ 1890 | var yCenterOffset = index * (this.drawingArea/this.steps), 1891 | yHeight = this.yCenter - yCenterOffset, 1892 | pointPosition; 1893 | 1894 | // Draw circular lines around the scale 1895 | if (this.lineWidth > 0){ 1896 | ctx.strokeStyle = this.lineColor; 1897 | ctx.lineWidth = this.lineWidth; 1898 | 1899 | if(this.lineArc){ 1900 | ctx.beginPath(); 1901 | ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); 1902 | ctx.closePath(); 1903 | ctx.stroke(); 1904 | } else{ 1905 | ctx.beginPath(); 1906 | for (var i=0;i= 0; i--) { 1943 | if (this.angleLineWidth > 0){ 1944 | var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); 1945 | ctx.beginPath(); 1946 | ctx.moveTo(this.xCenter, this.yCenter); 1947 | ctx.lineTo(outerPosition.x, outerPosition.y); 1948 | ctx.stroke(); 1949 | ctx.closePath(); 1950 | } 1951 | // Extra 3px out for some label spacing 1952 | var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); 1953 | ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); 1954 | ctx.fillStyle = this.pointLabelFontColor; 1955 | 1956 | var labelsCount = this.labels.length, 1957 | halfLabelsCount = this.labels.length/2, 1958 | quarterLabelsCount = halfLabelsCount/2, 1959 | upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), 1960 | exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); 1961 | if (i === 0){ 1962 | ctx.textAlign = 'center'; 1963 | } else if(i === halfLabelsCount){ 1964 | ctx.textAlign = 'center'; 1965 | } else if (i < halfLabelsCount){ 1966 | ctx.textAlign = 'left'; 1967 | } else { 1968 | ctx.textAlign = 'right'; 1969 | } 1970 | 1971 | // Set the correct text baseline based on outer positioning 1972 | if (exactQuarter){ 1973 | ctx.textBaseline = 'middle'; 1974 | } else if (upperHalf){ 1975 | ctx.textBaseline = 'bottom'; 1976 | } else { 1977 | ctx.textBaseline = 'top'; 1978 | } 1979 | 1980 | ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); 1981 | } 1982 | } 1983 | } 1984 | } 1985 | }); 1986 | 1987 | // Attach global event to resize each chart instance when the browser resizes 1988 | helpers.addEvent(window, "resize", (function(){ 1989 | // Basic debounce of resize function so it doesn't hurt performance when resizing browser. 1990 | var timeout; 1991 | return function(){ 1992 | clearTimeout(timeout); 1993 | timeout = setTimeout(function(){ 1994 | each(Chart.instances,function(instance){ 1995 | // If the responsive flag is set in the chart instance config 1996 | // Cascade the resize event down to the chart. 1997 | if (instance.options.responsive){ 1998 | instance.resize(instance.render, true); 1999 | } 2000 | }); 2001 | }, 50); 2002 | }; 2003 | })()); 2004 | 2005 | 2006 | if (amd) { 2007 | define(function(){ 2008 | return Chart; 2009 | }); 2010 | } else if (typeof module === 'object' && module.exports) { 2011 | module.exports = Chart; 2012 | } 2013 | 2014 | root.Chart = Chart; 2015 | 2016 | Chart.noConflict = function(){ 2017 | root.Chart = previous; 2018 | return Chart; 2019 | }; 2020 | 2021 | }).call(this); 2022 | 2023 | (function(){ 2024 | "use strict"; 2025 | 2026 | var root = this, 2027 | Chart = root.Chart, 2028 | helpers = Chart.helpers; 2029 | 2030 | 2031 | var defaultConfig = { 2032 | //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value 2033 | scaleBeginAtZero : true, 2034 | 2035 | //Boolean - Whether grid lines are shown across the chart 2036 | scaleShowGridLines : true, 2037 | 2038 | //String - Colour of the grid lines 2039 | scaleGridLineColor : "rgba(0,0,0,.05)", 2040 | 2041 | //Number - Width of the grid lines 2042 | scaleGridLineWidth : 1, 2043 | 2044 | //Boolean - Whether to show horizontal lines (except X axis) 2045 | scaleShowHorizontalLines: true, 2046 | 2047 | //Boolean - Whether to show vertical lines (except Y axis) 2048 | scaleShowVerticalLines: true, 2049 | 2050 | //Boolean - If there is a stroke on each bar 2051 | barShowStroke : true, 2052 | 2053 | //Number - Pixel width of the bar stroke 2054 | barStrokeWidth : 2, 2055 | 2056 | //Number - Spacing between each of the X value sets 2057 | barValueSpacing : 5, 2058 | 2059 | //Number - Spacing between data sets within X values 2060 | barDatasetSpacing : 1, 2061 | 2062 | //String - A legend template 2063 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 2064 | 2065 | }; 2066 | 2067 | 2068 | Chart.Type.extend({ 2069 | name: "Bar", 2070 | defaults : defaultConfig, 2071 | initialize: function(data){ 2072 | 2073 | //Expose options as a scope variable here so we can access it in the ScaleClass 2074 | var options = this.options; 2075 | 2076 | this.ScaleClass = Chart.Scale.extend({ 2077 | offsetGridLines : true, 2078 | calculateBarX : function(datasetCount, datasetIndex, barIndex){ 2079 | //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar 2080 | var xWidth = this.calculateBaseWidth(), 2081 | xAbsolute = this.calculateX(barIndex) - (xWidth/2), 2082 | barWidth = this.calculateBarWidth(datasetCount); 2083 | 2084 | return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; 2085 | }, 2086 | calculateBaseWidth : function(){ 2087 | return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); 2088 | }, 2089 | calculateBarWidth : function(datasetCount){ 2090 | //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset 2091 | var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); 2092 | 2093 | return (baseWidth / datasetCount); 2094 | } 2095 | }); 2096 | 2097 | this.datasets = []; 2098 | 2099 | //Set up tooltip events on the chart 2100 | if (this.options.showTooltips){ 2101 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2102 | var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; 2103 | 2104 | this.eachBars(function(bar){ 2105 | bar.restore(['fillColor', 'strokeColor']); 2106 | }); 2107 | helpers.each(activeBars, function(activeBar){ 2108 | activeBar.fillColor = activeBar.highlightFill; 2109 | activeBar.strokeColor = activeBar.highlightStroke; 2110 | }); 2111 | this.showTooltip(activeBars); 2112 | }); 2113 | } 2114 | 2115 | //Declare the extension of the default point, to cater for the options passed in to the constructor 2116 | this.BarClass = Chart.Rectangle.extend({ 2117 | strokeWidth : this.options.barStrokeWidth, 2118 | showStroke : this.options.barShowStroke, 2119 | ctx : this.chart.ctx 2120 | }); 2121 | 2122 | //Iterate through each of the datasets, and build this into a property of the chart 2123 | helpers.each(data.datasets,function(dataset,datasetIndex){ 2124 | 2125 | var datasetObject = { 2126 | label : dataset.label || null, 2127 | fillColor : dataset.fillColor, 2128 | strokeColor : dataset.strokeColor, 2129 | bars : [] 2130 | }; 2131 | 2132 | this.datasets.push(datasetObject); 2133 | 2134 | helpers.each(dataset.data,function(dataPoint,index){ 2135 | //Add a new point for each piece of data, passing any required data to draw. 2136 | datasetObject.bars.push(new this.BarClass({ 2137 | value : dataPoint, 2138 | label : data.labels[index], 2139 | datasetLabel: dataset.label, 2140 | strokeColor : dataset.strokeColor, 2141 | fillColor : dataset.fillColor, 2142 | highlightFill : dataset.highlightFill || dataset.fillColor, 2143 | highlightStroke : dataset.highlightStroke || dataset.strokeColor 2144 | })); 2145 | },this); 2146 | 2147 | },this); 2148 | 2149 | this.buildScale(data.labels); 2150 | 2151 | this.BarClass.prototype.base = this.scale.endPoint; 2152 | 2153 | this.eachBars(function(bar, index, datasetIndex){ 2154 | helpers.extend(bar, { 2155 | width : this.scale.calculateBarWidth(this.datasets.length), 2156 | x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), 2157 | y: this.scale.endPoint 2158 | }); 2159 | bar.save(); 2160 | }, this); 2161 | 2162 | this.render(); 2163 | }, 2164 | update : function(){ 2165 | this.scale.update(); 2166 | // Reset any highlight colours before updating. 2167 | helpers.each(this.activeElements, function(activeElement){ 2168 | activeElement.restore(['fillColor', 'strokeColor']); 2169 | }); 2170 | 2171 | this.eachBars(function(bar){ 2172 | bar.save(); 2173 | }); 2174 | this.render(); 2175 | }, 2176 | eachBars : function(callback){ 2177 | helpers.each(this.datasets,function(dataset, datasetIndex){ 2178 | helpers.each(dataset.bars, callback, this, datasetIndex); 2179 | },this); 2180 | }, 2181 | getBarsAtEvent : function(e){ 2182 | var barsArray = [], 2183 | eventPosition = helpers.getRelativePosition(e), 2184 | datasetIterator = function(dataset){ 2185 | barsArray.push(dataset.bars[barIndex]); 2186 | }, 2187 | barIndex; 2188 | 2189 | for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { 2190 | for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { 2191 | if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ 2192 | helpers.each(this.datasets, datasetIterator); 2193 | return barsArray; 2194 | } 2195 | } 2196 | } 2197 | 2198 | return barsArray; 2199 | }, 2200 | buildScale : function(labels){ 2201 | var self = this; 2202 | 2203 | var dataTotal = function(){ 2204 | var values = []; 2205 | self.eachBars(function(bar){ 2206 | values.push(bar.value); 2207 | }); 2208 | return values; 2209 | }; 2210 | 2211 | var scaleOptions = { 2212 | templateString : this.options.scaleLabel, 2213 | height : this.chart.height, 2214 | width : this.chart.width, 2215 | ctx : this.chart.ctx, 2216 | textColor : this.options.scaleFontColor, 2217 | fontSize : this.options.scaleFontSize, 2218 | fontStyle : this.options.scaleFontStyle, 2219 | fontFamily : this.options.scaleFontFamily, 2220 | valuesCount : labels.length, 2221 | beginAtZero : this.options.scaleBeginAtZero, 2222 | integersOnly : this.options.scaleIntegersOnly, 2223 | calculateYRange: function(currentHeight){ 2224 | var updatedRanges = helpers.calculateScaleRange( 2225 | dataTotal(), 2226 | currentHeight, 2227 | this.fontSize, 2228 | this.beginAtZero, 2229 | this.integersOnly 2230 | ); 2231 | helpers.extend(this, updatedRanges); 2232 | }, 2233 | xLabels : labels, 2234 | font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), 2235 | lineWidth : this.options.scaleLineWidth, 2236 | lineColor : this.options.scaleLineColor, 2237 | showHorizontalLines : this.options.scaleShowHorizontalLines, 2238 | showVerticalLines : this.options.scaleShowVerticalLines, 2239 | gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, 2240 | gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", 2241 | padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, 2242 | showLabels : this.options.scaleShowLabels, 2243 | display : this.options.showScale 2244 | }; 2245 | 2246 | if (this.options.scaleOverride){ 2247 | helpers.extend(scaleOptions, { 2248 | calculateYRange: helpers.noop, 2249 | steps: this.options.scaleSteps, 2250 | stepValue: this.options.scaleStepWidth, 2251 | min: this.options.scaleStartValue, 2252 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 2253 | }); 2254 | } 2255 | 2256 | this.scale = new this.ScaleClass(scaleOptions); 2257 | }, 2258 | addData : function(valuesArray,label){ 2259 | //Map the values array for each of the datasets 2260 | helpers.each(valuesArray,function(value,datasetIndex){ 2261 | //Add a new point for each piece of data, passing any required data to draw. 2262 | this.datasets[datasetIndex].bars.push(new this.BarClass({ 2263 | value : value, 2264 | label : label, 2265 | x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), 2266 | y: this.scale.endPoint, 2267 | width : this.scale.calculateBarWidth(this.datasets.length), 2268 | base : this.scale.endPoint, 2269 | strokeColor : this.datasets[datasetIndex].strokeColor, 2270 | fillColor : this.datasets[datasetIndex].fillColor 2271 | })); 2272 | },this); 2273 | 2274 | this.scale.addXLabel(label); 2275 | //Then re-render the chart. 2276 | this.update(); 2277 | }, 2278 | removeData : function(){ 2279 | this.scale.removeXLabel(); 2280 | //Then re-render the chart. 2281 | helpers.each(this.datasets,function(dataset){ 2282 | dataset.bars.shift(); 2283 | },this); 2284 | this.update(); 2285 | }, 2286 | reflow : function(){ 2287 | helpers.extend(this.BarClass.prototype,{ 2288 | y: this.scale.endPoint, 2289 | base : this.scale.endPoint 2290 | }); 2291 | var newScaleProps = helpers.extend({ 2292 | height : this.chart.height, 2293 | width : this.chart.width 2294 | }); 2295 | this.scale.update(newScaleProps); 2296 | }, 2297 | draw : function(ease){ 2298 | var easingDecimal = ease || 1; 2299 | this.clear(); 2300 | 2301 | var ctx = this.chart.ctx; 2302 | 2303 | this.scale.draw(easingDecimal); 2304 | 2305 | //Draw all the bars for each dataset 2306 | helpers.each(this.datasets,function(dataset,datasetIndex){ 2307 | helpers.each(dataset.bars,function(bar,index){ 2308 | if (bar.hasValue()){ 2309 | bar.base = this.scale.endPoint; 2310 | //Transition then draw 2311 | bar.transition({ 2312 | x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), 2313 | y : this.scale.calculateY(bar.value), 2314 | width : this.scale.calculateBarWidth(this.datasets.length) 2315 | }, easingDecimal).draw(); 2316 | } 2317 | },this); 2318 | 2319 | },this); 2320 | } 2321 | }); 2322 | 2323 | 2324 | }).call(this); 2325 | 2326 | (function(){ 2327 | "use strict"; 2328 | 2329 | var root = this, 2330 | Chart = root.Chart, 2331 | //Cache a local reference to Chart.helpers 2332 | helpers = Chart.helpers; 2333 | 2334 | var defaultConfig = { 2335 | //Boolean - Whether we should show a stroke on each segment 2336 | segmentShowStroke : true, 2337 | 2338 | //String - The colour of each segment stroke 2339 | segmentStrokeColor : "#fff", 2340 | 2341 | //Number - The width of each segment stroke 2342 | segmentStrokeWidth : 2, 2343 | 2344 | //The percentage of the chart that we cut out of the middle. 2345 | percentageInnerCutout : 50, 2346 | 2347 | //Number - Amount of animation steps 2348 | animationSteps : 100, 2349 | 2350 | //String - Animation easing effect 2351 | animationEasing : "easeOutBounce", 2352 | 2353 | //Boolean - Whether we animate the rotation of the Doughnut 2354 | animateRotate : true, 2355 | 2356 | //Boolean - Whether we animate scaling the Doughnut from the centre 2357 | animateScale : false, 2358 | 2359 | //String - A legend template 2360 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" 2361 | 2362 | }; 2363 | 2364 | 2365 | Chart.Type.extend({ 2366 | //Passing in a name registers this chart in the Chart namespace 2367 | name: "Doughnut", 2368 | //Providing a defaults will also register the deafults in the chart namespace 2369 | defaults : defaultConfig, 2370 | //Initialize is fired when the chart is initialized - Data is passed in as a parameter 2371 | //Config is automatically merged by the core of Chart.js, and is available at this.options 2372 | initialize: function(data){ 2373 | 2374 | //Declare segments as a static property to prevent inheriting across the Chart type prototype 2375 | this.segments = []; 2376 | this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; 2377 | 2378 | this.SegmentArc = Chart.Arc.extend({ 2379 | ctx : this.chart.ctx, 2380 | x : this.chart.width/2, 2381 | y : this.chart.height/2 2382 | }); 2383 | 2384 | //Set up tooltip events on the chart 2385 | if (this.options.showTooltips){ 2386 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2387 | var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; 2388 | 2389 | helpers.each(this.segments,function(segment){ 2390 | segment.restore(["fillColor"]); 2391 | }); 2392 | helpers.each(activeSegments,function(activeSegment){ 2393 | activeSegment.fillColor = activeSegment.highlightColor; 2394 | }); 2395 | this.showTooltip(activeSegments); 2396 | }); 2397 | } 2398 | this.calculateTotal(data); 2399 | 2400 | helpers.each(data,function(datapoint, index){ 2401 | this.addData(datapoint, index, true); 2402 | },this); 2403 | 2404 | this.render(); 2405 | }, 2406 | getSegmentsAtEvent : function(e){ 2407 | var segmentsArray = []; 2408 | 2409 | var location = helpers.getRelativePosition(e); 2410 | 2411 | helpers.each(this.segments,function(segment){ 2412 | if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); 2413 | },this); 2414 | return segmentsArray; 2415 | }, 2416 | addData : function(segment, atIndex, silent){ 2417 | var index = atIndex || this.segments.length; 2418 | this.segments.splice(index, 0, new this.SegmentArc({ 2419 | value : segment.value, 2420 | outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, 2421 | innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, 2422 | fillColor : segment.color, 2423 | highlightColor : segment.highlight || segment.color, 2424 | showStroke : this.options.segmentShowStroke, 2425 | strokeWidth : this.options.segmentStrokeWidth, 2426 | strokeColor : this.options.segmentStrokeColor, 2427 | startAngle : Math.PI * 1.5, 2428 | circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), 2429 | label : segment.label 2430 | })); 2431 | if (!silent){ 2432 | this.reflow(); 2433 | this.update(); 2434 | } 2435 | }, 2436 | calculateCircumference : function(value){ 2437 | return (Math.PI*2)*(Math.abs(value) / this.total); 2438 | }, 2439 | calculateTotal : function(data){ 2440 | this.total = 0; 2441 | helpers.each(data,function(segment){ 2442 | this.total += Math.abs(segment.value); 2443 | },this); 2444 | }, 2445 | update : function(){ 2446 | this.calculateTotal(this.segments); 2447 | 2448 | // Reset any highlight colours before updating. 2449 | helpers.each(this.activeElements, function(activeElement){ 2450 | activeElement.restore(['fillColor']); 2451 | }); 2452 | 2453 | helpers.each(this.segments,function(segment){ 2454 | segment.save(); 2455 | }); 2456 | this.render(); 2457 | }, 2458 | 2459 | removeData: function(atIndex){ 2460 | var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; 2461 | this.segments.splice(indexToDelete, 1); 2462 | this.reflow(); 2463 | this.update(); 2464 | }, 2465 | 2466 | reflow : function(){ 2467 | helpers.extend(this.SegmentArc.prototype,{ 2468 | x : this.chart.width/2, 2469 | y : this.chart.height/2 2470 | }); 2471 | this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; 2472 | helpers.each(this.segments, function(segment){ 2473 | segment.update({ 2474 | outerRadius : this.outerRadius, 2475 | innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout 2476 | }); 2477 | }, this); 2478 | }, 2479 | draw : function(easeDecimal){ 2480 | var animDecimal = (easeDecimal) ? easeDecimal : 1; 2481 | this.clear(); 2482 | helpers.each(this.segments,function(segment,index){ 2483 | segment.transition({ 2484 | circumference : this.calculateCircumference(segment.value), 2485 | outerRadius : this.outerRadius, 2486 | innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout 2487 | },animDecimal); 2488 | 2489 | segment.endAngle = segment.startAngle + segment.circumference; 2490 | 2491 | segment.draw(); 2492 | if (index === 0){ 2493 | segment.startAngle = Math.PI * 1.5; 2494 | } 2495 | //Check to see if it's the last segment, if not get the next and update the start angle 2496 | if (index < this.segments.length-1){ 2497 | this.segments[index+1].startAngle = segment.endAngle; 2498 | } 2499 | },this); 2500 | 2501 | } 2502 | }); 2503 | 2504 | Chart.types.Doughnut.extend({ 2505 | name : "Pie", 2506 | defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) 2507 | }); 2508 | 2509 | }).call(this); 2510 | (function(){ 2511 | "use strict"; 2512 | 2513 | var root = this, 2514 | Chart = root.Chart, 2515 | helpers = Chart.helpers; 2516 | 2517 | var defaultConfig = { 2518 | 2519 | ///Boolean - Whether grid lines are shown across the chart 2520 | scaleShowGridLines : true, 2521 | 2522 | //String - Colour of the grid lines 2523 | scaleGridLineColor : "rgba(0,0,0,.05)", 2524 | 2525 | //Number - Width of the grid lines 2526 | scaleGridLineWidth : 1, 2527 | 2528 | //Boolean - Whether to show horizontal lines (except X axis) 2529 | scaleShowHorizontalLines: true, 2530 | 2531 | //Boolean - Whether to show vertical lines (except Y axis) 2532 | scaleShowVerticalLines: true, 2533 | 2534 | //Boolean - Whether the line is curved between points 2535 | bezierCurve : true, 2536 | 2537 | //Number - Tension of the bezier curve between points 2538 | bezierCurveTension : 0.4, 2539 | 2540 | //Boolean - Whether to show a dot for each point 2541 | pointDot : true, 2542 | 2543 | //Number - Radius of each point dot in pixels 2544 | pointDotRadius : 4, 2545 | 2546 | //Number - Pixel width of point dot stroke 2547 | pointDotStrokeWidth : 1, 2548 | 2549 | //Number - amount extra to add to the radius to cater for hit detection outside the drawn point 2550 | pointHitDetectionRadius : 20, 2551 | 2552 | //Boolean - Whether to show a stroke for datasets 2553 | datasetStroke : true, 2554 | 2555 | //Number - Pixel width of dataset stroke 2556 | datasetStrokeWidth : 2, 2557 | 2558 | //Boolean - Whether to fill the dataset with a colour 2559 | datasetFill : true, 2560 | 2561 | //String - A legend template 2562 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 2563 | 2564 | }; 2565 | 2566 | 2567 | Chart.Type.extend({ 2568 | name: "Line", 2569 | defaults : defaultConfig, 2570 | initialize: function(data){ 2571 | //Declare the extension of the default point, to cater for the options passed in to the constructor 2572 | this.PointClass = Chart.Point.extend({ 2573 | strokeWidth : this.options.pointDotStrokeWidth, 2574 | radius : this.options.pointDotRadius, 2575 | display: this.options.pointDot, 2576 | hitDetectionRadius : this.options.pointHitDetectionRadius, 2577 | ctx : this.chart.ctx, 2578 | inRange : function(mouseX){ 2579 | return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); 2580 | } 2581 | }); 2582 | 2583 | this.datasets = []; 2584 | 2585 | //Set up tooltip events on the chart 2586 | if (this.options.showTooltips){ 2587 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2588 | var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; 2589 | this.eachPoints(function(point){ 2590 | point.restore(['fillColor', 'strokeColor']); 2591 | }); 2592 | helpers.each(activePoints, function(activePoint){ 2593 | activePoint.fillColor = activePoint.highlightFill; 2594 | activePoint.strokeColor = activePoint.highlightStroke; 2595 | }); 2596 | this.showTooltip(activePoints); 2597 | }); 2598 | } 2599 | 2600 | //Iterate through each of the datasets, and build this into a property of the chart 2601 | helpers.each(data.datasets,function(dataset){ 2602 | 2603 | var datasetObject = { 2604 | label : dataset.label || null, 2605 | fillColor : dataset.fillColor, 2606 | strokeColor : dataset.strokeColor, 2607 | pointColor : dataset.pointColor, 2608 | pointStrokeColor : dataset.pointStrokeColor, 2609 | points : [] 2610 | }; 2611 | 2612 | this.datasets.push(datasetObject); 2613 | 2614 | 2615 | helpers.each(dataset.data,function(dataPoint,index){ 2616 | //Add a new point for each piece of data, passing any required data to draw. 2617 | datasetObject.points.push(new this.PointClass({ 2618 | value : dataPoint, 2619 | label : data.labels[index], 2620 | datasetLabel: dataset.label, 2621 | strokeColor : dataset.pointStrokeColor, 2622 | fillColor : dataset.pointColor, 2623 | highlightFill : dataset.pointHighlightFill || dataset.pointColor, 2624 | highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor 2625 | })); 2626 | },this); 2627 | 2628 | this.buildScale(data.labels); 2629 | 2630 | 2631 | this.eachPoints(function(point, index){ 2632 | helpers.extend(point, { 2633 | x: this.scale.calculateX(index), 2634 | y: this.scale.endPoint 2635 | }); 2636 | point.save(); 2637 | }, this); 2638 | 2639 | },this); 2640 | 2641 | 2642 | this.render(); 2643 | }, 2644 | update : function(){ 2645 | this.scale.update(); 2646 | // Reset any highlight colours before updating. 2647 | helpers.each(this.activeElements, function(activeElement){ 2648 | activeElement.restore(['fillColor', 'strokeColor']); 2649 | }); 2650 | this.eachPoints(function(point){ 2651 | point.save(); 2652 | }); 2653 | this.render(); 2654 | }, 2655 | eachPoints : function(callback){ 2656 | helpers.each(this.datasets,function(dataset){ 2657 | helpers.each(dataset.points,callback,this); 2658 | },this); 2659 | }, 2660 | getPointsAtEvent : function(e){ 2661 | var pointsArray = [], 2662 | eventPosition = helpers.getRelativePosition(e); 2663 | helpers.each(this.datasets,function(dataset){ 2664 | helpers.each(dataset.points,function(point){ 2665 | if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); 2666 | }); 2667 | },this); 2668 | return pointsArray; 2669 | }, 2670 | buildScale : function(labels){ 2671 | var self = this; 2672 | 2673 | var dataTotal = function(){ 2674 | var values = []; 2675 | self.eachPoints(function(point){ 2676 | values.push(point.value); 2677 | }); 2678 | 2679 | return values; 2680 | }; 2681 | 2682 | var scaleOptions = { 2683 | templateString : this.options.scaleLabel, 2684 | height : this.chart.height, 2685 | width : this.chart.width, 2686 | ctx : this.chart.ctx, 2687 | textColor : this.options.scaleFontColor, 2688 | fontSize : this.options.scaleFontSize, 2689 | fontStyle : this.options.scaleFontStyle, 2690 | fontFamily : this.options.scaleFontFamily, 2691 | valuesCount : labels.length, 2692 | beginAtZero : this.options.scaleBeginAtZero, 2693 | integersOnly : this.options.scaleIntegersOnly, 2694 | calculateYRange : function(currentHeight){ 2695 | var updatedRanges = helpers.calculateScaleRange( 2696 | dataTotal(), 2697 | currentHeight, 2698 | this.fontSize, 2699 | this.beginAtZero, 2700 | this.integersOnly 2701 | ); 2702 | helpers.extend(this, updatedRanges); 2703 | }, 2704 | xLabels : labels, 2705 | font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), 2706 | lineWidth : this.options.scaleLineWidth, 2707 | lineColor : this.options.scaleLineColor, 2708 | showHorizontalLines : this.options.scaleShowHorizontalLines, 2709 | showVerticalLines : this.options.scaleShowVerticalLines, 2710 | gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, 2711 | gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", 2712 | padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, 2713 | showLabels : this.options.scaleShowLabels, 2714 | display : this.options.showScale 2715 | }; 2716 | 2717 | if (this.options.scaleOverride){ 2718 | helpers.extend(scaleOptions, { 2719 | calculateYRange: helpers.noop, 2720 | steps: this.options.scaleSteps, 2721 | stepValue: this.options.scaleStepWidth, 2722 | min: this.options.scaleStartValue, 2723 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 2724 | }); 2725 | } 2726 | 2727 | 2728 | this.scale = new Chart.Scale(scaleOptions); 2729 | }, 2730 | addData : function(valuesArray,label){ 2731 | //Map the values array for each of the datasets 2732 | 2733 | helpers.each(valuesArray,function(value,datasetIndex){ 2734 | //Add a new point for each piece of data, passing any required data to draw. 2735 | this.datasets[datasetIndex].points.push(new this.PointClass({ 2736 | value : value, 2737 | label : label, 2738 | x: this.scale.calculateX(this.scale.valuesCount+1), 2739 | y: this.scale.endPoint, 2740 | strokeColor : this.datasets[datasetIndex].pointStrokeColor, 2741 | fillColor : this.datasets[datasetIndex].pointColor 2742 | })); 2743 | },this); 2744 | 2745 | this.scale.addXLabel(label); 2746 | //Then re-render the chart. 2747 | this.update(); 2748 | }, 2749 | removeData : function(){ 2750 | this.scale.removeXLabel(); 2751 | //Then re-render the chart. 2752 | helpers.each(this.datasets,function(dataset){ 2753 | dataset.points.shift(); 2754 | },this); 2755 | this.update(); 2756 | }, 2757 | reflow : function(){ 2758 | var newScaleProps = helpers.extend({ 2759 | height : this.chart.height, 2760 | width : this.chart.width 2761 | }); 2762 | this.scale.update(newScaleProps); 2763 | }, 2764 | draw : function(ease){ 2765 | var easingDecimal = ease || 1; 2766 | this.clear(); 2767 | 2768 | var ctx = this.chart.ctx; 2769 | 2770 | // Some helper methods for getting the next/prev points 2771 | var hasValue = function(item){ 2772 | return item.value !== null; 2773 | }, 2774 | nextPoint = function(point, collection, index){ 2775 | return helpers.findNextWhere(collection, hasValue, index) || point; 2776 | }, 2777 | previousPoint = function(point, collection, index){ 2778 | return helpers.findPreviousWhere(collection, hasValue, index) || point; 2779 | }; 2780 | 2781 | this.scale.draw(easingDecimal); 2782 | 2783 | 2784 | helpers.each(this.datasets,function(dataset){ 2785 | var pointsWithValues = helpers.where(dataset.points, hasValue); 2786 | 2787 | //Transition each point first so that the line and point drawing isn't out of sync 2788 | //We can use this extra loop to calculate the control points of this dataset also in this loop 2789 | 2790 | helpers.each(dataset.points, function(point, index){ 2791 | if (point.hasValue()){ 2792 | point.transition({ 2793 | y : this.scale.calculateY(point.value), 2794 | x : this.scale.calculateX(index) 2795 | }, easingDecimal); 2796 | } 2797 | },this); 2798 | 2799 | 2800 | // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point 2801 | // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed 2802 | if (this.options.bezierCurve){ 2803 | helpers.each(pointsWithValues, function(point, index){ 2804 | var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; 2805 | point.controlPoints = helpers.splineCurve( 2806 | previousPoint(point, pointsWithValues, index), 2807 | point, 2808 | nextPoint(point, pointsWithValues, index), 2809 | tension 2810 | ); 2811 | 2812 | // Prevent the bezier going outside of the bounds of the graph 2813 | 2814 | // Cap puter bezier handles to the upper/lower scale bounds 2815 | if (point.controlPoints.outer.y > this.scale.endPoint){ 2816 | point.controlPoints.outer.y = this.scale.endPoint; 2817 | } 2818 | else if (point.controlPoints.outer.y < this.scale.startPoint){ 2819 | point.controlPoints.outer.y = this.scale.startPoint; 2820 | } 2821 | 2822 | // Cap inner bezier handles to the upper/lower scale bounds 2823 | if (point.controlPoints.inner.y > this.scale.endPoint){ 2824 | point.controlPoints.inner.y = this.scale.endPoint; 2825 | } 2826 | else if (point.controlPoints.inner.y < this.scale.startPoint){ 2827 | point.controlPoints.inner.y = this.scale.startPoint; 2828 | } 2829 | },this); 2830 | } 2831 | 2832 | 2833 | //Draw the line between all the points 2834 | ctx.lineWidth = this.options.datasetStrokeWidth; 2835 | ctx.strokeStyle = dataset.strokeColor; 2836 | ctx.beginPath(); 2837 | 2838 | helpers.each(pointsWithValues, function(point, index){ 2839 | if (index === 0){ 2840 | ctx.moveTo(point.x, point.y); 2841 | } 2842 | else{ 2843 | if(this.options.bezierCurve){ 2844 | var previous = previousPoint(point, pointsWithValues, index); 2845 | 2846 | ctx.bezierCurveTo( 2847 | previous.controlPoints.outer.x, 2848 | previous.controlPoints.outer.y, 2849 | point.controlPoints.inner.x, 2850 | point.controlPoints.inner.y, 2851 | point.x, 2852 | point.y 2853 | ); 2854 | } 2855 | else{ 2856 | ctx.lineTo(point.x,point.y); 2857 | } 2858 | } 2859 | }, this); 2860 | 2861 | ctx.stroke(); 2862 | 2863 | if (this.options.datasetFill && pointsWithValues.length > 0){ 2864 | //Round off the line by going to the base of the chart, back to the start, then fill. 2865 | ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); 2866 | ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); 2867 | ctx.fillStyle = dataset.fillColor; 2868 | ctx.closePath(); 2869 | ctx.fill(); 2870 | } 2871 | 2872 | //Now draw the points over the line 2873 | //A little inefficient double looping, but better than the line 2874 | //lagging behind the point positions 2875 | helpers.each(pointsWithValues,function(point){ 2876 | point.draw(); 2877 | }); 2878 | },this); 2879 | } 2880 | }); 2881 | 2882 | 2883 | }).call(this); 2884 | 2885 | (function(){ 2886 | "use strict"; 2887 | 2888 | var root = this, 2889 | Chart = root.Chart, 2890 | //Cache a local reference to Chart.helpers 2891 | helpers = Chart.helpers; 2892 | 2893 | var defaultConfig = { 2894 | //Boolean - Show a backdrop to the scale label 2895 | scaleShowLabelBackdrop : true, 2896 | 2897 | //String - The colour of the label backdrop 2898 | scaleBackdropColor : "rgba(255,255,255,0.75)", 2899 | 2900 | // Boolean - Whether the scale should begin at zero 2901 | scaleBeginAtZero : true, 2902 | 2903 | //Number - The backdrop padding above & below the label in pixels 2904 | scaleBackdropPaddingY : 2, 2905 | 2906 | //Number - The backdrop padding to the side of the label in pixels 2907 | scaleBackdropPaddingX : 2, 2908 | 2909 | //Boolean - Show line for each value in the scale 2910 | scaleShowLine : true, 2911 | 2912 | //Boolean - Stroke a line around each segment in the chart 2913 | segmentShowStroke : true, 2914 | 2915 | //String - The colour of the stroke on each segement. 2916 | segmentStrokeColor : "#fff", 2917 | 2918 | //Number - The width of the stroke value in pixels 2919 | segmentStrokeWidth : 2, 2920 | 2921 | //Number - Amount of animation steps 2922 | animationSteps : 100, 2923 | 2924 | //String - Animation easing effect. 2925 | animationEasing : "easeOutBounce", 2926 | 2927 | //Boolean - Whether to animate the rotation of the chart 2928 | animateRotate : true, 2929 | 2930 | //Boolean - Whether to animate scaling the chart from the centre 2931 | animateScale : false, 2932 | 2933 | //String - A legend template 2934 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" 2935 | }; 2936 | 2937 | 2938 | Chart.Type.extend({ 2939 | //Passing in a name registers this chart in the Chart namespace 2940 | name: "PolarArea", 2941 | //Providing a defaults will also register the deafults in the chart namespace 2942 | defaults : defaultConfig, 2943 | //Initialize is fired when the chart is initialized - Data is passed in as a parameter 2944 | //Config is automatically merged by the core of Chart.js, and is available at this.options 2945 | initialize: function(data){ 2946 | this.segments = []; 2947 | //Declare segment class as a chart instance specific class, so it can share props for this instance 2948 | this.SegmentArc = Chart.Arc.extend({ 2949 | showStroke : this.options.segmentShowStroke, 2950 | strokeWidth : this.options.segmentStrokeWidth, 2951 | strokeColor : this.options.segmentStrokeColor, 2952 | ctx : this.chart.ctx, 2953 | innerRadius : 0, 2954 | x : this.chart.width/2, 2955 | y : this.chart.height/2 2956 | }); 2957 | this.scale = new Chart.RadialScale({ 2958 | display: this.options.showScale, 2959 | fontStyle: this.options.scaleFontStyle, 2960 | fontSize: this.options.scaleFontSize, 2961 | fontFamily: this.options.scaleFontFamily, 2962 | fontColor: this.options.scaleFontColor, 2963 | showLabels: this.options.scaleShowLabels, 2964 | showLabelBackdrop: this.options.scaleShowLabelBackdrop, 2965 | backdropColor: this.options.scaleBackdropColor, 2966 | backdropPaddingY : this.options.scaleBackdropPaddingY, 2967 | backdropPaddingX: this.options.scaleBackdropPaddingX, 2968 | lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, 2969 | lineColor: this.options.scaleLineColor, 2970 | lineArc: true, 2971 | width: this.chart.width, 2972 | height: this.chart.height, 2973 | xCenter: this.chart.width/2, 2974 | yCenter: this.chart.height/2, 2975 | ctx : this.chart.ctx, 2976 | templateString: this.options.scaleLabel, 2977 | valuesCount: data.length 2978 | }); 2979 | 2980 | this.updateScaleRange(data); 2981 | 2982 | this.scale.update(); 2983 | 2984 | helpers.each(data,function(segment,index){ 2985 | this.addData(segment,index,true); 2986 | },this); 2987 | 2988 | //Set up tooltip events on the chart 2989 | if (this.options.showTooltips){ 2990 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 2991 | var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; 2992 | helpers.each(this.segments,function(segment){ 2993 | segment.restore(["fillColor"]); 2994 | }); 2995 | helpers.each(activeSegments,function(activeSegment){ 2996 | activeSegment.fillColor = activeSegment.highlightColor; 2997 | }); 2998 | this.showTooltip(activeSegments); 2999 | }); 3000 | } 3001 | 3002 | this.render(); 3003 | }, 3004 | getSegmentsAtEvent : function(e){ 3005 | var segmentsArray = []; 3006 | 3007 | var location = helpers.getRelativePosition(e); 3008 | 3009 | helpers.each(this.segments,function(segment){ 3010 | if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); 3011 | },this); 3012 | return segmentsArray; 3013 | }, 3014 | addData : function(segment, atIndex, silent){ 3015 | var index = atIndex || this.segments.length; 3016 | 3017 | this.segments.splice(index, 0, new this.SegmentArc({ 3018 | fillColor: segment.color, 3019 | highlightColor: segment.highlight || segment.color, 3020 | label: segment.label, 3021 | value: segment.value, 3022 | outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), 3023 | circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), 3024 | startAngle: Math.PI * 1.5 3025 | })); 3026 | if (!silent){ 3027 | this.reflow(); 3028 | this.update(); 3029 | } 3030 | }, 3031 | removeData: function(atIndex){ 3032 | var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; 3033 | this.segments.splice(indexToDelete, 1); 3034 | this.reflow(); 3035 | this.update(); 3036 | }, 3037 | calculateTotal: function(data){ 3038 | this.total = 0; 3039 | helpers.each(data,function(segment){ 3040 | this.total += segment.value; 3041 | },this); 3042 | this.scale.valuesCount = this.segments.length; 3043 | }, 3044 | updateScaleRange: function(datapoints){ 3045 | var valuesArray = []; 3046 | helpers.each(datapoints,function(segment){ 3047 | valuesArray.push(segment.value); 3048 | }); 3049 | 3050 | var scaleSizes = (this.options.scaleOverride) ? 3051 | { 3052 | steps: this.options.scaleSteps, 3053 | stepValue: this.options.scaleStepWidth, 3054 | min: this.options.scaleStartValue, 3055 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 3056 | } : 3057 | helpers.calculateScaleRange( 3058 | valuesArray, 3059 | helpers.min([this.chart.width, this.chart.height])/2, 3060 | this.options.scaleFontSize, 3061 | this.options.scaleBeginAtZero, 3062 | this.options.scaleIntegersOnly 3063 | ); 3064 | 3065 | helpers.extend( 3066 | this.scale, 3067 | scaleSizes, 3068 | { 3069 | size: helpers.min([this.chart.width, this.chart.height]), 3070 | xCenter: this.chart.width/2, 3071 | yCenter: this.chart.height/2 3072 | } 3073 | ); 3074 | 3075 | }, 3076 | update : function(){ 3077 | this.calculateTotal(this.segments); 3078 | 3079 | helpers.each(this.segments,function(segment){ 3080 | segment.save(); 3081 | }); 3082 | 3083 | this.reflow(); 3084 | this.render(); 3085 | }, 3086 | reflow : function(){ 3087 | helpers.extend(this.SegmentArc.prototype,{ 3088 | x : this.chart.width/2, 3089 | y : this.chart.height/2 3090 | }); 3091 | this.updateScaleRange(this.segments); 3092 | this.scale.update(); 3093 | 3094 | helpers.extend(this.scale,{ 3095 | xCenter: this.chart.width/2, 3096 | yCenter: this.chart.height/2 3097 | }); 3098 | 3099 | helpers.each(this.segments, function(segment){ 3100 | segment.update({ 3101 | outerRadius : this.scale.calculateCenterOffset(segment.value) 3102 | }); 3103 | }, this); 3104 | 3105 | }, 3106 | draw : function(ease){ 3107 | var easingDecimal = ease || 1; 3108 | //Clear & draw the canvas 3109 | this.clear(); 3110 | helpers.each(this.segments,function(segment, index){ 3111 | segment.transition({ 3112 | circumference : this.scale.getCircumference(), 3113 | outerRadius : this.scale.calculateCenterOffset(segment.value) 3114 | },easingDecimal); 3115 | 3116 | segment.endAngle = segment.startAngle + segment.circumference; 3117 | 3118 | // If we've removed the first segment we need to set the first one to 3119 | // start at the top. 3120 | if (index === 0){ 3121 | segment.startAngle = Math.PI * 1.5; 3122 | } 3123 | 3124 | //Check to see if it's the last segment, if not get the next and update the start angle 3125 | if (index < this.segments.length - 1){ 3126 | this.segments[index+1].startAngle = segment.endAngle; 3127 | } 3128 | segment.draw(); 3129 | }, this); 3130 | this.scale.draw(); 3131 | } 3132 | }); 3133 | 3134 | }).call(this); 3135 | (function(){ 3136 | "use strict"; 3137 | 3138 | var root = this, 3139 | Chart = root.Chart, 3140 | helpers = Chart.helpers; 3141 | 3142 | 3143 | 3144 | Chart.Type.extend({ 3145 | name: "Radar", 3146 | defaults:{ 3147 | //Boolean - Whether to show lines for each scale point 3148 | scaleShowLine : true, 3149 | 3150 | //Boolean - Whether we show the angle lines out of the radar 3151 | angleShowLineOut : true, 3152 | 3153 | //Boolean - Whether to show labels on the scale 3154 | scaleShowLabels : false, 3155 | 3156 | // Boolean - Whether the scale should begin at zero 3157 | scaleBeginAtZero : true, 3158 | 3159 | //String - Colour of the angle line 3160 | angleLineColor : "rgba(0,0,0,.1)", 3161 | 3162 | //Number - Pixel width of the angle line 3163 | angleLineWidth : 1, 3164 | 3165 | //String - Point label font declaration 3166 | pointLabelFontFamily : "'Arial'", 3167 | 3168 | //String - Point label font weight 3169 | pointLabelFontStyle : "normal", 3170 | 3171 | //Number - Point label font size in pixels 3172 | pointLabelFontSize : 10, 3173 | 3174 | //String - Point label font colour 3175 | pointLabelFontColor : "#666", 3176 | 3177 | //Boolean - Whether to show a dot for each point 3178 | pointDot : true, 3179 | 3180 | //Number - Radius of each point dot in pixels 3181 | pointDotRadius : 3, 3182 | 3183 | //Number - Pixel width of point dot stroke 3184 | pointDotStrokeWidth : 1, 3185 | 3186 | //Number - amount extra to add to the radius to cater for hit detection outside the drawn point 3187 | pointHitDetectionRadius : 20, 3188 | 3189 | //Boolean - Whether to show a stroke for datasets 3190 | datasetStroke : true, 3191 | 3192 | //Number - Pixel width of dataset stroke 3193 | datasetStrokeWidth : 2, 3194 | 3195 | //Boolean - Whether to fill the dataset with a colour 3196 | datasetFill : true, 3197 | 3198 | //String - A legend template 3199 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 3200 | 3201 | }, 3202 | 3203 | initialize: function(data){ 3204 | this.PointClass = Chart.Point.extend({ 3205 | strokeWidth : this.options.pointDotStrokeWidth, 3206 | radius : this.options.pointDotRadius, 3207 | display: this.options.pointDot, 3208 | hitDetectionRadius : this.options.pointHitDetectionRadius, 3209 | ctx : this.chart.ctx 3210 | }); 3211 | 3212 | this.datasets = []; 3213 | 3214 | this.buildScale(data); 3215 | 3216 | //Set up tooltip events on the chart 3217 | if (this.options.showTooltips){ 3218 | helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ 3219 | var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; 3220 | 3221 | this.eachPoints(function(point){ 3222 | point.restore(['fillColor', 'strokeColor']); 3223 | }); 3224 | helpers.each(activePointsCollection, function(activePoint){ 3225 | activePoint.fillColor = activePoint.highlightFill; 3226 | activePoint.strokeColor = activePoint.highlightStroke; 3227 | }); 3228 | 3229 | this.showTooltip(activePointsCollection); 3230 | }); 3231 | } 3232 | 3233 | //Iterate through each of the datasets, and build this into a property of the chart 3234 | helpers.each(data.datasets,function(dataset){ 3235 | 3236 | var datasetObject = { 3237 | label: dataset.label || null, 3238 | fillColor : dataset.fillColor, 3239 | strokeColor : dataset.strokeColor, 3240 | pointColor : dataset.pointColor, 3241 | pointStrokeColor : dataset.pointStrokeColor, 3242 | points : [] 3243 | }; 3244 | 3245 | this.datasets.push(datasetObject); 3246 | 3247 | helpers.each(dataset.data,function(dataPoint,index){ 3248 | //Add a new point for each piece of data, passing any required data to draw. 3249 | var pointPosition; 3250 | if (!this.scale.animation){ 3251 | pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); 3252 | } 3253 | datasetObject.points.push(new this.PointClass({ 3254 | value : dataPoint, 3255 | label : data.labels[index], 3256 | datasetLabel: dataset.label, 3257 | x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, 3258 | y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, 3259 | strokeColor : dataset.pointStrokeColor, 3260 | fillColor : dataset.pointColor, 3261 | highlightFill : dataset.pointHighlightFill || dataset.pointColor, 3262 | highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor 3263 | })); 3264 | },this); 3265 | 3266 | },this); 3267 | 3268 | this.render(); 3269 | }, 3270 | eachPoints : function(callback){ 3271 | helpers.each(this.datasets,function(dataset){ 3272 | helpers.each(dataset.points,callback,this); 3273 | },this); 3274 | }, 3275 | 3276 | getPointsAtEvent : function(evt){ 3277 | var mousePosition = helpers.getRelativePosition(evt), 3278 | fromCenter = helpers.getAngleFromPoint({ 3279 | x: this.scale.xCenter, 3280 | y: this.scale.yCenter 3281 | }, mousePosition); 3282 | 3283 | var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, 3284 | pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), 3285 | activePointsCollection = []; 3286 | 3287 | // If we're at the top, make the pointIndex 0 to get the first of the array. 3288 | if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ 3289 | pointIndex = 0; 3290 | } 3291 | 3292 | if (fromCenter.distance <= this.scale.drawingArea){ 3293 | helpers.each(this.datasets, function(dataset){ 3294 | activePointsCollection.push(dataset.points[pointIndex]); 3295 | }); 3296 | } 3297 | 3298 | return activePointsCollection; 3299 | }, 3300 | 3301 | buildScale : function(data){ 3302 | this.scale = new Chart.RadialScale({ 3303 | display: this.options.showScale, 3304 | fontStyle: this.options.scaleFontStyle, 3305 | fontSize: this.options.scaleFontSize, 3306 | fontFamily: this.options.scaleFontFamily, 3307 | fontColor: this.options.scaleFontColor, 3308 | showLabels: this.options.scaleShowLabels, 3309 | showLabelBackdrop: this.options.scaleShowLabelBackdrop, 3310 | backdropColor: this.options.scaleBackdropColor, 3311 | backdropPaddingY : this.options.scaleBackdropPaddingY, 3312 | backdropPaddingX: this.options.scaleBackdropPaddingX, 3313 | lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, 3314 | lineColor: this.options.scaleLineColor, 3315 | angleLineColor : this.options.angleLineColor, 3316 | angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, 3317 | // Point labels at the edge of each line 3318 | pointLabelFontColor : this.options.pointLabelFontColor, 3319 | pointLabelFontSize : this.options.pointLabelFontSize, 3320 | pointLabelFontFamily : this.options.pointLabelFontFamily, 3321 | pointLabelFontStyle : this.options.pointLabelFontStyle, 3322 | height : this.chart.height, 3323 | width: this.chart.width, 3324 | xCenter: this.chart.width/2, 3325 | yCenter: this.chart.height/2, 3326 | ctx : this.chart.ctx, 3327 | templateString: this.options.scaleLabel, 3328 | labels: data.labels, 3329 | valuesCount: data.datasets[0].data.length 3330 | }); 3331 | 3332 | this.scale.setScaleSize(); 3333 | this.updateScaleRange(data.datasets); 3334 | this.scale.buildYLabels(); 3335 | }, 3336 | updateScaleRange: function(datasets){ 3337 | var valuesArray = (function(){ 3338 | var totalDataArray = []; 3339 | helpers.each(datasets,function(dataset){ 3340 | if (dataset.data){ 3341 | totalDataArray = totalDataArray.concat(dataset.data); 3342 | } 3343 | else { 3344 | helpers.each(dataset.points, function(point){ 3345 | totalDataArray.push(point.value); 3346 | }); 3347 | } 3348 | }); 3349 | return totalDataArray; 3350 | })(); 3351 | 3352 | 3353 | var scaleSizes = (this.options.scaleOverride) ? 3354 | { 3355 | steps: this.options.scaleSteps, 3356 | stepValue: this.options.scaleStepWidth, 3357 | min: this.options.scaleStartValue, 3358 | max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) 3359 | } : 3360 | helpers.calculateScaleRange( 3361 | valuesArray, 3362 | helpers.min([this.chart.width, this.chart.height])/2, 3363 | this.options.scaleFontSize, 3364 | this.options.scaleBeginAtZero, 3365 | this.options.scaleIntegersOnly 3366 | ); 3367 | 3368 | helpers.extend( 3369 | this.scale, 3370 | scaleSizes 3371 | ); 3372 | 3373 | }, 3374 | addData : function(valuesArray,label){ 3375 | //Map the values array for each of the datasets 3376 | this.scale.valuesCount++; 3377 | helpers.each(valuesArray,function(value,datasetIndex){ 3378 | var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); 3379 | this.datasets[datasetIndex].points.push(new this.PointClass({ 3380 | value : value, 3381 | label : label, 3382 | x: pointPosition.x, 3383 | y: pointPosition.y, 3384 | strokeColor : this.datasets[datasetIndex].pointStrokeColor, 3385 | fillColor : this.datasets[datasetIndex].pointColor 3386 | })); 3387 | },this); 3388 | 3389 | this.scale.labels.push(label); 3390 | 3391 | this.reflow(); 3392 | 3393 | this.update(); 3394 | }, 3395 | removeData : function(){ 3396 | this.scale.valuesCount--; 3397 | this.scale.labels.shift(); 3398 | helpers.each(this.datasets,function(dataset){ 3399 | dataset.points.shift(); 3400 | },this); 3401 | this.reflow(); 3402 | this.update(); 3403 | }, 3404 | update : function(){ 3405 | this.eachPoints(function(point){ 3406 | point.save(); 3407 | }); 3408 | this.reflow(); 3409 | this.render(); 3410 | }, 3411 | reflow: function(){ 3412 | helpers.extend(this.scale, { 3413 | width : this.chart.width, 3414 | height: this.chart.height, 3415 | size : helpers.min([this.chart.width, this.chart.height]), 3416 | xCenter: this.chart.width/2, 3417 | yCenter: this.chart.height/2 3418 | }); 3419 | this.updateScaleRange(this.datasets); 3420 | this.scale.setScaleSize(); 3421 | this.scale.buildYLabels(); 3422 | }, 3423 | draw : function(ease){ 3424 | var easeDecimal = ease || 1, 3425 | ctx = this.chart.ctx; 3426 | this.clear(); 3427 | this.scale.draw(); 3428 | 3429 | helpers.each(this.datasets,function(dataset){ 3430 | 3431 | //Transition each point first so that the line and point drawing isn't out of sync 3432 | helpers.each(dataset.points,function(point,index){ 3433 | if (point.hasValue()){ 3434 | point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); 3435 | } 3436 | },this); 3437 | 3438 | 3439 | 3440 | //Draw the line between all the points 3441 | ctx.lineWidth = this.options.datasetStrokeWidth; 3442 | ctx.strokeStyle = dataset.strokeColor; 3443 | ctx.beginPath(); 3444 | helpers.each(dataset.points,function(point,index){ 3445 | if (index === 0){ 3446 | ctx.moveTo(point.x,point.y); 3447 | } 3448 | else{ 3449 | ctx.lineTo(point.x,point.y); 3450 | } 3451 | },this); 3452 | ctx.closePath(); 3453 | ctx.stroke(); 3454 | 3455 | ctx.fillStyle = dataset.fillColor; 3456 | ctx.fill(); 3457 | 3458 | //Now draw the points over the line 3459 | //A little inefficient double looping, but better than the line 3460 | //lagging behind the point positions 3461 | helpers.each(dataset.points,function(point){ 3462 | if (point.hasValue()){ 3463 | point.draw(); 3464 | } 3465 | }); 3466 | 3467 | },this); 3468 | 3469 | } 3470 | 3471 | }); 3472 | 3473 | 3474 | 3475 | 3476 | 3477 | }).call(this); --------------------------------------------------------------------------------