├── .github └── FUNDING.yml ├── src ├── screenshots │ └── screenshot-1.png ├── vendor │ ├── grafana │ │ ├── event.ts │ │ ├── colors.ts │ │ ├── ticks.ts │ │ ├── event_manager.ts │ │ └── time_series2.ts │ └── flot │ │ ├── jquery.flot.stackpercent.js │ │ ├── jquery.flot.crosshair.js │ │ ├── jquery.flot.orderbars.js │ │ ├── jquery.flot.fillbetween.js │ │ ├── jquery.flot.stack.js │ │ ├── jquery.flot.dashes.js │ │ ├── jquery.flot.fillbelow.js │ │ ├── jquery.flot.time.js │ │ └── jquery.flot.selection.js ├── template.ts ├── plugin.json ├── histogram.ts ├── axes_editor.ts ├── partials │ ├── tab_legend.html │ ├── axes_editor.html │ └── tab_display.html ├── thresholds_form.ts ├── series_overrides_ctrl.ts ├── data_processor.ts ├── threshold_manager.ts ├── graph_legend.ts ├── graph_tooltip.ts ├── img │ └── icn-graph-panel.svg └── module.ts ├── dist ├── screenshots │ └── screenshot-1.png ├── plugin.json ├── README.md ├── partials │ ├── tab_legend.html │ ├── axes_editor.html │ └── tab_display.html └── img │ └── icn-graph-panel.svg ├── .vscode └── launch.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: corpglory 4 | -------------------------------------------------------------------------------- /src/screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CorpGlory/grafana-multibar-graph-panel/HEAD/src/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /dist/screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CorpGlory/grafana-multibar-graph-panel/HEAD/dist/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /src/vendor/grafana/event.ts: -------------------------------------------------------------------------------- 1 | export class AnnotationEvent { 2 | dashboardId: number; 3 | panelId: number; 4 | userId: number; 5 | time: any; 6 | timeEnd: any; 7 | isRegion: boolean; 8 | text: string; 9 | type: string; 10 | tags: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | var template = ` 2 |
3 |
4 |
5 | 6 |
7 |
8 | `; 9 | 10 | export default template; 11 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Multibar Graph Panel", 4 | "id": "corpglory-multibar-graph-panel", 5 | 6 | "info": { 7 | "author": { 8 | "name": "Grafana Project", 9 | "url": "https://grafana.com" 10 | }, 11 | "logos": { 12 | "small": "img/icn-graph-panel.svg", 13 | "large": "img/icn-graph-panel.svg" 14 | }, 15 | "version": "0.2.5" 16 | }, 17 | "dependencies": { 18 | "grafanaVersion": "5.3.3+" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /dist/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Multibar Graph Panel", 4 | "id": "corpglory-multibar-graph-panel", 5 | 6 | "info": { 7 | "author": { 8 | "name": "Grafana Project", 9 | "url": "https://grafana.com" 10 | }, 11 | "logos": { 12 | "small": "img/icn-graph-panel.svg", 13 | "large": "img/icn-graph-panel.svg" 14 | }, 15 | "version": "0.2.5" 16 | }, 17 | "dependencies": { 18 | "grafanaVersion": "5.3.3+" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:3000/dashboard/new", 9 | "sourceMaps": true, 10 | "pathMapping": { 11 | "/public/plugins/corpglory-multibar-graph-panel": "${workspaceFolder}/dist" 12 | }, 13 | "webRoot": "${workspaceFolder}", 14 | "sourceMapPathOverrides": { 15 | "webpack:///./*": "${webRoot}/src/*" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | npm-debug.log 4 | coverage/ 5 | .aws-config.json 6 | awsconfig 7 | /emails/dist 8 | /public_gen 9 | /tmp 10 | vendor/phantomjs/phantomjs 11 | 12 | docs/AWS_S3_BUCKET 13 | docs/GIT_BRANCH 14 | docs/VERSION 15 | docs/GITCOMMIT 16 | docs/changed-files 17 | docs/changed-files 18 | 19 | # locally required config files 20 | public/css/*.min.css 21 | 22 | # Editor junk 23 | *.sublime-workspace 24 | *.swp 25 | .idea/ 26 | *.iml 27 | 28 | /data/* 29 | /bin/* 30 | 31 | conf/custom.ini 32 | fig.yml 33 | profile.cov 34 | .notouch 35 | .DS_Store 36 | .tscache 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "outDir": "dist", 5 | "target": "es5", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], 10 | "rootDir": "./src", 11 | "module": "esnext", 12 | "declaration": false, 13 | "allowSyntheticDefaultImports": true, 14 | "inlineSourceMap": false, 15 | "sourceMap": true, 16 | "noEmitOnError": false, 17 | "emitDecoratorMetadata": false, 18 | "experimentalDecorators": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": false, 21 | "noImplicitUseStrict": false, 22 | "noImplicitAny": false, 23 | "noUnusedLocals": false, 24 | "baseUrl": "./src", 25 | "allowUnreachableCode": true, 26 | "paths": { 27 | "@": [ 28 | "." 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "./src/**/*.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CorpGlory 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grafana-multibar-graph-panel", 3 | "version": "0.2.5", 4 | "description": "Webpack copy of Grafana default panel", 5 | "main": "dist/module", 6 | "scripts": { 7 | "build": "webpack --config build/webpack.prod.conf.js", 8 | "dev": "webpack --config build/webpack.dev.conf.js" 9 | }, 10 | "keywords": [], 11 | "author": "Alexey Velikiy", 12 | "license": "MIT", 13 | "repository": "https://github.com/CorpGlory/grafana-multibar-graph-panel", 14 | "devDependencies": { 15 | "@types/angular": "^1.6.55", 16 | "@types/flot": "0.0.31", 17 | "@types/grafana": "github:CorpGlory/types-grafana", 18 | "@types/jquery": "^3.3.30", 19 | "@types/lodash": "^4.14.136", 20 | "@types/moment": "^2.13.0", 21 | "@types/perfect-scrollbar": "^1.3.0", 22 | "@types/tinycolor2": "^1.4.2", 23 | "copy-webpack-plugin": "^5.0.4", 24 | "imports-loader": "^0.8.0", 25 | "jquery": "^3.4.1", 26 | "loader-utils": "^1.2.3", 27 | "ng-annotate-webpack-plugin": "^0.3.0", 28 | "perfect-scrollbar": "^1.4.0", 29 | "tether-drop": "^1.4.2", 30 | "tinycolor2": "^1.4.1", 31 | "ts-loader": "^6.0.4", 32 | "typescript": "^3.5.3", 33 | "webpack": "4.38.0", 34 | "webpack-cli": "^3.3.6" 35 | }, 36 | "dependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | > This project is not activly supported. 2 | > Please use https://github.com/chartwerk/grafana-chartwerk-panel (it has multibar support) 3 | 4 | # Multibar Graph Panel 5 | 6 | Webpack copy of [Grafana default panel](http://docs.grafana.org/features/panels/graph/) implementing https://github.com/grafana/grafana/issues/870 7 | 8 | The plugin is under development. Please goto [Build section](https://github.com/CorpGlory/grafana-multibar-graph-panel#build) to build it from source. 9 | 10 | Supported Grafana versions: 5.3.3+ 11 | 12 | # Screenshots 13 | 14 | ![Screenshot](https://github.com/CorpGlory/grafana-multibar-graph-panel/raw/master/src/screenshots/screenshot-1.png) 15 | 16 | # Build 17 | 18 | ``` 19 | npm install 20 | npm run build 21 | ``` 22 | 23 | # See also 24 | 25 | * [grafana-plugin-template-webpack](https://github.com/CorpGlory/grafana-plugin-template-webpack) 26 | * [grafana-graph-panel](https://github.com/CorpGlory/grafana-graph-panel) 27 | * [@types/grafana](https://github.com/CorpGlory/types-grafana) 28 | * more about Grafana from CorpGlory: https://corpglory.com/t/grafana/ 29 | 30 | ## About CorpGlory Inc. 31 | The project developed by [CorpGlory Inc.](https://corpglory.com/), a company which provides high quality software development, data visualization, Grafana and monitoring consulting. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This project is not activly supported. 2 | > Please use https://github.com/chartwerk/grafana-chartwerk-app (it has multibar support) 3 | 4 | # Multibar Graph Panel 5 | 6 | Webpack copy of [Grafana default panel](http://docs.grafana.org/features/panels/graph/) implementing https://github.com/grafana/grafana/issues/870 7 | 8 | The plugin is under development. Please goto [Build section](https://github.com/CorpGlory/grafana-multibar-graph-panel#build) to build it from source. 9 | 10 | Supported Grafana versions: 5.3.3+ 11 | 12 | # Screenshots 13 | 14 | ![Screenshot](https://github.com/CorpGlory/grafana-multibar-graph-panel/raw/master/src/screenshots/screenshot-1.png) 15 | 16 | # Build 17 | 18 | ``` 19 | npm install 20 | npm run build 21 | ``` 22 | 23 | # See also 24 | 25 | * [grafana-plugin-template-webpack](https://github.com/CorpGlory/grafana-plugin-template-webpack) 26 | * [grafana-graph-panel](https://github.com/CorpGlory/grafana-graph-panel) 27 | * [@types/grafana](https://github.com/CorpGlory/types-grafana) 28 | * https://github.com/CorpGlory/grafana-progress-list 29 | * more about Grafana from CorpGlory: https://corpglory.com/t/grafana/ 30 | 31 | ## About CorpGlory Inc. 32 | The project developed by [CorpGlory Inc.](https://corpglory.com/), a company which provides high quality software development, data visualization, Grafana and monitoring consulting. 33 | -------------------------------------------------------------------------------- /src/histogram.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /** 4 | * Convert series into array of series values. 5 | * @param data Array of series 6 | */ 7 | export function getSeriesValues(dataList: any[]): number[] { 8 | const VALUE_INDEX = 0; 9 | let values = []; 10 | 11 | // Count histogam stats 12 | for (let i = 0; i < dataList.length; i++) { 13 | let series = dataList[i]; 14 | let datapoints = series.datapoints; 15 | for (let j = 0; j < datapoints.length; j++) { 16 | if (datapoints[j][VALUE_INDEX] !== null) { 17 | values.push(datapoints[j][VALUE_INDEX]); 18 | } 19 | } 20 | } 21 | 22 | return values; 23 | } 24 | 25 | /** 26 | * Convert array of values into timeseries-like histogram: 27 | * [[val_1, count_1], [val_2, count_2], ..., [val_n, count_n]] 28 | * @param values 29 | * @param bucketSize 30 | */ 31 | export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { 32 | let histogram = {}; 33 | 34 | for (let i = 0; i < values.length; i++) { 35 | let bound = getBucketBound(values[i], bucketSize); 36 | if (histogram[bound]) { 37 | histogram[bound] = histogram[bound] + 1; 38 | } else { 39 | histogram[bound] = 1; 40 | } 41 | } 42 | 43 | let histogam_series = _.map(histogram, (count, bound) => { 44 | return [Number(bound), count]; 45 | }); 46 | 47 | // Sort by Y axis values 48 | return _.sortBy(histogam_series, point => point[0]); 49 | } 50 | 51 | function getBucketBound(value: number, bucketSize: number): number { 52 | return Math.floor(value / bucketSize) * bucketSize; 53 | } 54 | -------------------------------------------------------------------------------- /src/vendor/grafana/colors.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import tinycolor from 'tinycolor2'; 3 | 4 | export const PALETTE_ROWS = 4; 5 | export const PALETTE_COLUMNS = 14; 6 | export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; 7 | export const OK_COLOR = 'rgba(11, 237, 50, 1)'; 8 | export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; 9 | export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; 10 | export const REGION_FILL_ALPHA = 0.09; 11 | 12 | let colors = [ 13 | '#7EB26D', 14 | '#EAB839', 15 | '#6ED0E0', 16 | '#EF843C', 17 | '#E24D42', 18 | '#1F78C1', 19 | '#BA43A9', 20 | '#705DA0', 21 | '#508642', 22 | '#CCA300', 23 | '#447EBC', 24 | '#C15C17', 25 | '#890F02', 26 | '#0A437C', 27 | '#6D1F62', 28 | '#584477', 29 | '#B7DBAB', 30 | '#F4D598', 31 | '#70DBED', 32 | '#F9BA8F', 33 | '#F29191', 34 | '#82B5D8', 35 | '#E5A8E2', 36 | '#AEA2E0', 37 | '#629E51', 38 | '#E5AC0E', 39 | '#64B0C8', 40 | '#E0752D', 41 | '#BF1B00', 42 | '#0A50A1', 43 | '#962D82', 44 | '#614D93', 45 | '#9AC48A', 46 | '#F2C96D', 47 | '#65C5DB', 48 | '#F9934E', 49 | '#EA6460', 50 | '#5195CE', 51 | '#D683CE', 52 | '#806EB7', 53 | '#3F6833', 54 | '#967302', 55 | '#2F575E', 56 | '#99440A', 57 | '#58140C', 58 | '#052B51', 59 | '#511749', 60 | '#3F2B5B', 61 | '#E0F9D7', 62 | '#FCEACA', 63 | '#CFFAFF', 64 | '#F9E2D2', 65 | '#FCE2DE', 66 | '#BADFF4', 67 | '#F9D9F9', 68 | '#DEDAF7', 69 | ]; 70 | 71 | export function sortColorsByHue(hexColors) { 72 | let hslColors = _.map(hexColors, hexToHsl); 73 | 74 | let sortedHSLColors: any = _.sortBy(hslColors, ['h']); 75 | sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); 76 | sortedHSLColors = _.map(sortedHSLColors, chunk => { 77 | return _.sortBy(chunk, 'l'); 78 | }); 79 | sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); 80 | 81 | return _.map(sortedHSLColors, hslToHex); 82 | } 83 | 84 | export function hexToHsl(color) { 85 | return tinycolor(color).toHsl(); 86 | } 87 | 88 | export function hslToHex(color) { 89 | return tinycolor(color).toHexString(); 90 | } 91 | 92 | export let sortedColors = sortColorsByHue(colors); 93 | export default colors; 94 | -------------------------------------------------------------------------------- /src/axes_editor.ts: -------------------------------------------------------------------------------- 1 | 2 | import kbn from 'grafana/app/core/utils/kbn'; 3 | 4 | export class AxesEditorCtrl { 5 | panel: any; 6 | panelCtrl: any; 7 | unitFormats: any; 8 | logScales: any; 9 | xAxisModes: any; 10 | xAxisStatOptions: any; 11 | xNameSegment: any; 12 | 13 | /** @ngInject **/ 14 | constructor(private $scope, private $q) { 15 | this.panelCtrl = $scope.ctrl; 16 | this.panel = this.panelCtrl.panel; 17 | this.$scope.ctrl = this; 18 | 19 | this.unitFormats = kbn.getUnitFormats(); 20 | 21 | this.logScales = { 22 | linear: 1, 23 | 'log (base 2)': 2, 24 | 'log (base 10)': 10, 25 | 'log (base 32)': 32, 26 | 'log (base 1024)': 1024, 27 | }; 28 | 29 | this.xAxisModes = { 30 | Time: 'time', 31 | Series: 'series', 32 | Histogram: 'histogram', 33 | // 'Data field': 'field', 34 | }; 35 | 36 | this.xAxisStatOptions = [ 37 | { text: 'Avg', value: 'avg' }, 38 | { text: 'Min', value: 'min' }, 39 | { text: 'Max', value: 'max' }, 40 | { text: 'Total', value: 'total' }, 41 | { text: 'Count', value: 'count' }, 42 | { text: 'Current', value: 'current' }, 43 | ]; 44 | 45 | if (this.panel.xaxis.mode === 'custom') { 46 | if (!this.panel.xaxis.name) { 47 | this.panel.xaxis.name = 'specify field'; 48 | } 49 | } 50 | } 51 | 52 | setUnitFormat(axis, subItem) { 53 | axis.format = subItem.value; 54 | this.panelCtrl.render(); 55 | } 56 | 57 | render() { 58 | this.panelCtrl.render(); 59 | } 60 | 61 | xAxisModeChanged() { 62 | this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode(); 63 | this.panelCtrl.onDataReceived(this.panelCtrl.dataList); 64 | } 65 | 66 | xAxisValueChanged() { 67 | this.panelCtrl.onDataReceived(this.panelCtrl.dataList); 68 | } 69 | 70 | getDataFieldNames(onlyNumbers) { 71 | var props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers); 72 | var items = props.map(prop => { 73 | return { text: prop, value: prop }; 74 | }); 75 | 76 | return this.$q.when(items); 77 | } 78 | } 79 | 80 | /** @ngInject **/ 81 | export function axesEditorComponent() { 82 | 'use strict'; 83 | return { 84 | restrict: 'E', 85 | scope: true, 86 | templateUrl: 'public/plugins/corpglory-multibar-graph-panel/partials/axes_editor.html', 87 | controller: AxesEditorCtrl, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /dist/partials/tab_legend.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Options
4 | 7 | 8 | 11 | 12 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
Values
24 | 25 |
26 | 29 | 30 | 31 | 34 | 35 |
36 | 37 |
38 | 41 | 42 | 43 | 46 | 47 |
48 | 49 |
50 | 53 | 54 | 55 |
56 | 57 | 58 |
59 |
60 |
61 | 62 |
63 |
Hide series
64 | 67 | 68 | 71 | 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/partials/tab_legend.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Options
4 | 7 | 8 | 11 | 12 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
Values
24 | 25 |
26 | 29 | 30 | 31 | 34 | 35 |
36 | 37 |
38 | 41 | 42 | 43 | 46 | 47 |
48 | 49 |
50 | 53 | 54 | 55 |
56 | 57 | 58 |
59 |
60 |
61 | 62 |
63 |
Hide series
64 | 67 | 68 | 71 | 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.stackpercent.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var options = { 3 | series: { 4 | stackpercent: null 5 | } // or number/string 6 | }; 7 | 8 | function init(plot) { 9 | 10 | // will be built up dynamically as a hash from x-value, or y-value if horizontal 11 | var stackBases = {}; 12 | var processed = false; 13 | var stackSums = {}; 14 | 15 | //set percentage for stacked chart 16 | function processRawData(plot, series, data, datapoints) { 17 | if (!processed) { 18 | processed = true; 19 | stackSums = getStackSums(plot.getData()); 20 | } 21 | if (series.stackpercent == true) { 22 | var num = data.length; 23 | series.percents = []; 24 | var key_idx = 0; 25 | var value_idx = 1; 26 | if (series.bars && series.bars.horizontal && series.bars.horizontal === true) { 27 | key_idx = 1; 28 | value_idx = 0; 29 | } 30 | for (var j = 0; j < num; j++) { 31 | var sum = stackSums[data[j][key_idx] + ""]; 32 | if (sum > 0) { 33 | series.percents.push(data[j][value_idx] * 100 / sum); 34 | } else { 35 | series.percents.push(0); 36 | } 37 | } 38 | } 39 | } 40 | 41 | //calculate summary 42 | function getStackSums(_data) { 43 | var data_len = _data.length; 44 | var sums = {}; 45 | if (data_len > 0) { 46 | //caculate summary 47 | for (var i = 0; i < data_len; i++) { 48 | if (_data[i].stackpercent) { 49 | var key_idx = 0; 50 | var value_idx = 1; 51 | if (_data[i].bars && _data[i].bars.horizontal && _data[i].bars.horizontal === true) { 52 | key_idx = 1; 53 | value_idx = 0; 54 | } 55 | var num = _data[i].data.length; 56 | for (var j = 0; j < num; j++) { 57 | var value = 0; 58 | if (_data[i].data[j][1] != null) { 59 | value = _data[i].data[j][value_idx]; 60 | } 61 | if (sums[_data[i].data[j][key_idx] + ""]) { 62 | sums[_data[i].data[j][key_idx] + ""] += value; 63 | } else { 64 | sums[_data[i].data[j][key_idx] + ""] = value; 65 | } 66 | 67 | } 68 | } 69 | } 70 | } 71 | return sums; 72 | } 73 | 74 | function stackData(plot, s, datapoints) { 75 | if (!s.stackpercent) return; 76 | if (!processed) { 77 | stackSums = getStackSums(plot.getData()); 78 | } 79 | var newPoints = []; 80 | 81 | 82 | var key_idx = 0; 83 | var value_idx = 1; 84 | if (s.bars && s.bars.horizontal && s.bars.horizontal === true) { 85 | key_idx = 1; 86 | value_idx = 0; 87 | } 88 | 89 | for (var i = 0; i < datapoints.points.length; i += 3) { 90 | // note that the values need to be turned into absolute y-values. 91 | // in other words, if you were to stack (x, y1), (x, y2), and (x, y3), 92 | // (each from different series, which is where stackBases comes in), 93 | // you'd want the new points to be (x, y1, 0), (x, y1+y2, y1), (x, y1+y2+y3, y1+y2) 94 | // generally, (x, thisValue + (base up to this point), + (base up to this point)) 95 | if (!stackBases[datapoints.points[i + key_idx]]) { 96 | stackBases[datapoints.points[i + key_idx]] = 0; 97 | } 98 | newPoints[i + key_idx] = datapoints.points[i + key_idx]; 99 | newPoints[i + value_idx] = datapoints.points[i + value_idx] + stackBases[datapoints.points[i + key_idx]]; 100 | newPoints[i + 2] = stackBases[datapoints.points[i + key_idx]]; 101 | stackBases[datapoints.points[i + key_idx]] += datapoints.points[i + value_idx]; 102 | // change points to percentage values 103 | // you may need to set yaxis:{ max = 100 } 104 | if ( stackSums[newPoints[i+key_idx]+""] > 0 ){ 105 | newPoints[i + value_idx] = newPoints[i + value_idx] * 100 / stackSums[newPoints[i + key_idx] + ""]; 106 | newPoints[i + 2] = newPoints[i + 2] * 100 / stackSums[newPoints[i + key_idx] + ""]; 107 | } else { 108 | newPoints[i + value_idx] = 0; 109 | newPoints[i + 2] = 0; 110 | } 111 | } 112 | 113 | datapoints.points = newPoints; 114 | } 115 | 116 | plot.hooks.processRawData.push(processRawData); 117 | plot.hooks.processDatapoints.push(stackData); 118 | } 119 | 120 | $.plot.plugins.push({ 121 | init: init, 122 | options: options, 123 | name: 'stackpercent', 124 | version: '0.1' 125 | }); 126 | })(jQuery); 127 | -------------------------------------------------------------------------------- /src/vendor/grafana/ticks.ts: -------------------------------------------------------------------------------- 1 | import { getDataMinMax } from './time_series2'; 2 | 3 | /** 4 | * Calculate tick step. 5 | * Implementation from d3-array (ticks.js) 6 | * https://github.com/d3/d3-array/blob/master/src/ticks.js 7 | * @param start Start value 8 | * @param stop End value 9 | * @param count Ticks count 10 | */ 11 | export function tickStep(start: number, stop: number, count: number): number { 12 | let e10 = Math.sqrt(50), 13 | e5 = Math.sqrt(10), 14 | e2 = Math.sqrt(2); 15 | 16 | let step0 = Math.abs(stop - start) / Math.max(0, count), 17 | step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), 18 | error = step0 / step1; 19 | 20 | if (error >= e10) { 21 | step1 *= 10; 22 | } else if (error >= e5) { 23 | step1 *= 5; 24 | } else if (error >= e2) { 25 | step1 *= 2; 26 | } 27 | 28 | return stop < start ? -step1 : step1; 29 | } 30 | 31 | export function getScaledDecimals(decimals, tick_size) { 32 | return decimals - Math.floor(Math.log(tick_size) / Math.LN10); 33 | } 34 | 35 | /** 36 | * Calculate tick size based on min and max values, number of ticks and precision. 37 | * Implementation from Flot. 38 | * @param min Axis minimum 39 | * @param max Axis maximum 40 | * @param noTicks Number of ticks 41 | * @param tickDecimals Tick decimal precision 42 | */ 43 | export function getFlotTickSize(min: number, max: number, noTicks: number, tickDecimals: number) { 44 | var delta = (max - min) / noTicks, 45 | dec = -Math.floor(Math.log(delta) / Math.LN10), 46 | maxDec = tickDecimals; 47 | 48 | var magn = Math.pow(10, -dec), 49 | norm = delta / magn, // norm is between 1.0 and 10.0 50 | size; 51 | 52 | if (norm < 1.5) { 53 | size = 1; 54 | } else if (norm < 3) { 55 | size = 2; 56 | // special case for 2.5, requires an extra decimal 57 | if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { 58 | size = 2.5; 59 | ++dec; 60 | } 61 | } else if (norm < 7.5) { 62 | size = 5; 63 | } else { 64 | size = 10; 65 | } 66 | 67 | size *= magn; 68 | 69 | return size; 70 | } 71 | 72 | /** 73 | * Calculate axis range (min and max). 74 | * Implementation from Flot. 75 | */ 76 | export function getFlotRange(panelMin, panelMax, datamin, datamax) { 77 | const autoscaleMargin = 0.02; 78 | 79 | let min = +(panelMin != null ? panelMin : datamin); 80 | let max = +(panelMax != null ? panelMax : datamax); 81 | let delta = max - min; 82 | 83 | if (delta === 0.0) { 84 | // Grafana fix: wide Y min and max using increased wideFactor 85 | // when all series values are the same 86 | var wideFactor = 0.25; 87 | var widen = Math.abs(max === 0 ? 1 : max * wideFactor); 88 | 89 | if (panelMin === null) { 90 | min -= widen; 91 | } 92 | // always widen max if we couldn't widen min to ensure we 93 | // don't fall into min == max which doesn't work 94 | if (panelMax == null || panelMin != null) { 95 | max += widen; 96 | } 97 | } else { 98 | // consider autoscaling 99 | var margin = autoscaleMargin; 100 | if (margin != null) { 101 | if (panelMin == null) { 102 | min -= delta * margin; 103 | // make sure we don't go below zero if all values 104 | // are positive 105 | if (min < 0 && datamin != null && datamin >= 0) { 106 | min = 0; 107 | } 108 | } 109 | if (panelMax == null) { 110 | max += delta * margin; 111 | if (max > 0 && datamax != null && datamax <= 0) { 112 | max = 0; 113 | } 114 | } 115 | } 116 | } 117 | return { min, max }; 118 | } 119 | 120 | /** 121 | * Calculate tick decimals. 122 | * Implementation from Flot. 123 | */ 124 | export function getFlotTickDecimals(data, axis) { 125 | let { datamin, datamax } = getDataMinMax(data); 126 | let { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); 127 | let noTicks = 3; 128 | let tickDecimals, maxDec; 129 | let delta = (max - min) / noTicks; 130 | let dec = -Math.floor(Math.log(delta) / Math.LN10); 131 | 132 | let magn = Math.pow(10, -dec); 133 | // norm is between 1.0 and 10.0 134 | let norm = delta / magn; 135 | let size; 136 | 137 | if (norm < 1.5) { 138 | size = 1; 139 | } else if (norm < 3) { 140 | size = 2; 141 | // special case for 2.5, requires an extra decimal 142 | if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { 143 | size = 2.5; 144 | ++dec; 145 | } 146 | } else if (norm < 7.5) { 147 | size = 5; 148 | } else { 149 | size = 10; 150 | } 151 | 152 | size *= magn; 153 | 154 | tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); 155 | // grafana addition 156 | const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); 157 | return { tickDecimals, scaledDecimals }; 158 | } 159 | -------------------------------------------------------------------------------- /src/thresholds_form.ts: -------------------------------------------------------------------------------- 1 | import coreModule from 'grafana/app/core/core_module'; 2 | 3 | export class ThresholdFormCtrl { 4 | panelCtrl: any; 5 | panel: any; 6 | disabled: boolean; 7 | 8 | /** @ngInject */ 9 | constructor($scope) { 10 | this.panel = this.panelCtrl.panel; 11 | 12 | if (this.panel.alert) { 13 | this.disabled = true; 14 | } 15 | 16 | var unbindDestroy = $scope.$on('$destroy', () => { 17 | this.panelCtrl.editingThresholds = false; 18 | this.panelCtrl.render(); 19 | unbindDestroy(); 20 | }); 21 | 22 | this.panelCtrl.editingThresholds = true; 23 | } 24 | 25 | addThreshold() { 26 | this.panel.thresholds.push({ 27 | value: undefined, 28 | colorMode: 'critical', 29 | op: 'gt', 30 | fill: true, 31 | line: true, 32 | }); 33 | this.panelCtrl.render(); 34 | } 35 | 36 | removeThreshold(index) { 37 | this.panel.thresholds.splice(index, 1); 38 | this.panelCtrl.render(); 39 | } 40 | 41 | render() { 42 | this.panelCtrl.render(); 43 | } 44 | 45 | onFillColorChange(index) { 46 | return newColor => { 47 | this.panel.thresholds[index].fillColor = newColor; 48 | this.render(); 49 | }; 50 | } 51 | 52 | onLineColorChange(index) { 53 | return newColor => { 54 | this.panel.thresholds[index].lineColor = newColor; 55 | this.render(); 56 | }; 57 | } 58 | } 59 | 60 | var template = ` 61 |
62 |
Thresholds
63 |

64 | Visual thresholds options disabled. 65 | Visit the Alert tab update your thresholds.
66 | To re-enable thresholds, the alert rule must be deleted from this panel. 67 |

68 |
69 |
70 |
71 | 72 |
73 | 74 |
75 |
76 | 78 |
79 | 81 |
82 | 83 |
84 | 85 |
86 | 89 |
90 |
91 | 92 | 94 | 95 |
96 | 97 | 98 | 99 | 100 |
101 | 102 | 104 | 105 |
106 | 107 | 108 | 109 | 110 |
111 | 112 |
113 | 118 |
119 |
120 | 121 |
122 | 125 |
126 |
127 |
128 | `; 129 | 130 | coreModule.directive('multibarGraphPanelThresholdForm', function() { 131 | return { 132 | restrict: 'E', 133 | template: template, 134 | controller: ThresholdFormCtrl, 135 | bindToController: true, 136 | controllerAs: 'ctrl', 137 | scope: { 138 | panelCtrl: '=', 139 | }, 140 | }; 141 | }); 142 | -------------------------------------------------------------------------------- /src/vendor/grafana/event_manager.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import moment from 'moment'; 3 | import tinycolor from 'tinycolor2'; 4 | import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; 5 | import { AnnotationEvent } from './event'; 6 | import { 7 | DEFAULT_ANNOTATION_COLOR, 8 | OK_COLOR, 9 | ALERTING_COLOR, 10 | NO_DATA_COLOR, 11 | REGION_FILL_ALPHA 12 | } from './colors'; 13 | 14 | export class EventManager { 15 | event: AnnotationEvent; 16 | editorOpen: boolean; 17 | 18 | constructor(private panelCtrl: MetricsPanelCtrl) { } 19 | 20 | editorClosed() { 21 | this.event = null; 22 | this.editorOpen = false; 23 | this.panelCtrl.render(); 24 | } 25 | 26 | editorOpened() { 27 | this.editorOpen = true; 28 | } 29 | 30 | updateTime(range) { 31 | if (!this.event) { 32 | this.event = new AnnotationEvent(); 33 | this.event.dashboardId = this.panelCtrl.dashboard.id; 34 | this.event.panelId = this.panelCtrl.panel.id; 35 | } 36 | 37 | // update time 38 | this.event.time = moment(range.from); 39 | this.event.isRegion = false; 40 | if (range.to) { 41 | this.event.timeEnd = moment(range.to); 42 | this.event.isRegion = true; 43 | } 44 | 45 | this.panelCtrl.render(); 46 | } 47 | 48 | editEvent(event, elem?) { 49 | this.event = event; 50 | this.panelCtrl.render(); 51 | } 52 | 53 | addFlotEvents(annotations, flotOptions) { 54 | if (!this.event && annotations.length === 0) { 55 | return; 56 | } 57 | 58 | var types = { 59 | $__alerting: { 60 | color: ALERTING_COLOR, 61 | position: 'BOTTOM', 62 | markerSize: 5, 63 | }, 64 | $__ok: { 65 | color: OK_COLOR, 66 | position: 'BOTTOM', 67 | markerSize: 5, 68 | }, 69 | $__no_data: { 70 | color: NO_DATA_COLOR, 71 | position: 'BOTTOM', 72 | markerSize: 5, 73 | }, 74 | $__editing: { 75 | color: DEFAULT_ANNOTATION_COLOR, 76 | position: 'BOTTOM', 77 | markerSize: 5, 78 | }, 79 | }; 80 | 81 | if (this.event) { 82 | if (this.event.isRegion) { 83 | annotations = [ 84 | { 85 | isRegion: true, 86 | min: this.event.time.valueOf(), 87 | timeEnd: this.event.timeEnd.valueOf(), 88 | text: this.event.text, 89 | eventType: '$__editing', 90 | editModel: this.event, 91 | }, 92 | ]; 93 | } else { 94 | annotations = [ 95 | { 96 | min: this.event.time.valueOf(), 97 | text: this.event.text, 98 | editModel: this.event, 99 | eventType: '$__editing', 100 | }, 101 | ]; 102 | } 103 | } else { 104 | // annotations from query 105 | for (var i = 0; i < annotations.length; i++) { 106 | var item = annotations[i]; 107 | 108 | // add properties used by jquery flot events 109 | item.min = item.time; 110 | item.max = item.time; 111 | item.eventType = item.source.name; 112 | 113 | if (item.newState) { 114 | item.eventType = '$__' + item.newState; 115 | continue; 116 | } 117 | 118 | if (!types[item.source.name]) { 119 | types[item.source.name] = { 120 | color: item.source.iconColor, 121 | position: 'BOTTOM', 122 | markerSize: 5, 123 | }; 124 | } 125 | } 126 | } 127 | 128 | let regions = getRegions(annotations); 129 | addRegionMarking(regions, flotOptions); 130 | 131 | let eventSectionHeight = 20; 132 | let eventSectionMargin = 7; 133 | flotOptions.grid.eventSectionHeight = eventSectionMargin; 134 | flotOptions.xaxis.eventSectionHeight = eventSectionHeight; 135 | 136 | flotOptions.events = { 137 | levels: _.keys(types).length + 1, 138 | data: annotations, 139 | types: types, 140 | manager: this, 141 | }; 142 | } 143 | } 144 | 145 | function getRegions(events) { 146 | return _.filter(events, 'isRegion'); 147 | } 148 | 149 | function addRegionMarking(regions, flotOptions) { 150 | let markings = flotOptions.grid.markings; 151 | let defaultColor = DEFAULT_ANNOTATION_COLOR; 152 | let fillColor; 153 | 154 | _.each(regions, region => { 155 | if (region.source) { 156 | fillColor = region.source.iconColor || defaultColor; 157 | } else { 158 | fillColor = defaultColor; 159 | } 160 | 161 | fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA); 162 | markings.push({ 163 | xaxis: { from: region.min, to: region.timeEnd }, 164 | color: fillColor, 165 | }); 166 | }); 167 | } 168 | 169 | function addAlphaToRGB(colorString: string, alpha: number): string { 170 | let color = tinycolor(colorString); 171 | if (color.isValid()) { 172 | color.setAlpha(alpha); 173 | return color.toRgbString(); 174 | } else { 175 | return colorString; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /dist/partials/axes_editor.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
Left Y
5 |
Right Y
6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
X-Axis
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 | 58 | 59 |
60 | 61 |
62 |
 63 | Use the input field above to override x-axis time-date display to suit your needs.
 64 | List of supported variables:
 65 |   %a: weekday name
 66 |   %b: month name
 67 |   %d: day of month, zero-padded (01-31)
 68 |   %e: day of month, space-padded ( 1-31)
 69 |   %H: hours, 24-hour time, zero-padded (00-23)
 70 |   %I: hours, 12-hour time, zero-padded (01-12)
 71 |   %m: month, zero-padded (01-12)
 72 |   %M: minutes, zero-padded (00-59)
 73 |   %q: quarter (1-4)
 74 |   %S: seconds, zero-padded (00-59)
 75 |   %y: year (two digits)
 76 |   %Y: year (four digits)
 77 |   %p: am/pm
 78 |   %P: AM/PM (uppercase version of %p)
 79 |   %w: weekday as number (0-6, 0 being Sunday)
 80 | 
 81 | Examples:
 82 | "%d %b" = "2 Nov"
 83 | "DoW index: %w" = "DoW index: 1"
 84 | "%y; %m; %d" = "18; 10; 25"
 85 |         
86 |
87 |
88 | 89 | 90 |
91 | 92 | 93 |
94 | 95 | 96 |
97 | 98 | 99 |
100 | 101 |
102 | 103 |
104 | -------------------------------------------------------------------------------- /src/partials/axes_editor.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
Left Y
5 |
Right Y
6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
X-Axis
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 | 58 | 59 |
60 | 61 |
62 |
 63 | Use the input field above to override x-axis time-date display to suit your needs.
 64 | List of supported variables:
 65 |   %a: weekday name
 66 |   %b: month name
 67 |   %d: day of month, zero-padded (01-31)
 68 |   %e: day of month, space-padded ( 1-31)
 69 |   %H: hours, 24-hour time, zero-padded (00-23)
 70 |   %I: hours, 12-hour time, zero-padded (01-12)
 71 |   %m: month, zero-padded (01-12)
 72 |   %M: minutes, zero-padded (00-59)
 73 |   %q: quarter (1-4)
 74 |   %S: seconds, zero-padded (00-59)
 75 |   %y: year (two digits)
 76 |   %Y: year (four digits)
 77 |   %p: am/pm
 78 |   %P: AM/PM (uppercase version of %p)
 79 |   %w: weekday as number (0-6, 0 being Sunday)
 80 | 
 81 | Examples:
 82 | "%d %b" = "2 Nov"
 83 | "DoW index: %w" = "DoW index: 1"
 84 | "%y; %m; %d" = "18; 10; 25"
 85 |         
86 |
87 |
88 | 89 | 90 |
91 | 92 | 93 |
94 | 95 | 96 |
97 | 98 | 99 |
100 | 101 |
102 | 103 |
104 | -------------------------------------------------------------------------------- /src/series_overrides_ctrl.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import angular from 'angular'; 3 | 4 | export class SeriesOverridesCtrl { 5 | /** @ngInject */ 6 | constructor($scope, $element, popoverSrv) { 7 | $scope.overrideMenu = []; 8 | $scope.currentOverrides = []; 9 | $scope.override = $scope.override || {}; 10 | 11 | $scope.addOverrideOption = function(name, propertyName, values) { 12 | var option = { 13 | text: name, 14 | propertyName: propertyName, 15 | index: $scope.overrideMenu.lenght, 16 | values: values, 17 | submenu: _.map(values, function(value) { 18 | return { text: String(value), value: value }; 19 | }), 20 | }; 21 | 22 | $scope.overrideMenu.push(option); 23 | }; 24 | 25 | $scope.setOverride = function(item, subItem) { 26 | // handle color overrides 27 | if (item.propertyName === 'color') { 28 | $scope.openColorSelector($scope.override['color']); 29 | return; 30 | } 31 | 32 | $scope.override[item.propertyName] = subItem.value; 33 | 34 | // automatically disable lines for this series and the fill bellow to series 35 | // can be removed by the user if they still want lines 36 | if (item.propertyName === 'fillBelowTo') { 37 | $scope.override['lines'] = false; 38 | $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false }); 39 | } 40 | 41 | $scope.updateCurrentOverrides(); 42 | $scope.ctrl.render(); 43 | }; 44 | 45 | $scope.colorSelected = function(color) { 46 | $scope.override['color'] = color; 47 | $scope.updateCurrentOverrides(); 48 | $scope.ctrl.render(); 49 | }; 50 | 51 | $scope.openColorSelector = function(color) { 52 | var fakeSeries = { color: color }; 53 | popoverSrv.show({ 54 | element: $element.find('.dropdown')[0], 55 | position: 'top center', 56 | openOn: 'click', 57 | template: '', 58 | model: { 59 | autoClose: true, 60 | colorSelected: $scope.colorSelected, 61 | series: fakeSeries, 62 | }, 63 | onClose: function() { 64 | $scope.ctrl.render(); 65 | }, 66 | }); 67 | }; 68 | 69 | $scope.removeOverride = function(option) { 70 | delete $scope.override[option.propertyName]; 71 | $scope.updateCurrentOverrides(); 72 | $scope.ctrl.refresh(); 73 | }; 74 | 75 | $scope.getSeriesNames = function() { 76 | return _.map($scope.ctrl.seriesList, function(series: any) { 77 | return series.alias; 78 | }); 79 | }; 80 | 81 | $scope.updateCurrentOverrides = function() { 82 | $scope.currentOverrides = []; 83 | _.each($scope.overrideMenu, function(option) { 84 | var value = $scope.override[option.propertyName]; 85 | if (_.isUndefined(value)) { 86 | return; 87 | } 88 | $scope.currentOverrides.push({ 89 | name: option.text, 90 | propertyName: option.propertyName, 91 | value: String(value), 92 | }); 93 | }); 94 | }; 95 | 96 | $scope.addOverrideOption('Bars', 'bars', [true, false]); 97 | $scope.addOverrideOption('Lines', 'lines', [true, false]); 98 | $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 99 | $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 100 | $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); 101 | $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); 102 | $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); 103 | $scope.addOverrideOption('Dashes', 'dashes', [true, false]); 104 | $scope.addOverrideOption('Dash Length', 'dashLength', [ 105 | 1, 106 | 2, 107 | 3, 108 | 4, 109 | 5, 110 | 6, 111 | 7, 112 | 8, 113 | 9, 114 | 10, 115 | 11, 116 | 12, 117 | 13, 118 | 14, 119 | 15, 120 | 16, 121 | 17, 122 | 18, 123 | 19, 124 | 20, 125 | ]); 126 | $scope.addOverrideOption('Dash Space', 'spaceLength', [ 127 | 1, 128 | 2, 129 | 3, 130 | 4, 131 | 5, 132 | 6, 133 | 7, 134 | 8, 135 | 9, 136 | 10, 137 | 11, 138 | 12, 139 | 13, 140 | 14, 141 | 15, 142 | 16, 143 | 17, 144 | 18, 145 | 19, 146 | 20, 147 | ]); 148 | $scope.addOverrideOption('Points', 'points', [true, false]); 149 | $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]); 150 | $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); 151 | $scope.addOverrideOption('Color', 'color', ['change']); 152 | $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); 153 | $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); 154 | $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); 155 | $scope.addOverrideOption('Legend', 'legend', [true, false]); 156 | $scope.updateCurrentOverrides(); 157 | } 158 | } 159 | 160 | angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); 161 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.crosshair.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for showing crosshairs when the mouse hovers over the plot. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin supports these options: 7 | 8 | crosshair: { 9 | mode: null or "x" or "y" or "xy" 10 | color: color 11 | lineWidth: number 12 | } 13 | 14 | Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical 15 | crosshair that lets you trace the values on the x axis, "y" enables a 16 | horizontal crosshair and "xy" enables them both. "color" is the color of the 17 | crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of 18 | the drawn lines (default is 1). 19 | 20 | The plugin also adds four public methods: 21 | 22 | - setCrosshair( pos ) 23 | 24 | Set the position of the crosshair. Note that this is cleared if the user 25 | moves the mouse. "pos" is in coordinates of the plot and should be on the 26 | form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple 27 | axes), which is coincidentally the same format as what you get from a 28 | "plothover" event. If "pos" is null, the crosshair is cleared. 29 | 30 | - clearCrosshair() 31 | 32 | Clear the crosshair. 33 | 34 | - lockCrosshair(pos) 35 | 36 | Cause the crosshair to lock to the current location, no longer updating if 37 | the user moves the mouse. Optionally supply a position (passed on to 38 | setCrosshair()) to move it to. 39 | 40 | Example usage: 41 | 42 | var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; 43 | $("#graph").bind( "plothover", function ( evt, position, item ) { 44 | if ( item ) { 45 | // Lock the crosshair to the data point being hovered 46 | myFlot.lockCrosshair({ 47 | x: item.datapoint[ 0 ], 48 | y: item.datapoint[ 1 ] 49 | }); 50 | } else { 51 | // Return normal crosshair operation 52 | myFlot.unlockCrosshair(); 53 | } 54 | }); 55 | 56 | - unlockCrosshair() 57 | 58 | Free the crosshair to move again after locking it. 59 | */ 60 | 61 | (function ($) { 62 | var options = { 63 | crosshair: { 64 | mode: null, // one of null, "x", "y" or "xy", 65 | color: "rgba(170, 0, 0, 0.80)", 66 | lineWidth: 1 67 | } 68 | }; 69 | 70 | function init(plot) { 71 | // position of crosshair in pixels 72 | var crosshair = { x: -1, y: -1, locked: false }; 73 | 74 | plot.setCrosshair = function setCrosshair(pos) { 75 | if (!pos) 76 | crosshair.x = -1; 77 | else { 78 | var o = plot.p2c(pos); 79 | crosshair.x = Math.max(0, Math.min(o.left, plot.width())); 80 | crosshair.y = Math.max(0, Math.min(o.top, plot.height())); 81 | } 82 | 83 | plot.triggerRedrawOverlay(); 84 | }; 85 | 86 | plot.clearCrosshair = plot.setCrosshair; // passes null for pos 87 | 88 | plot.lockCrosshair = function lockCrosshair(pos) { 89 | if (pos) 90 | plot.setCrosshair(pos); 91 | crosshair.locked = true; 92 | }; 93 | 94 | plot.unlockCrosshair = function unlockCrosshair() { 95 | crosshair.locked = false; 96 | }; 97 | 98 | function onMouseOut(e) { 99 | if (crosshair.locked) 100 | return; 101 | 102 | if (crosshair.x != -1) { 103 | crosshair.x = -1; 104 | plot.triggerRedrawOverlay(); 105 | } 106 | } 107 | 108 | function onMouseMove(e) { 109 | if (crosshair.locked) 110 | return; 111 | 112 | if (plot.getSelection && plot.getSelection()) { 113 | crosshair.x = -1; // hide the crosshair while selecting 114 | return; 115 | } 116 | 117 | var offset = plot.offset(); 118 | crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); 119 | crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); 120 | plot.triggerRedrawOverlay(); 121 | } 122 | 123 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 124 | if (!plot.getOptions().crosshair.mode) 125 | return; 126 | 127 | eventHolder.mouseout(onMouseOut); 128 | eventHolder.mousemove(onMouseMove); 129 | }); 130 | 131 | plot.hooks.drawOverlay.push(function (plot, ctx) { 132 | var c = plot.getOptions().crosshair; 133 | if (!c.mode) 134 | return; 135 | 136 | var plotOffset = plot.getPlotOffset(); 137 | 138 | ctx.save(); 139 | ctx.translate(plotOffset.left, plotOffset.top); 140 | 141 | if (crosshair.x != -1) { 142 | var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; 143 | 144 | ctx.strokeStyle = c.color; 145 | ctx.lineWidth = c.lineWidth; 146 | ctx.lineJoin = "round"; 147 | 148 | ctx.beginPath(); 149 | if (c.mode.indexOf("x") != -1) { 150 | var drawX = Math.floor(crosshair.x) + adj; 151 | ctx.moveTo(drawX, 0); 152 | ctx.lineTo(drawX, plot.height()); 153 | } 154 | if (c.mode.indexOf("y") != -1) { 155 | var drawY = Math.floor(crosshair.y) + adj; 156 | ctx.moveTo(0, drawY); 157 | ctx.lineTo(plot.width(), drawY); 158 | } 159 | ctx.stroke(); 160 | } 161 | ctx.restore(); 162 | }); 163 | 164 | plot.hooks.shutdown.push(function (plot, eventHolder) { 165 | eventHolder.unbind("mouseout", onMouseOut); 166 | eventHolder.unbind("mousemove", onMouseMove); 167 | }); 168 | } 169 | 170 | $.plot.plugins.push({ 171 | init: init, 172 | options: options, 173 | name: 'crosshair', 174 | version: '1.0' 175 | }); 176 | })(jQuery); 177 | -------------------------------------------------------------------------------- /src/data_processor.ts: -------------------------------------------------------------------------------- 1 | 2 | import _ from 'lodash'; 3 | import TimeSeries from './vendor/grafana/time_series2'; 4 | import colors from './vendor/grafana/colors'; 5 | 6 | export class DataProcessor { 7 | constructor(private panel) {} 8 | 9 | getSeriesList(options) { 10 | if (!options.dataList || options.dataList.length === 0) { 11 | return []; 12 | } 13 | 14 | // auto detect xaxis mode 15 | var firstItem; 16 | if (options.dataList && options.dataList.length > 0) { 17 | firstItem = options.dataList[0]; 18 | let autoDetectMode = this.getAutoDetectXAxisMode(firstItem); 19 | if (this.panel.xaxis.mode !== autoDetectMode) { 20 | this.panel.xaxis.mode = autoDetectMode; 21 | this.setPanelDefaultsForNewXAxisMode(); 22 | } 23 | } 24 | 25 | switch (this.panel.xaxis.mode) { 26 | case 'series': 27 | case 'time': { 28 | return options.dataList.map((item, index) => { 29 | return this.timeSeriesHandler(item, index, options); 30 | }); 31 | } 32 | case 'histogram': { 33 | let histogramDataList = [ 34 | { 35 | target: 'count', 36 | datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))), 37 | }, 38 | ]; 39 | return histogramDataList.map((item, index) => { 40 | return this.timeSeriesHandler(item, index, options); 41 | }); 42 | } 43 | case 'field': { 44 | return this.customHandler(firstItem); 45 | } 46 | } 47 | } 48 | 49 | getAutoDetectXAxisMode(firstItem) { 50 | switch (firstItem.type) { 51 | case 'docs': 52 | return 'field'; 53 | case 'table': 54 | return 'field'; 55 | default: { 56 | if (this.panel.xaxis.mode === 'series') { 57 | return 'series'; 58 | } 59 | if (this.panel.xaxis.mode === 'histogram') { 60 | return 'histogram'; 61 | } 62 | return 'time'; 63 | } 64 | } 65 | } 66 | 67 | setPanelDefaultsForNewXAxisMode() { 68 | switch (this.panel.xaxis.mode) { 69 | case 'time': { 70 | this.panel.bars = false; 71 | this.panel.lines = true; 72 | this.panel.points = false; 73 | this.panel.legend.show = true; 74 | this.panel.tooltip.shared = true; 75 | this.panel.xaxis.values = []; 76 | break; 77 | } 78 | case 'series': { 79 | this.panel.bars = true; 80 | this.panel.lines = false; 81 | this.panel.points = false; 82 | this.panel.stack = false; 83 | this.panel.legend.show = false; 84 | this.panel.tooltip.shared = false; 85 | this.panel.xaxis.values = ['total']; 86 | break; 87 | } 88 | case 'histogram': { 89 | this.panel.bars = true; 90 | this.panel.lines = false; 91 | this.panel.points = false; 92 | this.panel.stack = false; 93 | this.panel.legend.show = false; 94 | this.panel.tooltip.shared = false; 95 | break; 96 | } 97 | } 98 | } 99 | 100 | timeSeriesHandler(seriesData, index, options) { 101 | var datapoints = seriesData.datapoints || []; 102 | var alias = seriesData.target; 103 | 104 | var colorIndex = index % colors.length; 105 | var color = this.panel.aliasColors[alias] || colors[colorIndex]; 106 | 107 | var series = new TimeSeries({ 108 | datapoints: datapoints, 109 | alias: alias, 110 | color: color, 111 | unit: seriesData.unit, 112 | }); 113 | 114 | if (datapoints && datapoints.length > 0) { 115 | var last = datapoints[datapoints.length - 1][1]; 116 | var from = options.range.from; 117 | if (last - from < -10000) { 118 | series.isOutsideRange = true; 119 | } 120 | } 121 | 122 | return series; 123 | } 124 | 125 | customHandler(dataItem) { 126 | let nameField = this.panel.xaxis.name; 127 | if (!nameField) { 128 | throw { 129 | message: 'No field name specified to use for x-axis, check your axes settings', 130 | }; 131 | } 132 | return []; 133 | } 134 | 135 | validateXAxisSeriesValue() { 136 | switch (this.panel.xaxis.mode) { 137 | case 'series': { 138 | if (this.panel.xaxis.values.length === 0) { 139 | this.panel.xaxis.values = ['total']; 140 | return; 141 | } 142 | 143 | var validOptions = this.getXAxisValueOptions({}); 144 | var found = _.find(validOptions, { value: this.panel.xaxis.values[0] }); 145 | if (!found) { 146 | this.panel.xaxis.values = ['total']; 147 | } 148 | return; 149 | } 150 | } 151 | } 152 | 153 | getDataFieldNames(dataList, onlyNumbers) { 154 | if (dataList.length === 0) { 155 | return []; 156 | } 157 | 158 | let fields = []; 159 | var firstItem = dataList[0]; 160 | let fieldParts = []; 161 | 162 | function getPropertiesRecursive(obj) { 163 | _.forEach(obj, (value, key) => { 164 | if (_.isObject(value)) { 165 | fieldParts.push(key); 166 | getPropertiesRecursive(value); 167 | } else { 168 | if (!onlyNumbers || _.isNumber(value)) { 169 | let field = fieldParts.concat(key).join('.'); 170 | fields.push(field); 171 | } 172 | } 173 | }); 174 | fieldParts.pop(); 175 | } 176 | 177 | if (firstItem.type === 'docs') { 178 | if (firstItem.datapoints.length === 0) { 179 | return []; 180 | } 181 | getPropertiesRecursive(firstItem.datapoints[0]); 182 | } 183 | 184 | return fields; 185 | } 186 | 187 | getXAxisValueOptions(options) { 188 | switch (this.panel.xaxis.mode) { 189 | case 'series': { 190 | return [ 191 | { text: 'Avg', value: 'avg' }, 192 | { text: 'Min', value: 'min' }, 193 | { text: 'Max', value: 'max' }, 194 | { text: 'Total', value: 'total' }, 195 | { text: 'Count', value: 'count' }, 196 | ]; 197 | } 198 | } 199 | 200 | return []; 201 | } 202 | 203 | pluckDeep(obj: any, property: string) { 204 | let propertyParts = property.split('.'); 205 | let value = obj; 206 | for (let i = 0; i < propertyParts.length; ++i) { 207 | if (value[propertyParts[i]]) { 208 | value = value[propertyParts[i]]; 209 | } else { 210 | return undefined; 211 | } 212 | } 213 | return value; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.orderbars.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Flot plugin to order bars side by side. 3 | * 4 | * Released under the MIT license by Benjamin BUFFET, 20-Sep-2010. 5 | * 6 | * This plugin is an alpha version. 7 | * 8 | * To activate the plugin you must specify the parameter "order" for the specific serie : 9 | * 10 | * $.plot($("#placeholder"), [{ data: [ ... ], bars :{ order = null or integer }]) 11 | * 12 | * If 2 series have the same order param, they are ordered by the position in the array; 13 | * 14 | * The plugin adjust the point by adding a value depanding of the barwidth 15 | * Exemple for 3 series (barwidth : 0.1) : 16 | * 17 | * first bar dГ©calage : -0.15 18 | * second bar dГ©calage : -0.05 19 | * third bar dГ©calage : 0.05 20 | * 21 | */ 22 | 23 | (function ($) { 24 | function init(plot) { 25 | var orderedBarSeries; 26 | var nbOfBarsToOrder; 27 | var borderWidth; 28 | var borderWidthInXabsWidth; 29 | var pixelInXWidthEquivalent = 1; 30 | var isHorizontal = false; 31 | 32 | /* 33 | * This method add shift to x values 34 | */ 35 | function reOrderBars(plot, serie, datapoints) { 36 | var shiftedPoints = null; 37 | 38 | if (serieNeedToBeReordered(serie)) { 39 | checkIfGraphIsHorizontal(serie); 40 | calculPixel2XWidthConvert(plot); 41 | retrieveBarSeries(plot); 42 | calculBorderAndBarWidth(serie); 43 | 44 | if (nbOfBarsToOrder >= 2) { 45 | // var position = findPosition(serie); 46 | // var decallage = 0; 47 | 48 | // var centerBarShift = calculCenterBarShift(); 49 | 50 | // if (isBarAtLeftOfCenter(position)) { 51 | // decallage = -1 * (sumWidth(orderedBarSeries, position - 1, Math.floor(nbOfBarsToOrder / 2) - 1)) - centerBarShift; 52 | // } else { 53 | // decallage = sumWidth(orderedBarSeries, Math.ceil(nbOfBarsToOrder / 2), position - 2) + centerBarShift + borderWidthInXabsWidth * 2; 54 | // } 55 | 56 | var position = findPosition(serie); 57 | var decallage = sumWidth(orderedBarSeries, 0, position-1); 58 | 59 | shiftedPoints = shiftPoints(datapoints, serie, decallage); 60 | datapoints.points = shiftedPoints; 61 | } 62 | } 63 | return shiftedPoints; 64 | } 65 | 66 | function serieNeedToBeReordered(serie) { 67 | return serie.bars != null 68 | && serie.bars.show 69 | && serie.bars.order != null; 70 | } 71 | 72 | function calculPixel2XWidthConvert(plot) { 73 | var gridDimSize = isHorizontal ? plot.getPlaceholder().innerHeight() : plot.getPlaceholder().innerWidth(); 74 | var minMaxValues = isHorizontal ? getAxeMinMaxValues(plot.getData(), 1) : getAxeMinMaxValues(plot.getData(), 0); 75 | var AxeSize = minMaxValues[1] - minMaxValues[0]; 76 | pixelInXWidthEquivalent = AxeSize / gridDimSize; 77 | } 78 | 79 | function getAxeMinMaxValues(series, AxeIdx) { 80 | var minMaxValues = new Array(); 81 | for (var i = 0; i < series.length; i++) { 82 | if(series[i].data.length === 0) { 83 | continue; 84 | } 85 | minMaxValues[0] = series[i].data[0][AxeIdx]; 86 | minMaxValues[1] = series[i].data[series[i].data.length - 1][AxeIdx]; 87 | } 88 | return minMaxValues; 89 | } 90 | 91 | function retrieveBarSeries(plot) { 92 | orderedBarSeries = findOthersBarsToReOrders(plot.getData()); 93 | nbOfBarsToOrder = orderedBarSeries.length; 94 | } 95 | 96 | function findOthersBarsToReOrders(series) { 97 | var retSeries = new Array(); 98 | 99 | for (var i = 0; i < series.length; i++) { 100 | if (series[i].bars.order != null && series[i].bars.show) { 101 | retSeries.push(series[i]); 102 | } 103 | } 104 | 105 | return retSeries.sort(sortByOrder); 106 | } 107 | 108 | function sortByOrder(serie1, serie2) { 109 | var x = serie1.bars.order; 110 | var y = serie2.bars.order; 111 | return ((x < y) ? -1 : ((x > y) ? 1 : 0)); 112 | } 113 | 114 | function calculBorderAndBarWidth(serie) { 115 | borderWidth = serie.bars.lineWidth ? serie.bars.lineWidth : 2; 116 | borderWidthInXabsWidth = borderWidth * pixelInXWidthEquivalent; 117 | } 118 | 119 | function checkIfGraphIsHorizontal(serie) { 120 | if (serie.bars.horizontal) { 121 | isHorizontal = true; 122 | } 123 | } 124 | 125 | function findPosition(serie) { 126 | var pos = 0 127 | for (var i = 0; i < orderedBarSeries.length; ++i) { 128 | if (serie == orderedBarSeries[i]) { 129 | pos = i; 130 | break; 131 | } 132 | } 133 | 134 | return pos + 1; 135 | } 136 | 137 | function calculCenterBarShift() { 138 | var width = 0; 139 | 140 | if (nbOfBarsToOrder % 2 != 0) 141 | width = (orderedBarSeries[Math.ceil(nbOfBarsToOrder / 2)].bars.barWidth) / 2; 142 | 143 | return width; 144 | } 145 | 146 | function isBarAtLeftOfCenter(position) { 147 | return position <= Math.ceil(nbOfBarsToOrder / 2); 148 | } 149 | 150 | // function sumWidth(series, start, end) { 151 | // var totalWidth = 0; 152 | 153 | // for (var i = start; i <= end; i++) { 154 | // totalWidth += series[i].bars.barWidth + borderWidthInXabsWidth * 2; 155 | // } 156 | 157 | // return totalWidth; 158 | // } 159 | 160 | function sumWidth(series, start, end) { 161 | var totalWidth = 0; 162 | 163 | for (var i = start; i < end; i++) { 164 | totalWidth += series[i].bars.barWidth; 165 | } 166 | 167 | return totalWidth; 168 | } 169 | 170 | function shiftPoints(datapoints, serie, dx) { 171 | var ps = datapoints.pointsize; 172 | var points = datapoints.points; 173 | var j = 0; 174 | for (var i = isHorizontal ? 1 : 0; i < points.length; i += ps) { 175 | points[i] += dx; 176 | //Adding the new x value in the serie to be abble to display the right tooltip value, 177 | //using the index 3 to not overide the third index. 178 | serie.data[j][3] = points[i]; 179 | j++; 180 | } 181 | 182 | return points; 183 | } 184 | 185 | plot.hooks.processDatapoints.push(reOrderBars); 186 | 187 | } 188 | 189 | var options = { 190 | series: { 191 | bars: { order: null } // or number/string 192 | } 193 | }; 194 | 195 | $.plot.plugins.push({ 196 | init: init, 197 | options: options, 198 | name: "orderBars", 199 | version: "0.2" 200 | }); 201 | 202 | })(jQuery); 203 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.fillbetween.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for computing bottoms for filled line and bar charts. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The case: you've got two series that you want to fill the area between. In Flot 7 | terms, you need to use one as the fill bottom of the other. You can specify the 8 | bottom of each data point as the third coordinate manually, or you can use this 9 | plugin to compute it for you. 10 | 11 | In order to name the other series, you need to give it an id, like this: 12 | 13 | var dataset = [ 14 | { data: [ ... ], id: "foo" } , // use default bottom 15 | { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom 16 | ]; 17 | 18 | $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); 19 | 20 | As a convenience, if the id given is a number that doesn't appear as an id in 21 | the series, it is interpreted as the index in the array instead (so fillBetween: 22 | 0 can also mean the first series). 23 | 24 | Internally, the plugin modifies the datapoints in each series. For line series, 25 | extra data points might be inserted through interpolation. Note that at points 26 | where the bottom line is not defined (due to a null point or start/end of line), 27 | the current line will show a gap too. The algorithm comes from the 28 | jquery.flot.stack.js plugin, possibly some code could be shared. 29 | 30 | */ 31 | 32 | (function ( $ ) { 33 | 34 | var options = { 35 | series: { 36 | fillBetween: null // or number 37 | } 38 | }; 39 | 40 | function init( plot ) { 41 | 42 | function findBottomSeries( s, allseries ) { 43 | 44 | var i; 45 | 46 | for ( i = 0; i < allseries.length; ++i ) { 47 | if ( allseries[ i ].id === s.fillBetween ) { 48 | return allseries[ i ]; 49 | } 50 | } 51 | 52 | if ( typeof s.fillBetween === "number" ) { 53 | if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { 54 | return null; 55 | } 56 | return allseries[ s.fillBetween ]; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | function computeFillBottoms( plot, s, datapoints ) { 63 | if ( s.fillBetween == null ) { 64 | return; 65 | } 66 | 67 | var other = findBottomSeries( s, plot.getData() ); 68 | 69 | if ( !other ) { 70 | return; 71 | } 72 | 73 | var ps = datapoints.pointsize, 74 | points = datapoints.points, 75 | otherps = other.datapoints.pointsize, 76 | otherpoints = other.datapoints.points, 77 | newpoints = [], 78 | px, py, intery, qx, qy, bottom, 79 | withlines = s.lines.show, 80 | withbottom = ps > 2 && datapoints.format[2].y, 81 | withsteps = withlines && s.lines.steps, 82 | fromgap = true, 83 | i = 0, 84 | j = 0, 85 | l, m; 86 | 87 | while ( true ) { 88 | 89 | if ( i >= points.length ) { 90 | break; 91 | } 92 | 93 | l = newpoints.length; 94 | 95 | if ( points[ i ] == null ) { 96 | 97 | // copy gaps 98 | 99 | for ( m = 0; m < ps; ++m ) { 100 | newpoints.push( points[ i + m ] ); 101 | } 102 | 103 | i += ps; 104 | 105 | } else if ( j >= otherpoints.length ) { 106 | 107 | // for lines, we can't use the rest of the points 108 | 109 | if ( !withlines ) { 110 | for ( m = 0; m < ps; ++m ) { 111 | newpoints.push( points[ i + m ] ); 112 | } 113 | } 114 | 115 | i += ps; 116 | 117 | } else if ( otherpoints[ j ] == null ) { 118 | 119 | // oops, got a gap 120 | 121 | for ( m = 0; m < ps; ++m ) { 122 | newpoints.push( null ); 123 | } 124 | 125 | fromgap = true; 126 | j += otherps; 127 | 128 | } else { 129 | 130 | // cases where we actually got two points 131 | 132 | px = points[ i ]; 133 | py = points[ i + 1 ]; 134 | qx = otherpoints[ j ]; 135 | qy = otherpoints[ j + 1 ]; 136 | bottom = 0; 137 | 138 | if ( px === qx ) { 139 | 140 | for ( m = 0; m < ps; ++m ) { 141 | newpoints.push( points[ i + m ] ); 142 | } 143 | 144 | //newpoints[ l + 1 ] += qy; 145 | bottom = qy; 146 | 147 | i += ps; 148 | j += otherps; 149 | 150 | } else if ( px > qx ) { 151 | 152 | // we got past point below, might need to 153 | // insert interpolated extra point 154 | 155 | if ( withlines && i > 0 && points[ i - ps ] != null ) { 156 | intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); 157 | newpoints.push( qx ); 158 | newpoints.push( intery ); 159 | for ( m = 2; m < ps; ++m ) { 160 | newpoints.push( points[ i + m ] ); 161 | } 162 | bottom = qy; 163 | } 164 | 165 | j += otherps; 166 | 167 | } else { // px < qx 168 | 169 | // if we come from a gap, we just skip this point 170 | 171 | if ( fromgap && withlines ) { 172 | i += ps; 173 | continue; 174 | } 175 | 176 | for ( m = 0; m < ps; ++m ) { 177 | newpoints.push( points[ i + m ] ); 178 | } 179 | 180 | // we might be able to interpolate a point below, 181 | // this can give us a better y 182 | 183 | if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { 184 | bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); 185 | } 186 | 187 | //newpoints[l + 1] += bottom; 188 | 189 | i += ps; 190 | } 191 | 192 | fromgap = false; 193 | 194 | if ( l !== newpoints.length && withbottom ) { 195 | newpoints[ l + 2 ] = bottom; 196 | } 197 | } 198 | 199 | // maintain the line steps invariant 200 | 201 | if ( withsteps && l !== newpoints.length && l > 0 && 202 | newpoints[ l ] !== null && 203 | newpoints[ l ] !== newpoints[ l - ps ] && 204 | newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { 205 | for (m = 0; m < ps; ++m) { 206 | newpoints[ l + ps + m ] = newpoints[ l + m ]; 207 | } 208 | newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; 209 | } 210 | } 211 | 212 | datapoints.points = newpoints; 213 | } 214 | 215 | plot.hooks.processDatapoints.push( computeFillBottoms ); 216 | } 217 | 218 | $.plot.plugins.push({ 219 | init: init, 220 | options: options, 221 | name: "fillbetween", 222 | version: "1.0" 223 | }); 224 | 225 | })(jQuery); 226 | -------------------------------------------------------------------------------- /dist/partials/tab_display.html: -------------------------------------------------------------------------------- 1 |
2 | 19 | 20 |
21 |
22 |
Draw Modes
23 | 24 | 25 | 26 |
27 |
28 |
Mode Options
29 | 30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
Hover tooltip
54 |
55 | 56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
Stacking & Null value
76 | 77 | 78 | 79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 |
Labels
90 |
91 | 92 |
93 | 94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 |
Series specific overrides Regex match example: /server[0-3]/i
103 |
104 |
105 | 106 |
107 |
108 | 109 |
110 |
111 | 120 |
121 | 122 |
123 | 124 | 125 |
126 | 127 |
128 |
129 |
130 | 131 |
132 | 135 |
136 |
137 |
138 | 139 | 142 |
143 | 144 |
145 | 146 |
147 | 148 | 149 | -------------------------------------------------------------------------------- /src/partials/tab_display.html: -------------------------------------------------------------------------------- 1 |
2 | 19 | 20 |
21 |
22 |
Draw Modes
23 | 24 | 25 | 26 |
27 |
28 |
Mode Options
29 | 30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
Hover tooltip
54 |
55 | 56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
Stacking & Null value
76 | 77 | 78 | 79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 |
Labels
90 |
91 | 92 |
93 | 94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 |
Series specific overrides Regex match example: /server[0-3]/i
103 |
104 |
105 | 106 |
107 |
108 | 109 |
110 |
111 | 120 |
121 | 122 |
123 | 124 | 125 |
126 | 127 |
128 |
129 |
130 | 131 |
132 | 135 |
136 |
137 |
138 | 139 | 142 |
143 | 144 |
145 | 146 |
147 | 148 | 149 | -------------------------------------------------------------------------------- /src/threshold_manager.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'lodash'; 3 | 4 | export class ThresholdManager { 5 | plot: any; 6 | placeholder: any; 7 | height: any; 8 | thresholds: any; 9 | needsCleanup: boolean; 10 | hasSecondYAxis: any; 11 | 12 | constructor(private panelCtrl) {} 13 | 14 | getHandleHtml(handleIndex, model, valueStr) { 15 | var stateClass = model.colorMode; 16 | if (model.colorMode === 'custom') { 17 | stateClass = 'critical'; 18 | } 19 | 20 | return ` 21 |
22 |
23 |
24 |
25 | 26 | ${valueStr} 27 |
28 |
`; 29 | } 30 | 31 | initDragging(evt) { 32 | var handleElem = $(evt.currentTarget).parents('.alert-handle-wrapper'); 33 | var handleIndex = $(evt.currentTarget).data('handleIndex'); 34 | 35 | var lastY = null; 36 | var posTop; 37 | var plot = this.plot; 38 | var panelCtrl = this.panelCtrl; 39 | var model = this.thresholds[handleIndex]; 40 | 41 | function dragging(evt) { 42 | if (lastY === null) { 43 | lastY = evt.clientY; 44 | } else { 45 | var diff = evt.clientY - lastY; 46 | posTop = posTop + diff; 47 | lastY = evt.clientY; 48 | handleElem.css({ top: posTop + diff }); 49 | } 50 | } 51 | 52 | function stopped() { 53 | // calculate graph level 54 | var graphValue = plot.c2p({ left: 0, top: posTop }).y; 55 | graphValue = parseInt(graphValue.toFixed(0)); 56 | model.value = graphValue; 57 | 58 | handleElem.off('mousemove', dragging); 59 | handleElem.off('mouseup', dragging); 60 | handleElem.off('mouseleave', dragging); 61 | 62 | // trigger digest and render 63 | panelCtrl.$scope.$apply(function() { 64 | panelCtrl.render(); 65 | panelCtrl.events.emit('threshold-changed', { 66 | threshold: model, 67 | handleIndex: handleIndex, 68 | }); 69 | }); 70 | } 71 | 72 | lastY = null; 73 | posTop = handleElem.position().top; 74 | 75 | handleElem.on('mousemove', dragging); 76 | handleElem.on('mouseup', stopped); 77 | handleElem.on('mouseleave', stopped); 78 | } 79 | 80 | cleanUp() { 81 | this.placeholder.find('.alert-handle-wrapper').remove(); 82 | this.needsCleanup = false; 83 | } 84 | 85 | renderHandle(handleIndex, defaultHandleTopPos) { 86 | var model = this.thresholds[handleIndex]; 87 | var value = model.value; 88 | var valueStr = value; 89 | var handleTopPos = 0; 90 | 91 | // handle no value 92 | if (!_.isNumber(value)) { 93 | valueStr = ''; 94 | handleTopPos = defaultHandleTopPos; 95 | } else { 96 | var valueCanvasPos = this.plot.p2c({ x: 0, y: value }); 97 | handleTopPos = Math.round(Math.min(Math.max(valueCanvasPos.top, 0), this.height) - 6); 98 | } 99 | 100 | var handleElem = $(this.getHandleHtml(handleIndex, model, valueStr)); 101 | this.placeholder.append(handleElem); 102 | 103 | handleElem.toggleClass('alert-handle-wrapper--no-value', valueStr === ''); 104 | handleElem.css({ top: handleTopPos }); 105 | } 106 | 107 | shouldDrawHandles() { 108 | return !this.hasSecondYAxis && this.panelCtrl.editingThresholds && this.panelCtrl.panel.thresholds.length > 0; 109 | } 110 | 111 | prepare(elem, data) { 112 | this.hasSecondYAxis = false; 113 | for (var i = 0; i < data.length; i++) { 114 | if (data[i].yaxis > 1) { 115 | this.hasSecondYAxis = true; 116 | break; 117 | } 118 | } 119 | 120 | if (this.shouldDrawHandles()) { 121 | var thresholdMargin = this.panelCtrl.panel.thresholds.length > 1 ? '220px' : '110px'; 122 | elem.css('margin-right', thresholdMargin); 123 | } else if (this.needsCleanup) { 124 | elem.css('margin-right', '0'); 125 | } 126 | } 127 | 128 | draw(plot) { 129 | this.thresholds = this.panelCtrl.panel.thresholds; 130 | this.plot = plot; 131 | this.placeholder = plot.getPlaceholder(); 132 | 133 | if (this.needsCleanup) { 134 | this.cleanUp(); 135 | } 136 | 137 | if (!this.shouldDrawHandles()) { 138 | return; 139 | } 140 | 141 | this.height = plot.height(); 142 | 143 | if (this.thresholds.length > 0) { 144 | this.renderHandle(0, 10); 145 | } 146 | if (this.thresholds.length > 1) { 147 | this.renderHandle(1, this.height - 30); 148 | } 149 | 150 | this.placeholder.off('mousedown', '.alert-handle'); 151 | this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this)); 152 | this.needsCleanup = true; 153 | } 154 | 155 | addFlotOptions(options, panel) { 156 | if (!panel.thresholds || panel.thresholds.length === 0) { 157 | return; 158 | } 159 | 160 | var gtLimit = Infinity; 161 | var ltLimit = -Infinity; 162 | var i, threshold, other; 163 | 164 | for (i = 0; i < panel.thresholds.length; i++) { 165 | threshold = panel.thresholds[i]; 166 | if (!_.isNumber(threshold.value)) { 167 | continue; 168 | } 169 | 170 | var limit; 171 | switch (threshold.op) { 172 | case 'gt': { 173 | limit = gtLimit; 174 | // if next threshold is less then op and greater value, then use that as limit 175 | if (panel.thresholds.length > i + 1) { 176 | other = panel.thresholds[i + 1]; 177 | if (other.value > threshold.value) { 178 | limit = other.value; 179 | ltLimit = limit; 180 | } 181 | } 182 | break; 183 | } 184 | case 'lt': { 185 | limit = ltLimit; 186 | // if next threshold is less then op and greater value, then use that as limit 187 | if (panel.thresholds.length > i + 1) { 188 | other = panel.thresholds[i + 1]; 189 | if (other.value < threshold.value) { 190 | limit = other.value; 191 | gtLimit = limit; 192 | } 193 | } 194 | break; 195 | } 196 | } 197 | 198 | var fillColor, lineColor; 199 | switch (threshold.colorMode) { 200 | case 'critical': { 201 | fillColor = 'rgba(234, 112, 112, 0.12)'; 202 | lineColor = 'rgba(237, 46, 24, 0.60)'; 203 | break; 204 | } 205 | case 'warning': { 206 | fillColor = 'rgba(235, 138, 14, 0.12)'; 207 | lineColor = 'rgba(247, 149, 32, 0.60)'; 208 | break; 209 | } 210 | case 'ok': { 211 | fillColor = 'rgba(11, 237, 50, 0.090)'; 212 | lineColor = 'rgba(6,163,69, 0.60)'; 213 | break; 214 | } 215 | case 'custom': { 216 | fillColor = threshold.fillColor; 217 | lineColor = threshold.lineColor; 218 | break; 219 | } 220 | } 221 | 222 | // fill 223 | if (threshold.fill) { 224 | options.grid.markings.push({ 225 | yaxis: { from: threshold.value, to: limit }, 226 | color: fillColor, 227 | }); 228 | } 229 | if (threshold.line) { 230 | options.grid.markings.push({ 231 | yaxis: { from: threshold.value, to: threshold.value }, 232 | color: lineColor, 233 | }); 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.stack.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for stacking data sets rather than overlyaing them. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin assumes the data is sorted on x (or y if stacking horizontally). 7 | For line charts, it is assumed that if a line has an undefined gap (from a 8 | null point), then the line above it should have the same gap - insert zeros 9 | instead of "null" if you want another behaviour. This also holds for the start 10 | and end of the chart. Note that stacking a mix of positive and negative values 11 | in most instances doesn't make sense (so it looks weird). 12 | 13 | Two or more series are stacked when their "stack" attribute is set to the same 14 | key (which can be any number or string or just "true"). To specify the default 15 | stack, you can set the stack option like this: 16 | 17 | series: { 18 | stack: null/false, true, or a key (number/string) 19 | } 20 | 21 | You can also specify it for a single series, like this: 22 | 23 | $.plot( $("#placeholder"), [{ 24 | data: [ ... ], 25 | stack: true 26 | }]) 27 | 28 | The stacking order is determined by the order of the data series in the array 29 | (later series end up on top of the previous). 30 | 31 | Internally, the plugin modifies the datapoints in each series, adding an 32 | offset to the y value. For line series, extra data points are inserted through 33 | interpolation. If there's a second y value, it's also adjusted (e.g for bar 34 | charts or filled areas). 35 | 36 | */ 37 | 38 | (function ($) { 39 | var options = { 40 | series: { stack: null } // or number/string 41 | }; 42 | 43 | function init(plot) { 44 | function findMatchingSeries(s, allseries) { 45 | var res = null; 46 | for (var i = 0; i < allseries.length; ++i) { 47 | if (s == allseries[i]) 48 | break; 49 | 50 | if (allseries[i].stack == s.stack) 51 | res = allseries[i]; 52 | } 53 | 54 | return res; 55 | } 56 | 57 | function stackData(plot, s, datapoints) { 58 | if (s.stack == null || s.stack === false) 59 | return; 60 | 61 | var other = findMatchingSeries(s, plot.getData()); 62 | if (!other) 63 | return; 64 | 65 | var ps = datapoints.pointsize, 66 | points = datapoints.points, 67 | otherps = other.datapoints.pointsize, 68 | otherpoints = other.datapoints.points, 69 | newpoints = [], 70 | px, py, intery, qx, qy, bottom, 71 | withlines = s.lines.show, 72 | horizontal = s.bars.horizontal, 73 | withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), 74 | withsteps = withlines && s.lines.steps, 75 | keyOffset = horizontal ? 1 : 0, 76 | accumulateOffset = horizontal ? 0 : 1, 77 | i = 0, j = 0, l, m; 78 | 79 | while (true) { 80 | if (i >= points.length && j >= otherpoints.length) 81 | break; 82 | 83 | l = newpoints.length; 84 | 85 | if (i < points.length && points[i] == null) { 86 | // copy gaps 87 | for (m = 0; m < ps; ++m) 88 | newpoints.push(points[i + m]); 89 | i += ps; 90 | } 91 | else if (i >= points.length) { 92 | // take the remaining points from the previous series 93 | for (m = 0; m < ps; ++m) 94 | newpoints.push(otherpoints[j + m]); 95 | if (withbottom) 96 | newpoints[l + 2] = otherpoints[j + accumulateOffset]; 97 | j += otherps; 98 | } 99 | else if (j >= otherpoints.length) { 100 | // take the remaining points from the current series 101 | for (m = 0; m < ps; ++m) 102 | newpoints.push(points[i + m]); 103 | i += ps; 104 | } 105 | else if (j < otherpoints.length && otherpoints[j] == null) { 106 | // ignore point 107 | j += otherps; 108 | } 109 | else { 110 | // cases where we actually got two points 111 | px = points[i + keyOffset]; 112 | py = points[i + accumulateOffset]; 113 | qx = otherpoints[j + keyOffset]; 114 | qy = otherpoints[j + accumulateOffset]; 115 | bottom = 0; 116 | 117 | if (px == qx) { 118 | for (m = 0; m < ps; ++m) 119 | newpoints.push(points[i + m]); 120 | 121 | newpoints[l + accumulateOffset] += qy; 122 | bottom = qy; 123 | 124 | i += ps; 125 | j += otherps; 126 | } 127 | else if (px > qx) { 128 | // take the point from the previous series so that next series will correctly stack 129 | if (i == 0) { 130 | for (m = 0; m < ps; ++m) 131 | newpoints.push(otherpoints[j + m]); 132 | bottom = qy; 133 | } 134 | // we got past point below, might need to 135 | // insert interpolated extra point 136 | if (i > 0 && points[i - ps] != null) { 137 | intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); 138 | newpoints.push(qx); 139 | newpoints.push(intery + qy); 140 | for (m = 2; m < ps; ++m) 141 | newpoints.push(points[i + m]); 142 | bottom = qy; 143 | } 144 | 145 | j += otherps; 146 | } 147 | else { // px < qx 148 | for (m = 0; m < ps; ++m) 149 | newpoints.push(points[i + m]); 150 | 151 | // we might be able to interpolate a point below, 152 | // this can give us a better y 153 | if (j > 0 && otherpoints[j - otherps] != null) 154 | bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); 155 | 156 | newpoints[l + accumulateOffset] += bottom; 157 | 158 | i += ps; 159 | } 160 | 161 | fromgap = false; 162 | 163 | if (l != newpoints.length && withbottom) 164 | newpoints[l + 2] = bottom; 165 | } 166 | 167 | // maintain the line steps invariant 168 | if (withsteps && l != newpoints.length && l > 0 169 | && newpoints[l] != null 170 | && newpoints[l] != newpoints[l - ps] 171 | && newpoints[l + 1] != newpoints[l - ps + 1]) { 172 | for (m = 0; m < ps; ++m) 173 | newpoints[l + ps + m] = newpoints[l + m]; 174 | newpoints[l + 1] = newpoints[l - ps + 1]; 175 | } 176 | } 177 | 178 | datapoints.points = newpoints; 179 | } 180 | 181 | plot.hooks.processDatapoints.push(stackData); 182 | } 183 | 184 | $.plot.plugins.push({ 185 | init: init, 186 | options: options, 187 | name: 'stack', 188 | version: '1.2' 189 | }); 190 | })(jQuery); 191 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.dashes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery.flot.dashes 3 | * 4 | * options = { 5 | * series: { 6 | * dashes: { 7 | * 8 | * // show 9 | * // default: false 10 | * // Whether to show dashes for the series. 11 | * show: , 12 | * 13 | * // lineWidth 14 | * // default: 2 15 | * // The width of the dashed line in pixels. 16 | * lineWidth: , 17 | * 18 | * // dashLength 19 | * // default: 10 20 | * // Controls the length of the individual dashes and the amount of 21 | * // space between them. 22 | * // If this is a number, the dashes and spaces will have that length. 23 | * // If this is an array, it is read as [ dashLength, spaceLength ] 24 | * dashLength: or 25 | * } 26 | * } 27 | * } 28 | */ 29 | (function($){ 30 | 31 | function init(plot) { 32 | 33 | plot.hooks.processDatapoints.push(function(plot, series, datapoints) { 34 | 35 | if (!series.dashes.show) return; 36 | 37 | plot.hooks.draw.push(function(plot, ctx) { 38 | 39 | var plotOffset = plot.getPlotOffset(), 40 | axisx = series.xaxis, 41 | axisy = series.yaxis; 42 | 43 | function plotDashes(xoffset, yoffset) { 44 | 45 | var points = datapoints.points, 46 | ps = datapoints.pointsize, 47 | prevx = null, 48 | prevy = null, 49 | dashRemainder = 0, 50 | dashOn = true, 51 | dashOnLength, 52 | dashOffLength; 53 | 54 | if (series.dashes.dashLength[0]) { 55 | dashOnLength = series.dashes.dashLength[0]; 56 | if (series.dashes.dashLength[1]) { 57 | dashOffLength = series.dashes.dashLength[1]; 58 | } else { 59 | dashOffLength = dashOnLength; 60 | } 61 | } else { 62 | dashOffLength = dashOnLength = series.dashes.dashLength; 63 | } 64 | 65 | ctx.beginPath(); 66 | 67 | for (var i = ps; i < points.length; i += ps) { 68 | 69 | var x1 = points[i - ps], 70 | y1 = points[i - ps + 1], 71 | x2 = points[i], 72 | y2 = points[i + 1]; 73 | 74 | if (x1 == null || x2 == null) continue; 75 | 76 | // clip with ymin 77 | if (y1 <= y2 && y1 < axisy.min) { 78 | if (y2 < axisy.min) continue; // line segment is outside 79 | // compute new intersection point 80 | x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 81 | y1 = axisy.min; 82 | } else if (y2 <= y1 && y2 < axisy.min) { 83 | if (y1 < axisy.min) continue; 84 | x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; 85 | y2 = axisy.min; 86 | } 87 | 88 | // clip with ymax 89 | if (y1 >= y2 && y1 > axisy.max) { 90 | if (y2 > axisy.max) continue; 91 | x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 92 | y1 = axisy.max; 93 | } else if (y2 >= y1 && y2 > axisy.max) { 94 | if (y1 > axisy.max) continue; 95 | x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; 96 | y2 = axisy.max; 97 | } 98 | 99 | // clip with xmin 100 | if (x1 <= x2 && x1 < axisx.min) { 101 | if (x2 < axisx.min) continue; 102 | y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 103 | x1 = axisx.min; 104 | } else if (x2 <= x1 && x2 < axisx.min) { 105 | if (x1 < axisx.min) continue; 106 | y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; 107 | x2 = axisx.min; 108 | } 109 | 110 | // clip with xmax 111 | if (x1 >= x2 && x1 > axisx.max) { 112 | if (x2 > axisx.max) continue; 113 | y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 114 | x1 = axisx.max; 115 | } else if (x2 >= x1 && x2 > axisx.max) { 116 | if (x1 > axisx.max) continue; 117 | y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; 118 | x2 = axisx.max; 119 | } 120 | 121 | if (x1 != prevx || y1 != prevy) { 122 | ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); 123 | } 124 | 125 | var ax1 = axisx.p2c(x1) + xoffset, 126 | ay1 = axisy.p2c(y1) + yoffset, 127 | ax2 = axisx.p2c(x2) + xoffset, 128 | ay2 = axisy.p2c(y2) + yoffset, 129 | dashOffset; 130 | 131 | function lineSegmentOffset(segmentLength) { 132 | 133 | var c = Math.sqrt(Math.pow(ax2 - ax1, 2) + Math.pow(ay2 - ay1, 2)); 134 | 135 | if (c <= segmentLength) { 136 | return { 137 | deltaX: ax2 - ax1, 138 | deltaY: ay2 - ay1, 139 | distance: c, 140 | remainder: segmentLength - c 141 | } 142 | } else { 143 | var xsign = ax2 > ax1 ? 1 : -1, 144 | ysign = ay2 > ay1 ? 1 : -1; 145 | return { 146 | deltaX: xsign * Math.sqrt(Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), 147 | deltaY: ysign * Math.sqrt(Math.pow(segmentLength, 2) - Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), 148 | distance: segmentLength, 149 | remainder: 0 150 | }; 151 | } 152 | } 153 | //-end lineSegmentOffset 154 | 155 | do { 156 | 157 | dashOffset = lineSegmentOffset( 158 | dashRemainder > 0 ? dashRemainder : 159 | dashOn ? dashOnLength : dashOffLength); 160 | 161 | if (dashOffset.deltaX != 0 || dashOffset.deltaY != 0) { 162 | if (dashOn) { 163 | ctx.lineTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); 164 | } else { 165 | ctx.moveTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); 166 | } 167 | } 168 | 169 | dashOn = !dashOn; 170 | dashRemainder = dashOffset.remainder; 171 | ax1 += dashOffset.deltaX; 172 | ay1 += dashOffset.deltaY; 173 | 174 | } while (dashOffset.distance > 0); 175 | 176 | prevx = x2; 177 | prevy = y2; 178 | } 179 | 180 | ctx.stroke(); 181 | } 182 | //-end plotDashes 183 | 184 | ctx.save(); 185 | ctx.translate(plotOffset.left, plotOffset.top); 186 | ctx.lineJoin = 'round'; 187 | 188 | var lw = series.dashes.lineWidth, 189 | sw = series.shadowSize; 190 | 191 | // FIXME: consider another form of shadow when filling is turned on 192 | if (lw > 0 && sw > 0) { 193 | // draw shadow as a thick and thin line with transparency 194 | ctx.lineWidth = sw; 195 | ctx.strokeStyle = "rgba(0,0,0,0.1)"; 196 | // position shadow at angle from the mid of line 197 | var angle = Math.PI/18; 198 | plotDashes(Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2)); 199 | ctx.lineWidth = sw/2; 200 | plotDashes(Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4)); 201 | } 202 | 203 | ctx.lineWidth = lw; 204 | ctx.strokeStyle = series.color; 205 | 206 | if (lw > 0) { 207 | plotDashes(0, 0); 208 | } 209 | 210 | ctx.restore(); 211 | 212 | }); 213 | //-end draw hook 214 | 215 | }); 216 | //-end processDatapoints hook 217 | 218 | } 219 | //-end init 220 | 221 | $.plot.plugins.push({ 222 | init: init, 223 | options: { 224 | series: { 225 | dashes: { 226 | show: false, 227 | lineWidth: 2, 228 | dashLength: 10 229 | } 230 | } 231 | }, 232 | name: 'dashes', 233 | version: '0.1' 234 | }); 235 | 236 | })(jQuery); 237 | -------------------------------------------------------------------------------- /src/graph_legend.ts: -------------------------------------------------------------------------------- 1 | import PerfectScrollbar from 'perfect-scrollbar'; 2 | import _ from 'lodash'; 3 | import $ from 'jquery'; 4 | 5 | 6 | export class GraphLegend { 7 | firstRender = true; 8 | ctrl: any; 9 | panel: any; 10 | data; 11 | seriesList; 12 | legendScrollbar; 13 | 14 | constructor(private $elem: JQuery, private popoverSrv, scope) { 15 | this.ctrl = scope.ctrl; 16 | this.panel = this.ctrl.panel; 17 | scope.$on('$destroy', () => { 18 | if (this.legendScrollbar) { 19 | this.legendScrollbar.destroy(); 20 | } 21 | }); 22 | } 23 | 24 | getSeriesIndexForElement(el) { 25 | return el.parents('[data-series-index]').data('series-index'); 26 | } 27 | 28 | openColorSelector(e) { 29 | // if we clicked inside poup container ignore click 30 | if ($(e.target).parents('.popover').length) { 31 | return; 32 | } 33 | 34 | var el = $(e.currentTarget).find('.fa-minus'); 35 | var index = this.getSeriesIndexForElement(el); 36 | var series = this.seriesList[index]; 37 | 38 | this.popoverSrv.show({ 39 | element: el[0], 40 | position: 'bottom left', 41 | targetAttachment: 'top left', 42 | template: 43 | '', 44 | openOn: 'hover', 45 | model: { 46 | series: series, 47 | toggleAxis: () => { 48 | this.ctrl.toggleAxis(series); 49 | }, 50 | colorSelected: color => { 51 | this.ctrl.changeSeriesColor(series, color); 52 | }, 53 | }, 54 | }); 55 | 56 | } 57 | 58 | toggleSeries(e) { 59 | var el = $(e.currentTarget); 60 | var index = this.getSeriesIndexForElement(el); 61 | var seriesInfo = this.seriesList[index]; 62 | var scrollPosition = this.$elem.find('tbody').scrollTop(); 63 | this.ctrl.toggleSeries(seriesInfo, e); 64 | this.$elem.find('tbody').scrollTop(scrollPosition); 65 | } 66 | 67 | sortLegend(e) { 68 | var el = $(e.currentTarget); 69 | var stat = el.data('stat'); 70 | 71 | if (stat !== this.panel.legend.sort) { 72 | this.panel.legend.sortDesc = null; 73 | } 74 | 75 | // if already sort ascending, disable sorting 76 | if (this.panel.legend.sortDesc === false) { 77 | this.panel.legend.sort = null; 78 | this.panel.legend.sortDesc = null; 79 | this.ctrl.render(); 80 | return; 81 | } 82 | 83 | this.panel.legend.sortDesc = !this.panel.legend.sortDesc; 84 | this.panel.legend.sort = stat; 85 | this.ctrl.render(); 86 | } 87 | 88 | getTableHeaderHtml(statName) { 89 | if (!this.panel.legend[statName]) { 90 | return ''; 91 | } 92 | var html = '' + statName; 93 | 94 | if (this.panel.legend.sort === statName) { 95 | var cssClass = this.panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; 96 | html += ' '; 97 | } 98 | 99 | return html + ''; 100 | } 101 | 102 | render() { 103 | this.data = this.ctrl.seriesList; 104 | if (!this.ctrl.panel.legend.show) { 105 | this.$elem.empty(); 106 | this.firstRender = true; 107 | return; 108 | } 109 | 110 | if (this.firstRender) { 111 | this.$elem.on('click', '.graph-legend-icon', this.openColorSelector.bind(this)); 112 | this.$elem.on('click', '.graph-legend-alias', this.toggleSeries.bind(this)); 113 | this.$elem.on('click', 'th', this.sortLegend.bind(this)); 114 | this.firstRender = false; 115 | } 116 | 117 | this.seriesList = this.data; 118 | 119 | this.$elem.empty(); 120 | 121 | // Set min-width if side style and there is a value, otherwise remove the CSS propery 122 | var width = this.panel.legend.rightSide && this.panel.legend.sideWidth ? this.panel.legend.sideWidth + 'px' : ''; 123 | this.$elem.css('min-width', width); 124 | 125 | this.$elem.toggleClass('graph-legend-table', this.panel.legend.alignAsTable === true); 126 | 127 | var tableHeaderElem; 128 | if (this.panel.legend.alignAsTable) { 129 | var header = ''; 130 | header += ''; 131 | if (this.panel.legend.values) { 132 | header += this.getTableHeaderHtml('min'); 133 | header += this.getTableHeaderHtml('max'); 134 | header += this.getTableHeaderHtml('avg'); 135 | header += this.getTableHeaderHtml('current'); 136 | header += this.getTableHeaderHtml('total'); 137 | } 138 | header += ''; 139 | tableHeaderElem = $(header); 140 | } 141 | 142 | if (this.panel.legend.sort) { 143 | this.seriesList = _.sortBy(this.seriesList, series => series.stats[this.panel.legend.sort]); 144 | if (this.panel.legend.sortDesc) { 145 | this.seriesList = this.seriesList.reverse(); 146 | } 147 | } 148 | 149 | // render first time for getting proper legend height 150 | if (!this.panel.legend.rightSide) { 151 | this.renderLegendElement(tableHeaderElem); 152 | this.$elem.empty(); 153 | } 154 | 155 | this.renderLegendElement(tableHeaderElem); 156 | } 157 | 158 | renderSeriesLegendElements() { 159 | let seriesElements = []; 160 | for (let i = 0; i < this.seriesList.length; i++) { 161 | var series = this.seriesList[i]; 162 | 163 | if (series.hideFromLegend(this.panel.legend)) { 164 | continue; 165 | } 166 | 167 | var html = '
'; 176 | html += '
'; 177 | html += ''; 178 | html += '
'; 179 | 180 | html += 181 | '' + series.aliasEscaped + ''; 182 | 183 | if (this.panel.legend.values) { 184 | var avg = series.formatValue(series.stats.avg); 185 | var current = series.formatValue(series.stats.current); 186 | var min = series.formatValue(series.stats.min); 187 | var max = series.formatValue(series.stats.max); 188 | var total = series.formatValue(series.stats.total); 189 | 190 | if (this.panel.legend.min) { 191 | html += '
' + min + '
'; 192 | } 193 | if (this.panel.legend.max) { 194 | html += '
' + max + '
'; 195 | } 196 | if (this.panel.legend.avg) { 197 | html += '
' + avg + '
'; 198 | } 199 | if (this.panel.legend.current) { 200 | html += '
' + current + '
'; 201 | } 202 | if (this.panel.legend.total) { 203 | html += '
' + total + '
'; 204 | } 205 | } 206 | 207 | html += '
'; 208 | seriesElements.push($(html)); 209 | } 210 | return seriesElements; 211 | } 212 | 213 | renderLegendElement(tableHeaderElem) { 214 | var seriesElements = this.renderSeriesLegendElements(); 215 | 216 | if (this.panel.legend.alignAsTable) { 217 | var tbodyElem = $(''); 218 | tbodyElem.append(tableHeaderElem); 219 | tbodyElem.append(seriesElements); 220 | this.$elem.append(tbodyElem); 221 | } else { 222 | this.$elem.append(seriesElements); 223 | } 224 | 225 | if (!this.panel.legend.rightSide) { 226 | this.addScrollbar(); 227 | } else { 228 | this.destroyScrollbar(); 229 | } 230 | } 231 | 232 | addScrollbar() { 233 | const scrollbarOptions = { 234 | // Number of pixels the content height can surpass the container height without enabling the scroll bar. 235 | scrollYMarginOffset: 2, 236 | suppressScrollX: true, 237 | }; 238 | 239 | if (!this.legendScrollbar) { 240 | this.legendScrollbar = new PerfectScrollbar(this.$elem[0], scrollbarOptions); 241 | } else { 242 | this.legendScrollbar.update(); 243 | } 244 | } 245 | 246 | destroyScrollbar() { 247 | if (this.legendScrollbar) { 248 | this.legendScrollbar.destroy(); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/graph_tooltip.ts: -------------------------------------------------------------------------------- 1 | export class GraphTooltip { 2 | 3 | private panel: any; 4 | private $tooltip: JQuery; 5 | private _visible = false; 6 | private _lastItem = undefined; 7 | 8 | constructor( 9 | private $elem: JQuery, private dashboard, 10 | private scope, private getSeriesFn 11 | ) { 12 | this.panel = scope.ctrl.panel; 13 | this.$tooltip = $('
'); 14 | } 15 | 16 | clear(plot) { 17 | this._visible = false; 18 | this.$tooltip.detach(); 19 | plot.clearCrosshair(); 20 | plot.unhighlight(); 21 | }; 22 | 23 | show(pos, item?) { 24 | if (item === undefined) { 25 | item = this._lastItem; 26 | } else { 27 | this._lastItem = item; 28 | } 29 | 30 | this._visible = true; 31 | var plot = this.$elem.data().plot; 32 | var plotData = plot.getData(); 33 | var xAxes = plot.getXAxes(); 34 | var xMode = xAxes[0].options.mode; 35 | var seriesList = this.getSeriesFn(); 36 | var allSeriesMode = this.panel.tooltip.shared; 37 | var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; 38 | var rangeDist = Math.abs(xAxes[0].max - xAxes[0].min); 39 | 40 | // if panelRelY is defined another panel wants us to show a tooltip 41 | // get pageX from position on x axis and pageY from relative position in original panel 42 | if (pos.panelRelY) { 43 | var pointOffset = plot.pointOffset({ x: pos.x }); 44 | if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > this.$elem.width()) { 45 | this.clear(plot); 46 | return; 47 | } 48 | pos.pageX = this.$elem.offset().left + pointOffset.left; 49 | pos.pageY = this.$elem.offset().top + this.$elem.height() * pos.panelRelY; 50 | var isVisible = pos.pageY >= $(window).scrollTop() && 51 | pos.pageY <= $(window).innerHeight() + $(window).scrollTop(); 52 | if (!isVisible) { 53 | this.clear(plot); 54 | return; 55 | } 56 | plot.setCrosshair(pos); 57 | allSeriesMode = true; 58 | 59 | if (this.dashboard.sharedCrosshairModeOnly()) { 60 | // if only crosshair mode we are done 61 | return; 62 | } 63 | } 64 | 65 | if (seriesList.length === 0) { 66 | return; 67 | } 68 | 69 | if (seriesList[0].hasMsResolution) { 70 | tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS'; 71 | } else { 72 | tooltipFormat = 'YYYY-MM-DD HH:mm:ss'; 73 | } 74 | 75 | if (allSeriesMode) { 76 | plot.unhighlight(); 77 | 78 | seriesHtml = ''; 79 | var seriesHoverInfo = this._getMultiSeriesPlotHoverInfo(plotData, pos); 80 | absoluteTime = this.dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); 81 | 82 | // Dynamically reorder the hovercard for the current time point if the 83 | // option is enabled. 84 | if (this.panel.tooltip.sort === 2) { 85 | seriesHoverInfo.series.sort((a: any, b: any) => b.value - a.value); 86 | } else if (this.panel.tooltip.sort === 1) { 87 | seriesHoverInfo.series.sort((a: any, b: any) => a.value - b.value); 88 | } 89 | 90 | for (i = 0; i < seriesHoverInfo.series.length; i++) { 91 | hoverInfo = seriesHoverInfo.series[i]; 92 | 93 | if (hoverInfo.hidden) { 94 | continue; 95 | } 96 | 97 | var highlightClass = ''; 98 | if (item && hoverInfo.index === item.seriesIndex) { 99 | highlightClass = 'graph-tooltip-list-item--highlight'; 100 | } 101 | 102 | series = seriesList[hoverInfo.index]; 103 | 104 | value = series.formatValue(hoverInfo.value); 105 | 106 | seriesHtml += '
'; 107 | seriesHtml += ' ' + hoverInfo.label + ':
'; 108 | seriesHtml += '
' + value + '
'; 109 | plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); 110 | } 111 | 112 | this._renderAndShow(absoluteTime, seriesHtml, pos, xMode); 113 | } 114 | // single series tooltip 115 | else if (item) { 116 | series = seriesList[item.seriesIndex]; 117 | group = '
'; 118 | group += ' ' + series.aliasEscaped + ':
'; 119 | 120 | if (this.panel.stack && this.panel.tooltip.value_type === 'individual') { 121 | value = item.datapoint[1] - item.datapoint[2]; 122 | } 123 | else { 124 | value = item.datapoint[1]; 125 | } 126 | 127 | value = series.formatValue(value); 128 | 129 | seriesHoverInfo = this._getMultiSeriesPlotHoverInfo(plotData, pos); 130 | absoluteTime = this.dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); 131 | 132 | group += '
' + value + '
'; 133 | 134 | this._renderAndShow(absoluteTime, group, pos, xMode); 135 | } 136 | // no hit 137 | else { 138 | this.$tooltip.detach(); 139 | } 140 | }; 141 | 142 | 143 | destroy() { 144 | this._visible = false; 145 | this.$tooltip.remove(); 146 | }; 147 | 148 | get visible() { return this._visible; } 149 | 150 | private _findHoverIndexFromDataPoints(posX, series, last) { 151 | var ps = series.datapoints.pointsize; 152 | var initial = last * ps; 153 | var len = series.datapoints.points.length; 154 | for (var j = initial; j < len; j += ps) { 155 | // Special case of a non stepped line, highlight the very last point just before a null point 156 | if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) 157 | //normal case 158 | || series.datapoints.points[j] > posX) { 159 | return Math.max(j - ps, 0) / ps; 160 | } 161 | } 162 | return j / ps - 1; 163 | }; 164 | 165 | private _findHoverIndexFromData(posX, series) { 166 | var lower = 0; 167 | var upper = series.data.length - 1; 168 | var middle; 169 | while (true) { 170 | if (lower > upper) { 171 | return Math.max(upper, 0); 172 | } 173 | middle = Math.floor((lower + upper) / 2); 174 | if (series.data[middle][0] === posX) { 175 | return middle; 176 | } else if (series.data[middle][0] < posX) { 177 | lower = middle + 1; 178 | } else { 179 | upper = middle - 1; 180 | } 181 | } 182 | }; 183 | 184 | private _renderAndShow(absoluteTime, innerHtml, pos, xMode) { 185 | if (xMode === 'time') { 186 | innerHtml = '
' + absoluteTime + '
' + innerHtml; 187 | } 188 | (this.$tooltip.html(innerHtml) as any).place_tt(pos.pageX + 20, pos.pageY); 189 | }; 190 | 191 | private _getMultiSeriesPlotHoverInfo(seriesList, pos): { series: any[][], time: any } { 192 | var value, series, hoverIndex, hoverDistance, pointTime, yaxis; 193 | // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis. 194 | var results = [[], [], []]; 195 | 196 | //now we know the current X (j) position for X and Y values 197 | var lastValue = 0; //needed for stacked values 198 | 199 | var minDistance, minTime; 200 | 201 | for (let i = 0; i < seriesList.length; i++) { 202 | series = seriesList[i]; 203 | 204 | if (!series.data.length || (this.panel.legend.hideEmpty && series.allIsNull)) { 205 | // Init value so that it does not brake series sorting 206 | results[0].push({ hidden: true, value: 0 }); 207 | continue; 208 | } 209 | 210 | if (!series.data.length || (this.panel.legend.hideZero && series.allIsZero)) { 211 | // Init value so that it does not brake series sorting 212 | results[0].push({ hidden: true, value: 0 }); 213 | continue; 214 | } 215 | 216 | hoverIndex = this._findHoverIndexFromData(pos.x, series); 217 | hoverDistance = pos.x - series.data[hoverIndex][0]; 218 | pointTime = series.data[hoverIndex][0]; 219 | 220 | // Take the closest point before the cursor, or if it does not exist, the closest after 221 | if (!minDistance 222 | || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) 223 | || (hoverDistance < 0 && hoverDistance > minDistance) 224 | ) { 225 | minDistance = hoverDistance; 226 | minTime = pointTime; 227 | } 228 | 229 | if (series.stack) { 230 | if (this.panel.tooltip.value_type === 'individual') { 231 | value = series.data[hoverIndex][1]; 232 | } else if (!series.stack) { 233 | value = series.data[hoverIndex][1]; 234 | } else { 235 | lastValue += series.data[hoverIndex][1]; 236 | value = lastValue; 237 | } 238 | } else { 239 | value = series.data[hoverIndex][1]; 240 | } 241 | 242 | // Highlighting multiple Points depending on the plot type 243 | if (series.lines.steps || series.stack) { 244 | // stacked and steppedLine plots can have series with different length. 245 | // Stacked series can increase its length on each new stacked serie if null points found, 246 | // to speed the index search we begin always on the last found hoverIndex. 247 | hoverIndex = this._findHoverIndexFromDataPoints(pos.x, series, hoverIndex); 248 | } 249 | 250 | // Be sure we have a yaxis so that it does not brake series sorting 251 | yaxis = 0; 252 | if (series.yaxis) { 253 | yaxis = series.yaxis.n; 254 | } 255 | 256 | results[yaxis].push({ 257 | value: value, 258 | hoverIndex: hoverIndex, 259 | color: series.color, 260 | label: series.aliasEscaped, 261 | time: pointTime, 262 | distance: hoverDistance, 263 | index: i 264 | }); 265 | } 266 | 267 | // Contat the 3 sub-arrays 268 | results = results[0].concat(results[1], results[2]); 269 | 270 | // Time of the point closer to pointer 271 | 272 | return { series: results, time: minTime }; 273 | }; 274 | } 275 | 276 | -------------------------------------------------------------------------------- /dist/img/icn-graph-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/img/icn-graph-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/vendor/grafana/time_series2.ts: -------------------------------------------------------------------------------- 1 | import kbn from 'grafana/app/core/utils/kbn'; 2 | import { getFlotTickDecimals } from './ticks'; 3 | import _ from 'lodash'; 4 | 5 | function matchSeriesOverride(aliasOrRegex, seriesAlias) { 6 | if (!aliasOrRegex) { 7 | return false; 8 | } 9 | 10 | if (aliasOrRegex[0] === '/') { 11 | var regex = kbn.stringToJsRegex(aliasOrRegex); 12 | return seriesAlias.match(regex) != null; 13 | } 14 | 15 | return aliasOrRegex === seriesAlias; 16 | } 17 | 18 | function translateFillOption(fill) { 19 | return fill === 0 ? 0.001 : fill / 10; 20 | } 21 | 22 | /** 23 | * Calculate decimals for legend and update values for each series. 24 | * @param data series data 25 | * @param panel 26 | */ 27 | export function updateLegendValues(data: TimeSeries[], panel) { 28 | for (let i = 0; i < data.length; i++) { 29 | let series = data[i]; 30 | let yaxes = panel.yaxes; 31 | const seriesYAxis = series.yaxis || 1; 32 | let axis = yaxes[seriesYAxis - 1]; 33 | let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); 34 | let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; 35 | 36 | // decimal override 37 | if (_.isNumber(panel.decimals)) { 38 | series.updateLegendValues(formater, panel.decimals, null); 39 | } else { 40 | // auto decimals 41 | // legend and tooltip gets one more decimal precision 42 | // than graph legend ticks 43 | tickDecimals = (tickDecimals || -1) + 1; 44 | series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); 45 | } 46 | } 47 | } 48 | 49 | export function getDataMinMax(data: TimeSeries[]) { 50 | let datamin = null; 51 | let datamax = null; 52 | 53 | for (let series of data) { 54 | if (datamax === null || datamax < series.stats.max) { 55 | datamax = series.stats.max; 56 | } 57 | if (datamin === null || datamin > series.stats.min) { 58 | datamin = series.stats.min; 59 | } 60 | } 61 | 62 | return { datamin, datamax }; 63 | } 64 | 65 | export default class TimeSeries { 66 | datapoints: any; 67 | id: string; 68 | label: string; 69 | alias: string; 70 | aliasEscaped: string; 71 | color: string; 72 | valueFormater: any; 73 | stats: any; 74 | legend: boolean; 75 | allIsNull: boolean; 76 | allIsZero: boolean; 77 | decimals: number; 78 | scaledDecimals: number; 79 | hasMsResolution: boolean; 80 | isOutsideRange: boolean; 81 | 82 | lines: any; 83 | dashes: any; 84 | bars: any; 85 | points: any; 86 | yaxis: any; 87 | zindex: any; 88 | stack: any; 89 | nullPointMode: any; 90 | fillBelowTo: any; 91 | transform: any; 92 | flotpairs: any; 93 | unit: any; 94 | 95 | constructor(opts) { 96 | this.datapoints = opts.datapoints; 97 | this.label = opts.alias; 98 | this.id = opts.alias; 99 | this.alias = opts.alias; 100 | this.aliasEscaped = _.escape(opts.alias); 101 | this.color = opts.color; 102 | this.valueFormater = kbn.valueFormats.none; 103 | this.stats = {}; 104 | this.legend = true; 105 | this.unit = opts.unit; 106 | this.hasMsResolution = this.isMsResolutionNeeded(); 107 | } 108 | 109 | applySeriesOverrides(overrides) { 110 | this.lines = {}; 111 | this.dashes = { 112 | dashLength: [], 113 | }; 114 | this.points = {}; 115 | this.bars = {}; 116 | this.yaxis = 1; 117 | this.zindex = 0; 118 | this.nullPointMode = null; 119 | delete this.stack; 120 | 121 | for (var i = 0; i < overrides.length; i++) { 122 | var override = overrides[i]; 123 | if (!matchSeriesOverride(override.alias, this.alias)) { 124 | continue; 125 | } 126 | if (override.lines !== void 0) { 127 | this.lines.show = override.lines; 128 | } 129 | if (override.dashes !== void 0) { 130 | this.dashes.show = override.dashes; 131 | this.lines.lineWidth = 0; 132 | } 133 | if (override.points !== void 0) { 134 | this.points.show = override.points; 135 | } 136 | if (override.bars !== void 0) { 137 | this.bars.show = override.bars; 138 | } 139 | if (override.fill !== void 0) { 140 | this.lines.fill = translateFillOption(override.fill); 141 | } 142 | if (override.stack !== void 0) { 143 | this.stack = override.stack; 144 | } 145 | if (override.linewidth !== void 0) { 146 | this.lines.lineWidth = this.dashes.show ? 0 : override.linewidth; 147 | this.dashes.lineWidth = override.linewidth; 148 | } 149 | if (override.dashLength !== void 0) { 150 | this.dashes.dashLength[0] = override.dashLength; 151 | } 152 | if (override.spaceLength !== void 0) { 153 | this.dashes.dashLength[1] = override.spaceLength; 154 | } 155 | if (override.nullPointMode !== void 0) { 156 | this.nullPointMode = override.nullPointMode; 157 | } 158 | if (override.pointradius !== void 0) { 159 | this.points.radius = override.pointradius; 160 | } 161 | if (override.steppedLine !== void 0) { 162 | this.lines.steps = override.steppedLine; 163 | } 164 | if (override.zindex !== void 0) { 165 | this.zindex = override.zindex; 166 | } 167 | if (override.fillBelowTo !== void 0) { 168 | this.fillBelowTo = override.fillBelowTo; 169 | } 170 | if (override.color !== void 0) { 171 | this.color = override.color; 172 | } 173 | if (override.transform !== void 0) { 174 | this.transform = override.transform; 175 | } 176 | if (override.legend !== void 0) { 177 | this.legend = override.legend; 178 | } 179 | 180 | if (override.yaxis !== void 0) { 181 | this.yaxis = override.yaxis; 182 | } 183 | } 184 | } 185 | 186 | getFlotPairs(fillStyle) { 187 | var result = []; 188 | 189 | this.stats.total = 0; 190 | this.stats.max = -Number.MAX_VALUE; 191 | this.stats.min = Number.MAX_VALUE; 192 | this.stats.logmin = Number.MAX_VALUE; 193 | this.stats.avg = null; 194 | this.stats.current = null; 195 | this.stats.first = null; 196 | this.stats.delta = 0; 197 | this.stats.diff = null; 198 | this.stats.range = null; 199 | this.stats.timeStep = Number.MAX_VALUE; 200 | this.allIsNull = true; 201 | this.allIsZero = true; 202 | 203 | var ignoreNulls = fillStyle === 'connected'; 204 | var nullAsZero = fillStyle === 'null as zero'; 205 | var currentTime; 206 | var currentValue; 207 | var nonNulls = 0; 208 | var previousTime; 209 | var previousValue = 0; 210 | var previousDeltaUp = true; 211 | 212 | for (var i = 0; i < this.datapoints.length; i++) { 213 | currentValue = this.datapoints[i][0]; 214 | currentTime = this.datapoints[i][1]; 215 | 216 | // Due to missing values we could have different timeStep all along the series 217 | // so we have to find the minimum one (could occur with aggregators such as ZimSum) 218 | if (previousTime !== undefined) { 219 | let timeStep = currentTime - previousTime; 220 | if (timeStep < this.stats.timeStep) { 221 | this.stats.timeStep = timeStep; 222 | } 223 | } 224 | previousTime = currentTime; 225 | 226 | if (currentValue === null) { 227 | if (ignoreNulls) { 228 | continue; 229 | } 230 | if (nullAsZero) { 231 | currentValue = 0; 232 | } 233 | } 234 | 235 | if (currentValue !== null) { 236 | if (_.isNumber(currentValue)) { 237 | this.stats.total += currentValue; 238 | this.allIsNull = false; 239 | nonNulls++; 240 | } 241 | 242 | if (currentValue > this.stats.max) { 243 | this.stats.max = currentValue; 244 | } 245 | 246 | if (currentValue < this.stats.min) { 247 | this.stats.min = currentValue; 248 | } 249 | 250 | if (this.stats.first === null) { 251 | this.stats.first = currentValue; 252 | } else { 253 | if (previousValue > currentValue) { 254 | // counter reset 255 | previousDeltaUp = false; 256 | if (i === this.datapoints.length - 1) { 257 | // reset on last 258 | this.stats.delta += currentValue; 259 | } 260 | } else { 261 | if (previousDeltaUp) { 262 | this.stats.delta += currentValue - previousValue; // normal increment 263 | } else { 264 | this.stats.delta += currentValue; // account for counter reset 265 | } 266 | previousDeltaUp = true; 267 | } 268 | } 269 | previousValue = currentValue; 270 | 271 | if (currentValue < this.stats.logmin && currentValue > 0) { 272 | this.stats.logmin = currentValue; 273 | } 274 | 275 | if (currentValue !== 0) { 276 | this.allIsZero = false; 277 | } 278 | } 279 | 280 | result.push([currentTime, currentValue]); 281 | } 282 | 283 | if (this.stats.max === -Number.MAX_VALUE) { 284 | this.stats.max = null; 285 | } 286 | if (this.stats.min === Number.MAX_VALUE) { 287 | this.stats.min = null; 288 | } 289 | 290 | if (result.length && !this.allIsNull) { 291 | this.stats.avg = this.stats.total / nonNulls; 292 | this.stats.current = result[result.length - 1][1]; 293 | if (this.stats.current === null && result.length > 1) { 294 | this.stats.current = result[result.length - 2][1]; 295 | } 296 | } 297 | if (this.stats.max !== null && this.stats.min !== null) { 298 | this.stats.range = this.stats.max - this.stats.min; 299 | } 300 | if (this.stats.current !== null && this.stats.first !== null) { 301 | this.stats.diff = this.stats.current - this.stats.first; 302 | } 303 | 304 | this.stats.count = result.length; 305 | return result; 306 | } 307 | 308 | updateLegendValues(formater, decimals, scaledDecimals) { 309 | this.valueFormater = formater; 310 | this.decimals = decimals; 311 | this.scaledDecimals = scaledDecimals; 312 | } 313 | 314 | formatValue(value) { 315 | if (!_.isFinite(value)) { 316 | value = null; // Prevent NaN formatting 317 | } 318 | return this.valueFormater(value, this.decimals, this.scaledDecimals); 319 | } 320 | 321 | isMsResolutionNeeded() { 322 | for (var i = 0; i < this.datapoints.length; i++) { 323 | if (this.datapoints[i][1] !== null) { 324 | var timestamp = this.datapoints[i][1].toString(); 325 | if (timestamp.length === 13 && timestamp % 1000 !== 0) { 326 | return true; 327 | } 328 | } 329 | } 330 | return false; 331 | } 332 | 333 | hideFromLegend(options) { 334 | if (options.hideEmpty && this.allIsNull) { 335 | return true; 336 | } 337 | // ignore series excluded via override 338 | if (!this.legend) { 339 | return true; 340 | } 341 | 342 | // ignore zero series 343 | if (options.hideZero && this.allIsZero) { 344 | return true; 345 | } 346 | 347 | return false; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { GraphRenderer } from './graph_renderer'; 3 | import { GraphLegend } from './graph_legend'; 4 | import './series_overrides_ctrl'; 5 | import './thresholds_form'; 6 | 7 | import template from './template'; 8 | import _ from 'lodash'; 9 | import config from 'grafana/app/core/config'; 10 | import { MetricsPanelCtrl, alertTab } from 'grafana/app/plugins/sdk'; 11 | import { DataProcessor } from './data_processor'; 12 | import { axesEditorComponent } from './axes_editor'; 13 | 14 | import $ from 'jquery'; 15 | 16 | class GraphCtrl extends MetricsPanelCtrl { 17 | static template = template; 18 | 19 | hiddenSeries: any = {}; 20 | seriesList: any = []; 21 | dataList: any = []; 22 | annotations: any = []; 23 | alertState: any; 24 | 25 | _panelPath: any; 26 | 27 | annotationsPromise: any; 28 | dataWarning: any; 29 | colors: any = []; 30 | subTabIndex: number; 31 | processor: DataProcessor; 32 | 33 | private _graphRenderer: GraphRenderer; 34 | private _graphLegend: GraphLegend; 35 | 36 | panelDefaults = { 37 | // datasource name, null = default datasource 38 | datasource: null, 39 | // sets client side (flot) or native graphite png renderer (png) 40 | renderer: 'flot', 41 | yaxes: [ 42 | { 43 | label: null, 44 | show: true, 45 | logBase: 1, 46 | min: null, 47 | max: null, 48 | format: 'short', 49 | }, 50 | { 51 | label: null, 52 | show: true, 53 | logBase: 1, 54 | min: null, 55 | max: null, 56 | format: 'short', 57 | }, 58 | ], 59 | xaxis: { 60 | show: true, 61 | mode: 'time', 62 | name: null, 63 | values: [], 64 | buckets: null, 65 | customDateFormatShow: false, 66 | customDateFormat: '' 67 | }, 68 | // show/hide lines 69 | lines: true, 70 | // fill factor 71 | fill: 1, 72 | // line width in pixels 73 | linewidth: 1, 74 | // show/hide dashed line 75 | dashes: false, 76 | // length of a dash 77 | dashLength: 10, 78 | // length of space between two dashes 79 | spaceLength: 10, 80 | // show hide points 81 | points: false, 82 | // point radius in pixels 83 | pointradius: 5, 84 | // show hide bars 85 | bars: false, 86 | // enable/disable stacking 87 | stack: false, 88 | // stack percentage mode 89 | percentage: false, 90 | // legend options 91 | legend: { 92 | show: true, // disable/enable legend 93 | values: false, // disable/enable legend values 94 | min: false, 95 | max: false, 96 | current: false, 97 | total: false, 98 | avg: false, 99 | }, 100 | // how null points should be handled 101 | nullPointMode: 'null', 102 | // staircase line mode 103 | steppedLine: false, 104 | // tooltip options 105 | tooltip: { 106 | value_type: 'individual', 107 | shared: true, 108 | sort: 0, 109 | }, 110 | // time overrides 111 | timeFrom: null, 112 | timeShift: null, 113 | // metric queries 114 | targets: [{}], 115 | // series color overrides 116 | aliasColors: {}, 117 | // other style overrides 118 | seriesOverrides: [], 119 | thresholds: [], 120 | displayBarsSideBySide: false, 121 | labelAlign: 'left' 122 | }; 123 | 124 | /** @ngInject */ 125 | constructor($scope, $injector, private annotationsSrv, private popoverSrv, private contextSrv) { 126 | super($scope, $injector); 127 | 128 | // hack to show alert threshold 129 | // visit link to find out why 130 | // https://github.com/grafana/grafana/blob/master/public/app/features/alerting/threshold_mapper.ts#L3 131 | // should make it 'corpglory-multibar-graph-panel' before save 132 | // https://github.com/CorpGlory/grafana-multibar-graph-panel/issues/6#issuecomment-377238048 133 | // this.panel.type='graph'; 134 | 135 | _.defaults(this.panel, this.panelDefaults); 136 | _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); 137 | _.defaults(this.panel.legend, this.panelDefaults.legend); 138 | _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); 139 | 140 | this.processor = new DataProcessor(this.panel); 141 | 142 | this.events.on('render', this.onRender.bind(this)); 143 | this.events.on('data-received', this.onDataReceived.bind(this)); 144 | this.events.on('data-error', this.onDataError.bind(this)); 145 | this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); 146 | this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); 147 | this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); 148 | } 149 | 150 | link(scope, elem, attrs, ctrl) { 151 | var $graphElem = $(elem[0]).find('#multibar-graph-panel'); 152 | var $legendElem = $(elem[0]).find('#multibar-graph-legend'); 153 | this._graphRenderer = new GraphRenderer( 154 | $graphElem, this.timeSrv, this.contextSrv, this.$scope 155 | ); 156 | this._graphLegend = new GraphLegend($legendElem, this.popoverSrv, this.$scope); 157 | } 158 | 159 | onInitEditMode() { 160 | var partialPath = this.panelPath + 'partials'; 161 | this.addEditorTab('Axes', axesEditorComponent, 2); 162 | this.addEditorTab('Legend', `${partialPath}/tab_legend.html`, 3); 163 | this.addEditorTab('Display', `${partialPath}/tab_display.html`, 4); 164 | 165 | if (config.alertingEnabled) { 166 | this.addEditorTab('Alert', alertTab, 5); 167 | } 168 | 169 | this.subTabIndex = 0; 170 | } 171 | 172 | onInitPanelActions(actions) { 173 | actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' }); 174 | actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' }); 175 | } 176 | 177 | issueQueries(datasource) { 178 | this.annotationsPromise = this.annotationsSrv.getAnnotations({ 179 | dashboard: this.dashboard, 180 | panel: this.panel, 181 | range: this.range, 182 | }); 183 | return super.issueQueries(datasource); 184 | } 185 | 186 | zoomOut(evt) { 187 | this.publishAppEvent('zoom-out', 2); 188 | } 189 | 190 | onDataSnapshotLoad(snapshotData) { 191 | this.annotationsPromise = this.annotationsSrv.getAnnotations({ 192 | dashboard: this.dashboard, 193 | panel: this.panel, 194 | range: this.range, 195 | }); 196 | this.onDataReceived(snapshotData); 197 | } 198 | 199 | onDataError(err) { 200 | this.seriesList = []; 201 | this.annotations = []; 202 | this.render([]); 203 | } 204 | 205 | async onDataReceived(dataList) { 206 | this.dataList = dataList; 207 | this.seriesList = this.processor.getSeriesList({ 208 | dataList: dataList, 209 | range: this.range, 210 | }); 211 | 212 | this.dataWarning = null; 213 | const datapointsCount = this.seriesList.reduce((prev, series) => { 214 | return prev + series.datapoints.length; 215 | }, 0); 216 | 217 | if (datapointsCount === 0) { 218 | this.dataWarning = { 219 | title: 'No data points', 220 | tip: 'No datapoints returned from data query', 221 | }; 222 | } else { 223 | for (let series of this.seriesList) { 224 | if (series.isOutsideRange) { 225 | this.dataWarning = { 226 | title: 'Data points outside time range', 227 | tip: 'Can be caused by timezone mismatch or missing time filter in query', 228 | }; 229 | break; 230 | } 231 | } 232 | } 233 | 234 | if(this.annotationsPromise !== undefined) { 235 | const result = await this.annotationsPromise; 236 | this.alertState = result.alertState; 237 | this.annotations = result.annotations; 238 | } 239 | this.loading = false; 240 | this.render(this.seriesList); 241 | } 242 | 243 | onRender(data) { 244 | if (!this.seriesList) { 245 | return; 246 | } 247 | 248 | for (let series of this.seriesList) { 249 | series.applySeriesOverrides(this.panel.seriesOverrides); 250 | } 251 | 252 | this._graphRenderer.render(data); 253 | this._graphLegend.render(); 254 | 255 | this._graphRenderer.renderPanel(); 256 | } 257 | 258 | changeSeriesColor(series, color) { 259 | series.color = color; 260 | this.panel.aliasColors[series.alias] = series.color; 261 | this.render(); 262 | } 263 | 264 | toggleSeries(serie, event) { 265 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 266 | if (this.hiddenSeries[serie.alias]) { 267 | delete this.hiddenSeries[serie.alias]; 268 | } else { 269 | this.hiddenSeries[serie.alias] = true; 270 | } 271 | } else { 272 | this.toggleSeriesExclusiveMode(serie); 273 | } 274 | this.render(); 275 | } 276 | 277 | toggleSeriesExclusiveMode(serie) { 278 | var hidden = this.hiddenSeries; 279 | 280 | if (hidden[serie.alias]) { 281 | delete hidden[serie.alias]; 282 | } 283 | 284 | // check if every other series is hidden 285 | var alreadyExclusive = _.every(this.seriesList, value => { 286 | if (value.alias === serie.alias) { 287 | return true; 288 | } 289 | 290 | return hidden[value.alias]; 291 | }); 292 | 293 | if (alreadyExclusive) { 294 | // remove all hidden series 295 | _.each(this.seriesList, value => { 296 | delete this.hiddenSeries[value.alias]; 297 | }); 298 | } else { 299 | // hide all but this serie 300 | _.each(this.seriesList, value => { 301 | if (value.alias === serie.alias) { 302 | return; 303 | } 304 | 305 | this.hiddenSeries[value.alias] = true; 306 | }); 307 | } 308 | } 309 | 310 | toggleAxis(info) { 311 | var override: any = _.find(this.panel.seriesOverrides, { alias: info.alias }); 312 | if (!override) { 313 | override = { alias: info.alias }; 314 | this.panel.seriesOverrides.push(override); 315 | } 316 | info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; 317 | this.render(); 318 | } 319 | 320 | addSeriesOverride(override) { 321 | this.panel.seriesOverrides.push(override || {}); 322 | } 323 | 324 | removeSeriesOverride(override) { 325 | this.panel.seriesOverrides = _.without(this.panel.seriesOverrides, override); 326 | this.render(); 327 | } 328 | 329 | toggleLegend() { 330 | this.panel.legend.show = !this.panel.legend.show; 331 | this.refresh(); 332 | } 333 | 334 | legendValuesOptionChanged() { 335 | var legend = this.panel.legend; 336 | legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; 337 | this.render(); 338 | } 339 | 340 | exportCsv() { 341 | var scope = this.$scope.$new(true); 342 | scope.seriesList = this.seriesList; 343 | this.publishAppEvent('show-modal', { 344 | templateHtml: '', 345 | scope, 346 | modalClass: 'modal--narrow', 347 | }); 348 | } 349 | 350 | get panelPath() { 351 | if (this._panelPath === undefined) { 352 | this._panelPath = './public/plugins/' + this.pluginId + '/'; 353 | } 354 | return this._panelPath; 355 | } 356 | } 357 | 358 | export { GraphCtrl, GraphCtrl as PanelCtrl }; 359 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.fillbelow.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; 3 | 4 | var options = { 5 | series: { 6 | fillBelowTo: null 7 | } 8 | }; 9 | 10 | function init(plot) { 11 | function findBelowSeries( series, allseries ) { 12 | 13 | var i; 14 | 15 | for ( i = 0; i < allseries.length; ++i ) { 16 | if ( allseries[ i ].id === series.fillBelowTo ) { 17 | return allseries[ i ]; 18 | } 19 | } 20 | 21 | return null; 22 | } 23 | 24 | /* top and bottom doesn't actually matter for this, we're just using it to help make this easier to think about */ 25 | /* this is a vector cross product operation */ 26 | function segmentIntersection(top_left_x, top_left_y, top_right_x, top_right_y, bottom_left_x, bottom_left_y, bottom_right_x, bottom_right_y) { 27 | var top_delta_x, top_delta_y, bottom_delta_x, bottom_delta_y, 28 | s, t; 29 | 30 | top_delta_x = top_right_x - top_left_x; 31 | top_delta_y = top_right_y - top_left_y; 32 | bottom_delta_x = bottom_right_x - bottom_left_x; 33 | bottom_delta_y = bottom_right_y - bottom_left_y; 34 | 35 | s = ( 36 | (-top_delta_y * (top_left_x - bottom_left_x)) + (top_delta_x * (top_left_y - bottom_left_y)) 37 | ) / ( 38 | -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y 39 | ); 40 | 41 | t = ( 42 | (bottom_delta_x * (top_left_y - bottom_left_y)) - (bottom_delta_y * (top_left_x - bottom_left_x)) 43 | ) / ( 44 | -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y 45 | ); 46 | 47 | // Collision detected 48 | if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { 49 | return [ 50 | top_left_x + (t * top_delta_x), // X 51 | top_left_y + (t * top_delta_y) // Y 52 | ]; 53 | } 54 | 55 | // No collision 56 | return null; 57 | } 58 | 59 | function plotDifferenceArea(plot, ctx, series) { 60 | if ( series.fillBelowTo === null ) { 61 | return; 62 | } 63 | 64 | var otherseries, 65 | 66 | ps, 67 | points, 68 | 69 | otherps, 70 | otherpoints, 71 | 72 | plotOffset, 73 | fillStyle; 74 | 75 | function openPolygon(x, y) { 76 | ctx.beginPath(); 77 | ctx.moveTo( 78 | series.xaxis.p2c(x) + plotOffset.left, 79 | series.yaxis.p2c(y) + plotOffset.top 80 | ); 81 | 82 | } 83 | 84 | function closePolygon() { 85 | ctx.closePath(); 86 | ctx.fill(); 87 | } 88 | 89 | function validateInput() { 90 | if (points.length/ps !== otherpoints.length/otherps) { 91 | console.error("Refusing to graph inconsistent number of points"); 92 | return false; 93 | } 94 | 95 | var i; 96 | for (i = 0; i < (points.length / ps); i++) { 97 | if ( 98 | points[i * ps] !== null && 99 | otherpoints[i * otherps] !== null && 100 | points[i * ps] !== otherpoints[i * otherps] 101 | ) { 102 | console.error("Refusing to graph points without matching value"); 103 | return false; 104 | } 105 | } 106 | 107 | return true; 108 | } 109 | 110 | function findNextStart(start_i, end_i) { 111 | console.assert(end_i > start_i, "expects the end index to be greater than the start index"); 112 | 113 | var start = ( 114 | start_i === 0 || 115 | points[start_i - 1] === null || 116 | otherpoints[start_i - 1] === null 117 | ), 118 | equal = false, 119 | i, 120 | intersect; 121 | 122 | for (i = start_i; i < end_i; i++) { 123 | // Take note of null points 124 | if ( 125 | points[(i * ps) + 1] === null || 126 | otherpoints[(i * ps) + 1] === null 127 | ) { 128 | equal = false; 129 | start = true; 130 | } 131 | 132 | // Take note of equal points 133 | else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { 134 | equal = true; 135 | start = false; 136 | } 137 | 138 | 139 | else if (points[(i * ps) + 1] > otherpoints[(i * otherps) + 1]) { 140 | // If we begin above the desired point 141 | if (start) { 142 | openPolygon(points[i * ps], points[(i * ps) + 1]); 143 | } 144 | 145 | // If an equal point preceeds this, start the polygon at that equal point 146 | else if (equal) { 147 | openPolygon(points[(i - 1) * ps], points[((i - 1) * ps) + 1]); 148 | } 149 | 150 | // Otherwise, find the intersection point, and start it there 151 | else { 152 | intersect = intersectionPoint(i); 153 | openPolygon(intersect[0], intersect[1]); 154 | } 155 | 156 | topTraversal(i, end_i); 157 | return; 158 | } 159 | 160 | // If we go below equal, equal at any preceeding point is irrelevant 161 | else { 162 | start = false; 163 | equal = false; 164 | } 165 | } 166 | } 167 | 168 | function intersectionPoint(right_i) { 169 | console.assert(right_i > 0, "expects the second point in the series line segment"); 170 | 171 | var i, intersect; 172 | 173 | for (i = 1; i < (otherpoints.length/otherps); i++) { 174 | intersect = segmentIntersection( 175 | points[(right_i - 1) * ps], points[((right_i - 1) * ps) + 1], 176 | points[right_i * ps], points[(right_i * ps) + 1], 177 | 178 | otherpoints[(i - 1) * otherps], otherpoints[((i - 1) * otherps) + 1], 179 | otherpoints[i * otherps], otherpoints[(i * otherps) + 1] 180 | ); 181 | 182 | if (intersect !== null) { 183 | return intersect; 184 | } 185 | } 186 | 187 | console.error("intersectionPoint() should only be called when an intersection happens"); 188 | } 189 | 190 | function bottomTraversal(start_i, end_i) { 191 | console.assert(start_i >= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)"); 192 | 193 | var i; 194 | 195 | for (i = start_i; i >= end_i; i--) { 196 | ctx.lineTo( 197 | otherseries.xaxis.p2c(otherpoints[i * otherps]) + plotOffset.left, 198 | otherseries.yaxis.p2c(otherpoints[(i * otherps) + 1]) + plotOffset.top 199 | ); 200 | } 201 | 202 | closePolygon(); 203 | } 204 | 205 | function topTraversal(start_i, end_i) { 206 | console.assert(start_i <= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)"); 207 | 208 | var i, 209 | intersect; 210 | 211 | for (i = start_i; i < end_i; i++) { 212 | if (points[(i * ps) + 1] === null && i > start_i) { 213 | bottomTraversal(i - 1, start_i); 214 | findNextStart(i, end_i); 215 | return; 216 | } 217 | 218 | else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { 219 | bottomTraversal(i, start_i); 220 | findNextStart(i, end_i); 221 | return; 222 | } 223 | 224 | else if (points[(i * ps) + 1] < otherpoints[(i * otherps) + 1]) { 225 | intersect = intersectionPoint(i); 226 | ctx.lineTo( 227 | series.xaxis.p2c(intersect[0]) + plotOffset.left, 228 | series.yaxis.p2c(intersect[1]) + plotOffset.top 229 | ); 230 | bottomTraversal(i, start_i); 231 | findNextStart(i, end_i); 232 | return; 233 | 234 | } 235 | 236 | else { 237 | ctx.lineTo( 238 | series.xaxis.p2c(points[i * ps]) + plotOffset.left, 239 | series.yaxis.p2c(points[(i * ps) + 1]) + plotOffset.top 240 | ); 241 | } 242 | } 243 | 244 | bottomTraversal(end_i, start_i); 245 | } 246 | 247 | 248 | // Begin processing 249 | 250 | otherseries = findBelowSeries( series, plot.getData() ); 251 | 252 | if ( !otherseries ) { 253 | return; 254 | } 255 | 256 | ps = series.datapoints.pointsize; 257 | points = series.datapoints.points; 258 | otherps = otherseries.datapoints.pointsize; 259 | otherpoints = otherseries.datapoints.points; 260 | plotOffset = plot.getPlotOffset(); 261 | 262 | if (!validateInput()) { 263 | return; 264 | } 265 | 266 | 267 | // Flot's getFillStyle() should probably be exposed somewhere 268 | fillStyle = $.color.parse(series.color); 269 | fillStyle.a = 0.4; 270 | fillStyle.normalize(); 271 | ctx.fillStyle = fillStyle.toString(); 272 | 273 | 274 | // Begin recursive bi-directional traversal 275 | findNextStart(0, points.length/ps); 276 | } 277 | 278 | plot.hooks.drawSeries.push(plotDifferenceArea); 279 | } 280 | 281 | $.plot.plugins.push({ 282 | init: init, 283 | options: options, 284 | name: "fillbelow", 285 | version: "0.1.0" 286 | }); 287 | 288 | })(jQuery); 289 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.time.js: -------------------------------------------------------------------------------- 1 | /* Pretty handling of time axes. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Set axis.mode to "time" to enable. See the section "Time series data" in 7 | API.txt for details. 8 | 9 | */ 10 | 11 | (function($) { 12 | 13 | var options = { 14 | xaxis: { 15 | timezone: null, // "browser" for local to the client or timezone for timezone-js 16 | timeformat: null, // format string to use 17 | twelveHourClock: false, // 12 or 24 time in time mode 18 | monthNames: null // list of names of months 19 | } 20 | }; 21 | 22 | // round to nearby lower multiple of base 23 | 24 | function floorInBase(n, base) { 25 | return base * Math.floor(n / base); 26 | } 27 | 28 | // Returns a string with the date d formatted according to fmt. 29 | // A subset of the Open Group's strftime format is supported. 30 | 31 | function formatDate(d, fmt, monthNames, dayNames) { 32 | 33 | if (typeof d.strftime == "function") { 34 | return d.strftime(fmt); 35 | } 36 | 37 | var leftPad = function(n, pad) { 38 | n = "" + n; 39 | pad = "" + (pad == null ? "0" : pad); 40 | return n.length == 1 ? pad + n : n; 41 | }; 42 | 43 | var r = []; 44 | var escape = false; 45 | var hours = d.getHours(); 46 | var isAM = hours < 12; 47 | 48 | if (monthNames == null) { 49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 50 | } 51 | 52 | if (dayNames == null) { 53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 54 | } 55 | 56 | var hours12; 57 | 58 | if (hours > 12) { 59 | hours12 = hours - 12; 60 | } else if (hours == 0) { 61 | hours12 = 12; 62 | } else { 63 | hours12 = hours; 64 | } 65 | 66 | for (var i = 0; i < fmt.length; ++i) { 67 | 68 | var c = fmt.charAt(i); 69 | 70 | if (escape) { 71 | switch (c) { 72 | case 'a': c = "" + dayNames[d.getDay()]; break; 73 | case 'b': c = "" + monthNames[d.getMonth()]; break; 74 | case 'd': c = leftPad(d.getDate(), ""); break; 75 | case 'e': c = leftPad(d.getDate(), " "); break; 76 | case 'h': // For back-compat with 0.7; remove in 1.0 77 | case 'H': c = leftPad(hours); break; 78 | case 'I': c = leftPad(hours12); break; 79 | case 'l': c = leftPad(hours12, " "); break; 80 | case 'm': c = leftPad(d.getMonth() + 1, ""); break; 81 | case 'M': c = leftPad(d.getMinutes()); break; 82 | // quarters not in Open Group's strftime specification 83 | case 'q': 84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break; 85 | case 'S': c = leftPad(d.getSeconds()); break; 86 | case 'y': c = leftPad(d.getFullYear() % 100); break; 87 | case 'Y': c = "" + d.getFullYear(); break; 88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; 89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; 90 | case 'w': c = "" + d.getDay(); break; 91 | } 92 | r.push(c); 93 | escape = false; 94 | } else { 95 | if (c == "%") { 96 | escape = true; 97 | } else { 98 | r.push(c); 99 | } 100 | } 101 | } 102 | 103 | return r.join(""); 104 | } 105 | 106 | // To have a consistent view of time-based data independent of which time 107 | // zone the client happens to be in we need a date-like object independent 108 | // of time zones. This is done through a wrapper that only calls the UTC 109 | // versions of the accessor methods. 110 | 111 | function makeUtcWrapper(d) { 112 | 113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { 114 | sourceObj[sourceMethod] = function() { 115 | return targetObj[targetMethod].apply(targetObj, arguments); 116 | }; 117 | }; 118 | 119 | var utc = { 120 | date: d 121 | }; 122 | 123 | // support strftime, if found 124 | 125 | if (d.strftime != undefined) { 126 | addProxyMethod(utc, "strftime", d, "strftime"); 127 | } 128 | 129 | addProxyMethod(utc, "getTime", d, "getTime"); 130 | addProxyMethod(utc, "setTime", d, "setTime"); 131 | 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; 133 | 134 | for (var p = 0; p < props.length; p++) { 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); 137 | } 138 | 139 | return utc; 140 | }; 141 | 142 | // select time zone strategy. This returns a date-like object tied to the 143 | // desired timezone 144 | 145 | function dateGenerator(ts, opts) { 146 | if (opts.timezone == "browser") { 147 | return new Date(ts); 148 | } else if (!opts.timezone || opts.timezone == "utc") { 149 | return makeUtcWrapper(new Date(ts)); 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { 151 | var d = new timezoneJS.Date(); 152 | // timezone-js is fickle, so be sure to set the time zone before 153 | // setting the time. 154 | d.setTimezone(opts.timezone); 155 | d.setTime(ts); 156 | return d; 157 | } else { 158 | return makeUtcWrapper(new Date(ts)); 159 | } 160 | } 161 | 162 | // map of app. size of time units in milliseconds 163 | 164 | var timeUnitSize = { 165 | "second": 1000, 166 | "minute": 60 * 1000, 167 | "hour": 60 * 60 * 1000, 168 | "day": 24 * 60 * 60 * 1000, 169 | "month": 30 * 24 * 60 * 60 * 1000, 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 172 | }; 173 | 174 | // the allowed tick sizes, after 1 year we use 175 | // an integer algorithm 176 | 177 | var baseSpec = [ 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], 179 | [30, "second"], 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], 181 | [30, "minute"], 182 | [1, "hour"], [2, "hour"], [4, "hour"], 183 | [8, "hour"], [12, "hour"], 184 | [1, "day"], [2, "day"], [3, "day"], 185 | [0.25, "month"], [0.5, "month"], [1, "month"], 186 | [2, "month"] 187 | ]; 188 | 189 | // we don't know which variant(s) we'll need yet, but generating both is 190 | // cheap 191 | 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], 193 | [1, "year"]]); 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], 195 | [1, "year"]]); 196 | 197 | function init(plot) { 198 | plot.hooks.processOptions.push(function (plot, options) { 199 | $.each(plot.getAxes(), function(axisName, axis) { 200 | 201 | var opts = axis.options; 202 | 203 | if (opts.mode == "time") { 204 | axis.tickGenerator = function(axis) { 205 | 206 | var ticks = []; 207 | var d = dateGenerator(axis.min, opts); 208 | var minSize = 0; 209 | 210 | // make quarter use a possibility if quarters are 211 | // mentioned in either of these options 212 | 213 | var spec = (opts.tickSize && opts.tickSize[1] === 214 | "quarter") || 215 | (opts.minTickSize && opts.minTickSize[1] === 216 | "quarter") ? specQuarters : specMonths; 217 | 218 | if (opts.minTickSize != null) { 219 | if (typeof opts.tickSize == "number") { 220 | minSize = opts.tickSize; 221 | } else { 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; 223 | } 224 | } 225 | 226 | for (var i = 0; i < spec.length - 1; ++i) { 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { 230 | break; 231 | } 232 | } 233 | 234 | var size = spec[i][0]; 235 | var unit = spec[i][1]; 236 | 237 | // special-case the possibility of several years 238 | 239 | if (unit == "year") { 240 | 241 | // if given a minTickSize in years, just use it, 242 | // ensuring that it's an integer 243 | 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { 245 | size = Math.floor(opts.minTickSize[0]); 246 | } else { 247 | 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); 249 | var norm = (axis.delta / timeUnitSize.year) / magn; 250 | 251 | if (norm < 1.5) { 252 | size = 1; 253 | } else if (norm < 3) { 254 | size = 2; 255 | } else if (norm < 7.5) { 256 | size = 5; 257 | } else { 258 | size = 10; 259 | } 260 | 261 | size *= magn; 262 | } 263 | 264 | // minimum size for years is 1 265 | 266 | if (size < 1) { 267 | size = 1; 268 | } 269 | } 270 | 271 | axis.tickSize = opts.tickSize || [size, unit]; 272 | var tickSize = axis.tickSize[0]; 273 | unit = axis.tickSize[1]; 274 | 275 | var step = tickSize * timeUnitSize[unit]; 276 | 277 | if (unit == "second") { 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); 279 | } else if (unit == "minute") { 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); 281 | } else if (unit == "hour") { 282 | d.setHours(floorInBase(d.getHours(), tickSize)); 283 | } else if (unit == "month") { 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); 285 | } else if (unit == "quarter") { 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, 287 | tickSize)); 288 | } else if (unit == "year") { 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); 290 | } 291 | 292 | // reset smaller components 293 | 294 | d.setMilliseconds(0); 295 | 296 | if (step >= timeUnitSize.minute) { 297 | d.setSeconds(0); 298 | } 299 | if (step >= timeUnitSize.hour) { 300 | d.setMinutes(0); 301 | } 302 | if (step >= timeUnitSize.day) { 303 | d.setHours(0); 304 | } 305 | if (step >= timeUnitSize.day * 4) { 306 | d.setDate(1); 307 | } 308 | if (step >= timeUnitSize.month * 2) { 309 | d.setMonth(floorInBase(d.getMonth(), 3)); 310 | } 311 | if (step >= timeUnitSize.quarter * 2) { 312 | d.setMonth(floorInBase(d.getMonth(), 6)); 313 | } 314 | if (step >= timeUnitSize.year) { 315 | d.setMonth(0); 316 | } 317 | 318 | var carry = 0; 319 | var v = Number.NaN; 320 | var prev; 321 | 322 | do { 323 | 324 | prev = v; 325 | v = d.getTime(); 326 | ticks.push(v); 327 | 328 | if (unit == "month" || unit == "quarter") { 329 | if (tickSize < 1) { 330 | 331 | // a bit complicated - we'll divide the 332 | // month/quarter up but we need to take 333 | // care of fractions so we don't end up in 334 | // the middle of a day 335 | 336 | d.setDate(1); 337 | var start = d.getTime(); 338 | d.setMonth(d.getMonth() + 339 | (unit == "quarter" ? 3 : 1)); 340 | var end = d.getTime(); 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); 342 | carry = d.getHours(); 343 | d.setHours(0); 344 | } else { 345 | d.setMonth(d.getMonth() + 346 | tickSize * (unit == "quarter" ? 3 : 1)); 347 | } 348 | } else if (unit == "year") { 349 | d.setFullYear(d.getFullYear() + tickSize); 350 | } else { 351 | d.setTime(v + step); 352 | } 353 | } while (v < axis.max && v != prev); 354 | 355 | return ticks; 356 | }; 357 | 358 | axis.tickFormatter = function (v, axis) { 359 | 360 | var d = dateGenerator(v, axis.options); 361 | 362 | // first check global format 363 | 364 | if (opts.timeformat != null) { 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); 366 | } 367 | 368 | // possibly use quarters if quarters are mentioned in 369 | // any of these places 370 | 371 | var useQuarters = (axis.options.tickSize && 372 | axis.options.tickSize[1] == "quarter") || 373 | (axis.options.minTickSize && 374 | axis.options.minTickSize[1] == "quarter"); 375 | 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; 377 | var span = axis.max - axis.min; 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; 380 | var fmt; 381 | 382 | if (t < timeUnitSize.minute) { 383 | fmt = hourCode + ":%M:%S" + suffix; 384 | } else if (t < timeUnitSize.day) { 385 | if (span < 2 * timeUnitSize.day) { 386 | fmt = hourCode + ":%M" + suffix; 387 | } else { 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; 389 | } 390 | } else if (t < timeUnitSize.month) { 391 | fmt = "%b %d"; 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || 393 | (!useQuarters && t < timeUnitSize.year)) { 394 | if (span < timeUnitSize.year) { 395 | fmt = "%b"; 396 | } else { 397 | fmt = "%b %Y"; 398 | } 399 | } else if (useQuarters && t < timeUnitSize.year) { 400 | if (span < timeUnitSize.year) { 401 | fmt = "Q%q"; 402 | } else { 403 | fmt = "Q%q %Y"; 404 | } 405 | } else { 406 | fmt = "%Y"; 407 | } 408 | 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); 410 | 411 | return rt; 412 | }; 413 | } 414 | }); 415 | }); 416 | } 417 | 418 | $.plot.plugins.push({ 419 | init: init, 420 | options: options, 421 | name: 'time', 422 | version: '1.0' 423 | }); 424 | 425 | // Time-axis support used to be in Flot core, which exposed the 426 | // formatDate function on the plot object. Various plugins depend 427 | // on the function, so we need to re-expose it here. 428 | 429 | $.plot.formatDate = formatDate; 430 | 431 | })(jQuery); 432 | -------------------------------------------------------------------------------- /src/vendor/flot/jquery.flot.selection.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for selecting regions of a plot. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin supports these options: 7 | 8 | selection: { 9 | mode: null or "x" or "y" or "xy", 10 | color: color, 11 | shape: "round" or "miter" or "bevel", 12 | minSize: number of pixels 13 | } 14 | 15 | Selection support is enabled by setting the mode to one of "x", "y" or "xy". 16 | In "x" mode, the user will only be able to specify the x range, similarly for 17 | "y" mode. For "xy", the selection becomes a rectangle where both ranges can be 18 | specified. "color" is color of the selection (if you need to change the color 19 | later on, you can get to it with plot.getOptions().selection.color). "shape" 20 | is the shape of the corners of the selection. 21 | 22 | "minSize" is the minimum size a selection can be in pixels. This value can 23 | be customized to determine the smallest size a selection can be and still 24 | have the selection rectangle be displayed. When customizing this value, the 25 | fact that it refers to pixels, not axis units must be taken into account. 26 | Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 27 | minute, setting "minSize" to 1 will not make the minimum selection size 1 28 | minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent 29 | "plotunselected" events from being fired when the user clicks the mouse without 30 | dragging. 31 | 32 | When selection support is enabled, a "plotselected" event will be emitted on 33 | the DOM element you passed into the plot function. The event handler gets a 34 | parameter with the ranges selected on the axes, like this: 35 | 36 | placeholder.bind( "plotselected", function( event, ranges ) { 37 | alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) 38 | // similar for yaxis - with multiple axes, the extra ones are in 39 | // x2axis, x3axis, ... 40 | }); 41 | 42 | The "plotselected" event is only fired when the user has finished making the 43 | selection. A "plotselecting" event is fired during the process with the same 44 | parameters as the "plotselected" event, in case you want to know what's 45 | happening while it's happening, 46 | 47 | A "plotunselected" event with no arguments is emitted when the user clicks the 48 | mouse to remove the selection. As stated above, setting "minSize" to 0 will 49 | destroy this behavior. 50 | 51 | The plugin allso adds the following methods to the plot object: 52 | 53 | - setSelection( ranges, preventEvent ) 54 | 55 | Set the selection rectangle. The passed in ranges is on the same form as 56 | returned in the "plotselected" event. If the selection mode is "x", you 57 | should put in either an xaxis range, if the mode is "y" you need to put in 58 | an yaxis range and both xaxis and yaxis if the selection mode is "xy", like 59 | this: 60 | 61 | setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); 62 | 63 | setSelection will trigger the "plotselected" event when called. If you don't 64 | want that to happen, e.g. if you're inside a "plotselected" handler, pass 65 | true as the second parameter. If you are using multiple axes, you can 66 | specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of 67 | xaxis, the plugin picks the first one it sees. 68 | 69 | - clearSelection( preventEvent ) 70 | 71 | Clear the selection rectangle. Pass in true to avoid getting a 72 | "plotunselected" event. 73 | 74 | - getSelection() 75 | 76 | Returns the current selection in the same format as the "plotselected" 77 | event. If there's currently no selection, the function returns null. 78 | 79 | */ 80 | 81 | (function ($) { 82 | function init(plot) { 83 | var selection = { 84 | first: { x: -1, y: -1}, second: { x: -1, y: -1}, 85 | show: false, 86 | active: false 87 | }; 88 | 89 | // FIXME: The drag handling implemented here should be 90 | // abstracted out, there's some similar code from a library in 91 | // the navigation plugin, this should be massaged a bit to fit 92 | // the Flot cases here better and reused. Doing this would 93 | // make this plugin much slimmer. 94 | var savedhandlers = {}; 95 | 96 | var mouseUpHandler = null; 97 | 98 | function onMouseMove(e) { 99 | if (selection.active) { 100 | updateSelection(e); 101 | 102 | plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); 103 | } 104 | } 105 | 106 | function onMouseDown(e) { 107 | if (e.which != 1) // only accept left-click 108 | return; 109 | 110 | // cancel out any text selections 111 | document.body.focus(); 112 | 113 | // prevent text selection and drag in old-school browsers 114 | if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { 115 | savedhandlers.onselectstart = document.onselectstart; 116 | document.onselectstart = function () { return false; }; 117 | } 118 | if (document.ondrag !== undefined && savedhandlers.ondrag == null) { 119 | savedhandlers.ondrag = document.ondrag; 120 | document.ondrag = function () { return false; }; 121 | } 122 | 123 | setSelectionPos(selection.first, e); 124 | 125 | selection.active = true; 126 | 127 | // this is a bit silly, but we have to use a closure to be 128 | // able to whack the same handler again 129 | mouseUpHandler = function (e) { onMouseUp(e); }; 130 | 131 | $(document).one("mouseup", mouseUpHandler); 132 | } 133 | 134 | function onMouseUp(e) { 135 | mouseUpHandler = null; 136 | 137 | // revert drag stuff for old-school browsers 138 | if (document.onselectstart !== undefined) 139 | document.onselectstart = savedhandlers.onselectstart; 140 | if (document.ondrag !== undefined) 141 | document.ondrag = savedhandlers.ondrag; 142 | 143 | // no more dragging 144 | selection.active = false; 145 | updateSelection(e); 146 | 147 | if (selectionIsSane()) 148 | triggerSelectedEvent(e); 149 | else { 150 | // this counts as a clear 151 | plot.getPlaceholder().trigger("plotunselected", [ ]); 152 | plot.getPlaceholder().trigger("plotselecting", [ null ]); 153 | } 154 | 155 | setTimeout(function() { 156 | plot.isSelecting = false; 157 | }, 10); 158 | 159 | return false; 160 | } 161 | 162 | function getSelection() { 163 | if (!selectionIsSane()) 164 | return null; 165 | 166 | if (!selection.show) return null; 167 | 168 | var r = {}, c1 = selection.first, c2 = selection.second; 169 | var axes = plot.getAxes(); 170 | // look if no axis is used 171 | var noAxisInUse = true; 172 | $.each(axes, function (name, axis) { 173 | if (axis.used) { 174 | anyUsed = false; 175 | } 176 | }) 177 | 178 | $.each(axes, function (name, axis) { 179 | if (axis.used || noAxisInUse) { 180 | var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); 181 | r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; 182 | } 183 | }); 184 | return r; 185 | } 186 | 187 | function triggerSelectedEvent(event) { 188 | var r = getSelection(); 189 | 190 | // Add ctrlKey and metaKey to event 191 | r.ctrlKey = event.ctrlKey; 192 | r.metaKey = event.metaKey; 193 | 194 | plot.getPlaceholder().trigger("plotselected", [ r ]); 195 | 196 | // backwards-compat stuff, to be removed in future 197 | if (r.xaxis && r.yaxis) 198 | plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); 199 | } 200 | 201 | function clamp(min, value, max) { 202 | return value < min ? min: (value > max ? max: value); 203 | } 204 | 205 | function setSelectionPos(pos, e) { 206 | var o = plot.getOptions(); 207 | var offset = plot.getPlaceholder().offset(); 208 | var plotOffset = plot.getPlotOffset(); 209 | pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); 210 | pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); 211 | 212 | if (o.selection.mode == "y") 213 | pos.x = pos == selection.first ? 0 : plot.width(); 214 | 215 | if (o.selection.mode == "x") 216 | pos.y = pos == selection.first ? 0 : plot.height(); 217 | } 218 | 219 | function updateSelection(pos) { 220 | if (pos.pageX == null) 221 | return; 222 | 223 | setSelectionPos(selection.second, pos); 224 | if (selectionIsSane()) { 225 | plot.isSelecting = true; 226 | selection.show = true; 227 | plot.triggerRedrawOverlay(); 228 | } 229 | else 230 | clearSelection(true); 231 | } 232 | 233 | function clearSelection(preventEvent) { 234 | if (selection.show) { 235 | selection.show = false; 236 | plot.triggerRedrawOverlay(); 237 | if (!preventEvent) 238 | plot.getPlaceholder().trigger("plotunselected", [ ]); 239 | } 240 | } 241 | 242 | // function taken from markings support in Flot 243 | function extractRange(ranges, coord) { 244 | var axis, from, to, key, axes = plot.getAxes(); 245 | 246 | for (var k in axes) { 247 | axis = axes[k]; 248 | if (axis.direction == coord) { 249 | key = coord + axis.n + "axis"; 250 | if (!ranges[key] && axis.n == 1) 251 | key = coord + "axis"; // support x1axis as xaxis 252 | if (ranges[key]) { 253 | from = ranges[key].from; 254 | to = ranges[key].to; 255 | break; 256 | } 257 | } 258 | } 259 | 260 | // backwards-compat stuff - to be removed in future 261 | if (!ranges[key]) { 262 | axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; 263 | from = ranges[coord + "1"]; 264 | to = ranges[coord + "2"]; 265 | } 266 | 267 | // auto-reverse as an added bonus 268 | if (from != null && to != null && from > to) { 269 | var tmp = from; 270 | from = to; 271 | to = tmp; 272 | } 273 | 274 | return { from: from, to: to, axis: axis }; 275 | } 276 | 277 | function setSelection(ranges, preventEvent) { 278 | var axis, range, o = plot.getOptions(); 279 | 280 | if (o.selection.mode == "y") { 281 | selection.first.x = 0; 282 | selection.second.x = plot.width(); 283 | } 284 | else { 285 | range = extractRange(ranges, "x"); 286 | 287 | selection.first.x = range.axis.p2c(range.from); 288 | selection.second.x = range.axis.p2c(range.to); 289 | } 290 | 291 | if (o.selection.mode == "x") { 292 | selection.first.y = 0; 293 | selection.second.y = plot.height(); 294 | } 295 | else { 296 | range = extractRange(ranges, "y"); 297 | 298 | selection.first.y = range.axis.p2c(range.from); 299 | selection.second.y = range.axis.p2c(range.to); 300 | } 301 | 302 | selection.show = true; 303 | plot.triggerRedrawOverlay(); 304 | if (!preventEvent && selectionIsSane()) 305 | triggerSelectedEvent(); 306 | } 307 | 308 | function selectionIsSane() { 309 | var minSize = plot.getOptions().selection.minSize; 310 | return Math.abs(selection.second.x - selection.first.x) >= minSize && 311 | Math.abs(selection.second.y - selection.first.y) >= minSize; 312 | } 313 | 314 | plot.clearSelection = clearSelection; 315 | plot.setSelection = setSelection; 316 | plot.getSelection = getSelection; 317 | 318 | plot.hooks.bindEvents.push(function(plot, eventHolder) { 319 | var o = plot.getOptions(); 320 | if (o.selection.mode != null) { 321 | eventHolder.mousemove(onMouseMove); 322 | eventHolder.mousedown(onMouseDown); 323 | } 324 | }); 325 | 326 | 327 | plot.hooks.drawOverlay.push(function (plot, ctx) { 328 | // draw selection 329 | if (selection.show && selectionIsSane()) { 330 | var plotOffset = plot.getPlotOffset(); 331 | var o = plot.getOptions(); 332 | 333 | ctx.save(); 334 | ctx.translate(plotOffset.left, plotOffset.top); 335 | 336 | var c = $.color.parse(o.selection.color); 337 | 338 | ctx.strokeStyle = c.scale('a', o.selection.strokeAlpha).toString(); 339 | ctx.lineWidth = 1; 340 | ctx.lineJoin = o.selection.shape; 341 | ctx.fillStyle = c.scale('a', o.selection.fillAlpha).toString(); 342 | 343 | var x = Math.min(selection.first.x, selection.second.x) + 0.5, 344 | y = Math.min(selection.first.y, selection.second.y) + 0.5, 345 | w = Math.abs(selection.second.x - selection.first.x) - 1, 346 | h = Math.abs(selection.second.y - selection.first.y) - 1; 347 | 348 | ctx.fillRect(x, y, w, h); 349 | ctx.strokeRect(x, y, w, h); 350 | 351 | ctx.restore(); 352 | } 353 | }); 354 | 355 | plot.hooks.shutdown.push(function (plot, eventHolder) { 356 | eventHolder.unbind("mousemove", onMouseMove); 357 | eventHolder.unbind("mousedown", onMouseDown); 358 | 359 | if (mouseUpHandler) 360 | $(document).unbind("mouseup", mouseUpHandler); 361 | }); 362 | 363 | } 364 | 365 | $.plot.plugins.push({ 366 | init: init, 367 | options: { 368 | selection: { 369 | mode: null, // one of null, "x", "y" or "xy" 370 | color: "#e8cfac", 371 | shape: "round", // one of "round", "miter", or "bevel" 372 | minSize: 5, // minimum number of pixels 373 | strokeAlpha: 0.8, 374 | fillAlpha: 0.4, 375 | } 376 | }, 377 | name: 'selection', 378 | version: '1.1' 379 | }); 380 | })(jQuery); 381 | --------------------------------------------------------------------------------