├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .prettierignore ├── .release-it.json ├── LICENSE ├── README.md ├── RELEASE.md ├── jest.config.js ├── package.json ├── specs ├── lib │ └── template_srv_stub.ts ├── panel.jest.ts ├── res │ ├── datasource │ │ └── series_random_walk.json │ └── panel_json_v004.json ├── series.jest.ts └── typing.d.ts ├── src ├── SeriesWrapper.ts ├── anno.ts ├── editor.ts ├── img │ ├── plotly_logo.svg │ ├── screenshot-multiple-trace.png │ ├── screenshot-options-new.png │ ├── screenshot-options.png │ ├── screenshot-scatter-1.png │ ├── screenshot-scatter-3d.png │ ├── screenshot-scatter.png │ └── screenshot-single-trace.png ├── lib │ ├── plotly-cartesian.min.js │ └── plotly.min.js ├── libLoader.ts ├── module.ts ├── partials │ ├── module.html │ ├── tab_display.html │ └── tab_traces.html ├── plugin.json └── typings.d.ts ├── tsconfig.jest.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-modules-commonjs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: circleci/node:8 7 | 8 | working_directory: ~/repo 9 | 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: yarn install 16 | command: 'yarn install --pure-lockfile --no-progress' 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "yarn.lock" }} 19 | paths: 20 | - node_modules 21 | - run: 22 | name: test 23 | command: 'yarn test' 24 | - run: 25 | name: build 26 | command: 'yarn build' 27 | - run: 28 | name: zip dist 29 | command: 'cd dist && zip -r9 ../grafana-plotly-panel-dev.zip *' 30 | - store_artifacts: 31 | path: grafana-plotly-panel-dev.zip 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | 4 | .tscache 5 | 6 | dist/ 7 | dist-*/ 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "assets": ["dist/*.zip"] 5 | }, 6 | "git": { 7 | "tagName": "v${version}", 8 | "requireCleanWorkingDir": true 9 | }, 10 | "scripts": { 11 | "afterBump": "git checkout -b build-${version}; yarn build; git add --verbose --force dist/;", 12 | "beforeStage": "cd dist; zip -r natel-plotly-panel-${version}.zip *", 13 | "afterRelease": "echo Successfully released ${name} v${version} to ${repo.repository}." 14 | }, 15 | "npm": { 16 | "publish": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Natel Energy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Plot.ly Panel for Grafana 2 | 3 | [![CircleCI](https://circleci.com/gh/NatelEnergy/grafana-plotly-panel/tree/master.svg?style=svg)](https://circleci.com/gh/NatelEnergy/grafana-plotly-panel/tree/master) 4 | [![dependencies Status](https://david-dm.org/NatelEnergy/grafana-plotly-panel/status.svg)](https://david-dm.org/NatelEnergy/grafana-plotly-panel) 5 | [![devDependencies Status](https://david-dm.org/NatelEnergy/grafana-plotly-panel/dev-status.svg)](https://david-dm.org/NatelEnergy/grafana-plotly-panel?type=dev) 6 | 7 | Render metrics using the plot.ly javascript framework 8 | 9 | Works with grafana 4, 5, and 6 10 | 11 | ### Screenshots 12 | 13 | ![Screenshot of scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-scatter.png) 14 | ![Screenshot of scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-single-trace.png?raw=true) 15 | ![Screenshot of scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-multiple-trace.png) 16 | ![Screenshot of 3d scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-scatter-3d.png) 17 | ![Screenshot of the options screen](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-options-new.png) 18 | 19 | ### Building 20 | 21 | To complie, run: 22 | 23 | ``` 24 | npm install -g yarn 25 | yarn install --pure-lockfile 26 | yarn build 27 | ``` 28 | 29 | ### Releasing 30 | 31 | This plugin uses [release-it](https://github.com/webpro/release-it) to release to GitHub. 32 | 33 | ``` 34 | env GITHUB_TOKEN=your_token yarn release-it patch 35 | ``` 36 | 37 | 38 | #### Changelog 39 | 40 | ##### v0.0.6 41 | 42 | - Fix axis range configuration bug [#49](https://github.com/NatelEnergy/grafana-plotly-panel/issues/49) 43 | - Add basic annotations support #57 (tchernobog) 44 | - Improve loading times for plotly.js and support loading from CDN 45 | - Assume date x-axis when 'auto' and the mapping has 'time' 46 | - Support Fixed-Ratio Axes 47 | - Tested with Grafana 6 48 | 49 | 50 | ##### v0.0.5 51 | 52 | - Upgrade plotly (v1.41+) 53 | - Better support for light theme. (#24, @cscheuermann81) 54 | - Support snapshots 55 | - Removing `dist` from master branch 56 | - Support of multiple time series's ([#9](https://github.com/NatelEnergy/grafana-plotly-panel/issues/9), [CorpGlory DevTeam](https://corpglory.com/)) 57 | - Support showing text from query (#11) 58 | - Template variable support 59 | - Improved metric mapping 60 | - Using webpack and basic jest tests 61 | 62 | ##### v0.0.4 63 | 64 | - Load plotly from npm (v1.31.2+) 65 | - Convert to TypeScript 66 | - Reasonable behavior when adding single metric 67 | - Formatting with prettier.js 68 | - Support for a single table query 69 | 70 | ##### v0.0.3 71 | 72 | - Improve options UI 73 | - Added range mode: "tozero" and "nonnegative" 74 | - Map metrics to X,Y,Z and color 75 | - Can now select 'date' type for each axis to support time 76 | - basic support to size marker with data 77 | 78 | ##### v0.0.2 79 | 80 | - Added ability to set color from a metric query. (#4, @lzgrablic01) 81 | - Show 3D axis names properly 82 | - Fix initalization to work with 4.2+ (isPanelVisible undefined) 83 | 84 | ##### v0.0.1 85 | 86 | - First working version 87 | 88 | ### Wishlist (help wanted) 89 | 90 | - sizeref helper. I think this depends on the data. likely need to find the range and pick a good value? From react? 91 | - nice to have: https://plot.ly/javascript/parallel-coordinates-plot/ 92 | 93 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | filling in the history: 2 | 3 | ``` 4 | git tag -a v0.0.1 187e7b5743f0dceb1034b03eaac62989af50d0c0 -m "Release v0.0.1"; 5 | git tag -a v0.0.2 68947cf1d73b7bcceceebaf795c60a7b76eaec87 -m "Release v0.0.2"; 6 | git tag -a v0.0.3 371cbf41006a534a671a9582a3e24ce3cf29d9b1 -m "Release v0.0.3"; 7 | git tag -a v0.0.4 df51dc99e63c781f2ddc9590b7f106e40648738d -m "Release v0.0.4"; 8 | git tag -a v0.0.5 fd20e71fc45a59475fb2f1fdde687f93978bde18 -m "Release v0.0.5"; 9 | git push --tags 10 | ``` 11 | 12 | Remove all old releases: 13 | 14 | ``` 15 | #Delete local tags. 16 | git tag -d $(git tag -l) 17 | #Fetch remote tags. 18 | git fetch 19 | #Delete remote tags. 20 | git push origin --delete $(git tag -l) # Pushing once should be faster than multiple times 21 | #Delete local tags. 22 | git tag -d $(git tag -l) 23 | ``` 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | globals: { 4 | 'ts-jest': { 5 | useBabelrc: true, 6 | tsConfigFile: 'tsconfig.jest.json', 7 | }, 8 | }, 9 | moduleNameMapper: { 10 | 'app/core/utils/datemath': 11 | '/node_modules/grafana-sdk-mocks/app/core/utils/datemath.ts', 12 | 'app/core/utils/kbn': '/src/__mocks__/kbn.ts', 13 | 'app/plugins/sdk': '/node_modules/grafana-sdk-mocks/app/plugins/sdk.ts', 14 | }, 15 | transformIgnorePatterns: ['/node_modules/(?!grafana-sdk-mocks)'], 16 | transform: { 17 | '.(ts|tsx)': '/node_modules/ts-jest/preprocessor.js', 18 | }, 19 | testRegex: '(\\.|/)([jt]est)\\.ts$', 20 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "natel-plotly-panel", 3 | "version": "0.0.7-dev", 4 | "description": "Plot.ly Panel Plugin for Grafana", 5 | "scripts": { 6 | "build": "webpack --config webpack.config.prod.js", 7 | "dev": "webpack --mode development", 8 | "watch": "webpack --mode development --watch", 9 | "test": "jest --config jest.config.js", 10 | "precommit": "pretty-quick --staged", 11 | "lint": "tslint -c tslint.json --project tsconfig.json", 12 | "format": "prettier-eslint --write \"src/**/*.{ts,tsx,json,css,js,jsx}\"", 13 | "zip": "yarn build && rm -f ../grafana-plotly-panel.zip && zip -r ../grafana-plotly-panel.zip dist" 14 | }, 15 | "author": "ryantxu", 16 | "license": "MIT", 17 | "keywords": [ 18 | "plotly", 19 | "scatter", 20 | "grafana", 21 | "plugin", 22 | "panel" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/NatelEnergy/grafana-plotly-panel.git" 27 | }, 28 | "lint-staged": { 29 | "src/**/*.{ts,tsx,json,css,js,jsx}": [ 30 | "prettier-eslint" 31 | ] 32 | }, 33 | "prettier": { 34 | "trailingComma": "es5", 35 | "singleQuote": true, 36 | "bracketSpacing": false, 37 | "printWidth": 100 38 | }, 39 | "dependencies": { 40 | "jquery": "^3.2.1", 41 | "lodash": "^4.17.10", 42 | "moment": "^2.22.1", 43 | "plotly.js": "^1.41", 44 | "scriptjs": "^2.5.9" 45 | }, 46 | "devDependencies": { 47 | "@types/jest": "^24.0.0", 48 | "@types/lodash": "^4.14.74", 49 | "@types/plotly.js": "^1.38.0", 50 | "@types/scriptjs": "^0.0.2", 51 | "babel-core": "^6.26.3", 52 | "babel-jest": "^23.0.1", 53 | "babel-loader": "^7.1.4", 54 | "babel-preset-env": "^1.7.0", 55 | "clean-webpack-plugin": "^1.0.1", 56 | "copy-webpack-plugin": "^4.5.1", 57 | "css-loader": "^2.1.0", 58 | "grafana-sdk-mocks": "github:grafana/grafana-sdk-mocks", 59 | "jest": "^24.1.0", 60 | "ng-annotate-webpack-plugin": "^0.3.0", 61 | "prettier": "^1.15.3", 62 | "prettier-eslint": "^8.8.0", 63 | "prettier-eslint-cli": "^4.7.0", 64 | "pretty-quick": "^1.10.0", 65 | "release-it": "^10", 66 | "replace-in-file-webpack-plugin": "^1.0.6", 67 | "style-loader": "^0.23.1", 68 | "ts-jest": "^23", 69 | "ts-loader": "^5.3.3", 70 | "typescript": "^3", 71 | "uglifyjs-webpack-plugin": "^2.1.1", 72 | "webpack": "^4.9.1", 73 | "webpack-cli": "^3.1" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /specs/lib/template_srv_stub.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default class TemplateSrvStub { 4 | variables = []; 5 | templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g}; 6 | data = {}; 7 | 8 | replace(text) { 9 | return _.template(text, this.templateSettings)(this.data); 10 | } 11 | 12 | getAdhocFilters() { 13 | return []; 14 | } 15 | 16 | variableExists() { 17 | return false; 18 | } 19 | 20 | highlightVariablesAsHtml(str) { 21 | return str; 22 | } 23 | 24 | setGrafanaVariable(name, value) { 25 | this.data[name] = value; 26 | } 27 | 28 | init() {} 29 | fillVariableValuesForUrl() {} 30 | updateTemplateData() {} 31 | } 32 | -------------------------------------------------------------------------------- /specs/panel.jest.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | //import TemplateSrv from './lib/template_srv_stub'; 4 | import {PlotlyPanelCtrl} from '../src/module'; 5 | 6 | import * as panel_json_v004 from './res/panel_json_v004.json'; 7 | //import {MetricsPanelCtrl} from 'app/plugins/sdk'; 8 | 9 | describe('Plotly Panel', () => { 10 | const injector = { 11 | get: () => { 12 | return { 13 | timeRange: () => { 14 | return { 15 | from: '', 16 | to: '', 17 | }; 18 | }, 19 | }; 20 | }, 21 | }; 22 | 23 | const scope = { 24 | $on: () => {}, 25 | }; 26 | 27 | PlotlyPanelCtrl.prototype.panel = { 28 | events: { 29 | on: () => {}, 30 | }, 31 | gridPos: { 32 | w: 100, 33 | }, 34 | }; 35 | 36 | const ctx = {}; 37 | beforeEach(() => { 38 | ctx.ctrl = new PlotlyPanelCtrl(scope, injector, null, null, null, null); 39 | ctx.ctrl.events = { 40 | emit: () => {}, 41 | }; 42 | ctx.ctrl.annotationsPromise = Promise.resolve({}); 43 | ctx.ctrl.updateTimeRange(); 44 | }); 45 | 46 | const epoch = 1505800000000; 47 | Date.now = () => epoch; 48 | 49 | describe('check Defaults', () => { 50 | beforeEach(() => { 51 | // nothing specal 52 | }); 53 | 54 | it('it should use default configs', () => { 55 | // console.log('SAME:', ctx.ctrl.panel.pconfig); 56 | // console.log(' >>>:', PlotlyPanelCtrl.defaults.pconfig); 57 | expect(JSON.stringify(ctx.ctrl.panel.pconfig)).toBe( 58 | JSON.stringify(PlotlyPanelCtrl.defaults.pconfig) 59 | ); 60 | }); 61 | }); 62 | 63 | describe('check migration from 0.0.4', () => { 64 | beforeEach(() => { 65 | ctx.ctrl.panel = panel_json_v004; 66 | ctx.ctrl.onPanelInitialized(); 67 | }); 68 | 69 | it('it should now have have a version', () => { 70 | expect(ctx.ctrl.panel.version).toBe(PlotlyPanelCtrl.configVersion); 71 | expect(ctx.ctrl.cfg.layout.margin).toBeUndefined(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /specs/res/datasource/series_random_walk.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "A": { 4 | "refId": "A", 5 | "series": [ 6 | { 7 | "name": "A-series", 8 | "points": [ 9 | [14.679051503914001, 1535475801896], 10 | [14.845709466741054, 1535475831896], 11 | [14.484874540260003, 1535475861896], 12 | [14.248895465988483, 1535475891896], 13 | [14.18955009829011, 1535475921896], 14 | [14.438450892024001, 1535475951896], 15 | [14.16239717353598, 1535475981896], 16 | [14.258808168639952, 1535476011896], 17 | [14.284544558408594, 1535476041896], 18 | [13.823092887424146, 1535476071896], 19 | [14.139839754136894, 1535476101896], 20 | [13.641754937673849, 1535476131896], 21 | [13.735586127843803, 1535476161896], 22 | [14.149768033070723, 1535476191896], 23 | [14.615270931528274, 1535476221896], 24 | [14.478241819299226, 1535476251896], 25 | [14.949917560584625, 1535476281896], 26 | [15.03709914807237, 1535476311896], 27 | [14.624937550277002, 1535476341896], 28 | [14.253974579142746, 1535476371896], 29 | [13.83330203493456, 1535476401896], 30 | [13.635943885697237, 1535476431896], 31 | [13.628917666585885, 1535476461896], 32 | [13.887390116727053, 1535476491896], 33 | [13.512566616383094, 1535476521896], 34 | [13.547128371819227, 1535476551896], 35 | [14.036121762637451, 1535476581896], 36 | [14.021701453726033, 1535476611896], 37 | [14.093256544203959, 1535476641896], 38 | [14.498276728368625, 1535476671896], 39 | [14.349577918952306, 1535476701896], 40 | [14.253785179075688, 1535476731896], 41 | [14.439114793694168, 1535476761896], 42 | [14.67520143377697, 1535476791896], 43 | [14.187245640990787, 1535476821896], 44 | [13.698099685568577, 1535476851896], 45 | [13.548229746070621, 1535476881896], 46 | [14.023189804630853, 1535476911896], 47 | [13.875170505191415, 1535476941896], 48 | [14.2100406613045, 1535476971896], 49 | [14.529515055474915, 1535477001896], 50 | [14.784137482434062, 1535477031896], 51 | [14.638765029738096, 1535477061896], 52 | [14.624808546387117, 1535477091896], 53 | [14.600439786525559, 1535477121896], 54 | [14.593622234019074, 1535477151896], 55 | [14.396728633172664, 1535477181896], 56 | [14.724232498856185, 1535477211896], 57 | [15.002845988571316, 1535477241896], 58 | [14.910594982730714, 1535477271896], 59 | [15.341707560490681, 1535477301896], 60 | [14.864422092984013, 1535477331896], 61 | [15.168849516840945, 1535477361896], 62 | [15.139401626414806, 1535477391896], 63 | [14.687048987157915, 1535477421896], 64 | [14.560769151840807, 1535477451896], 65 | [14.630605588950699, 1535477481896], 66 | [14.363020077838078, 1535477511896], 67 | [13.996619445399054, 1535477541896], 68 | [14.168260372431579, 1535477571896], 69 | [14.517286065668902, 1535477601896], 70 | [14.336759588224263, 1535477631896], 71 | [14.31617936915951, 1535477661896], 72 | [14.609089006575452, 1535477691896], 73 | [14.825376539044283, 1535477721896], 74 | [14.878891555510403, 1535477751896], 75 | [15.252162102553424, 1535477781896], 76 | [15.012738669386756, 1535477811896], 77 | [14.735904168598424, 1535477841896], 78 | [14.650704853731302, 1535477871896], 79 | [14.359635133986975, 1535477901896], 80 | [14.305421943774457, 1535477931896], 81 | [14.083856895669376, 1535477961896], 82 | [13.883093590789619, 1535477991896], 83 | [13.918390244271967, 1535478021896], 84 | [14.328270276007173, 1535478051896], 85 | [14.236697052099036, 1535478081896], 86 | [14.434285634861117, 1535478111896], 87 | [14.234025334997112, 1535478141896], 88 | [14.729029045247465, 1535478171896], 89 | [14.724985313832192, 1535478201896], 90 | [14.333969226892483, 1535478231896], 91 | [14.202696015599962, 1535478261896], 92 | [14.077612548428572, 1535478291896], 93 | [13.722770968751576, 1535478321896], 94 | [13.752904011166372, 1535478351896], 95 | [13.632245564503211, 1535478381896], 96 | [13.44881963232576, 1535478411896], 97 | [12.986513800031814, 1535478441896], 98 | [12.645454036402306, 1535478471896], 99 | [12.15834487704405, 1535478501896], 100 | [12.22149490822588, 1535478531896], 101 | [11.84767335311449, 1535478561896], 102 | [11.904674391594682, 1535478591896], 103 | [11.589040837192918, 1535478621896], 104 | [11.123416124435579, 1535478651896], 105 | [11.62246158165935, 1535478681896], 106 | [11.582038428383537, 1535478711896], 107 | [11.422421650109328, 1535478741896], 108 | [11.269860909348411, 1535478771896], 109 | [11.177596194893255, 1535478801896], 110 | [11.29598537516654, 1535478831896], 111 | [11.654450507899519, 1535478861896], 112 | [11.552668821324609, 1535478891896], 113 | [11.728487455397918, 1535478921896], 114 | [11.543230979820922, 1535478951896], 115 | [11.508805081021462, 1535478981896], 116 | [11.598116528587873, 1535479011896], 117 | [11.682951329547274, 1535479041896], 118 | [11.788509893913993, 1535479071896], 119 | [11.393580831705586, 1535479101896], 120 | [11.733698788404864, 1535479131896], 121 | [11.36370346887467, 1535479161896], 122 | [11.317723554567497, 1535479191896], 123 | [11.649235680243832, 1535479221896], 124 | [11.884031315639808, 1535479251896], 125 | [12.119907993945498, 1535479281896], 126 | [12.3749367381797, 1535479311896], 127 | [12.697847599232572, 1535479341896], 128 | [12.319255716351954, 1535479371896], 129 | [12.481954467663856, 1535479401896], 130 | [12.257986779557145, 1535479431896], 131 | [12.510566966775682, 1535479461896], 132 | [12.625537423757008, 1535479491896], 133 | [12.309247575545676, 1535479521896], 134 | [12.58008742135853, 1535479551896], 135 | [12.15812627935491, 1535479581896], 136 | [12.20243230861165, 1535479611896], 137 | [11.931076755441074, 1535479641896], 138 | [11.454090817888872, 1535479671896], 139 | [11.556513017179524, 1535479701896], 140 | [11.582868662869716, 1535479731896], 141 | [11.22396971088495, 1535479761896], 142 | [11.44681439150322, 1535479791896], 143 | [11.436083204638312, 1535479821896], 144 | [11.188823881540134, 1535479851896], 145 | [11.331205614896357, 1535479881896], 146 | [11.323455177635715, 1535479911896], 147 | [11.007356625761618, 1535479941896], 148 | [10.756404271929355, 1535479971896], 149 | [10.942847074264039, 1535480001896], 150 | [11.320617739552535, 1535480031896], 151 | [11.334998743154989, 1535480061896], 152 | [11.510483765631301, 1535480091896], 153 | [11.05576559777657, 1535480121896], 154 | [11.108134281322966, 1535480151896], 155 | [10.713709365058108, 1535480181896], 156 | [10.239835594518206, 1535480211896], 157 | [10.242702600013182, 1535480241896], 158 | [10.440488069332243, 1535480271896], 159 | [10.82477188231116, 1535480301896], 160 | [10.395774146443793, 1535480331896], 161 | [10.176723942007333, 1535480361896], 162 | [9.93161351923747, 1535480391896], 163 | [10.002800104429115, 1535480421896], 164 | [10.464210415410694, 1535480451896], 165 | [10.369992379776988, 1535480481896], 166 | [10.284720944190756, 1535480511896], 167 | [9.964638258443822, 1535480541896], 168 | [10.453423159205915, 1535480571896], 169 | [10.570370009589173, 1535480601896], 170 | [10.432081708329035, 1535480631896], 171 | [10.827560080545757, 1535480661896], 172 | [10.349546454122565, 1535480691896], 173 | [10.52338831393329, 1535480721896], 174 | [10.463505864107388, 1535480751896], 175 | [10.092041742439225, 1535480781896], 176 | [9.64279748372973, 1535480811896], 177 | [9.151882737989641, 1535480841896], 178 | [9.314213338125192, 1535480871896], 179 | [9.731752222691847, 1535480901896], 180 | [10.09165419680245, 1535480931896], 181 | [10.147034265260539, 1535480961896], 182 | [10.503885872481437, 1535480991896], 183 | [10.025039112054833, 1535481021896], 184 | [10.503835754839685, 1535481051896], 185 | [10.959405647445955, 1535481081896], 186 | [11.270269208565253, 1535481111896], 187 | [10.883535664845551, 1535481141896], 188 | [10.876718754642038, 1535481171896], 189 | [10.549609435173457, 1535481201896], 190 | [10.313140048745286, 1535481231896], 191 | [10.166972053619004, 1535481261896], 192 | [10.106067309872154, 1535481291896], 193 | [9.934700821056616, 1535481321896], 194 | [10.08625952824886, 1535481351896], 195 | [10.521263133276076, 1535481381896], 196 | [10.143131586007035, 1535481411896], 197 | [10.543348323444901, 1535481441896], 198 | [10.536514182616244, 1535481471896], 199 | [10.3235122381541, 1535481501896], 200 | [10.807316907966914, 1535481531896], 201 | [10.977090673768526, 1535481561896], 202 | [11.328480035585827, 1535481591896], 203 | [11.379466422690172, 1535481621896], 204 | [11.867684798836393, 1535481651896], 205 | [11.849785205023842, 1535481681896], 206 | [12.13525306463728, 1535481711896], 207 | [11.85558473500463, 1535481741896], 208 | [11.580036683113638, 1535481771896], 209 | [11.707266948348105, 1535481801896], 210 | [11.873363212075297, 1535481831896], 211 | [11.892201359057117, 1535481861896], 212 | [11.537366967890199, 1535481891896], 213 | [11.475776197605834, 1535481921896], 214 | [11.358134947338318, 1535481951896], 215 | [11.727724037413426, 1535481981896], 216 | [11.60185959977391, 1535482011896], 217 | [11.347076035675027, 1535482041896], 218 | [11.117199515208776, 1535482071896], 219 | [11.5287843109563, 1535482101896], 220 | [11.50969076994431, 1535482131896], 221 | [11.910029927257577, 1535482161896], 222 | [12.276295334299244, 1535482191896], 223 | [12.393705189466221, 1535482221896], 224 | [12.009989565497625, 1535482251896], 225 | [12.260713120225303, 1535482281896], 226 | [12.686961787657285, 1535482311896], 227 | [12.714117336023934, 1535482341896], 228 | [12.603293006786794, 1535482371896], 229 | [12.481719325580611, 1535482401896], 230 | [12.700584265399682, 1535482431896], 231 | [12.401579435906159, 1535482461896], 232 | [12.3975934594582, 1535482491896], 233 | [12.73208876801739, 1535482521896], 234 | [12.535586898159767, 1535482551896], 235 | [12.468318730061815, 1535482581896], 236 | [12.42062028601894, 1535482611896], 237 | [12.461959645677723, 1535482641896], 238 | [11.96746079917966, 1535482671896], 239 | [12.336725708990103, 1535482701896], 240 | [12.180231967766613, 1535482731896], 241 | [11.873165552777941, 1535482761896], 242 | [11.397955319318797, 1535482791896], 243 | [11.412825796043895, 1535482821896], 244 | [11.86127221055579, 1535482851896], 245 | [12.235003443113024, 1535482881896], 246 | [11.771248829069005, 1535482911896], 247 | [12.254296554732157, 1535482941896], 248 | [11.813046303971841, 1535482971896], 249 | [11.771240775763328, 1535483001896], 250 | [11.58739228857323, 1535483031896], 251 | [11.626746120815326, 1535483061896], 252 | [11.362972986872645, 1535483091896], 253 | [11.427696232044983, 1535483121896], 254 | [10.961483300252238, 1535483151896], 255 | [11.018888615692875, 1535483181896], 256 | [10.698778469504179, 1535483211896], 257 | [10.851379070680215, 1535483241896], 258 | [11.095888773646383, 1535483271896], 259 | [11.31724818612564, 1535483301896], 260 | [11.074548029986012, 1535483331896], 261 | [11.296292919895983, 1535483361896], 262 | [11.620271829803858, 1535483391896], 263 | [11.940851819828291, 1535483421896], 264 | [12.000152382681286, 1535483451896], 265 | [12.23518133657819, 1535483481896], 266 | [12.038466370384187, 1535483511896], 267 | [11.954963478441657, 1535483541896], 268 | [11.632378279669677, 1535483571896], 269 | [11.411659182646876, 1535483601896], 270 | [11.531720266944063, 1535483631896], 271 | [11.766553067444178, 1535483661896], 272 | [11.931825360292393, 1535483691896], 273 | [12.297086256806347, 1535483721896], 274 | [12.300374190270782, 1535483751896], 275 | [12.0867361465612, 1535483781896], 276 | [11.758814985255706, 1535483811896], 277 | [12.04733924300335, 1535483841896], 278 | [11.556633163852796, 1535483871896], 279 | [11.734948191374219, 1535483901896], 280 | [11.928263136856653, 1535483931896], 281 | [12.087555964536179, 1535483961896], 282 | [12.160646883140071, 1535483991896], 283 | [11.7118154671992, 1535484021896], 284 | [11.599898453299353, 1535484051896], 285 | [11.348622310720605, 1535484081896], 286 | [11.405308793393786, 1535484111896], 287 | [11.582533838297998, 1535484141896], 288 | [11.654652106249534, 1535484171896], 289 | [11.177530743777472, 1535484201896], 290 | [10.743182743512158, 1535484231896], 291 | [10.721457244572289, 1535484261896], 292 | [10.546862850579082, 1535484291896], 293 | [10.403214010277257, 1535484321896], 294 | [10.586930953825412, 1535484351896], 295 | [10.516953164300526, 1535484381896], 296 | [10.347071183633465, 1535484411896], 297 | [10.202694029784443, 1535484441896], 298 | [10.000394519618974, 1535484471896], 299 | [10.132319888295099, 1535484501896], 300 | [9.920955002254248, 1535484531896], 301 | [10.194930773181486, 1535484561896], 302 | [9.930638892117692, 1535484591896], 303 | [10.394810271688975, 1535484621896], 304 | [9.916574083780972, 1535484651896], 305 | [10.300942030866796, 1535484681896], 306 | [9.963024251928319, 1535484711896], 307 | [9.718904047971868, 1535484741896], 308 | [9.266255992778332, 1535484771896], 309 | [9.278394110579494, 1535484801896], 310 | [8.778396555478768, 1535484831896], 311 | [8.809913202464424, 1535484861896], 312 | [8.5132057792405, 1535484891896], 313 | [8.447044144161367, 1535484921896], 314 | [8.682051558051299, 1535484951896], 315 | [8.32327454793814, 1535484981896], 316 | [8.570580021521366, 1535485011896], 317 | [8.801461231127426, 1535485041896], 318 | [9.250843575180685, 1535485071896], 319 | [9.311656303056417, 1535485101896], 320 | [9.277733599845355, 1535485131896], 321 | [9.254033597246499, 1535485161896], 322 | [9.418485195069165, 1535485191896], 323 | [8.929945157647106, 1535485221896], 324 | [9.024837834168565, 1535485251896], 325 | [9.001906964394134, 1535485281896], 326 | [9.450057045513852, 1535485311896], 327 | [9.853886241030983, 1535485341896], 328 | [9.979809250932131, 1535485371896], 329 | [9.913525623546112, 1535485401896], 330 | [9.613455527368782, 1535485431896], 331 | [10.078916040699148, 1535485461896], 332 | [10.448928922277345, 1535485491896], 333 | [9.970222575526385, 1535485521896], 334 | [9.995057135108409, 1535485551896], 335 | [9.700597253566364, 1535485581896], 336 | [9.89870185315159, 1535485611896], 337 | [9.895239301902151, 1535485641896], 338 | [9.639186153269383, 1535485671896], 339 | [10.099297906062489, 1535485701896], 340 | [10.245974462471148, 1535485731896], 341 | [10.639011801705397, 1535485761896], 342 | [11.02717824451871, 1535485791896], 343 | [10.661053230098187, 1535485821896], 344 | [10.57148962811343, 1535485851896], 345 | [10.614572219544353, 1535485881896], 346 | [10.775108945975054, 1535485911896], 347 | [10.490787520113244, 1535485941896], 348 | [10.965476855139805, 1535485971896], 349 | [11.280423052431534, 1535486001896], 350 | [11.672001742246083, 1535486031896], 351 | [11.782400653916683, 1535486061896], 352 | [11.70868674791111, 1535486091896], 353 | [12.064648777812659, 1535486121896], 354 | [12.398360712958526, 1535486151896], 355 | [11.905490154467921, 1535486181896], 356 | [11.765730539417389, 1535486211896], 357 | [12.225203570426238, 1535486241896], 358 | [12.200955993460814, 1535486271896], 359 | [12.362065970807091, 1535486301896], 360 | [12.453716382053907, 1535486331896], 361 | [12.32850764889958, 1535486361896], 362 | [12.098173339511181, 1535486391896], 363 | [12.021277641159562, 1535486421896], 364 | [12.313095853580508, 1535486451896], 365 | [12.009182020635045, 1535486481896], 366 | [11.603174698312102, 1535486511896], 367 | [11.216162716296092, 1535486541896], 368 | [10.767793012003265, 1535486571896], 369 | [10.773640498246923, 1535486601896], 370 | [11.093216181491213, 1535486631896], 371 | [10.933824031389415, 1535486661896], 372 | [10.659928038725857, 1535486691896], 373 | [10.225636636240308, 1535486721896], 374 | [10.038874342947427, 1535486751896], 375 | [9.642369348463657, 1535486781896], 376 | [9.768446996159387, 1535486811896], 377 | [9.365125314798894, 1535486841896], 378 | [9.442337514747823, 1535486871896], 379 | [9.110842420834757, 1535486901896], 380 | [9.126116661941865, 1535486931896], 381 | [9.021747424000464, 1535486961896], 382 | [8.602737957550339, 1535486991896], 383 | [8.853154096809737, 1535487021896], 384 | [8.765450678689236, 1535487051896], 385 | [8.77817071459079, 1535487081896], 386 | [9.080093523104225, 1535487111896], 387 | [8.9453581481472, 1535487141896], 388 | [8.596742663263685, 1535487171896], 389 | [8.519480311180086, 1535487201896], 390 | [8.586467245889146, 1535487231896], 391 | [8.77284427747904, 1535487261896], 392 | [8.766585278569138, 1535487291896], 393 | [9.066785328926015, 1535487321896], 394 | [8.578825409662468, 1535487351896], 395 | [8.686501240010058, 1535487381896], 396 | [8.749950190513093, 1535487411896], 397 | [8.909768671442851, 1535487441896], 398 | [8.651753391703911, 1535487471896], 399 | [8.261242314341018, 1535487501896], 400 | [8.066385914078097, 1535487531896], 401 | [7.7511889287401665, 1535487561896], 402 | [8.164449213055276, 1535487591896], 403 | [7.794224041982624, 1535487621896], 404 | [8.008993476035883, 1535487651896], 405 | [7.830857416067071, 1535487681896], 406 | [7.700656566825358, 1535487711896], 407 | [7.818357504388807, 1535487741896], 408 | [7.645470053281848, 1535487771896], 409 | [7.483096769139694, 1535487801896], 410 | [7.325691561919383, 1535487831896], 411 | [7.112016329453184, 1535487861896], 412 | [7.534182777061737, 1535487891896], 413 | [7.99723324802668, 1535487921896], 414 | [7.572993053063963, 1535487951896], 415 | [7.229869498278656, 1535487981896], 416 | [6.765010049995112, 1535488011896], 417 | [6.579645326412668, 1535488041896], 418 | [6.272655966680149, 1535488071896], 419 | [6.685244957026752, 1535488101896], 420 | [6.7694210326663615, 1535488131896], 421 | [6.6001950160029885, 1535488161896], 422 | [6.880366142404086, 1535488191896], 423 | [6.988219116083355, 1535488221896], 424 | [7.170381720541225, 1535488251896], 425 | [6.852078046029824, 1535488281896], 426 | [6.733589679441536, 1535488311896], 427 | [6.422922705573241, 1535488341896], 428 | [6.277292669056109, 1535488371896], 429 | [6.105108714977855, 1535488401896], 430 | [6.144085159621269, 1535488431896], 431 | [6.419984962619861, 1535488461896], 432 | [5.928218706934628, 1535488491896], 433 | [6.313509604597867, 1535488521896], 434 | [5.870759720053473, 1535488551896], 435 | [5.772223344674832, 1535488581896], 436 | [5.509243517722745, 1535488611896], 437 | [5.311391299297365, 1535488641896], 438 | [5.786150600510748, 1535488671896], 439 | [6.208584331763871, 1535488701896], 440 | [5.921461177054171, 1535488731896], 441 | [6.322105390109767, 1535488761896], 442 | [6.170408313816647, 1535488791896], 443 | [6.274184749247823, 1535488821896], 444 | [5.863185051215275, 1535488851896], 445 | [5.922447476223339, 1535488881896], 446 | [5.442613390791959, 1535488911896], 447 | [5.721287135119313, 1535488941896], 448 | [5.233259546180417, 1535488971896], 449 | [5.6893639193242995, 1535489001896], 450 | [5.821187724967082, 1535489031896], 451 | [5.480922852606588, 1535489061896], 452 | [5.444165948371097, 1535489091896], 453 | [5.776769319750889, 1535489121896], 454 | [5.933720182116506, 1535489151896], 455 | [6.427673842719106, 1535489181896], 456 | [6.111247311523908, 1535489211896], 457 | [6.130742962759873, 1535489241896], 458 | [6.489023904175915, 1535489271896], 459 | [6.146628478979222, 1535489301896], 460 | [6.556680542100534, 1535489331896], 461 | [7.0168783153455125, 1535489361896], 462 | [7.222041409053185, 1535489391896], 463 | [7.630684389604419, 1535489421896], 464 | [8.037375916659686, 1535489451896], 465 | [7.923267882096079, 1535489481896], 466 | [8.03159010671409, 1535489511896], 467 | [8.224755751797158, 1535489541896], 468 | [8.10425576282869, 1535489571896], 469 | [7.747570644274805, 1535489601896], 470 | [7.757994077988546, 1535489631896], 471 | [7.472568900651703, 1535489661896], 472 | [7.186956442300981, 1535489691896], 473 | [7.050765417621417, 1535489721896], 474 | [7.489831394250668, 1535489751896], 475 | [7.3450376352865865, 1535489781896], 476 | [7.168913100928955, 1535489811896], 477 | [7.37193933664095, 1535489841896], 478 | [7.130761096324356, 1535489871896], 479 | [7.165061687324403, 1535489901896], 480 | [7.014039907872661, 1535489931896], 481 | [7.126291869488244, 1535489961896], 482 | [7.289748219214455, 1535489991896], 483 | [6.889478733077032, 1535490021896], 484 | [6.629177182939674, 1535490051896], 485 | [6.709229905503588, 1535490081896], 486 | [6.312587332808353, 1535490111896], 487 | [6.538812036707883, 1535490141896], 488 | [6.300887675529737, 1535490171896], 489 | [5.946949443005223, 1535490201896], 490 | [5.945745597316715, 1535490231896], 491 | [5.551241336982815, 1535490261896], 492 | [5.8795035531520545, 1535490291896], 493 | [5.388040036846616, 1535490321896], 494 | [5.007012802602027, 1535490351896], 495 | [5.4418324115123955, 1535490381896], 496 | [5.429405602312104, 1535490411896], 497 | [5.870228878640073, 1535490441896], 498 | [5.898426694731041, 1535490471896], 499 | [6.180766825263569, 1535490501896], 500 | [5.7950889869875475, 1535490531896], 501 | [6.272163732166447, 1535490561896], 502 | [5.92708621110359, 1535490591896], 503 | [5.4795675062620885, 1535490621896], 504 | [5.674873392942986, 1535490651896], 505 | [5.688313639381825, 1535490681896], 506 | [5.911445234915265, 1535490711896], 507 | [5.610285393249674, 1535490741896], 508 | [5.881471026198907, 1535490771896], 509 | [5.462806488263226, 1535490801896], 510 | [5.381067135725598, 1535490831896], 511 | [5.513024454017039, 1535490861896], 512 | [5.440702492283462, 1535490891896], 513 | [5.27004578190083, 1535490921896], 514 | [4.806322341266743, 1535490951896], 515 | [4.96436770007066, 1535490981896], 516 | [4.966460331584235, 1535491011896], 517 | [4.774541826323533, 1535491041896], 518 | [4.632940246490401, 1535491071896], 519 | [4.27244365993498, 1535491101896], 520 | [4.772436079521577, 1535491131896], 521 | [4.67093020359701, 1535491161896], 522 | [4.340727721890586, 1535491191896], 523 | [3.9688876907063415, 1535491221896], 524 | [3.8474767919262267, 1535491251896], 525 | [3.6325361974663144, 1535491281896], 526 | [3.744513256743324, 1535491311896], 527 | [3.3828033635753547, 1535491341896], 528 | [3.4370236228632716, 1535491371896], 529 | [3.136650676321968, 1535491401896], 530 | [3.0670319092305984, 1535491431896], 531 | [2.6813263105485863, 1535491461896], 532 | [2.4351267821598506, 1535491491896], 533 | [2.0080909719307805, 1535491521896], 534 | [1.6071597502678943, 1535491551896], 535 | [1.466141052661278, 1535491581896], 536 | [1.8129003387141935, 1535491611896], 537 | [1.4377971236022238, 1535491641896], 538 | [1.5036967161754728, 1535491671896], 539 | [1.4712062102586883, 1535491701896], 540 | [1.4521452632479626, 1535491731896], 541 | [1.4099410924558828, 1535491761896], 542 | [1.086612936826617, 1535491791896], 543 | [0.8653535587848975, 1535491821896], 544 | [0.9392960470267764, 1535491851896], 545 | [0.8488582023021465, 1535491881896], 546 | [1.0202796897310287, 1535491911896], 547 | [1.2456963088153037, 1535491941896], 548 | [1.2093594446295397, 1535491971896], 549 | [0.8925356396528553, 1535492001896], 550 | [0.8053253410492243, 1535492031896], 551 | [0.9429540644796839, 1535492061896], 552 | [0.9296449866947402, 1535492091896], 553 | [0.6319104289108423, 1535492121896], 554 | [1.1021524857878846, 1535492151896], 555 | [1.0854985252345233, 1535492181896], 556 | [1.4621545814890426, 1535492211896], 557 | [1.9558431668587608, 1535492241896], 558 | [1.713064359233882, 1535492271896], 559 | [1.8050028413494754, 1535492301896], 560 | [1.9121583154047883, 1535492331896], 561 | [1.9441264025017884, 1535492361896], 562 | [1.7519662032089178, 1535492391896], 563 | [1.5037122156614506, 1535492421896], 564 | [1.3097761460636583, 1535492451896], 565 | [1.3520489774993676, 1535492481896], 566 | [1.1381529897009774, 1535492511896], 567 | [1.609602554695602, 1535492541896], 568 | [1.961271999137071, 1535492571896], 569 | [1.4755675349277162, 1535492601896], 570 | [1.953637372607441, 1535492631896], 571 | [1.8173157020769355, 1535492661896], 572 | [1.7288841530154628, 1535492691896], 573 | [1.5708094946089886, 1535492721896], 574 | [1.6598887551963761, 1535492751896], 575 | [1.481350146853369, 1535492781896], 576 | [1.3624376771205462, 1535492811896], 577 | [0.9503710869842825, 1535492841896], 578 | [0.9811189981863603, 1535492871896], 579 | [1.288574784443421, 1535492901896], 580 | [1.1632432310009408, 1535492931896], 581 | [0.8687799323877237, 1535492961896], 582 | [1.3134059310167543, 1535492991896], 583 | [0.9342020954048627, 1535493021896], 584 | [0.5295540289180792, 1535493051896], 585 | [0.4376785274489629, 1535493081896], 586 | [0.7003680114119086, 1535493111896], 587 | [0.2534348977901116, 1535493141896], 588 | [0.5081699434340989, 1535493171896], 589 | [0.41475606084315064, 1535493201896], 590 | [0.8152474036292012, 1535493231896], 591 | [0.5385420744270581, 1535493261896], 592 | [0.5436712454843476, 1535493291896], 593 | [0.6837913007691441, 1535493321896], 594 | [0.2920843319675673, 1535493351896], 595 | [-0.07021299716153312, 1535493381896], 596 | [-0.41995778744488216, 1535493411896], 597 | [-0.7017193258859937, 1535493441896], 598 | [-0.6458318315207723, 1535493471896], 599 | [-0.5922507261009081, 1535493501896], 600 | [-0.6691282055463103, 1535493531896], 601 | [-0.42875863452109697, 1535493561896], 602 | [-0.023662721569895573, 1535493591896], 603 | [-0.14391649704630094, 1535493621896], 604 | [0.21610565161957274, 1535493651896], 605 | [-0.18524917270972874, 1535493681896], 606 | [-0.2205125421228482, 1535493711896], 607 | [-0.37584899767798136, 1535493741896], 608 | [-0.1449233045938521, 1535493771896], 609 | [0.31349785520464474, 1535493801896], 610 | [0.4456029257359146, 1535493831896], 611 | [0.20573978589051362, 1535493861896], 612 | [-0.008068792625815002, 1535493891896], 613 | [0.013325569415433891, 1535493921896], 614 | [0.3133565866540084, 1535493951896], 615 | [-0.13991918353968702, 1535493981896], 616 | [-0.35925368292107157, 1535494011896], 617 | [-0.7821593220787761, 1535494041896], 618 | [-0.5999116571289042, 1535494071896], 619 | [-0.1753540049697242, 1535494101896], 620 | [-0.019020798529905347, 1535494131896], 621 | [0.20456933023054458, 1535494161896], 622 | [-0.2565381909657471, 1535494191896], 623 | [-0.26854850604722474, 1535494221896], 624 | [-0.6324132046079811, 1535494251896], 625 | [-0.6675787815980037, 1535494281896], 626 | [-0.49486988498370643, 1535494311896], 627 | [-0.15924276630088374, 1535494341896], 628 | [-0.058282134621533976, 1535494371896], 629 | [-0.2838598444451467, 1535494401896], 630 | [0.14111608502125378, 1535494431896], 631 | [0.4798393958267832, 1535494461896], 632 | [0.6126251144584587, 1535494491896], 633 | [0.29145711319302076, 1535494521896], 634 | [0.25408678224332576, 1535494551896], 635 | [0.15278950172067146, 1535494581896], 636 | [0.15522125916086676, 1535494611896], 637 | [-0.25031867765550886, 1535494641896], 638 | [-0.06125872971232593, 1535494671896], 639 | [0.29120639810590043, 1535494701896], 640 | [0.029370915745928983, 1535494731896], 641 | [0.11556296688923628, 1535494761896], 642 | [0.11040384395111957, 1535494791896], 643 | [-0.08444078171600966, 1535494821896], 644 | [0.32772215866011856, 1535494851896], 645 | [-0.04115364983877201, 1535494881896], 646 | [-0.12414386174063835, 1535494911896], 647 | [0.09330110508986283, 1535494941896], 648 | [0.38212572522457283, 1535494971896], 649 | [0.24429001300310454, 1535495001896], 650 | [-0.14698746114357059, 1535495031896], 651 | [-0.05892121661685029, 1535495061896], 652 | [-0.040286250027872506, 1535495091896], 653 | [-0.050051950360534, 1535495121896], 654 | [0.19755951225862656, 1535495151896], 655 | [0.6134202019215866, 1535495181896], 656 | [0.6833107353513992, 1535495211896], 657 | [0.35022974419004416, 1535495241896], 658 | [0.7175773273956317, 1535495271896], 659 | [0.5055436525580876, 1535495301896], 660 | [0.846439517739507, 1535495331896], 661 | [0.9910064803495066, 1535495361896], 662 | [1.216850527419499, 1535495391896], 663 | [1.111341327812081, 1535495421896], 664 | [1.0763318491171054, 1535495451896], 665 | [1.477821844366602, 1535495481896], 666 | [1.7091344724834445, 1535495511896], 667 | [1.2438890857132279, 1535495541896], 668 | [1.1559923399177516, 1535495571896], 669 | [0.6899933999946481, 1535495601896], 670 | [0.8974592416675145, 1535495631896], 671 | [1.0131641266114662, 1535495661896], 672 | [1.1924894863584656, 1535495691896], 673 | [1.2300475749829234, 1535495721896], 674 | [1.048449957205969, 1535495751896], 675 | [1.1333511685536037, 1535495781896], 676 | [1.612151690718275, 1535495811896], 677 | [1.146394647326896, 1535495841896], 678 | [1.4476783682933316, 1535495871896], 679 | [1.110387156467561, 1535495901896], 680 | [0.9443756264998421, 1535495931896], 681 | [1.3250031918590017, 1535495961896], 682 | [1.0359004547598682, 1535495991896], 683 | [1.13907181437289, 1535496021896], 684 | [1.1485620811183588, 1535496051896], 685 | [0.9855292138597895, 1535496081896], 686 | [0.6575099650804045, 1535496111896], 687 | [0.6790565799051702, 1535496141896], 688 | [0.6143679770147819, 1535496171896], 689 | [0.44874522385147997, 1535496201896], 690 | [0.7986771537513839, 1535496231896], 691 | [0.4015678257540705, 1535496261896], 692 | [0.22258359219253715, 1535496291896], 693 | [0.004348250594228109, 1535496321896], 694 | [0.1348619340668505, 1535496351896], 695 | [0.4100367707183013, 1535496381896], 696 | [0.013846012982296985, 1535496411896], 697 | [0.304992530500457, 1535496441896], 698 | [0.35541564469653514, 1535496471896], 699 | [0.6118438109368582, 1535496501896], 700 | [0.4463352573915662, 1535496531896], 701 | [0.8308382632892302, 1535496561896], 702 | [1.0379958625478156, 1535496591896], 703 | [1.3935463750137775, 1535496621896], 704 | [1.7071403457184222, 1535496651896], 705 | [1.3743143264036826, 1535496681896], 706 | [1.7724960647086643, 1535496711896], 707 | [1.892896211346101, 1535496741896], 708 | [1.9205382808560703, 1535496771896], 709 | [2.2145594923841743, 1535496801896], 710 | [2.20107371955703, 1535496831896], 711 | [1.7102083080130734, 1535496861896], 712 | [2.1316874964899672, 1535496891896], 713 | [2.304141188504457, 1535496921896], 714 | [2.0451686232858446, 1535496951896], 715 | [2.2409108062318066, 1535496981896], 716 | [1.7524225272078267, 1535497011896], 717 | [1.9598120094485645, 1535497041896], 718 | [1.8775395671896071, 1535497071896], 719 | [1.795923795183318, 1535497101896], 720 | [1.7504161735839823, 1535497131896], 721 | [1.57713210906809, 1535497161896], 722 | [1.4988105834125118, 1535497191896], 723 | [1.2199458366217046, 1535497221896], 724 | [1.4645157652206113, 1535497251896], 725 | [1.03331742535184, 1535497281896], 726 | [1.4709917345232246, 1535497311896], 727 | [1.4680605535881197, 1535497341896], 728 | [1.0920188946605263, 1535497371896] 729 | ] 730 | } 731 | ], 732 | "tables": null 733 | } 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /specs/res/panel_json_v004.json: -------------------------------------------------------------------------------- 1 | { 2 | "datasource": "-- Grafana --", 3 | "gridPos": { 4 | "h": 9, 5 | "w": 12, 6 | "x": 0, 7 | "y": 0 8 | }, 9 | "id": 2, 10 | "pconfig": { 11 | "layout": { 12 | "autosize": false, 13 | "dragmode": "lasso", 14 | "font": { 15 | "color": "#D8D9DA", 16 | "family": "\"Open Sans\", Helvetica, Arial, sans-serif" 17 | }, 18 | "hovermode": "closest", 19 | "legend": { 20 | "orientation": "v" 21 | }, 22 | "margin": { 23 | "b": 45, 24 | "l": 65, 25 | "r": 20, 26 | "t": 0 27 | }, 28 | "paper_bgcolor": "rgba(0,0,0,0)", 29 | "plot_bgcolor": "#1f1d1d", 30 | "scene": { 31 | "xaxis": { 32 | "title": "X AXIS" 33 | }, 34 | "yaxis": { 35 | "title": "Y AXIS" 36 | }, 37 | "zaxis": { 38 | "title": "Z AXIS" 39 | } 40 | }, 41 | "showlegend": false, 42 | "xaxis": { 43 | "gridcolor": "#444444", 44 | "range": null, 45 | "rangemode": "normal", 46 | "showgrid": true, 47 | "type": "date", 48 | "zeroline": false 49 | }, 50 | "yaxis": { 51 | "gridcolor": "#444444", 52 | "range": null, 53 | "rangemode": "normal", 54 | "showgrid": true, 55 | "type": "linear", 56 | "zeroline": false 57 | } 58 | }, 59 | "mapping": { 60 | "color": "A-series", 61 | "size": "", 62 | "x": "@time", 63 | "y": "A-series", 64 | "z": null 65 | }, 66 | "settings": { 67 | "color_option": "ramp", 68 | "displayModeBar": false, 69 | "line": { 70 | "color": "#005f81", 71 | "dash": "solid", 72 | "shape": "linear", 73 | "width": 6 74 | }, 75 | "marker": { 76 | "color": "#33B5E5", 77 | "colorscale": "YIGnBu", 78 | "line": { 79 | "color": "#DDD", 80 | "width": 0 81 | }, 82 | "showscale": true, 83 | "size": 15, 84 | "sizemin": 3, 85 | "sizemode": "diameter", 86 | "sizeref": 0.2, 87 | "symbol": "circle" 88 | }, 89 | "mode": "lines+markers", 90 | "type": "scatter" 91 | } 92 | }, 93 | "targets": [ 94 | { 95 | "refId": "A", 96 | "resultFormat": "time_series", 97 | "scenarioId": "random_walk", 98 | "target": "select metric" 99 | } 100 | ], 101 | "title": "Panel Title", 102 | "type": "natel-plotly-panel" 103 | } 104 | -------------------------------------------------------------------------------- /specs/series.jest.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | SeriesWrapper, 5 | SeriesWrapperSeries, 6 | // SeriesWrapperTable, 7 | } from '../src/SeriesWrapper'; 8 | 9 | describe('Check Series Helper', () => { 10 | // $scope, $injector, $window, private $rootScope, public uiSegmentSrv 11 | 12 | // Skip those for now because they rely on real template expansion 13 | describe('check Series Helper', () => { 14 | const series = { 15 | datapoints: [[1.23, 100], [2.24, 101], [3.45, 102]], 16 | }; 17 | const helperValue: SeriesWrapper = new SeriesWrapperSeries('AAA', series, 'value'); 18 | const helperTime: SeriesWrapper = new SeriesWrapperSeries('AAA', series, 'time'); 19 | const helperIndex: SeriesWrapper = new SeriesWrapperSeries('AAA', series, 'index'); 20 | 21 | const arrValue = helperValue.toArray(); 22 | const arrTime = helperTime.toArray(); 23 | const arrIndex = helperIndex.toArray(); 24 | 25 | it('They should all have the same length', () => { 26 | expect(arrValue.length).toBe(series.datapoints.length); 27 | expect(arrTime.length).toBe(series.datapoints.length); 28 | expect(arrIndex.length).toBe(series.datapoints.length); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /specs/typing.d.ts: -------------------------------------------------------------------------------- 1 | // Load generic JSON 2 | declare module '*.json' { 3 | const value: any; 4 | export default value; 5 | } 6 | 7 | declare module 'app/features/dashboard/panel_model' { 8 | var config: any; 9 | export default config; 10 | } 11 | -------------------------------------------------------------------------------- /src/SeriesWrapper.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | // This gives a standard way to get a value for a given field 4 | export abstract class SeriesWrapper { 5 | refId: string; // From Query Target 6 | name: string; 7 | 8 | type?: 'string' | 'date' | 'boolean' | 'epoch' | 'number'; 9 | first?: any; 10 | count: number; 11 | 12 | /** @ngInject */ 13 | constructor(refId: string) { 14 | this.refId = refId; 15 | } 16 | 17 | protected setFirst(v: any) { 18 | this.first = v; 19 | if (_.isNumber(v)) { 20 | this.type = 'number'; 21 | } else if (_.isString(v)) { 22 | this.type = 'string'; 23 | } else if (typeof v === typeof true) { 24 | this.type = 'boolean'; 25 | } 26 | } 27 | 28 | // The best key for this field 29 | getKey(): string { 30 | return this.name; 31 | } 32 | 33 | // All ways to access this field 34 | getAllKeys(): string[] { 35 | return [this.getKey()]; 36 | } 37 | 38 | abstract toArray(): Array; 39 | } 40 | 41 | export class SeriesWrapperSeries extends SeriesWrapper { 42 | value: 'value' | 'index' | 'time'; 43 | 44 | /** @ngInject */ 45 | constructor(refId: string, public series: any, val: 'value' | 'index' | 'time') { 46 | super(refId); 47 | this.value = val; 48 | this.count = series.datapoints.length; 49 | this.name = series.target; 50 | 51 | if ('index' === val) { 52 | this.first = 0; 53 | this.type = 'number'; 54 | this.name += '@index'; 55 | return; 56 | } 57 | if ('value' === val) { 58 | _.forEach(series.datapoints, arr => { 59 | if (arr[0] !== null) { 60 | // 0 is an ok value so cant use if(arr[0]) 61 | this.setFirst(arr[0]); 62 | return false; 63 | } 64 | return true; // continue 65 | }); 66 | return; 67 | } 68 | if ('time' === val) { 69 | this.type = 'epoch'; 70 | this.first = series.datapoints[0][1]; 71 | this.name += '@time'; 72 | return; 73 | } 74 | } 75 | 76 | toArray(): any[] { 77 | if ('index' === this.value) { 78 | const arr = new Array(this.count); 79 | for (let i = 0; i < this.count; i++) { 80 | arr[i] = i; 81 | } 82 | return arr; 83 | } 84 | const idx = 'time' === this.value ? 1 : 0; 85 | return _.map(this.series.datapoints, arr => { 86 | return arr[idx]; 87 | }); 88 | } 89 | 90 | getAllKeys(): string[] { 91 | if (this.refId) { 92 | const vals = [this.name, this.refId + '@' + this.value, this.refId + '/' + this.name]; 93 | 94 | if ('A' === this.refId) { 95 | vals.push('@' + this.value); 96 | } 97 | return vals; 98 | } 99 | return [this.name]; 100 | } 101 | } 102 | 103 | export class SeriesWrapperTableRow extends SeriesWrapper { 104 | /** @ngInject */ 105 | constructor(refId: string, public table: any) { 106 | super(refId); 107 | 108 | this.name = refId + '@row'; 109 | } 110 | 111 | toArray(): any[] { 112 | const count = this.table.rows.length; 113 | const arr = new Array(count); 114 | for (let i = 0; i < count; i++) { 115 | arr[i] = i; 116 | } 117 | return arr; 118 | } 119 | } 120 | 121 | export class SeriesWrapperTable extends SeriesWrapper { 122 | /** @ngInject */ 123 | constructor(refId: string, public table: any, public index: number) { 124 | super(refId); 125 | this.count = table.rows.length; 126 | 127 | const col = table.columns[index]; 128 | if (!col) { 129 | throw new Error('Unkonwn Column: ' + index); 130 | } 131 | 132 | this.name = col.text; 133 | if ('time' === col.type) { 134 | this.type = 'epoch'; 135 | this.first = table.rows[0][index]; 136 | } else { 137 | for (let i = 0; i < this.count; i++) { 138 | const v = table.rows[i][index]; 139 | if (v !== null) { 140 | // 0 is an ok value so cant use if(v) 141 | this.setFirst(v); 142 | return; 143 | } 144 | } 145 | } 146 | } 147 | 148 | toArray(): any[] { 149 | return _.map(this.table.rows, row => { 150 | return row[this.index]; 151 | }); 152 | } 153 | 154 | getAllKeys(): string[] { 155 | if (this.refId) { 156 | return [this.getKey(), this.refId + '/' + this.name, this.refId + '[' + this.index + ']']; 157 | } 158 | return [this.getKey()]; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/anno.ts: -------------------------------------------------------------------------------- 1 | import {Shape, Data} from 'plotly.js'; 2 | 3 | export class AnnoInfo { 4 | trace: Data; 5 | shapes: Shape[]; 6 | 7 | constructor() { 8 | this.clear(); 9 | } 10 | 11 | clear() { 12 | this.shapes = []; 13 | this.trace = { 14 | mode: 'markers', 15 | type: 'scatter', 16 | hoverinfo: 'x+text', 17 | x: [], 18 | y: [], 19 | text: [], 20 | yaxis: 'y2', 21 | marker: { 22 | size: 15, 23 | symbol: 'triangle-up', 24 | color: [], 25 | }, 26 | }; 27 | } 28 | 29 | update(results: any): boolean { 30 | if (!results || !results.annotations) { 31 | this.clear(); 32 | return false; 33 | } 34 | 35 | const x: number[] = []; 36 | const y: number[] = []; 37 | const text: string[] = []; 38 | const color: string[] = []; 39 | 40 | this.shapes = results.annotations.map(a => { 41 | x.push(a.time); 42 | y.push(0); 43 | text.push(a.text); 44 | color.push(a.annotation.iconColor); 45 | 46 | return { 47 | type: 'line', // rect 48 | xref: 'x', 49 | yref: 'paper', 50 | x0: a.time, 51 | y0: 0, 52 | x1: a.time, 53 | y1: 1, 54 | 55 | visible: true, 56 | layer: 'above', 57 | 58 | fillcolor: a.annotation.iconColor, 59 | opacity: 0.8, 60 | line: { 61 | color: a.annotation.iconColor, 62 | width: 1, 63 | dash: 'dash', 64 | }, 65 | } as Shape; 66 | }); 67 | 68 | // Overwrite it with new points 69 | this.trace = {...this.trace, x, y, text} as any; 70 | (this.trace as any).marker.color = color; 71 | return x.length > 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/editor.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {PlotlyPanelCtrl} from './module'; 4 | 5 | class AxisInfo { 6 | label: string; 7 | layout: any; // The config saved in layout 8 | property: string; // mapping property to check in a trace 9 | segment: any; // The Grafana (); 16 | trace: any; // Trace Config 17 | traceIndex = 0; 18 | traces: any[]; // array of configs; 19 | 20 | symbol: any; // The Grafana { 42 | _.defaults(trace, PlotlyPanelCtrl.defaultTrace); 43 | const mapping = trace.mapping; 44 | if (!mapping.color) { 45 | mapping.color = defaultMappings.first; 46 | changed = true; 47 | } 48 | if (!mapping.x) { 49 | mapping.x = defaultMappings.time; 50 | changed = true; 51 | } 52 | if (!mapping.y) { 53 | mapping.y = defaultMappings.first; 54 | changed = true; 55 | } 56 | if (ctrl.is3d() && !mapping.z) { 57 | mapping.z = defaultMappings.first; 58 | changed = true; 59 | } 60 | }); 61 | return changed; 62 | } 63 | 64 | onConfigChanged() { 65 | this.onUpdateAxis(); // Every time???? 66 | 67 | // Initalize the axis 68 | for (let i = 0; i < this.axis.length; i++) { 69 | if (this.axis[i].layout.rangemode === 'between') { 70 | if (!_.isArray(this.axis[i].layout.range)) { 71 | this.axis[i].layout.range = [0, null]; 72 | } 73 | } else { 74 | delete this.axis[i].layout.range; 75 | } 76 | } 77 | 78 | this.ctrl.onConfigChanged(); 79 | } 80 | 81 | onUpdateAxis() { 82 | const mapping = this.trace.mapping; 83 | if (!mapping) { 84 | console.error('Missing mappings for trace', this.trace); 85 | return; 86 | } 87 | 88 | const layout = this.ctrl.cfg.layout; 89 | if (!layout.xaxis) { 90 | layout.xaxis = {}; 91 | } 92 | if (!layout.yaxis) { 93 | layout.yaxis = {}; 94 | } 95 | 96 | this.axis = []; 97 | this.axis.push({ 98 | label: 'X Axis', 99 | layout: layout.xaxis, 100 | property: 'x', 101 | segment: this.mapping.x, 102 | }); 103 | this.axis.push({ 104 | label: 'Y Axis', 105 | layout: layout.yaxis, 106 | property: 'y', 107 | segment: this.mapping.y, 108 | }); 109 | 110 | if (this.ctrl.is3d()) { 111 | if (!layout.zaxis) { 112 | layout.zaxis = {}; 113 | } 114 | this.axis.push({ 115 | label: 'Z Axis', 116 | layout: layout.zaxis, 117 | property: 'z', 118 | segment: this.mapping.z, 119 | }); 120 | } 121 | } 122 | 123 | //----------------------------------------------------------------------- 124 | // Manage Traces 125 | //----------------------------------------------------------------------- 126 | 127 | selectTrace(index: number) { 128 | this.traces = this.ctrl.cfg.traces; 129 | if (!this.traces || this.traces.length < 1) { 130 | this.traces = this.ctrl.cfg.traces = [_.deepClone(PlotlyPanelCtrl.defaultTrace)]; 131 | } 132 | if (index >= this.ctrl.cfg.traces.length) { 133 | index = this.ctrl.cfg.traces.length - 1; 134 | } 135 | this.trace = this.ctrl.cfg.traces[index]; 136 | this.traceIndex = index; 137 | 138 | _.defaults(this.trace, PlotlyPanelCtrl.defaultTrace); 139 | if (!this.trace.name) { 140 | this.trace.name = EditorHelper.createTraceName(index); 141 | } 142 | 143 | // The _defaults makes sure this is taken care of 144 | this.symbol = this.ctrl.uiSegmentSrv.newSegment({ 145 | value: this.trace.settings.marker.symbol, 146 | }); 147 | 148 | // Now set one for each key 149 | this.mapping = {}; 150 | _.forEach(this.trace.mapping, (value, key) => { 151 | this.updateSegMapping(value, key); 152 | }); 153 | 154 | console.log('Editor Info', this); 155 | 156 | this.onConfigChanged(); 157 | this.ctrl.refresh(); 158 | } 159 | 160 | private updateSegMapping(value, key, updateTrace = false) { 161 | if (REMOVE_KEY === value) { 162 | this.mapping[key] = this.ctrl.uiSegmentSrv.newSegment({ 163 | value: 'Select Metric', 164 | fake: true, 165 | }); 166 | value = null; // will set this value later 167 | } else if (value) { 168 | const s = this.ctrl.seriesByKey.get(value); 169 | const opts: any = { 170 | value: value, 171 | series: s, 172 | }; 173 | if (!s) { 174 | // opts.fake = true; 175 | opts.html = value + ' '; 176 | } 177 | this.mapping[key] = this.ctrl.uiSegmentSrv.newSegment(opts); 178 | } else { 179 | this.mapping[key] = this.ctrl.uiSegmentSrv.newSegment({ 180 | value: 'Select Metric', 181 | fake: true, 182 | }); 183 | } 184 | 185 | if (updateTrace) { 186 | this.trace.mapping[key] = value; 187 | console.log('SET', key, value, this.trace.mapping); 188 | } 189 | } 190 | 191 | createTrace() { 192 | let trace: any = {}; 193 | if (this.ctrl.cfg.traces.length > 0) { 194 | trace = _.cloneDeep(this.ctrl.cfg.traces[this.ctrl.cfg.traces.length - 1]); 195 | } else { 196 | trace = _.deepClone(PlotlyPanelCtrl.defaultTrace); 197 | } 198 | trace.name = EditorHelper.createTraceName(this.ctrl.traces.length); 199 | this.ctrl.cfg.traces.push(trace); 200 | this.selectTrace(this.ctrl.cfg.traces.length - 1); 201 | } 202 | 203 | removeCurrentTrace() { 204 | // TODO... better behavior 205 | if (this.traces.length <= 1) { 206 | console.error('Wont remove a single trace', this); 207 | return; 208 | } 209 | 210 | for (let i = 0; i < this.traces.length; i++) { 211 | if (this.trace === this.traces[i]) { 212 | this.traces.splice(i, 1); 213 | if (i >= this.traces.length) { 214 | i = this.traces.length - 1; 215 | } 216 | this.ctrl.onConfigChanged(); 217 | this.ctrl._updateTraceData(true); 218 | this.selectTrace(i); 219 | this.ctrl.refresh(); 220 | return; 221 | } 222 | } 223 | 224 | console.error('Could not find', this); 225 | } 226 | 227 | static createTraceName(idx: number) { 228 | return 'Trace ' + (idx + 1); 229 | } 230 | 231 | //----------------------------------------------------------------------- 232 | // SERIES 233 | //----------------------------------------------------------------------- 234 | 235 | getSeriesSegs(withRemove = false): Promise { 236 | return new Promise((resolve, reject) => { 237 | const series: any[] = []; 238 | 239 | if (withRemove) { 240 | series.push( 241 | this.ctrl.uiSegmentSrv.newSegment({ 242 | fake: true, 243 | value: REMOVE_KEY, 244 | series: null, 245 | }) 246 | ); 247 | } 248 | this.ctrl.series.forEach(s => { 249 | series.push( 250 | this.ctrl.uiSegmentSrv.newSegment({ 251 | value: s.name, 252 | series: s, 253 | }) 254 | ); 255 | }); 256 | 257 | //console.log('GET Segments:', withRemove, series); 258 | //console.log('ALL Series:', this.ctrl.series); 259 | resolve(series); 260 | }); 261 | } 262 | 263 | onAxisSeriesChanged(axis: AxisInfo) { 264 | this.updateSegMapping(axis.segment.value, axis.property, true); 265 | this.onConfigChanged(); 266 | } 267 | 268 | getTextSegments(): any[] { 269 | return [this.mapping.text]; 270 | } 271 | 272 | onTextMetricChanged(sss: any) { 273 | const seg = this.mapping.text; 274 | this.updateSegMapping(seg.value, 'text', true); 275 | this.onConfigChanged(); 276 | } 277 | 278 | getColorSegments(): any[] { 279 | if (this.trace.settings.color_option === 'ramp') { 280 | return [this.mapping.color]; 281 | } 282 | return []; 283 | } 284 | 285 | onColorChanged() { 286 | const seg = this.mapping.color; 287 | this.updateSegMapping(seg.value, 'color', true); 288 | this.onConfigChanged(); 289 | } 290 | 291 | //----------------------------------------------------------------------- 292 | // SYMBOLS 293 | //----------------------------------------------------------------------- 294 | 295 | onSymbolChanged() { 296 | this.trace.settings.marker.symbol = this.symbol.value; 297 | this.onConfigChanged(); 298 | } 299 | 300 | getSymbolSegs(): Promise { 301 | return new Promise((resolve, reject) => { 302 | const txt = [ 303 | 'circle', 304 | 'circle-open', 305 | 'circle-dot', 306 | 'circle-open-dot', 307 | 'square', 308 | 'square-open', 309 | 'square-dot', 310 | 'square-open-dot', 311 | 'diamond', 312 | 'diamond-open', 313 | 'diamond-dot', 314 | 'diamond-open-dot', 315 | 'cross', 316 | 'cross-open', 317 | 'cross-dot', 318 | 'cross-open-dot', 319 | 'x', 320 | 'x-open', 321 | 'x-dot', 322 | 'x-open-dot', 323 | 'triangle-up', 324 | 'triangle-up-open', 325 | 'triangle-up-dot', 326 | 'triangle-up-open-dot', 327 | 'triangle-down', 328 | 'triangle-down-open', 329 | 'triangle-down-dot', 330 | 'triangle-down-open-dot', 331 | 'triangle-left', 332 | 'triangle-left-open', 333 | 'triangle-left-dot', 334 | 'triangle-left-open-dot', 335 | 'triangle-right', 336 | 'triangle-right-open', 337 | 'triangle-right-dot', 338 | 'triangle-right-open-dot', 339 | 'triangle-ne', 340 | 'triangle-ne-open', 341 | 'triangle-ne-dot', 342 | 'triangle-ne-open-dot', 343 | 'triangle-se', 344 | 'triangle-se-open', 345 | 'triangle-se-dot', 346 | 'triangle-se-open-dot', 347 | 'triangle-sw', 348 | 'triangle-sw-open', 349 | 'triangle-sw-dot', 350 | 'triangle-sw-open-dot', 351 | 'triangle-nw', 352 | 'triangle-nw-open', 353 | 'triangle-nw-dot', 354 | 'triangle-nw-open-dot', 355 | 'pentagon', 356 | 'pentagon-open', 357 | 'pentagon-dot', 358 | 'pentagon-open-dot', 359 | 'hexagon', 360 | 'hexagon-open', 361 | 'hexagon-dot', 362 | 'hexagon-open-dot', 363 | 'hexagon2', 364 | 'hexagon2-open', 365 | 'hexagon2-dot', 366 | 'hexagon2-open-dot', 367 | 'octagon', 368 | 'octagon-open', 369 | 'octagon-dot', 370 | 'octagon-open-dot', 371 | 'star', 372 | 'star-open', 373 | 'star-dot', 374 | 'star-open-dot', 375 | 'hexagram', 376 | 'hexagram-open', 377 | 'hexagram-dot', 378 | 'hexagram-open-dot', 379 | 'star-triangle-up', 380 | 'star-triangle-up-open', 381 | 'star-triangle-up-dot', 382 | 'star-triangle-up-open-dot', 383 | 'star-triangle-down', 384 | 'star-triangle-down-open', 385 | 'star-triangle-down-dot', 386 | 'star-triangle-down-open-dot', 387 | 'star-square', 388 | 'star-square-open', 389 | 'star-square-dot', 390 | 'star-square-open-dot', 391 | 'star-diamond', 392 | 'star-diamond-open', 393 | 'star-diamond-dot', 394 | 'star-diamond-open-dot', 395 | 'diamond-tall', 396 | 'diamond-tall-open', 397 | 'diamond-tall-dot', 398 | 'diamond-tall-open-dot', 399 | 'diamond-wide', 400 | 'diamond-wide-open', 401 | 'diamond-wide-dot', 402 | 'diamond-wide-open-dot', 403 | 'hourglass', 404 | 'hourglass-open', 405 | 'bowtie', 406 | 'bowtie-open', 407 | 'circle-cross', 408 | 'circle-cross-open', 409 | 'circle-x', 410 | 'circle-x-open', 411 | 'square-cross', 412 | 'square-cross-open', 413 | 'square-x', 414 | 'square-x-open', 415 | 'diamond-cross', 416 | 'diamond-cross-open', 417 | 'diamond-x', 418 | 'diamond-x-open', 419 | 'cross-thin', 420 | 'cross-thin-open', 421 | 'x-thin', 422 | 'x-thin-open', 423 | 'asterisk', 424 | 'asterisk-open', 425 | 'hash', 426 | 'hash-open', 427 | 'hash-dot', 428 | 'hash-open-dot', 429 | 'y-up', 430 | 'y-up-open', 431 | 'y-down', 432 | 'y-down-open', 433 | 'y-left', 434 | 'y-left-open', 435 | 'y-right', 436 | 'y-right-open', 437 | 'line-ew', 438 | 'line-ew-open', 439 | 'line-ns', 440 | 'line-ns-open', 441 | 'line-ne', 442 | 'line-ne-open', 443 | 'line-nw', 444 | 'line-nw-open', 445 | ]; 446 | 447 | const segs: any[] = []; 448 | _.forEach(txt, val => { 449 | segs.push(this.ctrl.uiSegmentSrv.newSegment(val)); 450 | }); 451 | resolve(segs); 452 | }); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /src/img/plotly_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/screenshot-multiple-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-multiple-trace.png -------------------------------------------------------------------------------- /src/img/screenshot-options-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-options-new.png -------------------------------------------------------------------------------- /src/img/screenshot-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-options.png -------------------------------------------------------------------------------- /src/img/screenshot-scatter-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-scatter-1.png -------------------------------------------------------------------------------- /src/img/screenshot-scatter-3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-scatter-3d.png -------------------------------------------------------------------------------- /src/img/screenshot-scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-scatter.png -------------------------------------------------------------------------------- /src/img/screenshot-single-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/0049f910e05413e834afba9713051d366a26d02d/src/img/screenshot-single-trace.png -------------------------------------------------------------------------------- /src/lib/plotly-cartesian.min.js: -------------------------------------------------------------------------------- 1 | // This is not a real file 2 | // In dist, it is replaced by 3 | // the value from node_modules 4 | -------------------------------------------------------------------------------- /src/lib/plotly.min.js: -------------------------------------------------------------------------------- 1 | // This is not a real file 2 | // In dist, it is replaced by 3 | // the value from node_modules 4 | -------------------------------------------------------------------------------- /src/libLoader.ts: -------------------------------------------------------------------------------- 1 | import $script from 'scriptjs'; 2 | 3 | let loaded: any; // Plotly Library 4 | let isFull = false; 5 | let wasCDN = false; 6 | 7 | export function loadPlotly(cfg: any): Promise { 8 | if (loaded) { 9 | console.log('using already loaded value'); 10 | return Promise.resolve(loaded); 11 | } 12 | 13 | const needsFull = cfg.settings.type !== 'scatter'; 14 | let url = 'public/plugins/natel-plotly-panel/lib/plotly-cartesian.min.js'; 15 | if (cfg.loadFromCDN) { 16 | url = needsFull 17 | ? 'https://cdn.plot.ly/plotly-latest.min.js' 18 | : 'https://cdn.plot.ly/plotly-cartesian-latest.min.js'; 19 | } else if (needsFull) { 20 | url = 'public/plugins/natel-plotly-panel/lib/plotly.min.js'; 21 | } 22 | return new Promise((resolve, reject) => { 23 | $script(url, resolve); 24 | }).then(res => { 25 | isFull = needsFull; 26 | wasCDN = cfg.loadFromCDN; 27 | loaded = window['Plotly']; 28 | return loaded; 29 | }); 30 | } 31 | 32 | export function loadIfNecessary(cfg: any): Promise { 33 | if (!loaded) { 34 | return loadPlotly(cfg); 35 | } 36 | 37 | if (wasCDN !== cfg.loadFromCDN) { 38 | console.log('Use CDN', cfg.loadFromCDN); 39 | loaded = null; 40 | return loadPlotly(cfg); 41 | } 42 | 43 | const needsFull = cfg.settings.type !== 'scatter'; 44 | if (needsFull && !isFull) { 45 | console.log('Switching to the full plotly library'); 46 | loaded = null; 47 | return loadPlotly(cfg); 48 | } 49 | 50 | // No changes 51 | return Promise.resolve(null); 52 | } 53 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | /* -*- Mode: typescript; indent-tabs-mode: nil; typescript-indent-level: 2 -*- */ 2 | 3 | /// 4 | 5 | import {MetricsPanelCtrl} from 'app/plugins/sdk'; 6 | 7 | import _ from 'lodash'; 8 | import moment from 'moment'; 9 | import $ from 'jquery'; 10 | 11 | import { 12 | SeriesWrapper, 13 | SeriesWrapperSeries, 14 | SeriesWrapperTable, 15 | SeriesWrapperTableRow, 16 | } from './SeriesWrapper'; 17 | import {EditorHelper} from './editor'; 18 | 19 | import {loadPlotly, loadIfNecessary} from './libLoader'; 20 | import {AnnoInfo} from './anno'; 21 | import {Axis} from 'plotly.js'; 22 | 23 | let Plotly: any; // Loaded dynamically! 24 | 25 | class PlotlyPanelCtrl extends MetricsPanelCtrl { 26 | static templateUrl = 'partials/module.html'; 27 | static configVersion = 1; // An index to help config migration 28 | 29 | initialized: boolean; 30 | //$tooltip: any; 31 | 32 | static defaultTrace = { 33 | mapping: { 34 | x: null, 35 | y: null, 36 | z: null, 37 | text: null, 38 | color: null, 39 | size: null, 40 | }, 41 | show: { 42 | line: true, 43 | markers: true, 44 | }, 45 | settings: { 46 | line: { 47 | color: '#005f81', 48 | width: 6, 49 | dash: 'solid', 50 | shape: 'linear', 51 | }, 52 | marker: { 53 | size: 15, 54 | symbol: 'circle', 55 | color: '#33B5E5', 56 | colorscale: 'YlOrRd', 57 | sizemode: 'diameter', 58 | sizemin: 3, 59 | sizeref: 0.2, 60 | line: { 61 | color: '#DDD', 62 | width: 0, 63 | }, 64 | showscale: false, 65 | }, 66 | color_option: 'ramp', 67 | }, 68 | }; 69 | 70 | static yaxis2: Partial = { 71 | title: 'Annotations', 72 | type: 'linear', 73 | range: [0, 1], 74 | visible: false, 75 | }; 76 | 77 | static defaults = { 78 | pconfig: { 79 | loadFromCDN: false, 80 | showAnnotations: true, 81 | fixScale: '', 82 | traces: [PlotlyPanelCtrl.defaultTrace], 83 | settings: { 84 | type: 'scatter', 85 | displayModeBar: false, 86 | }, 87 | layout: { 88 | showlegend: false, 89 | legend: { 90 | orientation: 'h', 91 | }, 92 | dragmode: 'lasso', // (enumerated: "zoom" | "pan" | "select" | "lasso" | "orbit" | "turntable" ) 93 | hovermode: 'closest', 94 | font: { 95 | family: '"Open Sans", Helvetica, Arial, sans-serif', 96 | }, 97 | xaxis: { 98 | showgrid: true, 99 | zeroline: false, 100 | type: 'auto', 101 | rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ) 102 | }, 103 | yaxis: { 104 | showgrid: true, 105 | zeroline: false, 106 | type: 'linear', 107 | rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ), 108 | }, 109 | zaxis: { 110 | showgrid: true, 111 | zeroline: false, 112 | type: 'linear', 113 | rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ) 114 | }, 115 | }, 116 | }, 117 | }; 118 | 119 | graphDiv: any; 120 | annotations = new AnnoInfo(); 121 | series: SeriesWrapper[]; 122 | seriesByKey: Map = new Map(); 123 | seriesHash = '?'; 124 | 125 | traces: any[]; // The data sent directly to Plotly -- with a special __copy element 126 | layout: any; // The layout used by Plotly 127 | 128 | mouse: any; 129 | cfg: any; 130 | 131 | // For editor 132 | editor: EditorHelper; 133 | dataWarnings: string[]; // warnings about loading data 134 | 135 | /** @ngInject **/ 136 | constructor( 137 | $scope, 138 | $injector, 139 | $window, 140 | private $rootScope, 141 | public uiSegmentSrv, 142 | private annotationsSrv 143 | ) { 144 | super($scope, $injector); 145 | 146 | this.initialized = false; 147 | 148 | //this.$tooltip = $('
'); 149 | 150 | // defaults configs 151 | _.defaultsDeep(this.panel, PlotlyPanelCtrl.defaults); 152 | 153 | this.cfg = this.panel.pconfig; 154 | 155 | this.traces = []; 156 | 157 | // ?? This seems needed for tests?!! 158 | if (!this.events) { 159 | return; 160 | } 161 | 162 | loadPlotly(this.cfg).then(v => { 163 | Plotly = v; 164 | console.log('Plotly', v); 165 | 166 | // Wait till plotly exists has loaded before we handle any data 167 | this.events.on('render', this.onRender.bind(this)); 168 | this.events.on('data-received', this.onDataReceived.bind(this)); 169 | this.events.on('data-error', this.onDataError.bind(this)); 170 | this.events.on('panel-size-changed', this.onResize.bind(this)); 171 | this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); 172 | this.events.on('refresh', this.onRefresh.bind(this)); 173 | 174 | // Refresh after plotly is loaded 175 | this.refresh(); 176 | }); 177 | 178 | // Standard handlers 179 | this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); 180 | this.events.on('panel-initialized', this.onPanelInitialized.bind(this)); 181 | } 182 | 183 | getCssRule(selectorText): CSSStyleRule | null { 184 | const styleSheets = document.styleSheets; 185 | for (let idx = 0; idx < styleSheets.length; idx += 1) { 186 | const styleSheet = styleSheets[idx] as CSSStyleSheet; 187 | const rules = styleSheet.cssRules; 188 | for (let ruleIdx = 0; ruleIdx < rules.length; ruleIdx += 1) { 189 | const rule = rules[ruleIdx] as CSSStyleRule; 190 | if (rule.selectorText === selectorText) { 191 | return rule; 192 | } 193 | } 194 | } 195 | return null; 196 | } 197 | 198 | // Don't call resize too quickly 199 | doResize = _.debounce(() => { 200 | // https://github.com/alonho/angular-plotly/issues/26 201 | const e = window.getComputedStyle(this.graphDiv).display; 202 | if (!e || 'none' === e) { 203 | // not drawn! 204 | console.warn('resize a plot that is not drawn yet'); 205 | } else { 206 | const rect = this.graphDiv.getBoundingClientRect(); 207 | this.layout.width = rect.width; 208 | this.layout.height = this.height; 209 | Plotly.redraw(this.graphDiv); 210 | } 211 | }, 50); 212 | 213 | onResize() { 214 | if (this.graphDiv && this.layout && Plotly) { 215 | this.doResize(); // Debounced 216 | } 217 | } 218 | 219 | onDataError(err) { 220 | this.series = []; 221 | this.annotations.clear(); 222 | this.render(); 223 | } 224 | 225 | onRefresh() { 226 | // ignore fetching data if another panel is in fullscreen 227 | if (this.otherPanelInFullscreenMode()) { 228 | return; 229 | } 230 | 231 | if (this.graphDiv && this.initialized && Plotly) { 232 | Plotly.redraw(this.graphDiv); 233 | } 234 | } 235 | 236 | onInitEditMode() { 237 | this.editor = new EditorHelper(this); 238 | this.addEditorTab('Display', 'public/plugins/natel-plotly-panel/partials/tab_display.html', 2); 239 | this.addEditorTab('Traces', 'public/plugins/natel-plotly-panel/partials/tab_traces.html', 3); 240 | // this.editorTabIndex = 1; 241 | this.onConfigChanged(); // Sets up the axis info 242 | 243 | // Check the size in a little bit 244 | setTimeout(() => { 245 | console.log('RESIZE in editor'); 246 | this.onResize(); 247 | }, 500); 248 | } 249 | 250 | processConfigMigration() { 251 | console.log('Migrating Plotly Configuration to version: ' + PlotlyPanelCtrl.configVersion); 252 | 253 | // Remove some things that should not be saved 254 | const cfg = this.panel.pconfig; 255 | delete cfg.layout.plot_bgcolor; 256 | delete cfg.layout.paper_bgcolor; 257 | delete cfg.layout.autosize; 258 | delete cfg.layout.height; 259 | delete cfg.layout.width; 260 | delete cfg.layout.margin; 261 | delete cfg.layout.scene; 262 | if (!this.is3d()) { 263 | delete cfg.layout.zaxis; 264 | } 265 | 266 | // Move from 'markers-lines' to checkbox 267 | if (cfg.settings.mode) { 268 | const old = cfg.settings.mode; 269 | const show = { 270 | markers: old.indexOf('markers') >= 0, 271 | lines: old.indexOf('lines') >= 0, 272 | }; 273 | _.forEach(cfg.traces, trace => { 274 | trace.show = show; 275 | }); 276 | delete cfg.settings.mode; 277 | } 278 | 279 | // TODO... MORE Migrations 280 | console.log('After Migration:', cfg); 281 | this.cfg = cfg; 282 | this.panel.version = PlotlyPanelCtrl.configVersion; 283 | } 284 | 285 | onPanelInitialized() { 286 | if (!this.panel.version || PlotlyPanelCtrl.configVersion > this.panel.version) { 287 | this.processConfigMigration(); 288 | } 289 | this._updateTraceData(true); 290 | } 291 | 292 | deepCopyWithTemplates = obj => { 293 | if (_.isArray(obj)) { 294 | return obj.map(val => this.deepCopyWithTemplates(val)); 295 | } else if (_.isString(obj)) { 296 | return this.templateSrv.replace(obj, this.panel.scopedVars); 297 | } else if (_.isObject(obj)) { 298 | const copy = {}; 299 | _.forEach(obj, (v, k) => { 300 | copy[k] = this.deepCopyWithTemplates(v); 301 | }); 302 | return copy; 303 | } 304 | return obj; 305 | }; 306 | 307 | getProcessedLayout() { 308 | // Copy from config 309 | const layout = this.deepCopyWithTemplates(this.cfg.layout); 310 | layout.plot_bgcolor = 'transparent'; 311 | layout.paper_bgcolor = layout.plot_bgcolor; 312 | 313 | // Update the size 314 | const rect = this.graphDiv.getBoundingClientRect(); 315 | layout.autosize = false; // height is from the div 316 | layout.height = this.height; 317 | layout.width = rect.width; 318 | 319 | // Make sure it is something 320 | if (!layout.xaxis) { 321 | layout.xaxis = {}; 322 | } 323 | if (!layout.yaxis) { 324 | layout.yaxis = {}; 325 | } 326 | 327 | // Fixed scales 328 | if (this.cfg.fixScale) { 329 | if ('x' === this.cfg.fixScale) { 330 | layout.yaxis.scaleanchor = 'x'; 331 | } else if ('y' === this.cfg.fixScale) { 332 | layout.xaxis.scaleanchor = 'y'; 333 | } else if ('z' === this.cfg.fixScale) { 334 | layout.xaxis.scaleanchor = 'z'; 335 | layout.yaxis.scaleanchor = 'z'; 336 | } 337 | } 338 | 339 | if (this.is3d()) { 340 | if (!layout.zaxis) { 341 | layout.zaxis = {}; 342 | } 343 | 344 | // 3d uses 'scene' for the axis 345 | layout.scene = { 346 | xaxis: layout.xaxis, 347 | yaxis: layout.yaxis, 348 | zaxis: layout.zaxis, 349 | }; 350 | 351 | delete layout.xaxis; 352 | delete layout.yaxis; 353 | delete layout.zaxis; 354 | 355 | layout.margin = { 356 | l: 0, 357 | r: 0, 358 | t: 0, 359 | b: 5, 360 | pad: 0, 361 | }; 362 | } else { 363 | delete layout.zaxis; 364 | delete layout.scene; 365 | 366 | // Check if the X axis should be a date 367 | if (!layout.xaxis.type || layout.xaxis.type === 'auto') { 368 | const mapping = _.get(this.cfg, 'traces[0].mapping.x'); 369 | if (mapping && mapping.indexOf('time') >= 0) { 370 | layout.xaxis.type = 'date'; 371 | } 372 | } 373 | 374 | const isDate = layout.xaxis.type === 'date'; 375 | layout.margin = { 376 | l: layout.yaxis.title ? 50 : 35, 377 | r: 5, 378 | t: 0, 379 | b: layout.xaxis.title ? 65 : isDate ? 40 : 30, 380 | pad: 2, 381 | }; 382 | 383 | // Set the range to the query window 384 | if (isDate && !layout.xaxis.range) { 385 | const range = this.timeSrv.timeRange(); 386 | layout.xaxis.range = [range.from.valueOf(), range.to.valueOf()]; 387 | } 388 | 389 | // get the css rule of grafana graph axis text 390 | const labelStyle = this.getCssRule('div.flot-text'); 391 | if (labelStyle) { 392 | let color = labelStyle.style.color; 393 | if (!layout.font) { 394 | layout.font = {}; 395 | } 396 | layout.font.color = color; 397 | 398 | // make the grid a little more transparent 399 | color = $.color 400 | .parse(color) 401 | .scale('a', 0.22) 402 | .toString(); 403 | 404 | // set gridcolor (like grafana graph) 405 | layout.xaxis.gridcolor = color; 406 | layout.yaxis.gridcolor = color; 407 | } 408 | 409 | // Set the second axis 410 | layout.yaxis2 = PlotlyPanelCtrl.yaxis2; 411 | } 412 | return layout; 413 | } 414 | 415 | onRender() { 416 | // ignore fetching data if another panel is in fullscreen 417 | if (this.otherPanelInFullscreenMode() || !this.graphDiv) { 418 | return; 419 | } 420 | 421 | if (!Plotly) { 422 | return; 423 | } 424 | 425 | if (!this.initialized) { 426 | const s = this.cfg.settings; 427 | 428 | const options = { 429 | showLink: false, 430 | displaylogo: false, 431 | displayModeBar: s.displayModeBar, 432 | modeBarButtonsToRemove: ['sendDataToCloud'], //, 'select2d', 'lasso2d'] 433 | }; 434 | 435 | this.layout = this.getProcessedLayout(); 436 | this.layout.shapes = this.annotations.shapes; 437 | let traces = this.traces; 438 | if (this.annotations.shapes.length > 0) { 439 | traces = this.traces.concat(this.annotations.trace); 440 | } 441 | Plotly.react(this.graphDiv, traces, this.layout, options); 442 | 443 | this.graphDiv.on('plotly_click', data => { 444 | if (data === undefined || data.points === undefined) { 445 | return; 446 | } 447 | for (let i = 0; i < data.points.length; i++) { 448 | const idx = data.points[i].pointNumber; 449 | const ts = this.traces[0].ts[idx]; 450 | // console.log( 'CLICK!!!', ts, data ); 451 | const msg = data.points[i].x.toPrecision(4) + ', ' + data.points[i].y.toPrecision(4); 452 | this.$rootScope.appEvent('alert-success', [ 453 | msg, 454 | '@ ' + this.dashboard.formatDate(moment(ts)), 455 | ]); 456 | } 457 | }); 458 | 459 | // if(true) { 460 | // this.graphDiv.on('plotly_hover', (data, xxx) => { 461 | // console.log( 'HOVER!!!', data, xxx, this.mouse ); 462 | // if(data.points.length>0) { 463 | // var idx = 0; 464 | // var pt = data.points[idx]; 465 | 466 | // var body = '
'+ pt.pointNumber +'
'; 467 | // body += "
"; 468 | // body += pt.x + ', '+pt.y; 469 | // body += "
"; 470 | 471 | // //this.$tooltip.html( body ).place_tt( this.mouse.pageX + 10, this.mouse.pageY ); 472 | // } 473 | // }).on('plotly_unhover', (data) => { 474 | // //this.$tooltip.detach(); 475 | // }); 476 | // } 477 | 478 | this.graphDiv.on('plotly_selected', data => { 479 | if (data === undefined || data.points === undefined) { 480 | return; 481 | } 482 | 483 | if (data.points.length === 0) { 484 | console.log('Nothing Selected', data); 485 | return; 486 | } 487 | 488 | console.log('SELECTED', data); 489 | 490 | let min = Number.MAX_SAFE_INTEGER; 491 | let max = Number.MIN_SAFE_INTEGER; 492 | 493 | for (let i = 0; i < data.points.length; i++) { 494 | const found = data.points[i]; 495 | const idx = found.pointNumber; 496 | const ts = found.fullData.x[idx]; 497 | min = Math.min(min, ts); 498 | max = Math.max(max, ts); 499 | } 500 | 501 | // At least 2 seconds 502 | min -= 1000; 503 | max += 1000; 504 | 505 | const range = {from: moment.utc(min), to: moment.utc(max)}; 506 | 507 | console.log('SELECTED!!!', min, max, data.points.length, range); 508 | 509 | this.timeSrv.setTime(range); 510 | 511 | // rebuild the graph after query 512 | if (this.graphDiv) { 513 | Plotly.Plots.purge(this.graphDiv); 514 | this.graphDiv.innerHTML = ''; 515 | this.initialized = false; 516 | } 517 | }); 518 | this.initialized = true; 519 | } else if (this.initialized) { 520 | Plotly.redraw(this.graphDiv).then(() => { 521 | this.renderingCompleted(); 522 | }); 523 | } else { 524 | console.log('Not initialized yet!'); 525 | } 526 | } 527 | 528 | onDataSnapshotLoad(snapshot) { 529 | this.onDataReceived(snapshot); 530 | } 531 | 532 | _hadAnno = false; 533 | 534 | onDataReceived(dataList) { 535 | const finfo: SeriesWrapper[] = []; 536 | let seriesHash = '/'; 537 | if (dataList && dataList.length > 0) { 538 | const useRefID = dataList.length === this.panel.targets.length; 539 | dataList.forEach((series, sidx) => { 540 | let refId = ''; 541 | if (useRefID) { 542 | refId = _.get(this.panel, 'targets[' + sidx + '].refId'); 543 | if (!refId) { 544 | refId = String.fromCharCode('A'.charCodeAt(0) + sidx); 545 | } 546 | } 547 | if (series.columns) { 548 | for (let i = 0; i < series.columns.length; i++) { 549 | finfo.push(new SeriesWrapperTable(refId, series, i)); 550 | } 551 | finfo.push(new SeriesWrapperTableRow(refId, series)); 552 | } else if (series.target) { 553 | finfo.push(new SeriesWrapperSeries(refId, series, 'value')); 554 | finfo.push(new SeriesWrapperSeries(refId, series, 'time')); 555 | finfo.push(new SeriesWrapperSeries(refId, series, 'index')); 556 | } else { 557 | console.error('Unsupported Series response', sidx, series); 558 | } 559 | }); 560 | } 561 | this.seriesByKey.clear(); 562 | finfo.forEach(s => { 563 | s.getAllKeys().forEach(k => { 564 | this.seriesByKey.set(k, s); 565 | seriesHash += '$' + k; 566 | }); 567 | }); 568 | this.series = finfo; 569 | 570 | // Now Process the loaded data 571 | const hchanged = this.seriesHash !== seriesHash; 572 | if (hchanged && this.editor) { 573 | EditorHelper.updateMappings(this); 574 | this.editor.selectTrace(this.editor.traceIndex); 575 | this.editor.onConfigChanged(); 576 | } 577 | 578 | if (hchanged || !this.initialized) { 579 | this.onConfigChanged(); 580 | this.seriesHash = seriesHash; 581 | } 582 | 583 | // Support Annotations 584 | let annotationPromise = Promise.resolve(); 585 | if (!this.cfg.showAnnotations || this.is3d()) { 586 | this.annotations.clear(); 587 | if (this.layout) { 588 | if (this.layout.shapes) { 589 | this.onConfigChanged(); 590 | } 591 | this.layout.shapes = []; 592 | } 593 | } else { 594 | annotationPromise = this.annotationsSrv 595 | .getAnnotations({ 596 | dashboard: this.dashboard, 597 | panel: this.panel, 598 | range: this.range, 599 | }) 600 | .then(results => { 601 | const hasAnno = this.annotations.update(results); 602 | if (this.layout) { 603 | if (hasAnno !== this._hadAnno) { 604 | this.onConfigChanged(); 605 | } 606 | this.layout.shapes = this.annotations.shapes; 607 | } 608 | this._hadAnno = hasAnno; 609 | }); 610 | } 611 | 612 | // Load the real data changes 613 | annotationPromise.then(() => { 614 | this._updateTraceData(); 615 | this.render(); 616 | }); 617 | } 618 | 619 | __addCopyPath(trace: any, key: string, path: string) { 620 | if (key) { 621 | trace.__set.push({ 622 | key: key, 623 | path: path, 624 | }); 625 | const s: SeriesWrapper = this.seriesByKey.get(key); 626 | if (!s) { 627 | this.dataWarnings.push('Unable to find: ' + key + ' for ' + trace.name + ' // ' + path); 628 | } 629 | } 630 | } 631 | 632 | // This will update all trace settings *except* the data 633 | _updateTracesFromConfigs() { 634 | this.dataWarnings = []; 635 | 636 | // Make sure we have a trace 637 | if (this.cfg.traces == null || this.cfg.traces.length < 1) { 638 | this.cfg.traces = [_.cloneDeep(PlotlyPanelCtrl.defaultTrace)]; 639 | } 640 | 641 | const is3D = this.is3d(); 642 | this.traces = this.cfg.traces.map((tconfig, idx) => { 643 | const config = this.deepCopyWithTemplates(tconfig) || {}; 644 | _.defaults(config, PlotlyPanelCtrl.defaults); 645 | const mapping = config.mapping; 646 | 647 | const trace: any = { 648 | name: config.name || EditorHelper.createTraceName(idx), 649 | type: this.cfg.settings.type, 650 | mode: 'markers+lines', // really depends on config settings 651 | __set: [], // { key:? property:? } 652 | }; 653 | 654 | let mode = ''; 655 | if (config.show.markers) { 656 | mode += '+markers'; 657 | trace.marker = config.settings.marker; 658 | 659 | delete trace.marker.sizemin; 660 | delete trace.marker.sizemode; 661 | delete trace.marker.sizeref; 662 | 663 | if (config.settings.color_option === 'ramp') { 664 | this.__addCopyPath(trace, mapping.color, 'marker.color'); 665 | } else { 666 | delete trace.marker.colorscale; 667 | delete trace.marker.showscale; 668 | } 669 | } 670 | 671 | if (config.show.lines) { 672 | mode += '+lines'; 673 | trace.line = config.settings.line; 674 | } 675 | 676 | // Set the text 677 | this.__addCopyPath(trace, mapping.text, 'text'); 678 | this.__addCopyPath(trace, mapping.x, 'x'); 679 | this.__addCopyPath(trace, mapping.y, 'y'); 680 | 681 | if (is3D) { 682 | this.__addCopyPath(trace, mapping.z, 'z'); 683 | } 684 | 685 | // Set the trace mode 686 | if (mode) { 687 | trace.mode = mode.substring(1); 688 | } 689 | return trace; 690 | }); 691 | } 692 | 693 | // Fills in the required data into the trace values 694 | _updateTraceData(force = false): boolean { 695 | if (!this.series) { 696 | // console.log('NO Series data yet!'); 697 | return false; 698 | } 699 | 700 | if (force || !this.traces) { 701 | this._updateTracesFromConfigs(); 702 | } else if (this.traces.length !== this.cfg.traces.length) { 703 | console.log( 704 | 'trace number mismatch. Found: ' + 705 | this.traces.length + 706 | ', expect: ' + 707 | this.cfg.traces.length 708 | ); 709 | this._updateTracesFromConfigs(); 710 | } 711 | 712 | // Use zero when the metric value is missing 713 | // Plotly gets lots of errors when the values are missing 714 | let zero: any = []; 715 | this.traces.forEach(trace => { 716 | if (trace.__set) { 717 | trace.__set.forEach(v => { 718 | const s = this.seriesByKey.get(v.key); 719 | let vals: any[] = zero; 720 | if (s) { 721 | vals = s.toArray(); 722 | if (vals && vals.length > zero.length) { 723 | zero = Array.from(Array(3), () => 0); 724 | } 725 | } else { 726 | if (!this.error) { 727 | this.error = ''; 728 | } 729 | this.error += 'Unable to find: ' + v.key + ' (using zeros). '; 730 | } 731 | if (!vals) { 732 | vals = zero; 733 | } 734 | _.set(trace, v.path, vals); 735 | }); 736 | } 737 | }); 738 | 739 | //console.log('SetDATA', this.traces); 740 | return true; 741 | } 742 | 743 | onConfigChanged() { 744 | // Force reloading the traces 745 | this._updateTraceData(true); 746 | 747 | if (!Plotly) { 748 | return; 749 | } 750 | 751 | // Check if the plotly library changed 752 | loadIfNecessary(this.cfg).then(res => { 753 | if (res) { 754 | if (Plotly) { 755 | Plotly.purge(this.graphDiv); 756 | } 757 | Plotly = res; 758 | } 759 | 760 | // Updates the layout and redraw 761 | if (this.initialized && this.graphDiv) { 762 | if (!this.cfg.showAnnotations) { 763 | this.annotations.clear(); 764 | } 765 | 766 | const s = this.cfg.settings; 767 | const options = { 768 | showLink: false, 769 | displaylogo: false, 770 | displayModeBar: s.displayModeBar, 771 | modeBarButtonsToRemove: ['sendDataToCloud'], //, 'select2d', 'lasso2d'] 772 | }; 773 | this.layout = this.getProcessedLayout(); 774 | this.layout.shapes = this.annotations.shapes; 775 | let traces = this.traces; 776 | if (this.annotations.shapes.length > 0) { 777 | traces = this.traces.concat(this.annotations.trace); 778 | } 779 | console.log('ConfigChanged (traces)', traces); 780 | Plotly.react(this.graphDiv, traces, this.layout, options); 781 | } 782 | 783 | this.render(); // does not query again! 784 | }); 785 | } 786 | 787 | is3d() { 788 | return this.cfg.settings.type === 'scatter3d'; 789 | } 790 | 791 | link(scope, elem, attrs, ctrl) { 792 | this.graphDiv = elem.find('.plotly-spot')[0]; 793 | this.initialized = false; 794 | elem.on('mousemove', evt => { 795 | this.mouse = evt; 796 | }); 797 | 798 | //let p = $(this.graphDiv).parent().parent()[0]; 799 | //console.log( 'PLOT', this.graphDiv, p ); 800 | } 801 | } 802 | 803 | export {PlotlyPanelCtrl, PlotlyPanelCtrl as PanelCtrl}; 804 | -------------------------------------------------------------------------------- /src/partials/module.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/partials/tab_display.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Options
4 |
5 | 6 |
7 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 44 |
45 |
46 | 47 | 53 | 54 | 60 | 61 | 67 | 68 | 74 | 75 |
76 | 77 |
78 | 82 |
83 |
84 |
85 | 86 |
87 |
{{axis.label}}
88 | 89 |
90 | 91 | 95 |
96 | 97 |
98 | 99 |
100 | 108 |
109 |
110 | 111 |
112 | 113 |
114 | 122 |
123 |
124 | 125 |
126 | 127 | 133 |
134 |
135 | 136 | 142 |
143 | 144 | 145 | 146 |
147 | 148 |
149 | -------------------------------------------------------------------------------- /src/partials/tab_traces.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{warn}} 4 |
5 |
6 | 7 |
8 | 36 | 37 |
38 | 39 |
40 |
Trace
41 |
42 |
43 | 44 | 49 |
50 |
51 | 52 |
Metrics
53 |
54 |
56 | 57 | 61 |
62 |
63 |
64 | 65 |
66 |
Markers
67 | 73 | 74 |
75 |
76 | 77 | 81 |
82 | 83 |
84 | 85 | 91 |
92 | 93 | 124 | 125 |
126 | 127 |
128 | 132 |
133 |
134 | 135 |
138 | 139 | 141 | 142 | 145 | 146 |
147 | 148 |
150 | 151 | 155 |
156 | 157 |
158 | 159 |
160 | 181 |
182 |
183 | 185 |
186 |
187 | 188 |
189 |
Lines
190 | 196 | 197 |
198 | 199 |
200 | 201 | 203 |
204 |
205 | 206 |
207 | 215 |
216 |
217 |
218 | 219 |
220 | 228 |
229 |
230 | 231 |
232 | 233 | 238 | 239 | 242 | 243 |
244 |
245 |
246 | 247 |
248 |
Text
249 | 250 |
252 | 253 | 257 |
258 | 259 |
260 | 261 |
262 | 267 |
268 |
269 | 270 |
271 |
272 |
273 | 274 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Plotly", 4 | "id": "natel-plotly-panel", 5 | 6 | "info": { 7 | "description": "Scatter plots and more", 8 | "author": { 9 | "name": "Natel Energy" 10 | }, 11 | "keywords": ["plotly", "scatter", "panel"], 12 | "logos": { 13 | "small": "img/plotly_logo.svg", 14 | "large": "img/plotly_logo.svg" 15 | }, 16 | "links": [ 17 | {"name": "Plot.ly", "url": "https://plot.ly/javascript/"}, 18 | {"name": "Project Page", "url": "https://github.com/NatelEnergy/grafana-plotly-panel"}, 19 | {"name": "Natel Energy", "url": "http://www.natelenergy.com/"} 20 | ], 21 | "screenshots": [ 22 | {"name": "Scatter", "path": "img/screenshot-scatter.png"}, 23 | {"name": "Scatter 2D", "path": "img/screenshot-scatter-1.png"}, 24 | {"name": "Scatter 3D", "path": "img/screenshot-scatter-3d.png"}, 25 | {"name": "Options", "path": "img/screenshot-options.png"} 26 | ], 27 | "version": "%VERSION%", 28 | "updated": "%TODAY%" 29 | }, 30 | 31 | "dependencies": { 32 | "grafanaVersion": "4.x.x", 33 | "plugins": [] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // This allows typescript to import json files and not complain 2 | // See: https://hackernoon.com/import-json-into-typescript-8d465beded79 3 | declare module '*.json' { 4 | const value: any; 5 | export default value; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "noUnusedLocals": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "es6", 6 | "rootDir": "./src", 7 | "jsx": "react", 8 | "lib": ["dom", "es2015", "es2016"], 9 | "declaration": false, 10 | "allowSyntheticDefaultImports": true, 11 | "inlineSourceMap": false, 12 | "sourceMap": true, 13 | "noEmitOnError": false, 14 | "emitDecoratorMetadata": false, 15 | "experimentalDecorators": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noImplicitUseStrict": true, 19 | "noImplicitAny": false, 20 | "noUnusedLocals": true, 21 | "strictNullChecks": true 22 | }, 23 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "array-type": [true, "array-simple"], 4 | "arrow-return-shorthand": true, 5 | "ban": [true, {"name": "Array", "message": "tsstyle#array-constructor"}], 6 | "ban-types": [ 7 | true, 8 | ["Object", "Use {} instead."], 9 | ["String", "Use 'string' instead."], 10 | ["Number", "Use 'number' instead."], 11 | ["Boolean", "Use 'boolean' instead."] 12 | ], 13 | "interface-name": [true, "never-prefix"], 14 | "no-string-throw": true, 15 | "no-unused-expression": true, 16 | "no-unused-variable": false, 17 | "no-use-before-declare": false, 18 | "no-duplicate-variable": true, 19 | "curly": true, 20 | "class-name": true, 21 | "semicolon": [true, "always", "ignore-bound-class-methods"], 22 | "triple-equals": [true, "allow-null-check"], 23 | "comment-format": [false, "check-space"], 24 | "eofline": true, 25 | "forin": false, 26 | "indent": [true, "spaces", 2], 27 | "jsdoc-format": true, 28 | "label-position": true, 29 | "max-line-length": [true, 150], 30 | "member-access": [true, "no-public"], 31 | "new-parens": true, 32 | "no-angle-bracket-type-assertion": true, 33 | "no-arg": true, 34 | "no-bitwise": false, 35 | "no-conditional-assignment": true, 36 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 37 | "no-construct": true, 38 | "no-debugger": true, 39 | "no-empty": false, 40 | "no-eval": true, 41 | "no-inferrable-types": true, 42 | "no-namespace": [true, "allow-declarations"], 43 | "no-reference": true, 44 | "no-shadowed-variable": false, 45 | "no-string-literal": false, 46 | "no-switch-case-fall-through": false, 47 | "no-trailing-whitespace": true, 48 | "no-var-keyword": true, 49 | "object-literal-sort-keys": false, 50 | "one-line": [true, "check-open-brace", "check-catch", "check-else"], 51 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 52 | "prefer-const": true, 53 | "radix": true, 54 | "typedef-whitespace": [ 55 | true, 56 | { 57 | "call-signature": "nospace", 58 | "index-signature": "nospace", 59 | "parameter": "nospace", 60 | "property-declaration": "nospace", 61 | "variable-declaration": "nospace" 62 | } 63 | ], 64 | "variable-name": [ 65 | true, 66 | "check-format", 67 | "ban-keywords", 68 | "allow-leading-underscore", 69 | "allow-trailing-underscore", 70 | "allow-pascal-case" 71 | ], 72 | "use-isnan": true, 73 | "whitespace": [true, "check-branch", "check-decl", "check-type", "check-preblock"] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin'); 6 | 7 | const packageJson = require('./package.json'); 8 | 9 | module.exports = { 10 | node: { 11 | fs: 'empty', 12 | }, 13 | context: path.join(__dirname, 'src'), 14 | entry: { 15 | module: './module.ts', 16 | }, 17 | devtool: 'source-map', 18 | output: { 19 | filename: '[name].js', 20 | path: path.join(__dirname, 'dist'), 21 | libraryTarget: 'amd', 22 | }, 23 | externals: [ 24 | 'lodash', 25 | 'jquery', 26 | 'moment', 27 | 'slate', 28 | 'prismjs', 29 | 'slate-plain-serializer', 30 | 'slate-react', 31 | function(context, request, callback) { 32 | var prefix = 'app/'; 33 | if (request.indexOf(prefix) === 0) { 34 | return callback(null, request); 35 | } 36 | // The plotly.min.js 37 | if (request.indexOf('./lib/') === 0) { 38 | console.log( 'SKIP', request ); 39 | return callback(null, request); 40 | } 41 | callback(); 42 | }, 43 | ], 44 | plugins: [ 45 | new CleanWebpackPlugin('dist', {allowExternal: true}), 46 | new webpack.optimize.OccurrenceOrderPlugin(), 47 | new CopyWebpackPlugin([ 48 | {from: '../node_modules/plotly.js/dist/plotly.min.js', to: 'lib'}, 49 | {from: '../node_modules/plotly.js/dist/plotly-cartesian.min.js', to: 'lib'}, 50 | {from: 'plugin.json', to: '.'}, 51 | {from: '../README.md', to: '.'}, 52 | {from: '../LICENSE', to: '.'}, 53 | {from: 'partials/*', to: '.'}, 54 | {from: 'img/*', to: '.'}, 55 | ]), 56 | new ReplaceInFileWebpackPlugin([ 57 | { 58 | dir: 'dist', 59 | files: ['plugin.json'], 60 | rules: [ 61 | { 62 | search: '%VERSION%', 63 | replace: packageJson.version, 64 | }, 65 | { 66 | search: '%TODAY%', 67 | replace: new Date().toISOString().substring(0, 10), 68 | }, 69 | ], 70 | }, 71 | ]), 72 | ], 73 | resolve: { 74 | extensions: ['.ts', '.tsx', '.js'], 75 | }, 76 | module: { 77 | rules: [ 78 | { 79 | test: /\.tsx?$/, 80 | loaders: [ 81 | { 82 | loader: 'babel-loader', 83 | options: {presets: ['env']}, 84 | }, 85 | 'ts-loader', 86 | ], 87 | exclude: /(node_modules)/, 88 | }, 89 | { 90 | test: /\.css$/, 91 | use: [ 92 | { 93 | loader: 'style-loader', 94 | }, 95 | { 96 | loader: 'css-loader', 97 | options: { 98 | importLoaders: 1, 99 | sourceMap: true, 100 | }, 101 | }, 102 | ], 103 | }, 104 | ], 105 | }, 106 | }; 107 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const baseWebpackConfig = require('./webpack.config'); 2 | const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | 5 | var conf = baseWebpackConfig; 6 | conf.mode = 'production'; 7 | 8 | conf.plugins.push(new ngAnnotatePlugin()); 9 | conf.plugins.push( 10 | new UglifyJSPlugin({ 11 | sourceMap: true, 12 | }) 13 | ); 14 | 15 | module.exports = conf; 16 | --------------------------------------------------------------------------------