rozetko
7 years ago
46 changed files with 20539 additions and 0 deletions
@ -0,0 +1,38 @@ |
|||||||
|
node_modules |
||||||
|
dist |
||||||
|
npm-debug.log |
||||||
|
coverage/ |
||||||
|
.aws-config.json |
||||||
|
awsconfig |
||||||
|
/emails/dist |
||||||
|
/public_gen |
||||||
|
/tmp |
||||||
|
vendor/phantomjs/phantomjs |
||||||
|
|
||||||
|
docs/AWS_S3_BUCKET |
||||||
|
docs/GIT_BRANCH |
||||||
|
docs/VERSION |
||||||
|
docs/GITCOMMIT |
||||||
|
docs/changed-files |
||||||
|
docs/changed-files |
||||||
|
|
||||||
|
# locally required config files |
||||||
|
public/css/*.min.css |
||||||
|
|
||||||
|
# Editor junk |
||||||
|
*.sublime-workspace |
||||||
|
*.swp |
||||||
|
.idea/ |
||||||
|
*.iml |
||||||
|
|
||||||
|
/data/* |
||||||
|
/bin/* |
||||||
|
|
||||||
|
conf/custom.ini |
||||||
|
fig.yml |
||||||
|
profile.cov |
||||||
|
.notouch |
||||||
|
.DS_Store |
||||||
|
.tscache |
||||||
|
|
||||||
|
.vscode/ |
@ -0,0 +1,26 @@ |
|||||||
|
# Hastic Graph Panel |
||||||
|
|
||||||
|
A better version of default Grafana's Graph Panel |
||||||
|
|
||||||
|
Can render Anomalies & more. |
||||||
|
|
||||||
|
# Build |
||||||
|
|
||||||
|
``` |
||||||
|
npm install |
||||||
|
npm run build |
||||||
|
``` |
||||||
|
|
||||||
|
# Changelog |
||||||
|
|
||||||
|
[Improvements] |
||||||
|
|
||||||
|
* You can zoom during update |
||||||
|
|
||||||
|
|
||||||
|
# Credits |
||||||
|
|
||||||
|
Based on |
||||||
|
|
||||||
|
* [grafana-plugin-template-webpack-typescript](https://github.com/CorpGlory/grafana-plugin-template-webpack-typescript) |
||||||
|
* [@types/grafana](https://github.com/CorpGlory/types-grafana) |
@ -0,0 +1,51 @@ |
|||||||
|
const path = require('path'); |
||||||
|
const webpack = require('webpack'); |
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin'); |
||||||
|
|
||||||
|
function resolve(dir) { |
||||||
|
return path.join(__dirname, '..', dir) |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
target: 'node', |
||||||
|
context: resolve('src'), |
||||||
|
entry: './module.ts', |
||||||
|
output: { |
||||||
|
filename: "module.js", |
||||||
|
path: resolve('dist'), |
||||||
|
libraryTarget: "amd" |
||||||
|
}, |
||||||
|
externals: [ |
||||||
|
// remove the line below if you don't want to use buildin versions
|
||||||
|
'jquery', 'lodash', 'moment', 'angular', |
||||||
|
function(context, request, callback) { |
||||||
|
var prefix = 'grafana/'; |
||||||
|
if (request.indexOf(prefix) === 0) { |
||||||
|
return callback(null, request.substr(prefix.length)); |
||||||
|
} |
||||||
|
callback(); |
||||||
|
} |
||||||
|
], |
||||||
|
plugins: [ |
||||||
|
new webpack.optimize.OccurrenceOrderPlugin(), |
||||||
|
new CopyWebpackPlugin([ |
||||||
|
{ from: 'plugin.json' }, |
||||||
|
{ from: 'img/*' }, |
||||||
|
{ from: 'partials/*' } |
||||||
|
]) |
||||||
|
], |
||||||
|
resolve: { |
||||||
|
extensions: [".ts", ".js"], |
||||||
|
}, |
||||||
|
module: { |
||||||
|
rules: [ |
||||||
|
{ |
||||||
|
test: /\.tsx?$/,
|
||||||
|
loaders: [ |
||||||
|
"ts-loader" |
||||||
|
], |
||||||
|
exclude: /node_modules/, |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.devtool = "source-map"; |
||||||
|
conf.watch = true; |
||||||
|
conf.mode = 'development'; |
||||||
|
|
||||||
|
module.exports = conf; |
||||||
|
|
@ -0,0 +1,6 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.mode = 'development'; // cuz production wont work
|
||||||
|
|
||||||
|
module.exports = baseWebpackConfig; |
@ -0,0 +1,158 @@ |
|||||||
|
|
||||||
|
{ |
||||||
|
"alert": { |
||||||
|
"conditions": [ |
||||||
|
{ |
||||||
|
"evaluator": { |
||||||
|
"params": [ |
||||||
|
0.5 |
||||||
|
], |
||||||
|
"type": "gt" |
||||||
|
}, |
||||||
|
"operator": { |
||||||
|
"type": "and" |
||||||
|
}, |
||||||
|
"query": { |
||||||
|
"params": [ |
||||||
|
"A", |
||||||
|
"5m", |
||||||
|
"now" |
||||||
|
] |
||||||
|
}, |
||||||
|
"reducer": { |
||||||
|
"params": [], |
||||||
|
"type": "avg" |
||||||
|
}, |
||||||
|
"type": "query" |
||||||
|
} |
||||||
|
], |
||||||
|
"executionErrorState": "alerting", |
||||||
|
"frequency": "60s", |
||||||
|
"handler": 1, |
||||||
|
"name": "OkAlert", |
||||||
|
"noDataState": "no_data", |
||||||
|
"notifications": [] |
||||||
|
}, |
||||||
|
"aliasColors": {}, |
||||||
|
"analyticsType": "Anomaly detection", |
||||||
|
"anomalyType": "", |
||||||
|
"anomalyTypes": [ |
||||||
|
{ |
||||||
|
"algorithm": "unsupervised", |
||||||
|
"color": "rgba(214, 131, 206, 0.95)", |
||||||
|
"name": "cpu_utilization_unsupervised" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"algorithm": "unsupervised", |
||||||
|
"color": "red", |
||||||
|
"name": "AnomalyNamefff" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"algorithm": "unsupervised", |
||||||
|
"color": "red", |
||||||
|
"name": "AnomalyName" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"algorithm": "unsupervised", |
||||||
|
"color": "red", |
||||||
|
"name": "anomaly_nameret" |
||||||
|
} |
||||||
|
], |
||||||
|
"backendURL": "http://corpglory.com:8000", |
||||||
|
"bars": false, |
||||||
|
"dashLength": 10, |
||||||
|
"dashes": false, |
||||||
|
"datasource": "accelerometer", |
||||||
|
"fill": 1, |
||||||
|
"gridPos": { |
||||||
|
"h": 13, |
||||||
|
"w": 24, |
||||||
|
"x": 0, |
||||||
|
"y": 0 |
||||||
|
}, |
||||||
|
"id": 2, |
||||||
|
"legend": { |
||||||
|
"avg": false, |
||||||
|
"current": false, |
||||||
|
"max": false, |
||||||
|
"min": false, |
||||||
|
"show": true, |
||||||
|
"total": false, |
||||||
|
"values": false |
||||||
|
}, |
||||||
|
"lines": true, |
||||||
|
"linewidth": 1, |
||||||
|
"nullPointMode": "null", |
||||||
|
"percentage": false, |
||||||
|
"pointradius": 5, |
||||||
|
"points": false, |
||||||
|
"renderer": "flot", |
||||||
|
"seriesOverrides": [], |
||||||
|
"spaceLength": 10, |
||||||
|
"stack": false, |
||||||
|
"steppedLine": false, |
||||||
|
"targets": [ |
||||||
|
{ |
||||||
|
"groupBy": [], |
||||||
|
"measurement": "ec2_cpu_utilization", |
||||||
|
"orderByTime": "ASC", |
||||||
|
"policy": "default", |
||||||
|
"refId": "A", |
||||||
|
"resultFormat": "time_series", |
||||||
|
"select": [ |
||||||
|
[ |
||||||
|
{ |
||||||
|
"params": [ |
||||||
|
"value" |
||||||
|
], |
||||||
|
"type": "field" |
||||||
|
} |
||||||
|
] |
||||||
|
], |
||||||
|
"tags": [] |
||||||
|
} |
||||||
|
], |
||||||
|
"thresholds": [ |
||||||
|
{ |
||||||
|
"colorMode": "critical", |
||||||
|
"fill": true, |
||||||
|
"line": true, |
||||||
|
"op": "gt", |
||||||
|
"value": 0.5 |
||||||
|
} |
||||||
|
], |
||||||
|
"timeFrom": null, |
||||||
|
"timeShift": null, |
||||||
|
"title": "Panel Title", |
||||||
|
"tooltip": { |
||||||
|
"shared": true, |
||||||
|
"sort": 0, |
||||||
|
"value_type": "individual" |
||||||
|
}, |
||||||
|
"type": "corpglory-grafalys-graph-panel", |
||||||
|
"xaxis": { |
||||||
|
"buckets": null, |
||||||
|
"mode": "time", |
||||||
|
"name": null, |
||||||
|
"show": true, |
||||||
|
"values": [] |
||||||
|
}, |
||||||
|
"yaxes": [ |
||||||
|
{ |
||||||
|
"format": "short", |
||||||
|
"label": null, |
||||||
|
"logBase": 1, |
||||||
|
"max": null, |
||||||
|
"min": null, |
||||||
|
"show": true |
||||||
|
}, |
||||||
|
{ |
||||||
|
"format": "short", |
||||||
|
"label": null, |
||||||
|
"logBase": 1, |
||||||
|
"max": null, |
||||||
|
"min": null, |
||||||
|
"show": true |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
{ |
||||||
|
"name": "hastic-graph-panel", |
||||||
|
"version": "0.1.0", |
||||||
|
"description": "Grafalys default panel for rendering and interaction with analytic unit", |
||||||
|
"main": "dist/module", |
||||||
|
"scripts": { |
||||||
|
"build": "webpack --config build/webpack.prod.conf.js", |
||||||
|
"dev": "webpack --config build/webpack.dev.conf.js" |
||||||
|
}, |
||||||
|
"keywords": [], |
||||||
|
"author": "Alexey Velikiy", |
||||||
|
"license": "MIT", |
||||||
|
"repository": "https://github.com/CorpGlory/hastic/hastic-grafana-graph-panel", |
||||||
|
"devDependencies": { |
||||||
|
"@types/angular": "^1.6.43", |
||||||
|
"@types/flot": "0.0.31", |
||||||
|
"@types/grafana": "github:CorpGlory/types-grafana", |
||||||
|
"@types/jquery": "^3.3.0", |
||||||
|
"@types/lodash": "^4.14.104", |
||||||
|
"@types/moment": "^2.13.0", |
||||||
|
"@types/perfect-scrollbar": "^1.3.0", |
||||||
|
"@types/tinycolor2": "^1.4.0", |
||||||
|
"@types/md5": "^2.1.32", |
||||||
|
"babel-core": "^6.26.0", |
||||||
|
"babel-loader": "^7.1.2", |
||||||
|
"babel-preset-env": "^1.6.0", |
||||||
|
"copy-webpack-plugin": "^4.0.1", |
||||||
|
"loader-utils": "^1.1.0", |
||||||
|
"ts-loader": "^4.2.0", |
||||||
|
"typescript": "^2.8.3", |
||||||
|
"webpack": "^4.7.0", |
||||||
|
"webpack-cli": "^2.1.2", |
||||||
|
"md5": "^2.2.1" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"perfect-scrollbar": "^1.3.0", |
||||||
|
"tether-drop": "^1.4.2", |
||||||
|
"tinycolor2": "^1.4.1" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
import kbn from 'grafana/app/core/utils/kbn'; |
||||||
|
|
||||||
|
|
||||||
|
export class AxesEditorCtrl { |
||||||
|
panel: any; |
||||||
|
panelCtrl: any; |
||||||
|
unitFormats: any; |
||||||
|
logScales: any; |
||||||
|
xAxisModes: any; |
||||||
|
xAxisStatOptions: any; |
||||||
|
xNameSegment: any; |
||||||
|
|
||||||
|
/** @ngInject **/ |
||||||
|
constructor(private $scope, private $q) { |
||||||
|
this.panelCtrl = $scope.ctrl; |
||||||
|
this.panel = this.panelCtrl.panel; |
||||||
|
this.$scope.ctrl = this; |
||||||
|
|
||||||
|
this.unitFormats = kbn.getUnitFormats(); |
||||||
|
|
||||||
|
this.logScales = { |
||||||
|
linear: 1, |
||||||
|
'log (base 2)': 2, |
||||||
|
'log (base 10)': 10, |
||||||
|
'log (base 32)': 32, |
||||||
|
'log (base 1024)': 1024, |
||||||
|
}; |
||||||
|
|
||||||
|
this.xAxisModes = { |
||||||
|
Time: 'time', |
||||||
|
Series: 'series', |
||||||
|
Histogram: 'histogram', |
||||||
|
// 'Data field': 'field',
|
||||||
|
}; |
||||||
|
|
||||||
|
this.xAxisStatOptions = [ |
||||||
|
{ text: 'Avg', value: 'avg' }, |
||||||
|
{ text: 'Min', value: 'min' }, |
||||||
|
{ text: 'Max', value: 'max' }, |
||||||
|
{ text: 'Total', value: 'total' }, |
||||||
|
{ text: 'Count', value: 'count' }, |
||||||
|
{ text: 'Current', value: 'current' }, |
||||||
|
]; |
||||||
|
|
||||||
|
if (this.panel.xaxis.mode === 'custom') { |
||||||
|
if (!this.panel.xaxis.name) { |
||||||
|
this.panel.xaxis.name = 'specify field'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setUnitFormat(axis, subItem) { |
||||||
|
axis.format = subItem.value; |
||||||
|
this.panelCtrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
this.panelCtrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
xAxisModeChanged() { |
||||||
|
this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode(); |
||||||
|
this.panelCtrl.onDataReceived(this.panelCtrl.dataList); |
||||||
|
} |
||||||
|
|
||||||
|
xAxisValueChanged() { |
||||||
|
this.panelCtrl.onDataReceived(this.panelCtrl.dataList); |
||||||
|
} |
||||||
|
|
||||||
|
getDataFieldNames(onlyNumbers) { |
||||||
|
var props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers); |
||||||
|
var items = props.map(prop => { |
||||||
|
return { text: prop, value: prop }; |
||||||
|
}); |
||||||
|
return this.$q.when(items); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** @ngInject **/ |
||||||
|
export function axesEditorComponent() { |
||||||
|
'use strict'; |
||||||
|
return { |
||||||
|
restrict: 'E', |
||||||
|
scope: true, |
||||||
|
templateUrl: 'public/plugins/corpglory-grafalys-graph-panel/partials/axes_editor.html', |
||||||
|
controller: AxesEditorCtrl, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import _ from 'lodash'; |
||||||
|
import tinycolor from 'tinycolor2'; |
||||||
|
|
||||||
|
export const PALETTE_ROWS = 4; |
||||||
|
export const PALETTE_COLUMNS = 14; |
||||||
|
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; |
||||||
|
export const OK_COLOR = 'rgba(11, 237, 50, 1)'; |
||||||
|
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; |
||||||
|
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; |
||||||
|
export const REGION_FILL_ALPHA = 0.09; |
||||||
|
|
||||||
|
let colors = [ |
||||||
|
'#7EB26D', |
||||||
|
'#EAB839', |
||||||
|
'#6ED0E0', |
||||||
|
'#EF843C', |
||||||
|
'#E24D42', |
||||||
|
'#1F78C1', |
||||||
|
'#BA43A9', |
||||||
|
'#705DA0', |
||||||
|
'#508642', |
||||||
|
'#CCA300', |
||||||
|
'#447EBC', |
||||||
|
'#C15C17', |
||||||
|
'#890F02', |
||||||
|
'#0A437C', |
||||||
|
'#6D1F62', |
||||||
|
'#584477', |
||||||
|
'#B7DBAB', |
||||||
|
'#F4D598', |
||||||
|
'#70DBED', |
||||||
|
'#F9BA8F', |
||||||
|
'#F29191', |
||||||
|
'#82B5D8', |
||||||
|
'#E5A8E2', |
||||||
|
'#AEA2E0', |
||||||
|
'#629E51', |
||||||
|
'#E5AC0E', |
||||||
|
'#64B0C8', |
||||||
|
'#E0752D', |
||||||
|
'#BF1B00', |
||||||
|
'#0A50A1', |
||||||
|
'#962D82', |
||||||
|
'#614D93', |
||||||
|
'#9AC48A', |
||||||
|
'#F2C96D', |
||||||
|
'#65C5DB', |
||||||
|
'#F9934E', |
||||||
|
'#EA6460', |
||||||
|
'#5195CE', |
||||||
|
'#D683CE', |
||||||
|
'#806EB7', |
||||||
|
'#3F6833', |
||||||
|
'#967302', |
||||||
|
'#2F575E', |
||||||
|
'#99440A', |
||||||
|
'#58140C', |
||||||
|
'#052B51', |
||||||
|
'#511749', |
||||||
|
'#3F2B5B', |
||||||
|
'#E0F9D7', |
||||||
|
'#FCEACA', |
||||||
|
'#CFFAFF', |
||||||
|
'#F9E2D2', |
||||||
|
'#FCE2DE', |
||||||
|
'#BADFF4', |
||||||
|
'#F9D9F9', |
||||||
|
'#DEDAF7', |
||||||
|
]; |
||||||
|
|
||||||
|
|
||||||
|
export function hexToHsl(color) { |
||||||
|
return tinycolor(color).toHsl(); |
||||||
|
} |
||||||
|
|
||||||
|
export function hslToHex(color) { |
||||||
|
return tinycolor(color).toHexString(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export default colors; |
@ -0,0 +1,348 @@ |
|||||||
|
import { AnomalyService } from '../services/anomaly_service' |
||||||
|
|
||||||
|
import { |
||||||
|
AnomalyKey, AnomalyType, |
||||||
|
AnomalyTypesSet, AnomalySegment, AnomalySegmentsSearcher, AnomalySermentPair |
||||||
|
} from '../model/anomaly'; |
||||||
|
import { MetricExpanded } from '../model/metric' |
||||||
|
import { Segment, SegmentKey } from '../model/segment'; |
||||||
|
import { SegmentsSet } from '../model/segment_set'; |
||||||
|
import { SegmentArray } from '../model/segment_array'; |
||||||
|
import { Emitter } from 'grafana/app/core/utils/emitter' |
||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
export const REGION_FILL_ALPHA = 0.7; |
||||||
|
export const REGION_STROKE_ALPHA = 0.9; |
||||||
|
export const REGION_DELETE_COLOR_LIGHT = '#d1d1d1'; |
||||||
|
export const REGION_DELETE_COLOR_DARK = 'white'; |
||||||
|
|
||||||
|
|
||||||
|
export class AnomalyController { |
||||||
|
|
||||||
|
private _anomalyTypesSet: AnomalyTypesSet; |
||||||
|
private _selectedAnomalyKey: AnomalyKey = null; |
||||||
|
|
||||||
|
private _labelingDataAddedSegments: SegmentsSet<AnomalySegment>; |
||||||
|
private _labelingDataDeletedSegments: SegmentsSet<AnomalySegment>; |
||||||
|
private _newAnomalyType: AnomalyType = null; |
||||||
|
private _creatingNewAnomalyType: boolean = false; |
||||||
|
private _savingNewAnomalyType: boolean = false; |
||||||
|
private _tempIdCounted = -1; |
||||||
|
private _graphLocked = false; |
||||||
|
|
||||||
|
private _statusRunners: Set<AnomalyKey> = new Set<AnomalyKey>(); |
||||||
|
|
||||||
|
|
||||||
|
constructor(private _panelObject: any, private _anomalyService: AnomalyService, private _emitter: Emitter) { |
||||||
|
if(_panelObject.anomalyTypes === undefined) { |
||||||
|
_panelObject.anomalyTypes = []; |
||||||
|
} |
||||||
|
this._labelingDataAddedSegments = new SegmentArray<AnomalySegment>(); |
||||||
|
this._labelingDataDeletedSegments = new SegmentArray<AnomalySegment>(); |
||||||
|
this._anomalyTypesSet = new AnomalyTypesSet(this._panelObject.anomalyTypes); |
||||||
|
this.anomalyTypes.forEach(a => this.runAnomalyTypeAlertEnabledWaiter(a)); |
||||||
|
} |
||||||
|
|
||||||
|
getAnomalySegmentsSearcher(): AnomalySegmentsSearcher { |
||||||
|
return this._anomalySegmentsSearcher.bind(this); |
||||||
|
} |
||||||
|
|
||||||
|
private _anomalySegmentsSearcher(point: number): AnomalySermentPair[] { |
||||||
|
var result: AnomalySermentPair[] = []; |
||||||
|
this._anomalyTypesSet.anomalyTypes.forEach(at => { |
||||||
|
var segs = at.segments.findSegments(point); |
||||||
|
segs.forEach(s => { |
||||||
|
result.push({ anomalyType: at, segment: s }); |
||||||
|
}) |
||||||
|
}) |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
createAnomalyType() { |
||||||
|
this._newAnomalyType = new AnomalyType(); |
||||||
|
this._creatingNewAnomalyType = true; |
||||||
|
this._savingNewAnomalyType = false; |
||||||
|
} |
||||||
|
|
||||||
|
async saveNewAnomalyType(metricExpanded: MetricExpanded, panelId: number) { |
||||||
|
this._savingNewAnomalyType = true; |
||||||
|
await this._anomalyService.postNewAnomalyType(metricExpanded, this._newAnomalyType, panelId); |
||||||
|
this._anomalyTypesSet.addAnomalyType(this._newAnomalyType); |
||||||
|
this._creatingNewAnomalyType = false; |
||||||
|
this._savingNewAnomalyType = false; |
||||||
|
this.runAnomalyTypeAlertEnabledWaiter(this._newAnomalyType); |
||||||
|
} |
||||||
|
|
||||||
|
get creatingAnomalyType() { return this._creatingNewAnomalyType; } |
||||||
|
get savingAnomalyType() { return this._savingNewAnomalyType; } |
||||||
|
get newAnomalyType(): AnomalyType { return this._newAnomalyType; } |
||||||
|
|
||||||
|
get graphLocked() { return this._graphLocked; } |
||||||
|
set graphLocked(value) { |
||||||
|
this._graphLocked = value; |
||||||
|
} |
||||||
|
|
||||||
|
get labelingAnomaly(): AnomalyType { |
||||||
|
if(this._selectedAnomalyKey === null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return this._anomalyTypesSet.byKey(this._selectedAnomalyKey); |
||||||
|
} |
||||||
|
|
||||||
|
async toggleAnomalyTypeLabelingMode(key: AnomalyKey) { |
||||||
|
if(this.labelingAnomaly && this.labelingAnomaly.saving) { |
||||||
|
throw new Error('Can`t toggel during saving'); |
||||||
|
} |
||||||
|
if(this._selectedAnomalyKey === key) { |
||||||
|
return this.disableAnomalyLabeling(); |
||||||
|
} |
||||||
|
await this.disableAnomalyLabeling(); |
||||||
|
this._selectedAnomalyKey = key; |
||||||
|
this.labelingAnomaly.selected = true; |
||||||
|
this.toggleAnomalyVisibility(key, true); |
||||||
|
} |
||||||
|
|
||||||
|
async disableAnomalyLabeling() { |
||||||
|
if(this._selectedAnomalyKey === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.labelingAnomaly.saving = true; |
||||||
|
var newIds = await this._saveLabelingData(); |
||||||
|
this._labelingDataAddedSegments.getSegments().forEach((s, i) => { |
||||||
|
this.labelingAnomaly.segments.updateKey(s.key, newIds[i]); |
||||||
|
}) |
||||||
|
this.labelingAnomaly.saving = false; |
||||||
|
|
||||||
|
var anomaly = this.labelingAnomaly; |
||||||
|
this.dropLabeling(); |
||||||
|
this._runAnomalyTypeStatusWaiter(anomaly); |
||||||
|
} |
||||||
|
|
||||||
|
undoLabeling() { |
||||||
|
this._labelingDataAddedSegments.getSegments().forEach(s => { |
||||||
|
this.labelingAnomaly.segments.remove(s.key); |
||||||
|
}); |
||||||
|
this._labelingDataDeletedSegments.getSegments().forEach(s => { |
||||||
|
this.labelingAnomaly.segments.addSegment(s); |
||||||
|
}); |
||||||
|
this.dropLabeling(); |
||||||
|
} |
||||||
|
|
||||||
|
dropLabeling() { |
||||||
|
this._labelingDataAddedSegments.clear(); |
||||||
|
this._labelingDataDeletedSegments.clear(); |
||||||
|
this.labelingAnomaly.selected = false; |
||||||
|
this._selectedAnomalyKey = null; |
||||||
|
this._tempIdCounted = -1; |
||||||
|
} |
||||||
|
|
||||||
|
get labelingMode(): boolean { |
||||||
|
return this._selectedAnomalyKey !== null; |
||||||
|
} |
||||||
|
|
||||||
|
get labelingDeleteMode(): boolean { |
||||||
|
if(!this.labelingMode) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return this.labelingAnomaly.deleteMode; |
||||||
|
} |
||||||
|
|
||||||
|
addLabelSegment(segment: Segment) { |
||||||
|
var asegment = this.labelingAnomaly.addLabeledSegment(segment); |
||||||
|
this._labelingDataAddedSegments.addSegment(asegment); |
||||||
|
} |
||||||
|
|
||||||
|
get anomalyTypes(): AnomalyType[] { |
||||||
|
return this._anomalyTypesSet.anomalyTypes; |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyColorChange(key: AnomalyKey, value) { |
||||||
|
this._anomalyTypesSet.byKey(key).color = value; |
||||||
|
} |
||||||
|
|
||||||
|
fetchAnomalyTypesStatuses() { |
||||||
|
this.anomalyTypes.forEach(a => this._runAnomalyTypeStatusWaiter(a)); |
||||||
|
} |
||||||
|
|
||||||
|
async fetchAnomalyTypesSegments(from: number, to: number) { |
||||||
|
if(!_.isNumber(from)) { |
||||||
|
throw new Error('from isn`t number'); |
||||||
|
} |
||||||
|
if(!_.isNumber(+to)) { |
||||||
|
throw new Error('to isn`t number'); |
||||||
|
} |
||||||
|
var tasks = this.anomalyTypes.map(a => this.fetchSegments(a, from, to)); |
||||||
|
return Promise.all(tasks); |
||||||
|
} |
||||||
|
|
||||||
|
async fetchSegments(anomalyType: AnomalyType, from: number, to: number): Promise<void> { |
||||||
|
if(!_.isNumber(from)) { |
||||||
|
throw new Error('from isn`t number'); |
||||||
|
} |
||||||
|
if(!_.isNumber(+to)) { |
||||||
|
throw new Error('to isn`t number'); |
||||||
|
} |
||||||
|
var allSegmentsList = await this._anomalyService.getSegments(anomalyType.key, from, to); |
||||||
|
var allSegmentsSet = new SegmentArray(allSegmentsList); |
||||||
|
if(anomalyType.selected) { |
||||||
|
this._labelingDataAddedSegments.getSegments().forEach(s => allSegmentsSet.addSegment(s)); |
||||||
|
this._labelingDataDeletedSegments.getSegments().forEach(s => allSegmentsSet.remove(s.key)); |
||||||
|
} |
||||||
|
anomalyType.segments = allSegmentsSet; |
||||||
|
} |
||||||
|
|
||||||
|
private async _saveLabelingData(): Promise<SegmentKey[]> { |
||||||
|
var anomaly = this.labelingAnomaly; |
||||||
|
if(anomaly === null) { |
||||||
|
throw new Error('anomaly is not selected'); |
||||||
|
} |
||||||
|
|
||||||
|
if( |
||||||
|
this._labelingDataAddedSegments.length === 0 && |
||||||
|
this._labelingDataDeletedSegments.length === 0 |
||||||
|
) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
return this._anomalyService.updateSegments( |
||||||
|
anomaly.key, this._labelingDataAddedSegments, this._labelingDataDeletedSegments |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: move to renderer
|
||||||
|
updateFlotEvents(isEditMode, options) { |
||||||
|
if(options.grid.markings === undefined) { |
||||||
|
options.markings = []; |
||||||
|
} |
||||||
|
|
||||||
|
for(var i = 0; i < this.anomalyTypes.length; i++) { |
||||||
|
var anomalyType = this.anomalyTypes[i]; |
||||||
|
var borderColor = addAlphaToRGB(anomalyType.color, REGION_STROKE_ALPHA); |
||||||
|
var fillColor = addAlphaToRGB(anomalyType.color, REGION_FILL_ALPHA); |
||||||
|
var segments = anomalyType.segments.getSegments(); |
||||||
|
if(!anomalyType.visible) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if(isEditMode && this.labelingMode) { |
||||||
|
if(anomalyType.selected) { |
||||||
|
borderColor = addAlphaToRGB(borderColor, 0.7); |
||||||
|
fillColor = addAlphaToRGB(borderColor, 0.7); |
||||||
|
} else { |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var rangeDist = +options.xaxis.max - +options.xaxis.min; |
||||||
|
segments.forEach(s => { |
||||||
|
var expanded = s.expandDist(rangeDist, 0.01); |
||||||
|
options.grid.markings.push({ |
||||||
|
xaxis: { from: expanded.from, to: expanded.to }, |
||||||
|
color: fillColor |
||||||
|
}); |
||||||
|
options.grid.markings.push({ |
||||||
|
xaxis: { from: expanded.from, to: expanded.from }, |
||||||
|
color: borderColor |
||||||
|
}); |
||||||
|
options.grid.markings.push({ |
||||||
|
xaxis: { from: expanded.to, to: expanded.to }, |
||||||
|
color: borderColor |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
deleteLabelingAnomalySegmentsInRange(from: number, to: number) { |
||||||
|
var allRemovedSegs = this.labelingAnomaly.removeSegmentsInRange(from, to); |
||||||
|
allRemovedSegs.forEach(s => { |
||||||
|
if(!this._labelingDataAddedSegments.has(s.key)) { |
||||||
|
this._labelingDataDeletedSegments.addSegment(s); |
||||||
|
} |
||||||
|
}); |
||||||
|
this._labelingDataAddedSegments.removeInRange(from, to); |
||||||
|
} |
||||||
|
|
||||||
|
toggleDeleteMode() { |
||||||
|
if(!this.labelingMode) { |
||||||
|
throw new Error('Cant enter delete mode is labeling mode disabled'); |
||||||
|
} |
||||||
|
this.labelingAnomaly.deleteMode = !this.labelingAnomaly.deleteMode; |
||||||
|
} |
||||||
|
|
||||||
|
removeAnomalyType(key) { |
||||||
|
if(key === this._selectedAnomalyKey) { |
||||||
|
this.dropLabeling(); |
||||||
|
} |
||||||
|
this._anomalyTypesSet.removeAnomalyType(key); |
||||||
|
} |
||||||
|
|
||||||
|
private async _runAnomalyTypeStatusWaiter(anomalyType: AnomalyType) { |
||||||
|
if(anomalyType === undefined || anomalyType === null) { |
||||||
|
throw new Error('anomalyType not defined'); |
||||||
|
} |
||||||
|
|
||||||
|
if(this._statusRunners.has(anomalyType.key)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this._statusRunners.add(anomalyType.key); |
||||||
|
|
||||||
|
var statusGenerator = this._anomalyService.getAnomalyTypeStatusGenerator( |
||||||
|
anomalyType.key, 1000 |
||||||
|
); |
||||||
|
|
||||||
|
for await (const status of statusGenerator) { |
||||||
|
if(anomalyType.status !== status) { |
||||||
|
anomalyType.status = status; |
||||||
|
this._emitter.emit('anomaly-type-status-change', anomalyType); |
||||||
|
} |
||||||
|
if(!anomalyType.isActiveStatus) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this._statusRunners.delete(anomalyType.key); |
||||||
|
} |
||||||
|
|
||||||
|
async runAnomalyTypeAlertEnabledWaiter(anomalyType: AnomalyType) { |
||||||
|
var enabled = await this._anomalyService.getAlertEnabled(anomalyType.key); |
||||||
|
if(anomalyType.alertEnabled !== enabled) { |
||||||
|
anomalyType.alertEnabled = enabled; |
||||||
|
this._emitter.emit('anomaly-type-alert-change', anomalyType); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async toggleAnomalyTypeAlertEnabled(anomalyType: AnomalyType) { |
||||||
|
var enabled = anomalyType.alertEnabled; |
||||||
|
anomalyType.alertEnabled = undefined; |
||||||
|
await this._anomalyService.setAlertEnabled(anomalyType.key, enabled); |
||||||
|
anomalyType.alertEnabled = enabled; |
||||||
|
this._emitter.emit('anomaly-type-alert-change', anomalyType); |
||||||
|
} |
||||||
|
|
||||||
|
public getIdForNewLabelSegment() { |
||||||
|
this._tempIdCounted--; |
||||||
|
return this._tempIdCounted; |
||||||
|
} |
||||||
|
|
||||||
|
public toggleAnomalyVisibility(key: AnomalyKey, value?: boolean) { |
||||||
|
var anomaly = this._anomalyTypesSet.byKey(key); |
||||||
|
if(value !== undefined) { |
||||||
|
anomaly.visible = value; |
||||||
|
} else { |
||||||
|
anomaly.visible = !anomaly.visible; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
function addAlphaToRGB(colorString: string, alpha: number): string { |
||||||
|
let color = tinycolor(colorString); |
||||||
|
if (color.isValid()) { |
||||||
|
color.setAlpha(color.getAlpha() * alpha); |
||||||
|
return color.toRgbString(); |
||||||
|
} else { |
||||||
|
return colorString; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,214 @@ |
|||||||
|
import _ from 'lodash'; |
||||||
|
import TimeSeries from 'grafana/app/core/time_series2'; |
||||||
|
import colors from './colors'; |
||||||
|
|
||||||
|
export class DataProcessor { |
||||||
|
constructor(private panel) {} |
||||||
|
|
||||||
|
getSeriesList(options) { |
||||||
|
if (!options.dataList || options.dataList.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
// auto detect xaxis mode
|
||||||
|
var firstItem; |
||||||
|
if (options.dataList && options.dataList.length > 0) { |
||||||
|
firstItem = options.dataList[0]; |
||||||
|
let autoDetectMode = this.getAutoDetectXAxisMode(firstItem); |
||||||
|
if (this.panel.xaxis.mode !== autoDetectMode) { |
||||||
|
this.panel.xaxis.mode = autoDetectMode; |
||||||
|
this.setPanelDefaultsForNewXAxisMode(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
switch (this.panel.xaxis.mode) { |
||||||
|
case 'series': |
||||||
|
case 'time': { |
||||||
|
return options.dataList.map((item, index) => { |
||||||
|
return this.timeSeriesHandler(item, index, options); |
||||||
|
}); |
||||||
|
} |
||||||
|
case 'histogram': { |
||||||
|
let histogramDataList = [ |
||||||
|
{ |
||||||
|
target: 'count', |
||||||
|
datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))), |
||||||
|
}, |
||||||
|
]; |
||||||
|
return histogramDataList.map((item, index) => { |
||||||
|
return this.timeSeriesHandler(item, index, options); |
||||||
|
}); |
||||||
|
} |
||||||
|
case 'field': { |
||||||
|
return this.customHandler(firstItem); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getAutoDetectXAxisMode(firstItem) { |
||||||
|
switch (firstItem.type) { |
||||||
|
case 'docs': |
||||||
|
return 'field'; |
||||||
|
case 'table': |
||||||
|
return 'field'; |
||||||
|
default: { |
||||||
|
if (this.panel.xaxis.mode === 'series') { |
||||||
|
return 'series'; |
||||||
|
} |
||||||
|
if (this.panel.xaxis.mode === 'histogram') { |
||||||
|
return 'histogram'; |
||||||
|
} |
||||||
|
return 'time'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setPanelDefaultsForNewXAxisMode() { |
||||||
|
switch (this.panel.xaxis.mode) { |
||||||
|
case 'time': { |
||||||
|
this.panel.bars = false; |
||||||
|
this.panel.lines = true; |
||||||
|
this.panel.points = false; |
||||||
|
this.panel.legend.show = true; |
||||||
|
this.panel.tooltip.shared = true; |
||||||
|
this.panel.xaxis.values = []; |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'series': { |
||||||
|
this.panel.bars = true; |
||||||
|
this.panel.lines = false; |
||||||
|
this.panel.points = false; |
||||||
|
this.panel.stack = false; |
||||||
|
this.panel.legend.show = false; |
||||||
|
this.panel.tooltip.shared = false; |
||||||
|
this.panel.xaxis.values = ['total']; |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'histogram': { |
||||||
|
this.panel.bars = true; |
||||||
|
this.panel.lines = false; |
||||||
|
this.panel.points = false; |
||||||
|
this.panel.stack = false; |
||||||
|
this.panel.legend.show = false; |
||||||
|
this.panel.tooltip.shared = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
timeSeriesHandler(seriesData, index, options) { |
||||||
|
var datapoints = seriesData.datapoints || []; |
||||||
|
var alias = seriesData.target; |
||||||
|
|
||||||
|
var colorIndex = index % colors.length; |
||||||
|
var color = seriesData.color || this.panel.aliasColors[alias] || colors[colorIndex]; |
||||||
|
|
||||||
|
var series = new TimeSeries({ |
||||||
|
datapoints: datapoints, |
||||||
|
alias: alias, |
||||||
|
color: color, |
||||||
|
unit: seriesData.unit, |
||||||
|
}); |
||||||
|
|
||||||
|
if (datapoints && datapoints.length > 0) { |
||||||
|
var last = datapoints[datapoints.length - 1][1]; |
||||||
|
var from = options.range.from; |
||||||
|
if (last - from < -10000) { |
||||||
|
series.isOutsideRange = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return series; |
||||||
|
} |
||||||
|
|
||||||
|
customHandler(dataItem) { |
||||||
|
let nameField = this.panel.xaxis.name; |
||||||
|
if (!nameField) { |
||||||
|
throw { |
||||||
|
message: 'No field name specified to use for x-axis, check your axes settings', |
||||||
|
}; |
||||||
|
} |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
validateXAxisSeriesValue() { |
||||||
|
switch (this.panel.xaxis.mode) { |
||||||
|
case 'series': { |
||||||
|
if (this.panel.xaxis.values.length === 0) { |
||||||
|
this.panel.xaxis.values = ['total']; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var validOptions = this.getXAxisValueOptions({}); |
||||||
|
var found = _.find(validOptions, { value: this.panel.xaxis.values[0] }); |
||||||
|
if (!found) { |
||||||
|
this.panel.xaxis.values = ['total']; |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getDataFieldNames(dataList, onlyNumbers) { |
||||||
|
if (dataList.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
let fields = []; |
||||||
|
var firstItem = dataList[0]; |
||||||
|
let fieldParts = []; |
||||||
|
|
||||||
|
function getPropertiesRecursive(obj) { |
||||||
|
_.forEach(obj, (value, key) => { |
||||||
|
if (_.isObject(value)) { |
||||||
|
fieldParts.push(key); |
||||||
|
getPropertiesRecursive(value); |
||||||
|
} else { |
||||||
|
if (!onlyNumbers || _.isNumber(value)) { |
||||||
|
let field = fieldParts.concat(key).join('.'); |
||||||
|
fields.push(field); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
fieldParts.pop(); |
||||||
|
} |
||||||
|
|
||||||
|
if (firstItem.type === 'docs') { |
||||||
|
if (firstItem.datapoints.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
getPropertiesRecursive(firstItem.datapoints[0]); |
||||||
|
} |
||||||
|
|
||||||
|
return fields; |
||||||
|
} |
||||||
|
|
||||||
|
getXAxisValueOptions(options) { |
||||||
|
switch (this.panel.xaxis.mode) { |
||||||
|
case 'series': { |
||||||
|
return [ |
||||||
|
{ text: 'Avg', value: 'avg' }, |
||||||
|
{ text: 'Min', value: 'min' }, |
||||||
|
{ text: 'Max', value: 'max' }, |
||||||
|
{ text: 'Total', value: 'total' }, |
||||||
|
{ text: 'Count', value: 'count' }, |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
pluckDeep(obj: any, property: string) { |
||||||
|
let propertyParts = property.split('.'); |
||||||
|
let value = obj; |
||||||
|
for (let i = 0; i < propertyParts.length; ++i) { |
||||||
|
if (value[propertyParts[i]]) { |
||||||
|
value = value[propertyParts[i]]; |
||||||
|
} else { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,253 @@ |
|||||||
|
import PerfectScrollbar from 'perfect-scrollbar'; |
||||||
|
import * as $ from 'jquery'; |
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
|
||||||
|
export class GraphLegend { |
||||||
|
firstRender = true; |
||||||
|
ctrl: any; |
||||||
|
panel: any; |
||||||
|
data; |
||||||
|
seriesList; |
||||||
|
legendScrollbar; |
||||||
|
|
||||||
|
constructor(private $elem: JQuery<HTMLElement>, private popoverSrv, private scope) { |
||||||
|
this.ctrl = scope.ctrl; |
||||||
|
this.panel = this.ctrl.panel; |
||||||
|
scope.$on('$destroy', () => { |
||||||
|
if (this.legendScrollbar) { |
||||||
|
this.legendScrollbar.destroy(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
getSeriesIndexForElement(el) { |
||||||
|
return el.parents('[data-series-index]').data('series-index'); |
||||||
|
} |
||||||
|
|
||||||
|
openColorSelector(e) { |
||||||
|
// if we clicked inside poup container ignore click
|
||||||
|
if ($(e.target).parents('.popover').length) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var el = $(e.currentTarget).find('.fa-minus'); |
||||||
|
var index = this.getSeriesIndexForElement(el); |
||||||
|
var series = this.seriesList[index]; |
||||||
|
|
||||||
|
this.popoverSrv.show({ |
||||||
|
element: el[0], |
||||||
|
position: 'bottom left', |
||||||
|
targetAttachment: 'top left', |
||||||
|
template: |
||||||
|
'<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected"/>', |
||||||
|
openOn: 'hover', |
||||||
|
model: { |
||||||
|
series: series, |
||||||
|
toggleAxis: () => { |
||||||
|
this.ctrl.toggleAxis(series); |
||||||
|
}, |
||||||
|
colorSelected: color => { |
||||||
|
this.ctrl.changeSeriesColor(series, color); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
toggleSeries(e) { |
||||||
|
var el = $(e.currentTarget); |
||||||
|
var index = this.getSeriesIndexForElement(el); |
||||||
|
var seriesInfo = this.seriesList[index]; |
||||||
|
var scrollPosition = this.$elem.find('tbody').scrollTop(); |
||||||
|
this.ctrl.toggleSeries(seriesInfo, e); |
||||||
|
this.$elem.find('tbody').scrollTop(scrollPosition); |
||||||
|
} |
||||||
|
|
||||||
|
sortLegend(e) { |
||||||
|
var el = $(e.currentTarget); |
||||||
|
var stat = el.data('stat'); |
||||||
|
|
||||||
|
if (stat !== this.panel.legend.sort) { |
||||||
|
this.panel.legend.sortDesc = null; |
||||||
|
} |
||||||
|
|
||||||
|
// if already sort ascending, disable sorting
|
||||||
|
if (this.panel.legend.sortDesc === false) { |
||||||
|
this.panel.legend.sort = null; |
||||||
|
this.panel.legend.sortDesc = null; |
||||||
|
this.ctrl.render(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.panel.legend.sortDesc = !this.panel.legend.sortDesc; |
||||||
|
this.panel.legend.sort = stat; |
||||||
|
this.ctrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
getTableHeaderHtml(statName) { |
||||||
|
if (!this.panel.legend[statName]) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
var html = '<th class="pointer" data-stat="' + statName + '">' + statName; |
||||||
|
|
||||||
|
if (this.panel.legend.sort === statName) { |
||||||
|
var cssClass = this.panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; |
||||||
|
html += ' <span class="' + cssClass + '"></span>'; |
||||||
|
} |
||||||
|
|
||||||
|
return html + '</th>'; |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
this.data = this.ctrl.seriesList; |
||||||
|
if (!this.ctrl.panel.legend.show) { |
||||||
|
this.$elem.empty(); |
||||||
|
this.firstRender = true; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.firstRender) { |
||||||
|
this.$elem.on('click', '.graph-legend-icon', this.openColorSelector.bind(this)); |
||||||
|
this.$elem.on('click', '.graph-legend-alias', this.toggleSeries.bind(this)); |
||||||
|
this.$elem.on('click', 'th', this.sortLegend.bind(this)); |
||||||
|
this.firstRender = false; |
||||||
|
} |
||||||
|
|
||||||
|
this.seriesList = this.data; |
||||||
|
|
||||||
|
this.$elem.empty(); |
||||||
|
|
||||||
|
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
||||||
|
var width = this.panel.legend.rightSide && this.panel.legend.sideWidth ? this.panel.legend.sideWidth + 'px' : ''; |
||||||
|
this.$elem.css('min-width', width); |
||||||
|
|
||||||
|
this.$elem.toggleClass('graph-legend-table', this.panel.legend.alignAsTable === true); |
||||||
|
|
||||||
|
var tableHeaderElem; |
||||||
|
if (this.panel.legend.alignAsTable) { |
||||||
|
var header = '<tr>'; |
||||||
|
header += '<th colspan="2" style="text-align:left"></th>'; |
||||||
|
if (this.panel.legend.values) { |
||||||
|
header += this.getTableHeaderHtml('min'); |
||||||
|
header += this.getTableHeaderHtml('max'); |
||||||
|
header += this.getTableHeaderHtml('avg'); |
||||||
|
header += this.getTableHeaderHtml('current'); |
||||||
|
header += this.getTableHeaderHtml('total'); |
||||||
|
} |
||||||
|
header += '</tr>'; |
||||||
|
tableHeaderElem = $(header); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.panel.legend.sort) { |
||||||
|
this.seriesList = _.sortBy(this.seriesList, function(series) { |
||||||
|
return series.stats[this.panel.legend.sort]; |
||||||
|
}); |
||||||
|
if (this.panel.legend.sortDesc) { |
||||||
|
this.seriesList = this.seriesList.reverse(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// render first time for getting proper legend height
|
||||||
|
if (!this.panel.legend.rightSide) { |
||||||
|
this.renderLegendElement(tableHeaderElem); |
||||||
|
this.$elem.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
this.renderLegendElement(tableHeaderElem); |
||||||
|
} |
||||||
|
|
||||||
|
renderSeriesLegendElements() { |
||||||
|
let seriesElements = []; |
||||||
|
for (let i = 0; i < this.seriesList.length; i++) { |
||||||
|
var series = this.seriesList[i]; |
||||||
|
|
||||||
|
if (series.hideFromLegend(this.panel.legend)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
var html = '<div class="graph-legend-series'; |
||||||
|
|
||||||
|
if (series.yaxis === 2) { |
||||||
|
html += ' graph-legend-series--right-y'; |
||||||
|
} |
||||||
|
if (this.ctrl.hiddenSeries[series.alias]) { |
||||||
|
html += ' graph-legend-series-hidden'; |
||||||
|
} |
||||||
|
html += '" data-series-index="' + i + '">'; |
||||||
|
html += '<div class="graph-legend-icon">'; |
||||||
|
html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>'; |
||||||
|
html += '</div>'; |
||||||
|
|
||||||
|
html += |
||||||
|
'<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>'; |
||||||
|
|
||||||
|
if (this.panel.legend.values) { |
||||||
|
var avg = series.formatValue(series.stats.avg); |
||||||
|
var current = series.formatValue(series.stats.current); |
||||||
|
var min = series.formatValue(series.stats.min); |
||||||
|
var max = series.formatValue(series.stats.max); |
||||||
|
var total = series.formatValue(series.stats.total); |
||||||
|
|
||||||
|
if (this.panel.legend.min) { |
||||||
|
html += '<div class="graph-legend-value min">' + min + '</div>'; |
||||||
|
} |
||||||
|
if (this.panel.legend.max) { |
||||||
|
html += '<div class="graph-legend-value max">' + max + '</div>'; |
||||||
|
} |
||||||
|
if (this.panel.legend.avg) { |
||||||
|
html += '<div class="graph-legend-value avg">' + avg + '</div>'; |
||||||
|
} |
||||||
|
if (this.panel.legend.current) { |
||||||
|
html += '<div class="graph-legend-value current">' + current + '</div>'; |
||||||
|
} |
||||||
|
if (this.panel.legend.total) { |
||||||
|
html += '<div class="graph-legend-value total">' + total + '</div>'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
html += '</div>'; |
||||||
|
seriesElements.push($(html)); |
||||||
|
} |
||||||
|
return seriesElements; |
||||||
|
} |
||||||
|
|
||||||
|
renderLegendElement(tableHeaderElem) { |
||||||
|
var seriesElements = this.renderSeriesLegendElements(); |
||||||
|
|
||||||
|
if (this.panel.legend.alignAsTable) { |
||||||
|
var tbodyElem = $('<tbody></tbody>'); |
||||||
|
tbodyElem.append(tableHeaderElem); |
||||||
|
tbodyElem.append(seriesElements); |
||||||
|
this.$elem.append(tbodyElem); |
||||||
|
} else { |
||||||
|
this.$elem.append(seriesElements); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.panel.legend.rightSide) { |
||||||
|
this.addScrollbar(); |
||||||
|
} else { |
||||||
|
this.destroyScrollbar(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
addScrollbar() { |
||||||
|
const scrollbarOptions = { |
||||||
|
// Number of pixels the content height can surpass the container height without enabling the scroll bar.
|
||||||
|
scrollYMarginOffset: 2, |
||||||
|
suppressScrollX: true, |
||||||
|
}; |
||||||
|
|
||||||
|
if (!this.legendScrollbar) { |
||||||
|
this.legendScrollbar = new PerfectScrollbar(this.$elem[0], scrollbarOptions); |
||||||
|
} else { |
||||||
|
this.legendScrollbar.update(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
destroyScrollbar() { |
||||||
|
if (this.legendScrollbar) { |
||||||
|
this.legendScrollbar.destroy(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,843 @@ |
|||||||
|
import { Segment } from './model/segment'; |
||||||
|
import { GraphTooltip } from './graph_tooltip'; |
||||||
|
import { ThresholdManager } from './threshold_manager'; |
||||||
|
import { convertValuesToHistogram, getSeriesValues } from './histogram'; |
||||||
|
import { |
||||||
|
AnomalyController, |
||||||
|
REGION_FILL_ALPHA as ANOMALY_REGION_FILL_ALPHA, |
||||||
|
REGION_STROKE_ALPHA as ANOMALY_REGION_STROKE_ALPHA, |
||||||
|
REGION_DELETE_COLOR_LIGHT as ANOMALY_REGION_DELETE_COLOR_LIGHT, |
||||||
|
REGION_DELETE_COLOR_DARK as ANOMALY_REGION_DELETE_COLOR_DARK |
||||||
|
} from './controllers/anomaly_controller'; |
||||||
|
|
||||||
|
|
||||||
|
import './vendor/flot/jquery.flot'; |
||||||
|
import './vendor/flot/jquery.flot.time'; |
||||||
|
import './vendor/flot/jquery.flot.selection'; |
||||||
|
import './vendor/flot/jquery.flot.stack'; |
||||||
|
import './vendor/flot/jquery.flot.stackpercent'; |
||||||
|
import './vendor/flot/jquery.flot.fillbelow'; |
||||||
|
import './vendor/flot/jquery.flot.crosshair'; |
||||||
|
import './vendor/flot/jquery.flot.dashes'; |
||||||
|
import './vendor/flot/jquery.flot.events'; |
||||||
|
|
||||||
|
// import { EventManager } from 'grafana/app/features/annotations/event_manager';
|
||||||
|
import TimeSeries from 'grafana/app/core/time_series2'; |
||||||
|
import { getFlotTickDecimals } from 'grafana/app/core/utils/ticks'; |
||||||
|
import { tickStep } from 'grafana/app/core/utils/ticks'; |
||||||
|
import { appEvents, coreModule } from 'grafana/app/core/core'; |
||||||
|
import kbn from 'grafana/app/core/utils/kbn'; |
||||||
|
|
||||||
|
import * as $ from 'jquery'; |
||||||
|
import _ from 'lodash'; |
||||||
|
import moment from 'moment'; |
||||||
|
|
||||||
|
|
||||||
|
const COLOR_SELECTION = '#666'; |
||||||
|
|
||||||
|
|
||||||
|
export class GraphRenderer { |
||||||
|
|
||||||
|
private _anomalyController: AnomalyController; |
||||||
|
private data: any; |
||||||
|
private tooltip: GraphTooltip; |
||||||
|
private thresholdManager: ThresholdManager; |
||||||
|
private panelWidth: number; |
||||||
|
private plot: any; |
||||||
|
private sortedSeries: any; |
||||||
|
private ctrl: any; |
||||||
|
private dashboard: any; |
||||||
|
private panel: any; |
||||||
|
// private eventManager;
|
||||||
|
private flotOptions: any = {} |
||||||
|
private $elem: JQuery<HTMLElement>; |
||||||
|
private annotations: any[]; |
||||||
|
private contextSrv: any; |
||||||
|
private popoverSrv: any; |
||||||
|
private scope: any; |
||||||
|
private timeSrv: any; |
||||||
|
private _graphMousePosition: any; |
||||||
|
|
||||||
|
constructor ($elem: JQuery<HTMLElement>, timeSrv, popoverSrv, contextSrv, scope) { |
||||||
|
|
||||||
|
var self = this; |
||||||
|
this.$elem = $elem; |
||||||
|
this.ctrl = scope.ctrl; |
||||||
|
this.dashboard = this.ctrl.dashboard; |
||||||
|
this.panel = this.ctrl.panel; |
||||||
|
|
||||||
|
this.timeSrv = timeSrv; |
||||||
|
this.popoverSrv = popoverSrv; |
||||||
|
this.contextSrv = contextSrv; |
||||||
|
this.scope = scope; |
||||||
|
|
||||||
|
this._anomalyController = this.ctrl.anomalyController; |
||||||
|
if(this._anomalyController === undefined) { |
||||||
|
throw new Error('anomalyController is undefined'); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
this.annotations = []; |
||||||
|
this.panelWidth = 0; |
||||||
|
|
||||||
|
// this.eventManager = new EventManager(this.ctrl);
|
||||||
|
this.flotOptions = {} |
||||||
|
this.thresholdManager = new ThresholdManager(this.ctrl); |
||||||
|
this.tooltip = new GraphTooltip( |
||||||
|
$elem, this.dashboard, scope, () => this.sortedSeries, |
||||||
|
this._anomalyController.getAnomalySegmentsSearcher() |
||||||
|
); |
||||||
|
|
||||||
|
// panel events
|
||||||
|
this.ctrl.events.on('panel-teardown', () => { |
||||||
|
this.thresholdManager = null; |
||||||
|
|
||||||
|
if (this.plot) { |
||||||
|
this.plot.destroy(); |
||||||
|
this.plot = null; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// global events
|
||||||
|
appEvents.on('graph-hover', this._onGraphHover.bind(this), scope); |
||||||
|
appEvents.on('graph-hover-clear', this._onGraphHoverClear.bind(this), scope); |
||||||
|
|
||||||
|
$elem.bind('plotselected', (event, selectionEvent) => { |
||||||
|
if (this.panel.xaxis.mode !== 'time') { |
||||||
|
// Skip if panel in histogram or series mode
|
||||||
|
this.plot.clearSelection(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(this._isAnomalyEvent(selectionEvent)) { |
||||||
|
this.plot.clearSelection(); |
||||||
|
var id = this._anomalyController.getIdForNewLabelSegment() |
||||||
|
var segment = new Segment( |
||||||
|
id, |
||||||
|
Math.round(selectionEvent.xaxis.from), |
||||||
|
Math.round(selectionEvent.xaxis.to) |
||||||
|
); |
||||||
|
if(this._anomalyController.labelingDeleteMode) { |
||||||
|
this._anomalyController.deleteLabelingAnomalySegmentsInRange( |
||||||
|
segment.from, segment.to |
||||||
|
); |
||||||
|
} else { |
||||||
|
this._anomalyController.addLabelSegment(segment); |
||||||
|
} |
||||||
|
this._renderPanel(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ((selectionEvent.ctrlKey || selectionEvent.metaKey) && contextSrv.isEditor) { |
||||||
|
// Add annotation
|
||||||
|
setTimeout(() => { |
||||||
|
// this.eventManager.updateTime(selectionEvent.xaxis);
|
||||||
|
}, 100); |
||||||
|
} else { |
||||||
|
scope.$apply(function() { |
||||||
|
timeSrv.setTime({ |
||||||
|
from: moment.utc(selectionEvent.xaxis.from), |
||||||
|
to: moment.utc(selectionEvent.xaxis.to), |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
$elem.bind('plotclick', (event, flotEvent, item) => { |
||||||
|
if (this.panel.xaxis.mode !== 'time') { |
||||||
|
// Skip if panel in histogram or series mode
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if(this._isAnomalyEvent(flotEvent)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ((flotEvent.ctrlKey || flotEvent.metaKey) && contextSrv.isEditor) { |
||||||
|
// Skip if range selected (added in "plotselected" event handler)
|
||||||
|
let isRangeSelection = flotEvent.x !== flotEvent.x1; |
||||||
|
if (!isRangeSelection) { |
||||||
|
setTimeout(() => { |
||||||
|
// this.eventManager.updateTime({ from: flotEvent.x, to: null });
|
||||||
|
}, 100); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
$elem.mouseleave(() => { |
||||||
|
if (this.panel.tooltip.shared) { |
||||||
|
var plot = $elem.data().plot; |
||||||
|
if (plot) { |
||||||
|
this.tooltip.clear(plot); |
||||||
|
} |
||||||
|
} |
||||||
|
appEvents.emit('graph-hover-clear'); |
||||||
|
}); |
||||||
|
|
||||||
|
$elem.bind("plothover", (event, pos, item) => { |
||||||
|
self.tooltip.show(pos, item); |
||||||
|
pos.panelRelY = (pos.pageY - $elem.offset().top) / $elem.height(); |
||||||
|
self._graphMousePosition = this.plot.p2c(pos); |
||||||
|
appEvents.emit('graph-hover', { pos: pos, panel: this.panel }); |
||||||
|
}); |
||||||
|
|
||||||
|
$elem.bind("plotclick", (event, pos, item) => { |
||||||
|
appEvents.emit('graph-click', { pos: pos, panel: this.panel, item: item }); |
||||||
|
}); |
||||||
|
|
||||||
|
$elem.mousedown(e => { |
||||||
|
this._anomalyController.graphLocked = true; |
||||||
|
this._chooseSelectionColor(e); |
||||||
|
}); |
||||||
|
|
||||||
|
$(document).mouseup(e => { |
||||||
|
this._anomalyController.graphLocked = false; |
||||||
|
}) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
public render(renderData) { |
||||||
|
this.data = renderData || this.data; |
||||||
|
if (!this.data) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.annotations = this.ctrl.annotations || []; |
||||||
|
this._buildFlotPairs(this.data); |
||||||
|
updateLegendValues(this.data, this.panel); |
||||||
|
this._renderPanel(); |
||||||
|
if(this.tooltip.visible) { |
||||||
|
var pos = this.plot.c2p(this._graphMousePosition); |
||||||
|
var canvasOffset = this.$elem.find('.flot-overlay').offset(); |
||||||
|
this.tooltip.show(pos); |
||||||
|
this.plot.setCrosshair(pos); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _onGraphHover(evt: any) { |
||||||
|
|
||||||
|
if (!this.dashboard.sharedTooltipModeEnabled()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// ignore if we are the emitter
|
||||||
|
if (!this.plot || evt.panel.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this._graphMousePosition = this.plot.p2c(evt.pos); |
||||||
|
this.tooltip.show(evt.pos); |
||||||
|
} |
||||||
|
|
||||||
|
private _onGraphHoverClear() { |
||||||
|
if (this.plot) { |
||||||
|
this.tooltip.clear(this.plot); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _shouldAbortRender() { |
||||||
|
if (!this.data) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.panelWidth === 0) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private _drawHook(plot) { |
||||||
|
// add left axis labels
|
||||||
|
if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) { |
||||||
|
$("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>") |
||||||
|
.text(this.panel.yaxes[0].label) |
||||||
|
.appendTo(this.$elem); |
||||||
|
} |
||||||
|
|
||||||
|
// add right axis labels
|
||||||
|
if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) { |
||||||
|
$("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>") |
||||||
|
.text(this.panel.yaxes[1].label) |
||||||
|
.appendTo(this.$elem); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.ctrl.dataWarning) { |
||||||
|
$(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.$elem); |
||||||
|
} |
||||||
|
|
||||||
|
this.thresholdManager.draw(plot); |
||||||
|
} |
||||||
|
|
||||||
|
private _processOffsetHook(plot, gridMargin) { |
||||||
|
var left = this.panel.yaxes[0]; |
||||||
|
var right = this.panel.yaxes[1]; |
||||||
|
if (left.show && left.label) { |
||||||
|
gridMargin.left = 20; |
||||||
|
} |
||||||
|
if (right.show && right.label) { |
||||||
|
gridMargin.right = 20; |
||||||
|
} |
||||||
|
|
||||||
|
// apply y-axis min/max options
|
||||||
|
var yaxis = plot.getYAxes(); |
||||||
|
for (var i = 0; i < yaxis.length; i++) { |
||||||
|
var axis = yaxis[i]; |
||||||
|
var panelOptions = this.panel.yaxes[i]; |
||||||
|
axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max; |
||||||
|
axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Series could have different timeSteps,
|
||||||
|
// let's find the smallest one so that bars are correctly rendered.
|
||||||
|
// In addition, only take series which are rendered as bars for this.
|
||||||
|
private _getMinTimeStepOfSeries(data) { |
||||||
|
var min = Number.MAX_VALUE; |
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) { |
||||||
|
if (!data[i].stats.timeStep) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (this.panel.bars) { |
||||||
|
if (data[i].bars && data[i].bars.show === false) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (data[i].stats.timeStep < min) { |
||||||
|
min = data[i].stats.timeStep; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return min; |
||||||
|
} |
||||||
|
|
||||||
|
// Function for rendering panel
|
||||||
|
private _renderPanel() { |
||||||
|
|
||||||
|
this.panelWidth = this.$elem.width(); |
||||||
|
if (this._shouldAbortRender()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// give space to alert editing
|
||||||
|
this.thresholdManager.prepare(this.$elem, this.data); |
||||||
|
|
||||||
|
// un-check dashes if lines are unchecked
|
||||||
|
this.panel.dashes = this.panel.lines ? this.panel.dashes : false; |
||||||
|
|
||||||
|
// Populate element
|
||||||
|
this._buildFlotOptions(this.panel); |
||||||
|
this._prepareXAxis(this.panel); |
||||||
|
this._configureYAxisOptions(this.data); |
||||||
|
this.thresholdManager.addFlotOptions(this.flotOptions, this.panel); |
||||||
|
// this.eventManager.addFlotEvents(this.annotations, this.flotOptions);
|
||||||
|
this._anomalyController.updateFlotEvents(this.contextSrv.isEditor, this.flotOptions); |
||||||
|
|
||||||
|
this.sortedSeries = this._sortSeries(this.data, this.panel); |
||||||
|
this._callPlot(true); |
||||||
|
} |
||||||
|
|
||||||
|
private _chooseSelectionColor(e) { |
||||||
|
var color = COLOR_SELECTION; |
||||||
|
var fillAlpha = 0.4; |
||||||
|
var strokeAlpha = 0.4; |
||||||
|
if(this._isAnomalyEvent(e)) { |
||||||
|
if(this._anomalyController.labelingDeleteMode) { |
||||||
|
color = this.contextSrv.user.lightTheme ?
|
||||||
|
ANOMALY_REGION_DELETE_COLOR_LIGHT : |
||||||
|
ANOMALY_REGION_DELETE_COLOR_DARK; |
||||||
|
} else { |
||||||
|
color = this._anomalyController.labelingAnomaly.color; |
||||||
|
} |
||||||
|
fillAlpha = ANOMALY_REGION_FILL_ALPHA; |
||||||
|
strokeAlpha = ANOMALY_REGION_STROKE_ALPHA; |
||||||
|
} |
||||||
|
this.plot.getOptions().selection.color = color |
||||||
|
} |
||||||
|
|
||||||
|
private _buildFlotPairs(data) { |
||||||
|
for (let i = 0; i < data.length; i++) { |
||||||
|
let series = data[i]; |
||||||
|
series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode); |
||||||
|
|
||||||
|
// if hidden remove points and disable stack
|
||||||
|
if (this.ctrl.hiddenSeries[series.alias]) { |
||||||
|
series.data = []; |
||||||
|
series.stack = false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _prepareXAxis(panel) { |
||||||
|
switch (panel.xaxis.mode) { |
||||||
|
case 'series': { |
||||||
|
this.flotOptions.series.bars.barWidth = 0.7; |
||||||
|
this.flotOptions.series.bars.align = 'center'; |
||||||
|
|
||||||
|
for (let i = 0; i < this.data.length; i++) { |
||||||
|
let series = this.data[i]; |
||||||
|
series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; |
||||||
|
} |
||||||
|
|
||||||
|
this._addXSeriesAxis(); |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'histogram': { |
||||||
|
let bucketSize: number; |
||||||
|
let values = getSeriesValues(this.data); |
||||||
|
|
||||||
|
if (this.data.length && values.length) { |
||||||
|
let histMin = _.min(_.map(this.data, (s:any) => s.stats.min)); |
||||||
|
let histMax = _.max(_.map(this.data, (s:any) => s.stats.max)); |
||||||
|
let ticks = panel.xaxis.buckets || this.panelWidth / 50; |
||||||
|
bucketSize = tickStep(histMin, histMax, ticks); |
||||||
|
let histogram = convertValuesToHistogram(values, bucketSize); |
||||||
|
this.data[0].data = histogram; |
||||||
|
this.flotOptions.series.bars.barWidth = bucketSize * 0.8; |
||||||
|
} else { |
||||||
|
bucketSize = 0; |
||||||
|
} |
||||||
|
|
||||||
|
this._addXHistogramAxis(bucketSize); |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'table': { |
||||||
|
this.flotOptions.series.bars.barWidth = 0.7; |
||||||
|
this.flotOptions.series.bars.align = 'center'; |
||||||
|
this._addXTableAxis(); |
||||||
|
break; |
||||||
|
} |
||||||
|
default: { |
||||||
|
this.flotOptions.series.bars.barWidth = this._getMinTimeStepOfSeries(this.data) / 1.5; |
||||||
|
this._addTimeAxis(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _callPlot(incrementRenderCounter) { |
||||||
|
try { |
||||||
|
this.plot = $.plot(this.$elem, this.sortedSeries, this.flotOptions); |
||||||
|
if (this.ctrl.renderError) { |
||||||
|
delete this.ctrl.error; |
||||||
|
delete this.ctrl.inspector; |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log('flotcharts error', e); |
||||||
|
this.ctrl.error = e.message || 'Render Error'; |
||||||
|
this.ctrl.renderError = true; |
||||||
|
this.ctrl.inspector = { error: e }; |
||||||
|
} |
||||||
|
|
||||||
|
if (incrementRenderCounter) { |
||||||
|
this.ctrl.renderingCompleted(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _buildFlotOptions(panel) { |
||||||
|
const stack = panel.stack ? true : null; |
||||||
|
this.flotOptions = { |
||||||
|
hooks: { |
||||||
|
draw: [this._drawHook.bind(this)], |
||||||
|
processOffset: [this._processOffsetHook.bind(this)], |
||||||
|
}, |
||||||
|
legend: { show: false }, |
||||||
|
series: { |
||||||
|
stackpercent: panel.stack ? panel.percentage : false, |
||||||
|
stack: panel.percentage ? null : stack, |
||||||
|
lines: { |
||||||
|
show: panel.lines, |
||||||
|
zero: false, |
||||||
|
fill: this._translateFillOption(panel.fill), |
||||||
|
lineWidth: panel.dashes ? 0 : panel.linewidth, |
||||||
|
steps: panel.steppedLine, |
||||||
|
}, |
||||||
|
dashes: { |
||||||
|
show: panel.dashes, |
||||||
|
lineWidth: panel.linewidth, |
||||||
|
dashLength: [panel.dashLength, panel.spaceLength], |
||||||
|
}, |
||||||
|
bars: { |
||||||
|
show: panel.bars, |
||||||
|
fill: 1, |
||||||
|
barWidth: 1, |
||||||
|
zero: false, |
||||||
|
lineWidth: 0, |
||||||
|
}, |
||||||
|
points: { |
||||||
|
show: panel.points, |
||||||
|
fill: 1, |
||||||
|
fillColor: false, |
||||||
|
radius: panel.points ? panel.pointradius : 2, |
||||||
|
}, |
||||||
|
shadowSize: 0, |
||||||
|
}, |
||||||
|
yaxes: [], |
||||||
|
xaxis: {}, |
||||||
|
grid: { |
||||||
|
minBorderMargin: 0, |
||||||
|
markings: [], |
||||||
|
backgroundColor: null, |
||||||
|
borderWidth: 0, |
||||||
|
hoverable: true, |
||||||
|
clickable: true, |
||||||
|
color: '#c8c8c8', |
||||||
|
margin: { left: 0, right: 0 }, |
||||||
|
labelMarginX: 0, |
||||||
|
}, |
||||||
|
selection: { |
||||||
|
mode: 'x' |
||||||
|
}, |
||||||
|
crosshair: { |
||||||
|
mode: 'x', |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private _sortSeries(series, panel) { |
||||||
|
var sortBy = panel.legend.sort; |
||||||
|
var sortOrder = panel.legend.sortDesc; |
||||||
|
var haveSortBy = sortBy !== null || sortBy !== undefined; |
||||||
|
var haveSortOrder = sortOrder !== null || sortOrder !== undefined; |
||||||
|
var shouldSortBy = panel.stack && haveSortBy && haveSortOrder; |
||||||
|
var sortDesc = panel.legend.sortDesc === true ? -1 : 1; |
||||||
|
|
||||||
|
series.sort((x, y) => { |
||||||
|
if (x.zindex > y.zindex) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
if (x.zindex < y.zindex) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
if (shouldSortBy) { |
||||||
|
if (x.stats[sortBy] > y.stats[sortBy]) { |
||||||
|
return 1 * sortDesc; |
||||||
|
} |
||||||
|
if (x.stats[sortBy] < y.stats[sortBy]) { |
||||||
|
return -1 * sortDesc; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return 0; |
||||||
|
}); |
||||||
|
|
||||||
|
return series; |
||||||
|
} |
||||||
|
|
||||||
|
private _translateFillOption(fill) { |
||||||
|
if (this.panel.percentage && this.panel.stack) { |
||||||
|
return fill === 0 ? 0.001 : fill / 10; |
||||||
|
} else { |
||||||
|
return fill / 10; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _addTimeAxis() { |
||||||
|
var ticks = this.panelWidth / 100; |
||||||
|
var min = _.isUndefined(this.ctrl.range.from) ? null : this.ctrl.range.from.valueOf(); |
||||||
|
var max = _.isUndefined(this.ctrl.range.to) ? null : this.ctrl.range.to.valueOf(); |
||||||
|
|
||||||
|
this.flotOptions.xaxis = { |
||||||
|
timezone: this.dashboard.getTimezone(), |
||||||
|
show: this.panel.xaxis.show, |
||||||
|
mode: 'time', |
||||||
|
min: min, |
||||||
|
max: max, |
||||||
|
label: 'Datetime', |
||||||
|
ticks: ticks, |
||||||
|
timeformat: this._timeFormat(ticks, min, max), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private _addXSeriesAxis() { |
||||||
|
var ticks = _.map(this.data, function(series: any, index) { |
||||||
|
return [index + 1, series.alias]; |
||||||
|
}); |
||||||
|
|
||||||
|
this.flotOptions.xaxis = { |
||||||
|
timezone: this.dashboard.getTimezone(), |
||||||
|
show: this.panel.xaxis.show, |
||||||
|
mode: null, |
||||||
|
min: 0, |
||||||
|
max: ticks.length + 1, |
||||||
|
label: 'Datetime', |
||||||
|
ticks: ticks, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private _addXHistogramAxis(bucketSize) { |
||||||
|
let ticks, min, max; |
||||||
|
let defaultTicks = this.panelWidth / 50; |
||||||
|
|
||||||
|
if (this.data.length && bucketSize) { |
||||||
|
ticks = _.map(this.data[0].data, point => point[0]); |
||||||
|
min = _.min(ticks); |
||||||
|
max = _.max(ticks); |
||||||
|
|
||||||
|
// Adjust tick step
|
||||||
|
let tickStep = bucketSize; |
||||||
|
let ticks_num = Math.floor((max - min) / tickStep); |
||||||
|
while (ticks_num > defaultTicks) { |
||||||
|
tickStep = tickStep * 2; |
||||||
|
ticks_num = Math.ceil((max - min) / tickStep); |
||||||
|
} |
||||||
|
|
||||||
|
// Expand ticks for pretty view
|
||||||
|
min = Math.floor(min / tickStep) * tickStep; |
||||||
|
max = Math.ceil(max / tickStep) * tickStep; |
||||||
|
|
||||||
|
ticks = []; |
||||||
|
for (let i = min; i <= max; i += tickStep) { |
||||||
|
ticks.push(i); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// Set defaults if no data
|
||||||
|
ticks = defaultTicks / 2; |
||||||
|
min = 0; |
||||||
|
max = 1; |
||||||
|
} |
||||||
|
|
||||||
|
this.flotOptions.xaxis = { |
||||||
|
timezone: this.dashboard.getTimezone(), |
||||||
|
show: this.panel.xaxis.show, |
||||||
|
mode: null, |
||||||
|
min: min, |
||||||
|
max: max, |
||||||
|
label: 'Histogram', |
||||||
|
ticks: ticks, |
||||||
|
}; |
||||||
|
|
||||||
|
// Use 'short' format for histogram values
|
||||||
|
this._configureAxisMode(this.flotOptions.xaxis, 'short'); |
||||||
|
} |
||||||
|
|
||||||
|
private _addXTableAxis() { |
||||||
|
var ticks = _.map(this.data, function(series: any, seriesIndex) { |
||||||
|
return _.map(series.datapoints, function(point, pointIndex) { |
||||||
|
var tickIndex = seriesIndex * series.datapoints.length + pointIndex; |
||||||
|
return [tickIndex + 1, point[1]]; |
||||||
|
}); |
||||||
|
}); |
||||||
|
ticks = _.flatten(ticks, true); |
||||||
|
|
||||||
|
this.flotOptions.xaxis = { |
||||||
|
timezone: this.dashboard.getTimezone(), |
||||||
|
show: this.panel.xaxis.show, |
||||||
|
mode: null, |
||||||
|
min: 0, |
||||||
|
max: ticks.length + 1, |
||||||
|
label: 'Datetime', |
||||||
|
ticks: ticks, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private _configureYAxisOptions(data) { |
||||||
|
var defaults = { |
||||||
|
position: 'left', |
||||||
|
show: this.panel.yaxes[0].show, |
||||||
|
index: 1, |
||||||
|
logBase: this.panel.yaxes[0].logBase || 1, |
||||||
|
min: this._parseNumber(this.panel.yaxes[0].min), |
||||||
|
max: this._parseNumber(this.panel.yaxes[0].max), |
||||||
|
tickDecimals: this.panel.yaxes[0].decimals, |
||||||
|
}; |
||||||
|
|
||||||
|
this.flotOptions.yaxes.push(defaults); |
||||||
|
|
||||||
|
if (_.find(data, { yaxis: 2 })) { |
||||||
|
var secondY = _.clone(defaults); |
||||||
|
secondY.index = 2; |
||||||
|
secondY.show = this.panel.yaxes[1].show; |
||||||
|
secondY.logBase = this.panel.yaxes[1].logBase || 1; |
||||||
|
secondY.position = 'right'; |
||||||
|
secondY.min = this._parseNumber(this.panel.yaxes[1].min); |
||||||
|
secondY.max = this._parseNumber(this.panel.yaxes[1].max); |
||||||
|
secondY.tickDecimals = this.panel.yaxes[1].decimals; |
||||||
|
this.flotOptions.yaxes.push(secondY); |
||||||
|
|
||||||
|
this._applyLogScale(this.flotOptions.yaxes[1], data); |
||||||
|
this._configureAxisMode(this.flotOptions.yaxes[1], this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format); |
||||||
|
} |
||||||
|
this._applyLogScale(this.flotOptions.yaxes[0], data); |
||||||
|
this._configureAxisMode(this.flotOptions.yaxes[0], this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format); |
||||||
|
} |
||||||
|
|
||||||
|
private _parseNumber(value: any) { |
||||||
|
if (value === null || typeof value === 'undefined') { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return _.toNumber(value); |
||||||
|
} |
||||||
|
|
||||||
|
private _applyLogScale(axis, data) { |
||||||
|
if (axis.logBase === 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const minSetToZero = axis.min === 0; |
||||||
|
|
||||||
|
if (axis.min < Number.MIN_VALUE) { |
||||||
|
axis.min = null; |
||||||
|
} |
||||||
|
if (axis.max < Number.MIN_VALUE) { |
||||||
|
axis.max = null; |
||||||
|
} |
||||||
|
|
||||||
|
var series, i; |
||||||
|
var max = axis.max, |
||||||
|
min = axis.min; |
||||||
|
|
||||||
|
for (i = 0; i < data.length; i++) { |
||||||
|
series = data[i]; |
||||||
|
if (series.yaxis === axis.index) { |
||||||
|
if (!max || max < series.stats.max) { |
||||||
|
max = series.stats.max; |
||||||
|
} |
||||||
|
if (!min || min > series.stats.logmin) { |
||||||
|
min = series.stats.logmin; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
axis.transform = function(v) { |
||||||
|
return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase); |
||||||
|
}; |
||||||
|
axis.inverseTransform = function(v) { |
||||||
|
return Math.pow(axis.logBase, v); |
||||||
|
}; |
||||||
|
|
||||||
|
if (!max && !min) { |
||||||
|
max = axis.inverseTransform(+2); |
||||||
|
min = axis.inverseTransform(-2); |
||||||
|
} else if (!max) { |
||||||
|
max = min * axis.inverseTransform(+4); |
||||||
|
} else if (!min) { |
||||||
|
min = max * axis.inverseTransform(-4); |
||||||
|
} |
||||||
|
|
||||||
|
if (axis.min) { |
||||||
|
min = axis.inverseTransform(Math.ceil(axis.transform(axis.min))); |
||||||
|
} else { |
||||||
|
min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min))); |
||||||
|
} |
||||||
|
if (axis.max) { |
||||||
|
max = axis.inverseTransform(Math.floor(axis.transform(axis.max))); |
||||||
|
} else { |
||||||
|
max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max))); |
||||||
|
} |
||||||
|
|
||||||
|
if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (Number.isFinite(min) && Number.isFinite(max)) { |
||||||
|
if (minSetToZero) { |
||||||
|
axis.min = 0.1; |
||||||
|
min = 1; |
||||||
|
} |
||||||
|
|
||||||
|
axis.ticks = this._generateTicksForLogScaleYAxis(min, max, axis.logBase); |
||||||
|
if (minSetToZero) { |
||||||
|
axis.ticks.unshift(0.1); |
||||||
|
} |
||||||
|
if (axis.ticks[axis.ticks.length - 1] > axis.max) { |
||||||
|
axis.max = axis.ticks[axis.ticks.length - 1]; |
||||||
|
} |
||||||
|
} else { |
||||||
|
axis.ticks = [1, 2]; |
||||||
|
delete axis.min; |
||||||
|
delete axis.max; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private _generateTicksForLogScaleYAxis(min, max, logBase) { |
||||||
|
let ticks = []; |
||||||
|
|
||||||
|
var nextTick; |
||||||
|
for (nextTick = min; nextTick <= max; nextTick *= logBase) { |
||||||
|
ticks.push(nextTick); |
||||||
|
} |
||||||
|
|
||||||
|
const maxNumTicks = Math.ceil(this.ctrl.height / 25); |
||||||
|
const numTicks = ticks.length; |
||||||
|
if (numTicks > maxNumTicks) { |
||||||
|
const factor = Math.ceil(numTicks / maxNumTicks) * logBase; |
||||||
|
ticks = []; |
||||||
|
|
||||||
|
for (nextTick = min; nextTick <= max * factor; nextTick *= factor) { |
||||||
|
ticks.push(nextTick); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ticks; |
||||||
|
} |
||||||
|
|
||||||
|
private _configureAxisMode(axis, format) { |
||||||
|
axis.tickFormatter = function(val, axis) { |
||||||
|
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private _timeFormat(ticks, min, max) { |
||||||
|
if (min && max && ticks) { |
||||||
|
var range = max - min; |
||||||
|
var secPerTick = range / ticks / 1000; |
||||||
|
var oneDay = 86400000; |
||||||
|
var oneYear = 31536000000; |
||||||
|
|
||||||
|
if (secPerTick <= 45) { |
||||||
|
return '%H:%M:%S'; |
||||||
|
} |
||||||
|
if (secPerTick <= 7200 || range <= oneDay) { |
||||||
|
return '%H:%M'; |
||||||
|
} |
||||||
|
if (secPerTick <= 80000) { |
||||||
|
return '%m/%d %H:%M'; |
||||||
|
} |
||||||
|
if (secPerTick <= 2419200 || range <= oneYear) { |
||||||
|
return '%m/%d'; |
||||||
|
} |
||||||
|
return '%Y-%m'; |
||||||
|
} |
||||||
|
|
||||||
|
return '%H:%M'; |
||||||
|
} |
||||||
|
|
||||||
|
private _isAnomalyEvent(obj: any) { |
||||||
|
return (obj.ctrlKey || obj.metaKey) && |
||||||
|
this.contextSrv.isEditor && |
||||||
|
this._anomalyController.labelingMode; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
function updateLegendValues(data: TimeSeries[], panel) { |
||||||
|
for (let i = 0; i < data.length; i++) { |
||||||
|
let series = data[i]; |
||||||
|
let yaxes = panel.yaxes; |
||||||
|
const seriesYAxis = series.yaxis || 1; |
||||||
|
let axis = yaxes[seriesYAxis - 1]; |
||||||
|
let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); |
||||||
|
let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; |
||||||
|
|
||||||
|
// decimal override
|
||||||
|
if (_.isNumber(panel.decimals)) { |
||||||
|
series.updateLegendValues(formater, panel.decimals, null); |
||||||
|
} else { |
||||||
|
// auto decimals
|
||||||
|
// legend and tooltip gets one more decimal precision
|
||||||
|
// than graph legend ticks
|
||||||
|
tickDecimals = (tickDecimals || -1) + 1; |
||||||
|
series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,310 @@ |
|||||||
|
import { AnomalyType, AnomalySegment, AnomalySegmentsSearcher } from "model/anomaly"; |
||||||
|
|
||||||
|
export class GraphTooltip { |
||||||
|
|
||||||
|
private ctrl: any; |
||||||
|
private panel: any; |
||||||
|
private $tooltip: JQuery<HTMLElement>; |
||||||
|
private _visible = false; |
||||||
|
private _lastItem = undefined; |
||||||
|
|
||||||
|
constructor( |
||||||
|
private $elem: JQuery<HTMLElement>, private dashboard,
|
||||||
|
private scope, private getSeriesFn, |
||||||
|
private _anomalySegmentsSearcher: AnomalySegmentsSearcher |
||||||
|
) { |
||||||
|
this.ctrl = scope.ctrl; |
||||||
|
this.panel = this.ctrl.panel; |
||||||
|
this.$tooltip = $('<div class="graph-tooltip">'); |
||||||
|
} |
||||||
|
|
||||||
|
clear(plot) { |
||||||
|
this._visible = false; |
||||||
|
this.$tooltip.detach(); |
||||||
|
plot.clearCrosshair(); |
||||||
|
plot.unhighlight(); |
||||||
|
}; |
||||||
|
|
||||||
|
show(pos, item?) { |
||||||
|
if(item === undefined) { |
||||||
|
item = this._lastItem; |
||||||
|
} else { |
||||||
|
this._lastItem = item; |
||||||
|
} |
||||||
|
|
||||||
|
this._visible = true; |
||||||
|
var plot = this.$elem.data().plot; |
||||||
|
var plotData = plot.getData(); |
||||||
|
var xAxes = plot.getXAxes(); |
||||||
|
var xMode = xAxes[0].options.mode; |
||||||
|
var seriesList = this.getSeriesFn(); |
||||||
|
var allSeriesMode = this.panel.tooltip.shared; |
||||||
|
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; |
||||||
|
|
||||||
|
// if panelRelY is defined another panel wants us to show a tooltip
|
||||||
|
// get pageX from position on x axis and pageY from relative position in original panel
|
||||||
|
if (pos.panelRelY) { |
||||||
|
var pointOffset = plot.pointOffset({x: pos.x}); |
||||||
|
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > this.$elem.width()) { |
||||||
|
this.clear(plot); |
||||||
|
return; |
||||||
|
} |
||||||
|
pos.pageX = this.$elem.offset().left + pointOffset.left; |
||||||
|
pos.pageY = this.$elem.offset().top + this.$elem.height() * pos.panelRelY; |
||||||
|
var isVisible = pos.pageY >= $(window).scrollTop() &&
|
||||||
|
pos.pageY <= $(window).innerHeight() + $(window).scrollTop(); |
||||||
|
if (!isVisible) { |
||||||
|
this.clear(plot); |
||||||
|
return; |
||||||
|
} |
||||||
|
plot.setCrosshair(pos); |
||||||
|
allSeriesMode = true; |
||||||
|
|
||||||
|
if (this.dashboard.sharedCrosshairModeOnly()) { |
||||||
|
// if only crosshair mode we are done
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (seriesList.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (seriesList[0].hasMsResolution) { |
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS'; |
||||||
|
} else { |
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss'; |
||||||
|
} |
||||||
|
|
||||||
|
if (allSeriesMode) { |
||||||
|
plot.unhighlight(); |
||||||
|
|
||||||
|
var seriesHoverInfo = this._getMultiSeriesPlotHoverInfo(plotData, pos); |
||||||
|
seriesHtml = ''; |
||||||
|
absoluteTime = this.dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); |
||||||
|
|
||||||
|
// Dynamically reorder the hovercard for the current time point if the
|
||||||
|
// option is enabled.
|
||||||
|
if (this.panel.tooltip.sort === 2) { |
||||||
|
seriesHoverInfo.series.sort((a: any, b: any) => b.value - a.value); |
||||||
|
} else if (this.panel.tooltip.sort === 1) { |
||||||
|
seriesHoverInfo.series.sort((a: any, b: any) => a.value - b.value); |
||||||
|
} |
||||||
|
|
||||||
|
for (i = 0; i < seriesHoverInfo.series.length; i++) { |
||||||
|
hoverInfo = seriesHoverInfo.series[i]; |
||||||
|
|
||||||
|
if (hoverInfo.hidden) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
var highlightClass = ''; |
||||||
|
if (item && hoverInfo.index === item.seriesIndex) { |
||||||
|
highlightClass = 'graph-tooltip-list-item--highlight'; |
||||||
|
} |
||||||
|
|
||||||
|
series = seriesList[hoverInfo.index]; |
||||||
|
|
||||||
|
value = series.formatValue(hoverInfo.value); |
||||||
|
|
||||||
|
seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">'; |
||||||
|
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>'; |
||||||
|
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>'; |
||||||
|
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); |
||||||
|
} |
||||||
|
|
||||||
|
seriesHtml += this._appendAnomaliesHTML(pos.x); |
||||||
|
|
||||||
|
this._renderAndShow(absoluteTime, seriesHtml, pos, xMode); |
||||||
|
} |
||||||
|
// single series tooltip
|
||||||
|
else if (item) { |
||||||
|
series = seriesList[item.seriesIndex]; |
||||||
|
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">'; |
||||||
|
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>'; |
||||||
|
|
||||||
|
if (this.panel.stack && this.panel.tooltip.value_type === 'individual') { |
||||||
|
value = item.datapoint[1] - item.datapoint[2]; |
||||||
|
} |
||||||
|
else { |
||||||
|
value = item.datapoint[1]; |
||||||
|
} |
||||||
|
|
||||||
|
value = series.formatValue(value); |
||||||
|
|
||||||
|
absoluteTime = this.dashboard.formatDate(item.datapoint[0], tooltipFormat); |
||||||
|
|
||||||
|
group += '<div class="graph-tooltip-value">' + value + '</div>'; |
||||||
|
|
||||||
|
group += this._appendAnomaliesHTML(pos.x); |
||||||
|
|
||||||
|
this._renderAndShow(absoluteTime, group, pos, xMode); |
||||||
|
} |
||||||
|
// no hit
|
||||||
|
else { |
||||||
|
this.$tooltip.detach(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
destroy() { |
||||||
|
this._visible = false; |
||||||
|
this.$tooltip.remove(); |
||||||
|
}; |
||||||
|
|
||||||
|
get visible() { return this._visible; } |
||||||
|
|
||||||
|
private _findHoverIndexFromDataPoints(posX, series, last) { |
||||||
|
var ps = series.datapoints.pointsize; |
||||||
|
var initial = last*ps; |
||||||
|
var len = series.datapoints.points.length; |
||||||
|
for (var j = initial; j < len; j += ps) { |
||||||
|
// Special case of a non stepped line, highlight the very last point just before a null point
|
||||||
|
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) |
||||||
|
//normal case
|
||||||
|
|| series.datapoints.points[j] > posX) { |
||||||
|
return Math.max(j - ps, 0)/ps; |
||||||
|
} |
||||||
|
} |
||||||
|
return j/ps - 1; |
||||||
|
}; |
||||||
|
|
||||||
|
private _findHoverIndexFromData(posX, series) { |
||||||
|
var lower = 0; |
||||||
|
var upper = series.data.length - 1; |
||||||
|
var middle; |
||||||
|
while (true) { |
||||||
|
if (lower > upper) { |
||||||
|
return Math.max(upper, 0); |
||||||
|
} |
||||||
|
middle = Math.floor((lower + upper) / 2); |
||||||
|
if (series.data[middle][0] === posX) { |
||||||
|
return middle; |
||||||
|
} else if (series.data[middle][0] < posX) { |
||||||
|
lower = middle + 1; |
||||||
|
} else { |
||||||
|
upper = middle - 1; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private _appendAnomaliesHTML(pos: number): string { |
||||||
|
var result = ''; |
||||||
|
var segments = this._anomalySegmentsSearcher(pos); |
||||||
|
if(segments.length === 0) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
segments.forEach(s => { |
||||||
|
var from = this.dashboard.formatDate(s.segment.from, 'HH:mm:ss.SSS'); |
||||||
|
var to = this.dashboard.formatDate(s.segment.to, 'HH:mm:ss.SSS'); |
||||||
|
|
||||||
|
result += ` |
||||||
|
<div class="graph-tooltip-list-item"> |
||||||
|
<div class="graph-tooltip-series-name"> |
||||||
|
<i class="fa fa-exclamation" style="color:${s.anomalyType.color}"></i> |
||||||
|
${s.anomalyType.name}: |
||||||
|
</div> |
||||||
|
<div class="graph-tooltip-value"> |
||||||
|
<i class="fa ${ s.segment.labeled ? "fa-thumb-tack" : "fa-search-plus" }" aria-hidden="true"></i> |
||||||
|
${from} — ${to} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private _renderAndShow(absoluteTime, innerHtml, pos, xMode) { |
||||||
|
if (xMode === 'time') { |
||||||
|
innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml; |
||||||
|
} |
||||||
|
(this.$tooltip.html(innerHtml) as any).place_tt(pos.pageX + 20, pos.pageY); |
||||||
|
}; |
||||||
|
|
||||||
|
private _getMultiSeriesPlotHoverInfo(seriesList, pos): { series: any[][], time: any } { |
||||||
|
var value, series, hoverIndex, hoverDistance, pointTime, yaxis; |
||||||
|
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
|
||||||
|
var results = [[],[],[]]; |
||||||
|
|
||||||
|
//now we know the current X (j) position for X and Y values
|
||||||
|
var lastValue = 0; //needed for stacked values
|
||||||
|
|
||||||
|
var minDistance, minTime; |
||||||
|
|
||||||
|
for (let i = 0; i < seriesList.length; i++) { |
||||||
|
series = seriesList[i]; |
||||||
|
|
||||||
|
if (!series.data.length || (this.panel.legend.hideEmpty && series.allIsNull)) { |
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 }); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (!series.data.length || (this.panel.legend.hideZero && series.allIsZero)) { |
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 }); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
hoverIndex = this._findHoverIndexFromData(pos.x, series); |
||||||
|
hoverDistance = pos.x - series.data[hoverIndex][0]; |
||||||
|
pointTime = series.data[hoverIndex][0]; |
||||||
|
|
||||||
|
// Take the closest point before the cursor, or if it does not exist, the closest after
|
||||||
|
if (! minDistance |
||||||
|
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0)) |
||||||
|
|| (hoverDistance < 0 && hoverDistance > minDistance) |
||||||
|
) { |
||||||
|
minDistance = hoverDistance; |
||||||
|
minTime = pointTime; |
||||||
|
} |
||||||
|
|
||||||
|
if (series.stack) { |
||||||
|
if (this.panel.tooltip.value_type === 'individual') { |
||||||
|
value = series.data[hoverIndex][1]; |
||||||
|
} else if (!series.stack) { |
||||||
|
value = series.data[hoverIndex][1]; |
||||||
|
} else { |
||||||
|
lastValue += series.data[hoverIndex][1]; |
||||||
|
value = lastValue; |
||||||
|
} |
||||||
|
} else { |
||||||
|
value = series.data[hoverIndex][1]; |
||||||
|
} |
||||||
|
|
||||||
|
// Highlighting multiple Points depending on the plot type
|
||||||
|
if (series.lines.steps || series.stack) { |
||||||
|
// stacked and steppedLine plots can have series with different length.
|
||||||
|
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||||
|
// to speed the index search we begin always on the last found hoverIndex.
|
||||||
|
hoverIndex = this._findHoverIndexFromDataPoints(pos.x, series, hoverIndex); |
||||||
|
} |
||||||
|
|
||||||
|
// Be sure we have a yaxis so that it does not brake series sorting
|
||||||
|
yaxis = 0; |
||||||
|
if (series.yaxis) { |
||||||
|
yaxis = series.yaxis.n; |
||||||
|
} |
||||||
|
|
||||||
|
results[yaxis].push({ |
||||||
|
value: value, |
||||||
|
hoverIndex: hoverIndex, |
||||||
|
color: series.color, |
||||||
|
label: series.aliasEscaped, |
||||||
|
time: pointTime, |
||||||
|
distance: hoverDistance, |
||||||
|
index: i |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Contat the 3 sub-arrays
|
||||||
|
results = results[0].concat(results[1], results[2]); |
||||||
|
|
||||||
|
// Time of the point closer to pointer
|
||||||
|
|
||||||
|
return { series: results, time: minTime }; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,54 @@ |
|||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert series into array of series values. |
||||||
|
* @param data Array of series |
||||||
|
*/ |
||||||
|
export function getSeriesValues(dataList: any[]): number[] { |
||||||
|
const VALUE_INDEX = 0; |
||||||
|
let values = []; |
||||||
|
|
||||||
|
// Count histogam stats
|
||||||
|
for (let i = 0; i < dataList.length; i++) { |
||||||
|
let series = dataList[i]; |
||||||
|
let datapoints = series.datapoints; |
||||||
|
for (let j = 0; j < datapoints.length; j++) { |
||||||
|
if (datapoints[j][VALUE_INDEX] !== null) { |
||||||
|
values.push(datapoints[j][VALUE_INDEX]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return values; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Convert array of values into timeseries-like histogram: |
||||||
|
* [[val_1, count_1], [val_2, count_2], ..., [val_n, count_n]] |
||||||
|
* @param values |
||||||
|
* @param bucketSize |
||||||
|
*/ |
||||||
|
export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { |
||||||
|
let histogram = {}; |
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) { |
||||||
|
let bound = getBucketBound(values[i], bucketSize); |
||||||
|
if (histogram[bound]) { |
||||||
|
histogram[bound] = histogram[bound] + 1; |
||||||
|
} else { |
||||||
|
histogram[bound] = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let histogam_series = _.map(histogram, (count, bound) => { |
||||||
|
return [Number(bound), count]; |
||||||
|
}); |
||||||
|
|
||||||
|
// Sort by Y axis values
|
||||||
|
return _.sortBy(histogam_series, point => point[0]); |
||||||
|
} |
||||||
|
|
||||||
|
function getBucketBound(value: number, bucketSize: number): number { |
||||||
|
return Math.floor(value / bucketSize) * bucketSize; |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
import { SegmentsSet } from './segment_set'; |
||||||
|
import { SegmentArray } from './segment_array'; |
||||||
|
import { Segment, SegmentKey } from './segment'; |
||||||
|
import { Metric } from './metric'; |
||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
export type AnomalySermentPair = { anomalyType: AnomalyType, segment: AnomalySegment }; |
||||||
|
export type AnomalySegmentsSearcher = (point: number) => AnomalySermentPair[]; |
||||||
|
|
||||||
|
export type AnomalyKey = string; |
||||||
|
|
||||||
|
export class AnomalySegment extends Segment { |
||||||
|
constructor(public labeled: boolean, key: SegmentKey, from: number, to: number) { |
||||||
|
super(key, from, to); |
||||||
|
if(!_.isBoolean(labeled)) { |
||||||
|
throw new Error('labeled value is not boolean'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class AnomalyType { |
||||||
|
|
||||||
|
private _selected: boolean = false; |
||||||
|
private _deleteMode: boolean = false; |
||||||
|
private _saving: boolean = false; |
||||||
|
private _segmentSet = new SegmentArray<AnomalySegment>(); |
||||||
|
private _status: string; |
||||||
|
private _metric: Metric; |
||||||
|
|
||||||
|
private _alertEnabled?: boolean; |
||||||
|
|
||||||
|
constructor(private _panelObject?: any) { |
||||||
|
if(_panelObject === undefined) { |
||||||
|
this._panelObject = {}; |
||||||
|
} |
||||||
|
_.defaults(this._panelObject, { |
||||||
|
name: 'anomaly_name', confidence: 0.2, color: 'red' |
||||||
|
}); |
||||||
|
|
||||||
|
//this._metric = new Metric(_panelObject.metric);
|
||||||
|
} |
||||||
|
|
||||||
|
get key(): AnomalyKey { return this.name; } |
||||||
|
|
||||||
|
set name(value: string) { this._panelObject.name = value; } |
||||||
|
get name(): string { return this._panelObject.name; } |
||||||
|
|
||||||
|
set confidence(value: number) { this._panelObject.confidence = value; } |
||||||
|
get confidence(): number { return this._panelObject.confidence; } |
||||||
|
|
||||||
|
set color(value: string) { this._panelObject.color = value; } |
||||||
|
get color(): string { return this._panelObject.color; } |
||||||
|
|
||||||
|
get selected(): boolean { return this._selected; } |
||||||
|
set selected(value: boolean) { this._selected = value; } |
||||||
|
|
||||||
|
get deleteMode(): boolean { return this._deleteMode; } |
||||||
|
set deleteMode(value: boolean) { this._deleteMode = value; } |
||||||
|
|
||||||
|
get saving(): boolean { return this._saving; } |
||||||
|
set saving(value: boolean) { this._saving = value; } |
||||||
|
|
||||||
|
get visible(): boolean {
|
||||||
|
return (this._panelObject.visible === undefined) ? true : this._panelObject.visible |
||||||
|
} |
||||||
|
set visible(value: boolean) { |
||||||
|
this._panelObject.visible = value; |
||||||
|
} |
||||||
|
|
||||||
|
get metric() { return this._metric; } |
||||||
|
|
||||||
|
addLabeledSegment(segment: Segment): AnomalySegment { |
||||||
|
var asegment = new AnomalySegment(true, segment.key, segment.from, segment.to); |
||||||
|
this._segmentSet.addSegment(asegment); |
||||||
|
return asegment; |
||||||
|
} |
||||||
|
|
||||||
|
removeSegmentsInRange(from: number, to: number): AnomalySegment[] { |
||||||
|
return this._segmentSet.removeInRange(from, to); |
||||||
|
} |
||||||
|
|
||||||
|
get segments(): SegmentsSet<AnomalySegment> { return this._segmentSet; } |
||||||
|
set segments(value: SegmentsSet<AnomalySegment>) { |
||||||
|
this._segmentSet.setSegments(value.getSegments()); |
||||||
|
} |
||||||
|
|
||||||
|
get status() { return this._status; } |
||||||
|
set status(value) { |
||||||
|
if( |
||||||
|
value !== 'ready' &&
|
||||||
|
value !== 'learning' &&
|
||||||
|
value !== 'pending' &&
|
||||||
|
value !== 'failed' |
||||||
|
) { |
||||||
|
throw new Error('Unsupported status value: ' + value); |
||||||
|
} |
||||||
|
this._status = value; |
||||||
|
} |
||||||
|
|
||||||
|
get isActiveStatus() { |
||||||
|
return this.status !== 'ready' && this.status !== 'failed'; |
||||||
|
} |
||||||
|
|
||||||
|
get panelObject() { return this._panelObject; } |
||||||
|
|
||||||
|
get alertEnabled(): boolean { |
||||||
|
return this._alertEnabled; |
||||||
|
} |
||||||
|
|
||||||
|
set alertEnabled(value) { |
||||||
|
this._alertEnabled = value; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
export class AnomalyTypesSet { |
||||||
|
|
||||||
|
private _mapAnomalyKeyIndex: Map<AnomalyKey, number>; |
||||||
|
private _anomalyTypes: AnomalyType[]; |
||||||
|
|
||||||
|
constructor(private _panelObject: any[]) { |
||||||
|
if(_panelObject === undefined) { |
||||||
|
throw new Error('panel object can`t be undefined'); |
||||||
|
} |
||||||
|
this._mapAnomalyKeyIndex = new Map<AnomalyKey, number>(); |
||||||
|
this._anomalyTypes = _panelObject.map(p => new AnomalyType(p)); |
||||||
|
this._rebuildIndex(); |
||||||
|
} |
||||||
|
|
||||||
|
get anomalyTypes() { return this._anomalyTypes; } |
||||||
|
|
||||||
|
addAnomalyType(anomalyType: AnomalyType) { |
||||||
|
this._panelObject.push(anomalyType.panelObject); |
||||||
|
this._mapAnomalyKeyIndex[anomalyType.name] = this._anomalyTypes.length; |
||||||
|
this._anomalyTypes.push(anomalyType); |
||||||
|
} |
||||||
|
|
||||||
|
removeAnomalyType(key: AnomalyKey) { |
||||||
|
var index = this._mapAnomalyKeyIndex[key]; |
||||||
|
this._panelObject.splice(index, 1); |
||||||
|
this._anomalyTypes.splice(index, 1); |
||||||
|
this._rebuildIndex(); |
||||||
|
} |
||||||
|
|
||||||
|
_rebuildIndex() { |
||||||
|
this._anomalyTypes.forEach((a, i) => { |
||||||
|
this._mapAnomalyKeyIndex[a.key] = i; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
byKey(key: AnomalyKey): AnomalyType { |
||||||
|
return this._anomalyTypes[this._mapAnomalyKeyIndex[key]]; |
||||||
|
} |
||||||
|
|
||||||
|
byIndex(index: number): AnomalyType { |
||||||
|
return this._anomalyTypes[index]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
import _ from 'lodash'; |
||||||
|
import md5 from 'md5'; |
||||||
|
|
||||||
|
|
||||||
|
export type TargetHash = string; |
||||||
|
|
||||||
|
export class Target { |
||||||
|
private _data: any; |
||||||
|
constructor(any) { |
||||||
|
this._data = _.cloneDeep(any); |
||||||
|
this._strip(); |
||||||
|
} |
||||||
|
|
||||||
|
private _strip() { |
||||||
|
delete this._data.alias; |
||||||
|
} |
||||||
|
|
||||||
|
getHash(): TargetHash { |
||||||
|
return md5(JSON.stringify(this._data)); |
||||||
|
} |
||||||
|
|
||||||
|
getJSON() { |
||||||
|
return this._data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class Metric { |
||||||
|
constructor(private _panelObj: any) { |
||||||
|
if(_panelObj === undefined) { |
||||||
|
throw new Error('_panelObj is undefined'); |
||||||
|
} |
||||||
|
} |
||||||
|
get datasource(): string { return this._panelObj.datasource; } |
||||||
|
get targetHashs(): TargetHash[] { return this._panelObj.targetHashs; } |
||||||
|
} |
||||||
|
|
||||||
|
export class MetricExpanded { |
||||||
|
private _targets: Target[]; |
||||||
|
constructor(public datasource: string, targets: any[]) { |
||||||
|
this._targets = targets.map(t => new Target(t)); |
||||||
|
} |
||||||
|
|
||||||
|
toJSON(): any { |
||||||
|
return { |
||||||
|
datasource: this.datasource, |
||||||
|
targets: this._targets.map(t => t.getJSON()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class MetricMap { |
||||||
|
private _cache: Map<TargetHash, Target> = new Map<TargetHash, Target>(); |
||||||
|
constructor(datasource: string, targets: Target[]) { |
||||||
|
targets.forEach(t => { |
||||||
|
this._cache.set(t.getHash(), t); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
export type SegmentKey = number; |
||||||
|
|
||||||
|
export class Segment { |
||||||
|
constructor(private _key: SegmentKey, public from: number, public to: number) { |
||||||
|
if(isNaN(this._key)) { |
||||||
|
throw new Error('Key can`t be NaN'); |
||||||
|
} |
||||||
|
if(isNaN(+from)) { |
||||||
|
throw new Error('from can`t be NaN'); |
||||||
|
} |
||||||
|
if(isNaN(+to)) { |
||||||
|
throw new Error('to can`t be NaN'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
get key(): SegmentKey { return this._key; } |
||||||
|
set key(value) { this._key = value; } |
||||||
|
|
||||||
|
get middle() { return (this.from + this.to) / 2; } |
||||||
|
|
||||||
|
get length() { |
||||||
|
return Math.max(this.from, this.to) - Math.min(this.from, this.to); |
||||||
|
} |
||||||
|
|
||||||
|
expandDist(allDist: number, portion: number): Segment { |
||||||
|
if(allDist * portion < this.length) { |
||||||
|
return new Segment(this._key, this.from, this.to); |
||||||
|
} |
||||||
|
var p = Math.round(this.middle - allDist * portion / 2); |
||||||
|
var q = Math.round(this.middle + allDist * portion / 2); |
||||||
|
p = Math.min(p, this.from); |
||||||
|
q = Math.max(q, this.to); |
||||||
|
return new Segment(this._key, p, q); |
||||||
|
} |
||||||
|
|
||||||
|
equals(segment: Segment) { |
||||||
|
return this._key === segment._key; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
import { SegmentsSet } from './segment_set'; |
||||||
|
import { Segment, SegmentKey } from './segment'; |
||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
|
||||||
|
export class SegmentArray<T extends Segment> implements SegmentsSet<T> { |
||||||
|
private _segments: T[]; |
||||||
|
private _keyToSegment: Map<SegmentKey, T> = new Map<SegmentKey, T>(); |
||||||
|
|
||||||
|
constructor(private segments?: T[]) { |
||||||
|
this.setSegments(segments); |
||||||
|
} |
||||||
|
|
||||||
|
getSegments(from?: number, to?: number): T[] { |
||||||
|
if(from === undefined) { |
||||||
|
from = -Infinity; |
||||||
|
} |
||||||
|
if(to === undefined) { |
||||||
|
to = Infinity; |
||||||
|
} |
||||||
|
var result = []; |
||||||
|
for(var i = 0; i < this._segments.length; i++) { |
||||||
|
var s = this._segments[i]; |
||||||
|
if(from <= s.from && s.to <= to) { |
||||||
|
result.push(s); |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
setSegments(segments: T[]) { |
||||||
|
this._segments = []; |
||||||
|
this._keyToSegment.clear(); |
||||||
|
if(segments) { |
||||||
|
segments.forEach(s => { |
||||||
|
this.addSegment(s); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
addSegment(segment: T) { |
||||||
|
if(this.has(segment.key)) { |
||||||
|
throw new Error(`Segment with key ${segment.key} exists in set`); |
||||||
|
} |
||||||
|
this._keyToSegment.set(segment.key, segment); |
||||||
|
this._segments.push(segment); |
||||||
|
} |
||||||
|
|
||||||
|
findSegments(point: number): T[] { |
||||||
|
return this._segments.filter(s => (s.from <= point) && (point <= s.to)); |
||||||
|
} |
||||||
|
|
||||||
|
removeInRange(from: number, to: number): T[] { |
||||||
|
var deleted = []; |
||||||
|
var newSegments = []; |
||||||
|
for(var i = 0; i < this._segments.length; i++) { |
||||||
|
var s = this._segments[i]; |
||||||
|
if(from <= s.from && s.to <= to) { |
||||||
|
this._keyToSegment.delete(s.key); |
||||||
|
deleted.push(s); |
||||||
|
} else { |
||||||
|
newSegments.push(s); |
||||||
|
} |
||||||
|
} |
||||||
|
this._segments = newSegments; |
||||||
|
return deleted; |
||||||
|
} |
||||||
|
|
||||||
|
get length() { |
||||||
|
return this._segments.length; |
||||||
|
} |
||||||
|
|
||||||
|
clear() { |
||||||
|
this._segments = []; |
||||||
|
this._keyToSegment.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
has(key: SegmentKey): boolean { |
||||||
|
return this._keyToSegment.has(key); |
||||||
|
} |
||||||
|
|
||||||
|
remove(key: SegmentKey): boolean { |
||||||
|
if(!this.has(key)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
var index = this._segments.findIndex(s => s.key === key); |
||||||
|
this._segments.splice(index, 1); |
||||||
|
this._keyToSegment.delete(key); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
updateKey(fromKey: SegmentKey, toKey: SegmentKey) { |
||||||
|
var segment = this._keyToSegment.get(fromKey); |
||||||
|
this._keyToSegment.delete(fromKey); |
||||||
|
segment.key = toKey; |
||||||
|
this._keyToSegment.set(toKey, segment); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import { Segment, SegmentKey } from './segment' |
||||||
|
|
||||||
|
export interface SegmentsSet<T extends Segment> { |
||||||
|
getSegments(from?: number, to?: number): T[]; |
||||||
|
setSegments(segments: T[]): void; |
||||||
|
addSegment(segment: T): void; |
||||||
|
findSegments(point: number): T[]; |
||||||
|
removeInRange(from: number, to: number): T[]; |
||||||
|
remove(key: SegmentKey): boolean; |
||||||
|
has(key: SegmentKey): boolean; |
||||||
|
clear(): void; |
||||||
|
updateKey(fromKey: SegmentKey, toKey: SegmentKey): void; |
||||||
|
length: number; |
||||||
|
} |
@ -0,0 +1,547 @@ |
|||||||
|
import './series_overrides_ctrl'; |
||||||
|
import './thresholds_form'; |
||||||
|
|
||||||
|
import template from './template'; |
||||||
|
|
||||||
|
import { GraphRenderer } from './graph_renderer'; |
||||||
|
import { GraphLegend } from './graph_legend'; |
||||||
|
import { DataProcessor } from './data_processor'; |
||||||
|
import { Metric, MetricExpanded } from './model/metric'; |
||||||
|
import { AnomalyKey, AnomalyType } from './model/anomaly'; |
||||||
|
import { AnomalyService } from './services/anomaly_service'; |
||||||
|
import { AnomalyController } from './controllers/anomaly_controller'; |
||||||
|
|
||||||
|
import { axesEditorComponent } from './axes_editor'; |
||||||
|
|
||||||
|
import { MetricsPanelCtrl, alertTab } from 'grafana/app/plugins/sdk'; |
||||||
|
import { BackendSrv } from 'grafana/app/core/services/backend_srv'; |
||||||
|
import { appEvents } from 'grafana/app/core/core' |
||||||
|
import config from 'grafana/app/core/config'; |
||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
|
||||||
|
class GraphCtrl extends MetricsPanelCtrl { |
||||||
|
static template = template; |
||||||
|
|
||||||
|
hiddenSeries: any = {}; |
||||||
|
seriesList: any = []; |
||||||
|
dataList: any = []; |
||||||
|
annotations: any = []; |
||||||
|
alertState: any; |
||||||
|
|
||||||
|
_panelPath: any; |
||||||
|
|
||||||
|
annotationsPromise: any; |
||||||
|
dataWarning: any; |
||||||
|
colors: any = []; |
||||||
|
subTabIndex: number; |
||||||
|
processor: DataProcessor; |
||||||
|
|
||||||
|
datasourceRequest: Object; |
||||||
|
backendURL: string; |
||||||
|
analyticsTypes: Array<String> = ['Anomaly detection', 'Pettern Detection (not implemented yet)']; |
||||||
|
anomalyTypes = []; // TODO: remove it later. Only for alert tab
|
||||||
|
anomalyController: AnomalyController; |
||||||
|
|
||||||
|
_graphRenderer: GraphRenderer; |
||||||
|
_graphLegend: GraphLegend; |
||||||
|
|
||||||
|
panelDefaults = { |
||||||
|
// datasource name, null = default datasource
|
||||||
|
datasource: null, |
||||||
|
// sets client side (flot) or native graphite png renderer (png)
|
||||||
|
renderer: 'flot', |
||||||
|
yaxes: [ |
||||||
|
{ |
||||||
|
label: null, |
||||||
|
show: true, |
||||||
|
logBase: 1, |
||||||
|
min: null, |
||||||
|
max: null, |
||||||
|
format: 'short', |
||||||
|
}, |
||||||
|
{ |
||||||
|
label: null, |
||||||
|
show: true, |
||||||
|
logBase: 1, |
||||||
|
min: null, |
||||||
|
max: null, |
||||||
|
format: 'short', |
||||||
|
}, |
||||||
|
], |
||||||
|
xaxis: { |
||||||
|
show: true, |
||||||
|
mode: 'time', |
||||||
|
name: null, |
||||||
|
values: [], |
||||||
|
buckets: null, |
||||||
|
}, |
||||||
|
// show/hide lines
|
||||||
|
lines: true, |
||||||
|
// fill factor
|
||||||
|
fill: 1, |
||||||
|
// line width in pixels
|
||||||
|
linewidth: 1, |
||||||
|
// show/hide dashed line
|
||||||
|
dashes: false, |
||||||
|
// length of a dash
|
||||||
|
dashLength: 10, |
||||||
|
// length of space between two dashes
|
||||||
|
spaceLength: 10, |
||||||
|
// show hide points
|
||||||
|
points: false, |
||||||
|
// point radius in pixels
|
||||||
|
pointradius: 5, |
||||||
|
// show hide bars
|
||||||
|
bars: false, |
||||||
|
// enable/disable stacking
|
||||||
|
stack: false, |
||||||
|
// stack percentage mode
|
||||||
|
percentage: false, |
||||||
|
// legend options
|
||||||
|
legend: { |
||||||
|
show: true, // disable/enable legend
|
||||||
|
values: false, // disable/enable legend values
|
||||||
|
min: false, |
||||||
|
max: false, |
||||||
|
current: false, |
||||||
|
total: false, |
||||||
|
avg: false, |
||||||
|
}, |
||||||
|
// how null points should be handled
|
||||||
|
nullPointMode: 'null', |
||||||
|
// staircase line mode
|
||||||
|
steppedLine: false, |
||||||
|
// tooltip options
|
||||||
|
tooltip: { |
||||||
|
value_type: 'individual', |
||||||
|
shared: true, |
||||||
|
sort: 0, |
||||||
|
}, |
||||||
|
// time overrides
|
||||||
|
timeFrom: null, |
||||||
|
timeShift: null, |
||||||
|
// metric queries
|
||||||
|
targets: [{}], |
||||||
|
// series color overrides
|
||||||
|
aliasColors: {}, |
||||||
|
// other style overrides
|
||||||
|
seriesOverrides: [], |
||||||
|
thresholds: [], |
||||||
|
anomalyType: '', |
||||||
|
analyticsType: 'Anomaly detection', |
||||||
|
backendURL: 'http://localhost:8000' |
||||||
|
}; |
||||||
|
|
||||||
|
/** @ngInject */ |
||||||
|
constructor( |
||||||
|
$scope, $injector, private annotationsSrv,
|
||||||
|
private keybindingSrv, private backendSrv,
|
||||||
|
private popoverSrv, private contextSrv |
||||||
|
) { |
||||||
|
super($scope, $injector); |
||||||
|
|
||||||
|
_.defaults(this.panel, this.panelDefaults); |
||||||
|
_.defaults(this.panel.tooltip, this.panelDefaults.tooltip); |
||||||
|
_.defaults(this.panel.legend, this.panelDefaults.legend); |
||||||
|
_.defaults(this.panel.xaxis, this.panelDefaults.xaxis); |
||||||
|
|
||||||
|
this.processor = new DataProcessor(this.panel); |
||||||
|
|
||||||
|
var anomalyService = new AnomalyService(this.panel.backendURL, backendSrv as BackendSrv); |
||||||
|
this.anomalyController = new AnomalyController(this.panel, anomalyService, this.events); |
||||||
|
this.anomalyTypes = this.panel.anomalyTypes; |
||||||
|
keybindingSrv.bind('d', this.onDKey.bind(this)); |
||||||
|
|
||||||
|
this.events.on('render', this.onRender.bind(this)); |
||||||
|
this.events.on('data-received', this.onDataReceived.bind(this)); |
||||||
|
this.events.on('data-error', this.onDataError.bind(this)); |
||||||
|
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); |
||||||
|
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); |
||||||
|
this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); |
||||||
|
this.events.on('anomaly-type-alert-change', () => { |
||||||
|
this.$scope.$digest() |
||||||
|
}); |
||||||
|
this.events.on('anomaly-type-status-change', async (anomalyType: AnomalyType) => { |
||||||
|
if(anomalyType === undefined) { |
||||||
|
throw new Error('anomalyType is undefined'); |
||||||
|
} |
||||||
|
if(anomalyType.status === 'ready') { |
||||||
|
await this.anomalyController.fetchSegments(anomalyType, +this.range.from, +this.range.to); |
||||||
|
} |
||||||
|
this.render(this.seriesList); |
||||||
|
this.$scope.$digest(); |
||||||
|
}); |
||||||
|
|
||||||
|
appEvents.on('ds-request-response', data => { |
||||||
|
console.log(data) |
||||||
|
let params = data.config.params; |
||||||
|
this.datasourceRequest = { |
||||||
|
url: params.url, |
||||||
|
type: params.inspect.type, |
||||||
|
method: params.method, |
||||||
|
data: params.data, |
||||||
|
...params |
||||||
|
}; |
||||||
|
console.log(this.datasourceRequest) |
||||||
|
}); |
||||||
|
|
||||||
|
this.anomalyController.fetchAnomalyTypesStatuses(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
link(scope, elem, attrs, ctrl) { |
||||||
|
var $graphElem = $(elem[0]).find('#graphPanel'); |
||||||
|
var $legendElem = $(elem[0]).find('#graphLegend'); |
||||||
|
this._graphRenderer = new GraphRenderer( |
||||||
|
$graphElem, this.timeSrv, this.popoverSrv, this.contextSrv,this.$scope |
||||||
|
); |
||||||
|
this._graphLegend = new GraphLegend($legendElem, this.popoverSrv, this.$scope); |
||||||
|
} |
||||||
|
|
||||||
|
onInitEditMode() { |
||||||
|
var partialPath = this.panelPath + 'partials'; |
||||||
|
this.addEditorTab('Analytics', `${partialPath}/tab_analytics.html`, 2); |
||||||
|
this.addEditorTab('Axes', axesEditorComponent, 3); |
||||||
|
this.addEditorTab('Legend', `${partialPath}/tab_legend.html`, 4); |
||||||
|
this.addEditorTab('Display', `${partialPath}/tab_display.html`, 5); |
||||||
|
|
||||||
|
if (config.alertingEnabled) { |
||||||
|
this.addEditorTab('Alert', alertTab, 6); |
||||||
|
} |
||||||
|
|
||||||
|
this.subTabIndex = 0; |
||||||
|
} |
||||||
|
|
||||||
|
onInitPanelActions(actions) { |
||||||
|
actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' }); |
||||||
|
actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' }); |
||||||
|
} |
||||||
|
|
||||||
|
issueQueries(datasource) { |
||||||
|
this.annotationsPromise = this.annotationsSrv.getAnnotations({ |
||||||
|
dashboard: this.dashboard, |
||||||
|
panel: this.panel, |
||||||
|
range: this.range, |
||||||
|
}); |
||||||
|
return super.issueQueries(datasource); |
||||||
|
} |
||||||
|
|
||||||
|
zoomOut(evt) { |
||||||
|
this.publishAppEvent('zoom-out', 2); |
||||||
|
} |
||||||
|
|
||||||
|
onDataSnapshotLoad(snapshotData) { |
||||||
|
this.annotationsPromise = this.annotationsSrv.getAnnotations({ |
||||||
|
dashboard: this.dashboard, |
||||||
|
panel: this.panel, |
||||||
|
range: this.range, |
||||||
|
}); |
||||||
|
this.onDataReceived(snapshotData); |
||||||
|
} |
||||||
|
|
||||||
|
onDataError(err) { |
||||||
|
this.seriesList = []; |
||||||
|
this.annotations = []; |
||||||
|
this.render([]); |
||||||
|
} |
||||||
|
|
||||||
|
async onDataReceived(dataList) { |
||||||
|
|
||||||
|
this.dataList = dataList; |
||||||
|
this.seriesList = this.processor.getSeriesList({ |
||||||
|
dataList: dataList, |
||||||
|
range: this.range, |
||||||
|
}); |
||||||
|
|
||||||
|
//this.onPredictionReceived(this.seriesList);
|
||||||
|
|
||||||
|
this.dataWarning = null; |
||||||
|
const hasSomePoint = this.seriesList.some(s => s.datapoints.length > 0); |
||||||
|
|
||||||
|
if (!hasSomePoint) { |
||||||
|
this.dataWarning = { |
||||||
|
title: 'No data points', |
||||||
|
tip: 'No datapoints returned from data query', |
||||||
|
}; |
||||||
|
} else { |
||||||
|
for (let series of this.seriesList) { |
||||||
|
if (series.isOutsideRange) { |
||||||
|
this.dataWarning = { |
||||||
|
title: 'Data points outside time range', |
||||||
|
tip: 'Can be caused by timezone mismatch or missing time filter in query', |
||||||
|
}; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var loadTasks = [ |
||||||
|
this.annotationsPromise, |
||||||
|
this.anomalyController.fetchAnomalyTypesSegments(+this.range.from, +this.range.to) |
||||||
|
]; |
||||||
|
|
||||||
|
var results = await Promise.all(loadTasks); |
||||||
|
this.loading = false; |
||||||
|
this.alertState = results[0].alertState; |
||||||
|
this.annotations = results[0].annotations; |
||||||
|
this.render(this.seriesList); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
onPredictionReceived(spanList) { |
||||||
|
var predictions = []; |
||||||
|
for (var span of spanList) { |
||||||
|
var predictionLow = { |
||||||
|
target: '', |
||||||
|
color: '', |
||||||
|
datapoints: [] |
||||||
|
}; |
||||||
|
var predictionHigh = { |
||||||
|
target: '', |
||||||
|
color: '', |
||||||
|
datapoints: [] |
||||||
|
}; |
||||||
|
|
||||||
|
for (var datapoint of span.datapoints) { |
||||||
|
predictionHigh.datapoints.push([datapoint[0] + 2, datapoint[1]]); |
||||||
|
predictionLow.datapoints.push([datapoint[0] - 2, datapoint[1]]); |
||||||
|
} |
||||||
|
|
||||||
|
predictionHigh.target = `${span.label} high`; |
||||||
|
predictionLow.target = `${span.label} low`; |
||||||
|
predictionHigh.color = span.color; |
||||||
|
predictionLow.color = span.color; |
||||||
|
predictions.push(predictionHigh, predictionLow); |
||||||
|
} |
||||||
|
var predictionSeries = this.processor.getSeriesList({ |
||||||
|
dataList: predictions, |
||||||
|
range: this.range |
||||||
|
}); |
||||||
|
for (var serie of predictionSeries) { |
||||||
|
serie.prediction = true; |
||||||
|
this.seriesList.push(serie); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onRender(data) { |
||||||
|
if (!this.seriesList) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (let series of this.seriesList) { |
||||||
|
if (series.prediction) { |
||||||
|
var overrideItem = _.find( |
||||||
|
this.panel.seriesOverrides, |
||||||
|
el => el.alias === series.alias |
||||||
|
) |
||||||
|
if (overrideItem !== undefined) { |
||||||
|
this.addSeriesOverride({ |
||||||
|
alias: series.alias, |
||||||
|
linewidth: 0, |
||||||
|
legend: false, |
||||||
|
// if pointradius === 0 -> point still shows, that's why pointradius === -1
|
||||||
|
pointradius: -1, |
||||||
|
fill: 3 |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
series.applySeriesOverrides(this.panel.seriesOverrides); |
||||||
|
|
||||||
|
if (series.unit) { |
||||||
|
this.panel.yaxes[series.yaxis - 1].format = series.unit; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!this.anomalyController.graphLocked) { |
||||||
|
this._graphLegend.render(); |
||||||
|
this._graphRenderer.render(data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
changeSeriesColor(series, color) { |
||||||
|
series.color = color; |
||||||
|
this.panel.aliasColors[series.alias] = series.color; |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
toggleSeries(serie, event) { |
||||||
|
if (event.ctrlKey || event.metaKey || event.shiftKey) { |
||||||
|
if (this.hiddenSeries[serie.alias]) { |
||||||
|
delete this.hiddenSeries[serie.alias]; |
||||||
|
} else { |
||||||
|
this.hiddenSeries[serie.alias] = true; |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.toggleSeriesExclusiveMode(serie); |
||||||
|
} |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
toggleSeriesExclusiveMode(serie) { |
||||||
|
var hidden = this.hiddenSeries; |
||||||
|
|
||||||
|
if (hidden[serie.alias]) { |
||||||
|
delete hidden[serie.alias]; |
||||||
|
} |
||||||
|
|
||||||
|
// check if every other series is hidden
|
||||||
|
var alreadyExclusive = _.every(this.seriesList, value => { |
||||||
|
if (value.alias === serie.alias) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return hidden[value.alias]; |
||||||
|
}); |
||||||
|
|
||||||
|
if (alreadyExclusive) { |
||||||
|
// remove all hidden series
|
||||||
|
_.each(this.seriesList, value => { |
||||||
|
delete this.hiddenSeries[value.alias]; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// hide all but this serie
|
||||||
|
_.each(this.seriesList, value => { |
||||||
|
if (value.alias === serie.alias) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.hiddenSeries[value.alias] = true; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
toggleAxis(info) { |
||||||
|
var override = _.find(this.panel.seriesOverrides, { alias: info.alias }); |
||||||
|
if (!override) { |
||||||
|
override = { alias: info.alias }; |
||||||
|
this.panel.seriesOverrides.push(override); |
||||||
|
} |
||||||
|
info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2; |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
addSeriesOverride(override) { |
||||||
|
this.panel.seriesOverrides.push(override || {}); |
||||||
|
} |
||||||
|
|
||||||
|
removeSeriesOverride(override) { |
||||||
|
this.panel.seriesOverrides = _.without(this.panel.seriesOverrides, override); |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
toggleLegend() { |
||||||
|
this.panel.legend.show = !this.panel.legend.show; |
||||||
|
this.refresh(); |
||||||
|
} |
||||||
|
|
||||||
|
legendValuesOptionChanged() { |
||||||
|
var legend = this.panel.legend; |
||||||
|
legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
exportCsv() { |
||||||
|
var scope = this.$scope.$new(true); |
||||||
|
scope.seriesList = this.seriesList; |
||||||
|
this.publishAppEvent('show-modal', { |
||||||
|
templateHtml: '<export-data-modal data="seriesList"></export-data-modal>', |
||||||
|
scope, |
||||||
|
modalClass: 'modal--narrow', |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
getAnnotationsByTag(tag) { |
||||||
|
var res = []; |
||||||
|
for (var annotation of this.annotations) { |
||||||
|
if (annotation.tags.indexOf(tag) >= 0) { |
||||||
|
res.push(annotation); |
||||||
|
} |
||||||
|
} |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
get annotationTags() { |
||||||
|
var res = []; |
||||||
|
for (var annotation of this.annotations) { |
||||||
|
for (var tag of annotation.tags) { |
||||||
|
if (res.indexOf(tag) < 0) { |
||||||
|
res.push(tag); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
get panelPath() { |
||||||
|
if (this._panelPath === undefined) { |
||||||
|
this._panelPath = '/public/plugins/' + this.pluginId + '/'; |
||||||
|
} |
||||||
|
return this._panelPath; |
||||||
|
} |
||||||
|
|
||||||
|
createNewAnomalyType() { |
||||||
|
this.anomalyController.createAnomalyType(); |
||||||
|
} |
||||||
|
|
||||||
|
async saveAnomalyType() { |
||||||
|
await this.anomalyController.saveNewAnomalyType( |
||||||
|
new MetricExpanded(this.panel.datasource, this.panel.targets), |
||||||
|
this.panel.id |
||||||
|
); |
||||||
|
this.$scope.$digest(); |
||||||
|
this.render(this.seriesList); |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyColorChange(key: AnomalyKey, value) { |
||||||
|
this.anomalyController.onAnomalyColorChange(key, value); |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyRemove(key) { |
||||||
|
this.anomalyController.removeAnomalyType(key as string); |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyCancelLabeling(key) { |
||||||
|
this.$scope.$root.appEvent('confirm-modal', { |
||||||
|
title: 'Clear anomaly labeling', |
||||||
|
text2: 'Your changes will be lost.', |
||||||
|
yesText: 'Clear', |
||||||
|
icon: 'fa-warning', |
||||||
|
altActionText: 'Save', |
||||||
|
onAltAction: () => { |
||||||
|
this.onToggleAnomalyTypeLabelingMode(key); |
||||||
|
}, |
||||||
|
onConfirm: () => { |
||||||
|
this.anomalyController.undoLabeling(); |
||||||
|
this.render(); |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async onToggleAnomalyTypeLabelingMode(key) { |
||||||
|
await this.anomalyController.toggleAnomalyTypeLabelingMode(key as AnomalyKey); |
||||||
|
this.$scope.$digest(); |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
|
||||||
|
onDKey() { |
||||||
|
if(!this.anomalyController.labelingMode) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.anomalyController.toggleDeleteMode(); |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyAlertChange(anomalyType: AnomalyType) { |
||||||
|
this.anomalyController.toggleAnomalyTypeAlertEnabled(anomalyType); |
||||||
|
} |
||||||
|
|
||||||
|
onAnomalyToggleVisibility(key: AnomalyKey) { |
||||||
|
this.anomalyController.toggleAnomalyVisibility(key); |
||||||
|
this.render(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export { GraphCtrl, GraphCtrl as PanelCtrl }; |
@ -0,0 +1,72 @@ |
|||||||
|
<div class="editor-row"> |
||||||
|
<div class="section gf-form-group" ng-repeat="yaxis in ctrl.panel.yaxes"> |
||||||
|
|
||||||
|
<h5 class="section-heading" ng-show="$index === 0">Left Y</h5> |
||||||
|
<h5 class="section-heading" ng-show="$index === 1">Right Y</h5> |
||||||
|
|
||||||
|
<gf-form-switch class="gf-form" label="Show" label-class="width-6" checked="yaxis.show" on-change="ctrl.render()"></gf-form-switch> |
||||||
|
|
||||||
|
<div ng-if="yaxis.show"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Unit</label> |
||||||
|
<div |
||||||
|
class="gf-form-dropdown-typeahead max-width-20" ng-model="yaxis.format" |
||||||
|
dropdown-typeahead2="ctrl.unitFormats" dropdown-typeahead-on-select="ctrl.setUnitFormat(yaxis, $subItem)" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Scale</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-20"> |
||||||
|
<select class="gf-form-input" ng-model="yaxis.logBase" ng-options="v as k for (k, v) in ctrl.logScales" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-inline"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Y-Min</label> |
||||||
|
<input type="text" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="yaxis.min" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Y-Max</label> |
||||||
|
<input type="text" class="gf-form-input width-5" placeholder="auto" empty-to-null ng-model="yaxis.max" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Decimals</label> |
||||||
|
<input type="number" class="gf-form-input max-width-20" placeholder="auto" empty-to-null bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Label</label> |
||||||
|
<input type="text" class="gf-form-input max-width-20" ng-model="yaxis.label" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">X-Axis</h5> |
||||||
|
<gf-form-switch class="gf-form" label="Show" label-class="width-6" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Mode</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-15"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisModeChanged()"> </select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Series mode --> |
||||||
|
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'series'"> |
||||||
|
<label class="gf-form-label width-6">Value</label> |
||||||
|
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisValueChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Histogram mode --> |
||||||
|
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'"> |
||||||
|
<label class="gf-form-label width-6">Buckets</label> |
||||||
|
<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right"> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
@ -0,0 +1,144 @@ |
|||||||
|
<h5> Analytics </h5> |
||||||
|
<div class="editor-row"> |
||||||
|
<div class="section gf-form-group"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-8"> Analytics type </label> |
||||||
|
<div class="gf-form-select-wrapper"> |
||||||
|
<select class="gf-form-input width-12" |
||||||
|
ng-model="ctrl.panel.analyticsType" |
||||||
|
ng-options="source as source for source in ctrl.analyticsTypes" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h5> Anomaly Types </h5> |
||||||
|
<div class="editor-row"> |
||||||
|
<div class="gf-form" ng-repeat="anomalyType in ctrl.anomalyController.anomalyTypes"> |
||||||
|
|
||||||
|
<label class="gf-form-label width-4"> Name </label> |
||||||
|
<input |
||||||
|
type="text" class="gf-form-input max-width-15" |
||||||
|
ng-model="anomalyType.name" |
||||||
|
ng-disabled="true" |
||||||
|
> |
||||||
|
|
||||||
|
<!-- |
||||||
|
<label class="gf-form-label width-6"> Confidence </label> |
||||||
|
<input |
||||||
|
type="number" class="gf-form-input width-5 ng-valid ng-scope ng-empty ng-dirty ng-valid-number ng-touched" |
||||||
|
placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" |
||||||
|
data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur="" data-original-title="" title="" |
||||||
|
/> |
||||||
|
--> |
||||||
|
|
||||||
|
<label class="gf-form-label width-6"> Color </label> |
||||||
|
<span class="gf-form-label"> |
||||||
|
<color-picker |
||||||
|
color="anomalyType.color" |
||||||
|
onChange="ctrl.onAnomalyColorChange.bind(ctrl, anomalyType.key)" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
|
||||||
|
<label class="gf-form-label"> |
||||||
|
<a class="pointer" tabindex="1" ng-click="ctrl.onToggleAnomalyTypeLabelingMode(anomalyType.key)"> |
||||||
|
<i class="fa fa-bar-chart" ng-if="!anomalyType.saving"></i> |
||||||
|
<i class="fa fa-spinner fa-spin" ng-if="anomalyType.saving"></i> |
||||||
|
<b ng-if="anomalyType.selected && !anomalyType.deleteMode && !anomalyType.saving"> labeling </b> |
||||||
|
<b ng-if="anomalyType.selected && anomalyType.deleteMode && !anomalyType.saving"> deleting </b> |
||||||
|
<b ng-if="anomalyType.saving" ng-disabled="true"> saving... </b> |
||||||
|
</a> |
||||||
|
</label> |
||||||
|
|
||||||
|
|
||||||
|
<label class="gf-form-label"> Alerts: </label> |
||||||
|
<label |
||||||
|
class="gf-form-label text-center" |
||||||
|
style="width: 4rem" |
||||||
|
ng-if="anomalyType.alertEnabled === undefined" |
||||||
|
bs-tooltip="'Alarting status isn`t available. Wait please.'" |
||||||
|
> |
||||||
|
<i class="fa fa-spinner fa-spin"></i> |
||||||
|
</label> |
||||||
|
|
||||||
|
<gf-form-switch |
||||||
|
ng-if="anomalyType.alertEnabled !== undefined" |
||||||
|
on-change="ctrl.onAnomalyAlertChange(anomalyType)" |
||||||
|
checked="anomalyType.alertEnabled" |
||||||
|
style="height: 36px;" |
||||||
|
/> |
||||||
|
|
||||||
|
<label class="gf-form-label"> |
||||||
|
<a |
||||||
|
ng-if="anomalyType.visible" |
||||||
|
ng-disabled="anomalyType.selected" |
||||||
|
bs-tooltip="'Hide. It`s visible now.'" |
||||||
|
ng-click="ctrl.onAnomalyToggleVisibility(anomalyType.key)" |
||||||
|
class="pointer" |
||||||
|
> |
||||||
|
<i class="fa fa-eye"></i> |
||||||
|
</a> |
||||||
|
|
||||||
|
<a |
||||||
|
ng-if="!anomalyType.visible" |
||||||
|
ng-disabled="anomalyType.selected" |
||||||
|
bs-tooltip="'Show. It`s hidden now.'" |
||||||
|
ng-click="ctrl.onAnomalyToggleVisibility(anomalyType.key)" |
||||||
|
class="pointer" |
||||||
|
> |
||||||
|
<i class="fa fa-eye-slash"></i> |
||||||
|
</a> |
||||||
|
</label> |
||||||
|
|
||||||
|
<label class="gf-form-label"> |
||||||
|
<a |
||||||
|
ng-if="!anomalyType.selected" |
||||||
|
ng-click="ctrl.onAnomalyRemove(anomalyType.key)" |
||||||
|
class="pointer" |
||||||
|
> |
||||||
|
<i class="fa fa-trash"></i> |
||||||
|
</a> |
||||||
|
|
||||||
|
<a |
||||||
|
ng-if="anomalyType.selected" |
||||||
|
ng-click="ctrl.onAnomalyCancelLabeling(anomalyType.key)" |
||||||
|
class="pointer" |
||||||
|
> |
||||||
|
<i class="fa fa-ban"></i> |
||||||
|
</a> |
||||||
|
</label> |
||||||
|
|
||||||
|
<label> |
||||||
|
<i ng-if="anomalyType.status === 'learning'" class="grafana-tip fa fa-leanpub ng-scope" bs-tooltip="'Learning'"></i> |
||||||
|
<i ng-if="anomalyType.status === 'pending'" class="grafana-tip fa fa-list-ul ng-scope" bs-tooltip="'Pending'"></i> |
||||||
|
<i ng-if="anomalyType.status === 'failed'" class="grafana-tip fa fa-exclamation-circle ng-scope" bs-tooltip="'Failed'"></i> |
||||||
|
</label> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="editor-row" ng-if="ctrl.anomalyController.creatingAnomalyType"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-4"> Name </label> |
||||||
|
<input |
||||||
|
type="text" class="gf-form-input max-width-15" |
||||||
|
ng-model="ctrl.anomalyController.newAnomalyType.name" |
||||||
|
ng-change="ctrl.onAnomalyNameChange()" |
||||||
|
> |
||||||
|
|
||||||
|
<label class="gf-form-label"> |
||||||
|
<a class="pointer" tabindex="1" ng-click="ctrl.saveAnomalyType()"> |
||||||
|
<b ng-if="!ctrl.anomalyController.savingAnomalyType"> create </b> |
||||||
|
<b ng-if="ctrl.anomalyController.savingAnomalyType" ng-disabled="true"> saving... </b> |
||||||
|
</a> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-button-row" ng-if="!ctrl.anomalyController.creatingAnomalyType"> |
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.createNewAnomalyType()"> |
||||||
|
<i class="fa fa-plus"></i> |
||||||
|
Add an Anomaly Type |
||||||
|
</button> |
||||||
|
</div> |
@ -0,0 +1,135 @@ |
|||||||
|
<div class="edit-tab-with-sidemenu"> |
||||||
|
<aside class="edit-sidemenu-aside"> |
||||||
|
<ul class="edit-sidemenu"> |
||||||
|
<li ng-class="{active: ctrl.subTabIndex === 0}"> |
||||||
|
<a ng-click="ctrl.subTabIndex = 0">Draw options</a> |
||||||
|
</li> |
||||||
|
<li ng-class="{active: ctrl.subTabIndex === 1}"> |
||||||
|
<a ng-click="ctrl.subTabIndex = 1"> |
||||||
|
Series overrides <span class="muted">({{ctrl.panel.seriesOverrides.length}})</span> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li ng-class="{active: ctrl.subTabIndex === 2}"> |
||||||
|
<a ng-click="ctrl.subTabIndex = 2"> |
||||||
|
Thresholds <span class="muted">({{ctrl.panel.thresholds.length}})</span> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</aside> |
||||||
|
|
||||||
|
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 0"> |
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Draw Modes</h5> |
||||||
|
<gf-form-switch class="gf-form" label="Bars" label-class="width-5" checked="ctrl.panel.bars" on-change="ctrl.render()"></gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" label="Lines" label-class="width-5" checked="ctrl.panel.lines" on-change="ctrl.render()"></gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" label="Points" label-class="width-5" checked="ctrl.panel.points" on-change="ctrl.render()"></gf-form-switch> |
||||||
|
</div> |
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Mode Options</h5> |
||||||
|
<div class="gf-form" ng-show="ctrl.panel.lines"> |
||||||
|
<label class="gf-form-label width-8">Fill</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-5"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="gf-form" ng-show="(ctrl.panel.lines)"> |
||||||
|
<label class="gf-form-label width-8">Line Width</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-5"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<gf-form-switch ng-show="ctrl.panel.lines" class="gf-form" label="Staircase" label-class="width-8" checked="ctrl.panel.steppedLine" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<div class="gf-form" ng-show="ctrl.panel.points"> |
||||||
|
<label class="gf-form-label width-8">Point Radius</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-5"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Hover tooltip</h5> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-9">Mode</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-8"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.shared" ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-9">Sort order</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-8"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.sort" ng-options="f.value as f.text for f in [{text: 'None', value: 0}, {text: 'Increasing', value: 1}, {text: 'Decreasing', value: 2}]" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="gf-form" ng-show="ctrl.panel.stack"> |
||||||
|
<label class="gf-form-label width-9">Stacked value</label> |
||||||
|
<div class="gf-form-select-wrapper max-width-8"> |
||||||
|
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Stacking & Null value</h5> |
||||||
|
<gf-form-switch class="gf-form" label="Stack" label-class="width-7" checked="ctrl.panel.stack" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" ng-show="ctrl.panel.stack" label="Percent" label-class="width-7" checked="ctrl.panel.percentage" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-7">Null value</label> |
||||||
|
<div class="gf-form-select-wrapper"> |
||||||
|
<select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 1"> |
||||||
|
<div class="gf-form-group"> |
||||||
|
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5> |
||||||
|
<div class="gf-form-inline" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label">alias or regex</label> |
||||||
|
</div> |
||||||
|
<div class="gf-form width-15"> |
||||||
|
<input type="text" ng-model="override.alias" bs-typeahead="getSeriesNames" ng-blur="ctrl.render()" data-min-length=0 data-items=100 class="gf-form-input width-15"> |
||||||
|
</div> |
||||||
|
<div class="gf-form" ng-repeat="option in currentOverrides"> |
||||||
|
<label class="gf-form-label"> |
||||||
|
<i class="pointer fa fa-remove" ng-click="removeOverride(option)"></i> |
||||||
|
<span ng-show="option.propertyName === 'color'"> |
||||||
|
Color: <i class="fa fa-circle" ng-style="{color:option.value}"></i> |
||||||
|
</span> |
||||||
|
<span ng-show="option.propertyName !== 'color'"> |
||||||
|
{{option.name}}: {{option.value}} |
||||||
|
</span> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<span class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)"> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow"> |
||||||
|
<div class="gf-form-label gf-form-label--grow"></div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label"> |
||||||
|
<i class="fa fa-trash pointer" ng-click="ctrl.removeSeriesOverride(override)"></i> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.addSeriesOverride()"> |
||||||
|
<i class="fa fa-plus"></i> Add override |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 2"> |
||||||
|
<grafalys-graph-threshold-form panel-ctrl="ctrl"></grafalys-graph-threshold-form> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
@ -0,0 +1,73 @@ |
|||||||
|
<div class="editor-row"> |
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Options</h5> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="Show" label-class="width-7" |
||||||
|
checked="ctrl.panel.legend.show" on-change="ctrl.refresh()"> |
||||||
|
</gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="As Table" label-class="width-7" |
||||||
|
checked="ctrl.panel.legend.alignAsTable" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="To the right" label-class="width-7" |
||||||
|
checked="ctrl.panel.legend.rightSide" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<div ng-if="ctrl.panel.legend.rightSide" class="gf-form"> |
||||||
|
<label class="gf-form-label width-7">Width</label> |
||||||
|
<input type="number" class="gf-form-input max-width-5" placeholder="250" bs-tooltip="'Set a min-width for the legend side table/block'" data-placement="right" ng-model="ctrl.panel.legend.sideWidth" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Values</h5> |
||||||
|
|
||||||
|
<div class="gf-form-inline"> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="Min" label-class="width-4" |
||||||
|
checked="ctrl.panel.legend.min" on-change="ctrl.legendValuesOptionChanged()"> |
||||||
|
</gf-form-switch> |
||||||
|
|
||||||
|
<gf-form-switch class="gf-form max-width-12" |
||||||
|
label="Max" label-class="width-6" switch-class="max-width-5" |
||||||
|
checked="ctrl.panel.legend.max" on-change="ctrl.legendValuesOptionChanged()"> |
||||||
|
</gf-form-switch> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-inline"> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="Avg" label-class="width-4" |
||||||
|
checked="ctrl.panel.legend.avg" on-change="ctrl.legendValuesOptionChanged()"> |
||||||
|
</gf-form-switch> |
||||||
|
|
||||||
|
<gf-form-switch class="gf-form max-width-12" |
||||||
|
label="Current" label-class="width-6" switch-class="max-width-5" |
||||||
|
checked="ctrl.panel.legend.current" on-change="ctrl.legendValuesOptionChanged()"> |
||||||
|
</gf-form-switch> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-inline"> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="Total" label-class="width-4" |
||||||
|
checked="ctrl.panel.legend.total" on-change="ctrl.legendValuesOptionChanged()"> |
||||||
|
</gf-form-switch> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label width-6">Decimals</label> |
||||||
|
<input type="number" class="gf-form-input width-5" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section gf-form-group"> |
||||||
|
<h5 class="section-heading">Hide series</h5> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="With only nulls" label-class="width-10" |
||||||
|
checked="ctrl.panel.legend.hideEmpty" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
<gf-form-switch class="gf-form" |
||||||
|
label="With only zeros" label-class="width-10" |
||||||
|
checked="ctrl.panel.legend.hideZero" on-change="ctrl.render()"> |
||||||
|
</gf-form-switch> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"type": "panel", |
||||||
|
"name": "Hastic Graph Panel", |
||||||
|
"id": "hastic-graph-panel", |
||||||
|
|
||||||
|
"info": { |
||||||
|
"author": { |
||||||
|
"name": "CorpGlory Team", |
||||||
|
"url": "https://corpglory.com" |
||||||
|
}, |
||||||
|
"logos": { |
||||||
|
"small": "img/icn-graph-panel.svg", |
||||||
|
"large": "img/icn-graph-panel.svg" |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"dependencies": { |
||||||
|
"grafanaVersion": "5.1.x" |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,160 @@ |
|||||||
|
import _ from 'lodash'; |
||||||
|
import angular from 'angular'; |
||||||
|
|
||||||
|
export class SeriesOverridesCtrl { |
||||||
|
/** @ngInject */ |
||||||
|
constructor($scope, $element, popoverSrv) { |
||||||
|
$scope.overrideMenu = []; |
||||||
|
$scope.currentOverrides = []; |
||||||
|
$scope.override = $scope.override || {}; |
||||||
|
|
||||||
|
$scope.addOverrideOption = function(name, propertyName, values) { |
||||||
|
var option = { |
||||||
|
text: name, |
||||||
|
propertyName: propertyName, |
||||||
|
index: $scope.overrideMenu.lenght, |
||||||
|
values: values, |
||||||
|
submenu: _.map(values, function(value) { |
||||||
|
return { text: String(value), value: value }; |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.overrideMenu.push(option); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.setOverride = function(item, subItem) { |
||||||
|
// handle color overrides
|
||||||
|
if (item.propertyName === 'color') { |
||||||
|
$scope.openColorSelector($scope.override['color']); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$scope.override[item.propertyName] = subItem.value; |
||||||
|
|
||||||
|
// automatically disable lines for this series and the fill bellow to series
|
||||||
|
// can be removed by the user if they still want lines
|
||||||
|
if (item.propertyName === 'fillBelowTo') { |
||||||
|
$scope.override['lines'] = false; |
||||||
|
$scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false }); |
||||||
|
} |
||||||
|
|
||||||
|
$scope.updateCurrentOverrides(); |
||||||
|
$scope.ctrl.render(); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.colorSelected = function(color) { |
||||||
|
$scope.override['color'] = color; |
||||||
|
$scope.updateCurrentOverrides(); |
||||||
|
$scope.ctrl.render(); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.openColorSelector = function(color) { |
||||||
|
var fakeSeries = { color: color }; |
||||||
|
popoverSrv.show({ |
||||||
|
element: $element.find('.dropdown')[0], |
||||||
|
position: 'top center', |
||||||
|
openOn: 'click', |
||||||
|
template: '<series-color-picker series="series" onColorChange="colorSelected" />', |
||||||
|
model: { |
||||||
|
autoClose: true, |
||||||
|
colorSelected: $scope.colorSelected, |
||||||
|
series: fakeSeries, |
||||||
|
}, |
||||||
|
onClose: function() { |
||||||
|
$scope.ctrl.render(); |
||||||
|
}, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.removeOverride = function(option) { |
||||||
|
delete $scope.override[option.propertyName]; |
||||||
|
$scope.updateCurrentOverrides(); |
||||||
|
$scope.ctrl.refresh(); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.getSeriesNames = function() { |
||||||
|
return _.map($scope.ctrl.seriesList, function(series: any) { |
||||||
|
return series.alias; |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.updateCurrentOverrides = function() { |
||||||
|
$scope.currentOverrides = []; |
||||||
|
_.each($scope.overrideMenu, function(option) { |
||||||
|
var value = $scope.override[option.propertyName]; |
||||||
|
if (_.isUndefined(value)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$scope.currentOverrides.push({ |
||||||
|
name: option.text, |
||||||
|
propertyName: option.propertyName, |
||||||
|
value: String(value), |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.addOverrideOption('Bars', 'bars', [true, false]); |
||||||
|
$scope.addOverrideOption('Lines', 'lines', [true, false]); |
||||||
|
$scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); |
||||||
|
$scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); |
||||||
|
$scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); |
||||||
|
$scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); |
||||||
|
$scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); |
||||||
|
$scope.addOverrideOption('Dashes', 'dashes', [true, false]); |
||||||
|
$scope.addOverrideOption('Dash Length', 'dashLength', [ |
||||||
|
1, |
||||||
|
2, |
||||||
|
3, |
||||||
|
4, |
||||||
|
5, |
||||||
|
6, |
||||||
|
7, |
||||||
|
8, |
||||||
|
9, |
||||||
|
10, |
||||||
|
11, |
||||||
|
12, |
||||||
|
13, |
||||||
|
14, |
||||||
|
15, |
||||||
|
16, |
||||||
|
17, |
||||||
|
18, |
||||||
|
19, |
||||||
|
20, |
||||||
|
]); |
||||||
|
$scope.addOverrideOption('Dash Space', 'spaceLength', [ |
||||||
|
1, |
||||||
|
2, |
||||||
|
3, |
||||||
|
4, |
||||||
|
5, |
||||||
|
6, |
||||||
|
7, |
||||||
|
8, |
||||||
|
9, |
||||||
|
10, |
||||||
|
11, |
||||||
|
12, |
||||||
|
13, |
||||||
|
14, |
||||||
|
15, |
||||||
|
16, |
||||||
|
17, |
||||||
|
18, |
||||||
|
19, |
||||||
|
20, |
||||||
|
]); |
||||||
|
$scope.addOverrideOption('Points', 'points', [true, false]); |
||||||
|
$scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]); |
||||||
|
$scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); |
||||||
|
$scope.addOverrideOption('Color', 'color', ['change']); |
||||||
|
$scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); |
||||||
|
$scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); |
||||||
|
$scope.addOverrideOption('Transform', 'transform', ['negative-Y']); |
||||||
|
$scope.addOverrideOption('Legend', 'legend', [true, false]); |
||||||
|
$scope.updateCurrentOverrides(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); |
@ -0,0 +1,100 @@ |
|||||||
|
import { Segment, SegmentKey } from '../model/segment'; |
||||||
|
import { MetricExpanded } from '../model/metric'; |
||||||
|
import { SegmentsSet } from '../model/segment_set'; |
||||||
|
import { AnomalyKey, AnomalyType, AnomalySegment } from '../model/anomaly'; |
||||||
|
|
||||||
|
import { BackendSrv } from 'grafana/app/core/services/backend_srv'; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class AnomalyService { |
||||||
|
constructor(private _backendURL: string, private _backendSrv: BackendSrv) { |
||||||
|
} |
||||||
|
|
||||||
|
async postNewAnomalyType(metric: MetricExpanded, newAnomalyType: AnomalyType, panelId: number) { |
||||||
|
return this._backendSrv.post( |
||||||
|
this._backendURL + '/anomalies',
|
||||||
|
{ |
||||||
|
name: newAnomalyType.name, |
||||||
|
metric: metric.toJSON(), |
||||||
|
panelUrl: window.location.origin + window.location.pathname + `?panelId=${panelId}&fullscreen` |
||||||
|
} |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
async updateSegments( |
||||||
|
key: AnomalyKey, addedSegments: SegmentsSet<Segment>, removedSegments: SegmentsSet<Segment> |
||||||
|
): Promise<SegmentKey[]> { |
||||||
|
|
||||||
|
const getJSONs = (segs: SegmentsSet<Segment>) => segs.getSegments().map(segment => ({ |
||||||
|
"start": segment.from, |
||||||
|
"finish": segment.to |
||||||
|
})); |
||||||
|
|
||||||
|
var payload = { |
||||||
|
name: key, |
||||||
|
added_segments: getJSONs(addedSegments), |
||||||
|
removed_segments: removedSegments.getSegments().map(s => s.key) |
||||||
|
} |
||||||
|
|
||||||
|
var data = await this._backendSrv.patch(this._backendURL + '/segments', payload); |
||||||
|
if(data.added_ids === undefined) { |
||||||
|
throw new Error('Server didn`t send added_ids'); |
||||||
|
} |
||||||
|
|
||||||
|
return data.added_ids as SegmentKey[]; |
||||||
|
} |
||||||
|
|
||||||
|
async getSegments(key: AnomalyKey, from?: number, to?: number): Promise<AnomalySegment[]> { |
||||||
|
var payload: any = { anomaly_id: key }; |
||||||
|
if(from !== undefined) { |
||||||
|
payload['from'] = from; |
||||||
|
} |
||||||
|
if(to !== undefined) { |
||||||
|
payload['to'] = to; |
||||||
|
} |
||||||
|
var data = await this._backendSrv.get( |
||||||
|
this._backendURL + '/segments', |
||||||
|
payload |
||||||
|
); |
||||||
|
if(data.segments === undefined) { |
||||||
|
throw new Error('Server didn`t return segments array'); |
||||||
|
} |
||||||
|
var segments = data.segments as { id: number, start: number, finish: number, labeled: boolean }[]; |
||||||
|
return segments.map(s => new AnomalySegment(s.labeled, s.id, s.start, s.finish)); |
||||||
|
} |
||||||
|
|
||||||
|
async * getAnomalyTypeStatusGenerator(key: AnomalyKey, duration: number) { |
||||||
|
let statusCheck = async () => { |
||||||
|
var data = await this._backendSrv.get( |
||||||
|
this._backendURL + '/anomalies/status', { name: key } |
||||||
|
); |
||||||
|
return data.status as string; |
||||||
|
} |
||||||
|
|
||||||
|
let timeout = async () => new Promise( |
||||||
|
resolve => setTimeout(resolve, duration) |
||||||
|
); |
||||||
|
|
||||||
|
while(true) { |
||||||
|
yield await statusCheck(); |
||||||
|
await timeout(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async getAlertEnabled(key: AnomalyKey): Promise<boolean> { |
||||||
|
var data = await this._backendSrv.get( |
||||||
|
this._backendURL + '/alerts', { anomaly_id: key } |
||||||
|
); |
||||||
|
return data.enable as boolean; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async setAlertEnabled(key: AnomalyKey, value: boolean): Promise<void> { |
||||||
|
return this._backendSrv.post( |
||||||
|
this._backendURL + '/alerts', { anomaly_id: key, enable: value } |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
var template = ` |
||||||
|
<div class="graph-panel" ng-class="{'graph-panel--legend-right': ctrl.panel.legend.rightSide}"> |
||||||
|
<div class="graph-panel__chart" id="graphPanel" ng-dblclick="ctrl.zoomOut()" /> |
||||||
|
<div class="grafalys-graph-legend" id="graphLegend" /> |
||||||
|
</div> |
||||||
|
`;
|
||||||
|
|
||||||
|
export default template; |
@ -0,0 +1,238 @@ |
|||||||
|
import './vendor/flot/jquery.flot'; |
||||||
|
import * as $ from 'jquery'; |
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
export class ThresholdManager { |
||||||
|
plot: any; |
||||||
|
placeholder: any; |
||||||
|
height: any; |
||||||
|
thresholds: any; |
||||||
|
needsCleanup: boolean; |
||||||
|
hasSecondYAxis: any; |
||||||
|
|
||||||
|
constructor(private panelCtrl) {} |
||||||
|
|
||||||
|
getHandleHtml(handleIndex, model, valueStr) { |
||||||
|
var stateClass = model.colorMode; |
||||||
|
if (model.colorMode === 'custom') { |
||||||
|
stateClass = 'critical'; |
||||||
|
} |
||||||
|
|
||||||
|
return ` |
||||||
|
<div class="alert-handle-wrapper alert-handle-wrapper--T${handleIndex}"> |
||||||
|
<div class="alert-handle-line alert-handle-line--${stateClass}"> |
||||||
|
</div> |
||||||
|
<div class="alert-handle" data-handle-index="${handleIndex}"> |
||||||
|
<i class="icon-gf icon-gf-${stateClass} alert-state-${stateClass}"></i> |
||||||
|
<span class="alert-handle-value">${valueStr}<i class="alert-handle-grip"></i></span> |
||||||
|
</div> |
||||||
|
</div>`;
|
||||||
|
} |
||||||
|
|
||||||
|
initDragging(evt) { |
||||||
|
var handleElem = $(evt.currentTarget).parents('.alert-handle-wrapper'); |
||||||
|
var handleIndex = $(evt.currentTarget).data('handleIndex'); |
||||||
|
|
||||||
|
var lastY = null; |
||||||
|
var posTop; |
||||||
|
var plot = this.plot; |
||||||
|
var panelCtrl = this.panelCtrl; |
||||||
|
var model = this.thresholds[handleIndex]; |
||||||
|
|
||||||
|
function dragging(evt) { |
||||||
|
if (lastY === null) { |
||||||
|
lastY = evt.clientY; |
||||||
|
} else { |
||||||
|
var diff = evt.clientY - lastY; |
||||||
|
posTop = posTop + diff; |
||||||
|
lastY = evt.clientY; |
||||||
|
handleElem.css({ top: posTop + diff }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function stopped() { |
||||||
|
// calculate graph level
|
||||||
|
var graphValue = plot.c2p({ left: 0, top: posTop }).y; |
||||||
|
graphValue = parseInt(graphValue.toFixed(0)); |
||||||
|
model.value = graphValue; |
||||||
|
|
||||||
|
handleElem.off('mousemove', dragging); |
||||||
|
handleElem.off('mouseup', dragging); |
||||||
|
handleElem.off('mouseleave', dragging); |
||||||
|
|
||||||
|
// trigger digest and render
|
||||||
|
panelCtrl.$scope.$apply(function() { |
||||||
|
panelCtrl.render(); |
||||||
|
panelCtrl.events.emit('threshold-changed', { |
||||||
|
threshold: model, |
||||||
|
handleIndex: handleIndex, |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
lastY = null; |
||||||
|
posTop = handleElem.position().top; |
||||||
|
|
||||||
|
handleElem.on('mousemove', dragging); |
||||||
|
handleElem.on('mouseup', stopped); |
||||||
|
handleElem.on('mouseleave', stopped); |
||||||
|
} |
||||||
|
|
||||||
|
cleanUp() { |
||||||
|
this.placeholder.find('.alert-handle-wrapper').remove(); |
||||||
|
this.needsCleanup = false; |
||||||
|
} |
||||||
|
|
||||||
|
renderHandle(handleIndex, defaultHandleTopPos) { |
||||||
|
var model = this.thresholds[handleIndex]; |
||||||
|
var value = model.value; |
||||||
|
var valueStr = value; |
||||||
|
var handleTopPos = 0; |
||||||
|
|
||||||
|
// handle no value
|
||||||
|
if (!_.isNumber(value)) { |
||||||
|
valueStr = ''; |
||||||
|
handleTopPos = defaultHandleTopPos; |
||||||
|
} else { |
||||||
|
var valueCanvasPos = this.plot.p2c({ x: 0, y: value }); |
||||||
|
handleTopPos = Math.round(Math.min(Math.max(valueCanvasPos.top, 0), this.height) - 6); |
||||||
|
} |
||||||
|
|
||||||
|
var handleElem = $(this.getHandleHtml(handleIndex, model, valueStr)); |
||||||
|
this.placeholder.append(handleElem); |
||||||
|
|
||||||
|
handleElem.toggleClass('alert-handle-wrapper--no-value', valueStr === ''); |
||||||
|
handleElem.css({ top: handleTopPos }); |
||||||
|
} |
||||||
|
|
||||||
|
shouldDrawHandles() { |
||||||
|
return !this.hasSecondYAxis && this.panelCtrl.editingThresholds && this.panelCtrl.panel.thresholds.length > 0; |
||||||
|
} |
||||||
|
|
||||||
|
prepare(elem, data) { |
||||||
|
this.hasSecondYAxis = false; |
||||||
|
for (var i = 0; i < data.length; i++) { |
||||||
|
if (data[i].yaxis > 1) { |
||||||
|
this.hasSecondYAxis = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.shouldDrawHandles()) { |
||||||
|
var thresholdMargin = this.panelCtrl.panel.thresholds.length > 1 ? '220px' : '110px'; |
||||||
|
elem.css('margin-right', thresholdMargin); |
||||||
|
} else if (this.needsCleanup) { |
||||||
|
elem.css('margin-right', '0'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
draw(plot) { |
||||||
|
this.thresholds = this.panelCtrl.panel.thresholds; |
||||||
|
this.plot = plot; |
||||||
|
this.placeholder = plot.getPlaceholder(); |
||||||
|
|
||||||
|
if (this.needsCleanup) { |
||||||
|
this.cleanUp(); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.shouldDrawHandles()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.height = plot.height(); |
||||||
|
|
||||||
|
if (this.thresholds.length > 0) { |
||||||
|
this.renderHandle(0, 10); |
||||||
|
} |
||||||
|
if (this.thresholds.length > 1) { |
||||||
|
this.renderHandle(1, this.height - 30); |
||||||
|
} |
||||||
|
|
||||||
|
this.placeholder.off('mousedown', '.alert-handle'); |
||||||
|
this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this)); |
||||||
|
this.needsCleanup = true; |
||||||
|
} |
||||||
|
|
||||||
|
addFlotOptions(options, panel) { |
||||||
|
if (!panel.thresholds || panel.thresholds.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var gtLimit = Infinity; |
||||||
|
var ltLimit = -Infinity; |
||||||
|
var i, threshold, other; |
||||||
|
|
||||||
|
for (i = 0; i < panel.thresholds.length; i++) { |
||||||
|
threshold = panel.thresholds[i]; |
||||||
|
if (!_.isNumber(threshold.value)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
var limit; |
||||||
|
switch (threshold.op) { |
||||||
|
case 'gt': { |
||||||
|
limit = gtLimit; |
||||||
|
// if next threshold is less then op and greater value, then use that as limit
|
||||||
|
if (panel.thresholds.length > i + 1) { |
||||||
|
other = panel.thresholds[i + 1]; |
||||||
|
if (other.value > threshold.value) { |
||||||
|
limit = other.value; |
||||||
|
ltLimit = limit; |
||||||
|
} |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'lt': { |
||||||
|
limit = ltLimit; |
||||||
|
// if next threshold is less then op and greater value, then use that as limit
|
||||||
|
if (panel.thresholds.length > i + 1) { |
||||||
|
other = panel.thresholds[i + 1]; |
||||||
|
if (other.value < threshold.value) { |
||||||
|
limit = other.value; |
||||||
|
gtLimit = limit; |
||||||
|
} |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var fillColor, lineColor; |
||||||
|
switch (threshold.colorMode) { |
||||||
|
case 'critical': { |
||||||
|
fillColor = 'rgba(234, 112, 112, 0.12)'; |
||||||
|
lineColor = 'rgba(237, 46, 24, 0.60)'; |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'warning': { |
||||||
|
fillColor = 'rgba(235, 138, 14, 0.12)'; |
||||||
|
lineColor = 'rgba(247, 149, 32, 0.60)'; |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'ok': { |
||||||
|
fillColor = 'rgba(11, 237, 50, 0.090)'; |
||||||
|
lineColor = 'rgba(6,163,69, 0.60)'; |
||||||
|
break; |
||||||
|
} |
||||||
|
case 'custom': { |
||||||
|
fillColor = threshold.fillColor; |
||||||
|
lineColor = threshold.lineColor; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// fill
|
||||||
|
if (threshold.fill) { |
||||||
|
options.grid.markings.push({ |
||||||
|
yaxis: { from: threshold.value, to: limit }, |
||||||
|
color: fillColor, |
||||||
|
}); |
||||||
|
} |
||||||
|
if (threshold.line) { |
||||||
|
options.grid.markings.push({ |
||||||
|
yaxis: { from: threshold.value, to: threshold.value }, |
||||||
|
color: lineColor, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,142 @@ |
|||||||
|
|
||||||
|
import coreModule from 'grafana/app/core/core_module'; |
||||||
|
|
||||||
|
export class ThresholdFormCtrl { |
||||||
|
panelCtrl: any; |
||||||
|
panel: any; |
||||||
|
disabled: boolean; |
||||||
|
|
||||||
|
/** @ngInject */ |
||||||
|
constructor($scope) { |
||||||
|
this.panel = this.panelCtrl.panel; |
||||||
|
|
||||||
|
if (this.panel.alert) { |
||||||
|
this.disabled = true; |
||||||
|
} |
||||||
|
|
||||||
|
var unbindDestroy = $scope.$on('$destroy', () => { |
||||||
|
this.panelCtrl.editingThresholds = false; |
||||||
|
this.panelCtrl.render(); |
||||||
|
unbindDestroy(); |
||||||
|
}); |
||||||
|
|
||||||
|
this.panelCtrl.editingThresholds = true; |
||||||
|
} |
||||||
|
|
||||||
|
addThreshold() { |
||||||
|
this.panel.thresholds.push({ |
||||||
|
value: undefined, |
||||||
|
colorMode: 'critical', |
||||||
|
op: 'gt', |
||||||
|
fill: true, |
||||||
|
line: true, |
||||||
|
}); |
||||||
|
this.panelCtrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
removeThreshold(index) { |
||||||
|
this.panel.thresholds.splice(index, 1); |
||||||
|
this.panelCtrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
this.panelCtrl.render(); |
||||||
|
} |
||||||
|
|
||||||
|
onFillColorChange(index) { |
||||||
|
return newColor => { |
||||||
|
this.panel.thresholds[index].fillColor = newColor; |
||||||
|
this.render(); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
onLineColorChange(index) { |
||||||
|
return newColor => { |
||||||
|
this.panel.thresholds[index].lineColor = newColor; |
||||||
|
this.render(); |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var template = ` |
||||||
|
<div class="gf-form-group"> |
||||||
|
<h5>Thresholds</h5> |
||||||
|
<p class="muted" ng-show="ctrl.disabled"> |
||||||
|
Visual thresholds options <strong>disabled.</strong> |
||||||
|
Visit the Alert tab update your thresholds. <br> |
||||||
|
To re-enable thresholds, the alert rule must be deleted from this panel. |
||||||
|
</p> |
||||||
|
<div ng-class="{'thresholds-form-disabled': ctrl.disabled}"> |
||||||
|
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.thresholds"> |
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label">T{{$index+1}}</label> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<div class="gf-form-select-wrapper"> |
||||||
|
<select class="gf-form-input" ng-model="threshold.op" |
||||||
|
ng-options="f for f in ['gt', 'lt']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"></select> |
||||||
|
</div> |
||||||
|
<input type="number" ng-model="threshold.value" class="gf-form-input width-8" |
||||||
|
ng-change="ctrl.render()" placeholder="value" ng-disabled="ctrl.disabled"> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label">Color</label> |
||||||
|
<div class="gf-form-select-wrapper"> |
||||||
|
<select class="gf-form-input" ng-model="threshold.colorMode" |
||||||
|
ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<gf-form-switch class="gf-form" label="Fill" checked="threshold.fill" |
||||||
|
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch> |
||||||
|
|
||||||
|
<div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'"> |
||||||
|
<label class="gf-form-label">Fill color</label> |
||||||
|
<span class="gf-form-label"> |
||||||
|
<color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<gf-form-switch class="gf-form" label="Line" checked="threshold.line" |
||||||
|
on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch> |
||||||
|
|
||||||
|
<div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'"> |
||||||
|
<label class="gf-form-label">Line color</label> |
||||||
|
<span class="gf-form-label"> |
||||||
|
<color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form"> |
||||||
|
<label class="gf-form-label"> |
||||||
|
<a class="pointer" ng-click="ctrl.removeThreshold($index)" ng-disabled="ctrl.disabled"> |
||||||
|
<i class="fa fa-trash"></i> |
||||||
|
</a> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-button-row"> |
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.addThreshold()" ng-disabled="ctrl.disabled"> |
||||||
|
<i class="fa fa-plus"></i> Add Threshold |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`;
|
||||||
|
|
||||||
|
coreModule.directive('grafalysGraphThresholdForm', function() { |
||||||
|
return { |
||||||
|
restrict: 'E', |
||||||
|
template: template, |
||||||
|
controller: ThresholdFormCtrl, |
||||||
|
bindToController: true, |
||||||
|
controllerAs: 'ctrl', |
||||||
|
scope: { |
||||||
|
panelCtrl: '=', |
||||||
|
}, |
||||||
|
}; |
||||||
|
}); |
@ -0,0 +1,176 @@ |
|||||||
|
/* Flot plugin for showing crosshairs when the mouse hovers over the plot. |
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
The plugin supports these options: |
||||||
|
|
||||||
|
crosshair: { |
||||||
|
mode: null or "x" or "y" or "xy" |
||||||
|
color: color |
||||||
|
lineWidth: number |
||||||
|
} |
||||||
|
|
||||||
|
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical |
||||||
|
crosshair that lets you trace the values on the x axis, "y" enables a |
||||||
|
horizontal crosshair and "xy" enables them both. "color" is the color of the |
||||||
|
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of |
||||||
|
the drawn lines (default is 1). |
||||||
|
|
||||||
|
The plugin also adds four public methods: |
||||||
|
|
||||||
|
- setCrosshair( pos ) |
||||||
|
|
||||||
|
Set the position of the crosshair. Note that this is cleared if the user |
||||||
|
moves the mouse. "pos" is in coordinates of the plot and should be on the |
||||||
|
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple |
||||||
|
axes), which is coincidentally the same format as what you get from a |
||||||
|
"plothover" event. If "pos" is null, the crosshair is cleared. |
||||||
|
|
||||||
|
- clearCrosshair() |
||||||
|
|
||||||
|
Clear the crosshair. |
||||||
|
|
||||||
|
- lockCrosshair(pos) |
||||||
|
|
||||||
|
Cause the crosshair to lock to the current location, no longer updating if |
||||||
|
the user moves the mouse. Optionally supply a position (passed on to |
||||||
|
setCrosshair()) to move it to. |
||||||
|
|
||||||
|
Example usage: |
||||||
|
|
||||||
|
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; |
||||||
|
$("#graph").bind( "plothover", function ( evt, position, item ) { |
||||||
|
if ( item ) { |
||||||
|
// Lock the crosshair to the data point being hovered
|
||||||
|
myFlot.lockCrosshair({ |
||||||
|
x: item.datapoint[ 0 ], |
||||||
|
y: item.datapoint[ 1 ] |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// Return normal crosshair operation
|
||||||
|
myFlot.unlockCrosshair(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
- unlockCrosshair() |
||||||
|
|
||||||
|
Free the crosshair to move again after locking it. |
||||||
|
*/ |
||||||
|
|
||||||
|
(function ($) { |
||||||
|
var options = { |
||||||
|
crosshair: { |
||||||
|
mode: null, // one of null, "x", "y" or "xy",
|
||||||
|
color: "rgba(170, 0, 0, 0.80)", |
||||||
|
lineWidth: 1 |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
// position of crosshair in pixels
|
||||||
|
var crosshair = { x: -1, y: -1, locked: false }; |
||||||
|
|
||||||
|
plot.setCrosshair = function setCrosshair(pos) { |
||||||
|
if (!pos) |
||||||
|
crosshair.x = -1; |
||||||
|
else { |
||||||
|
var o = plot.p2c(pos); |
||||||
|
crosshair.x = Math.max(0, Math.min(o.left, plot.width())); |
||||||
|
crosshair.y = Math.max(0, Math.min(o.top, plot.height())); |
||||||
|
} |
||||||
|
|
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
}; |
||||||
|
|
||||||
|
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
|
||||||
|
|
||||||
|
plot.lockCrosshair = function lockCrosshair(pos) { |
||||||
|
if (pos) |
||||||
|
plot.setCrosshair(pos); |
||||||
|
crosshair.locked = true; |
||||||
|
}; |
||||||
|
|
||||||
|
plot.unlockCrosshair = function unlockCrosshair() { |
||||||
|
crosshair.locked = false; |
||||||
|
}; |
||||||
|
|
||||||
|
function onMouseOut(e) { |
||||||
|
if (crosshair.locked) |
||||||
|
return; |
||||||
|
|
||||||
|
if (crosshair.x != -1) { |
||||||
|
crosshair.x = -1; |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseMove(e) { |
||||||
|
if (crosshair.locked) |
||||||
|
return; |
||||||
|
|
||||||
|
if (plot.getSelection && plot.getSelection()) { |
||||||
|
crosshair.x = -1; // hide the crosshair while selecting
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var offset = plot.offset(); |
||||||
|
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); |
||||||
|
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} |
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function (plot, eventHolder) { |
||||||
|
if (!plot.getOptions().crosshair.mode) |
||||||
|
return; |
||||||
|
|
||||||
|
eventHolder.mouseout(onMouseOut); |
||||||
|
eventHolder.mousemove(onMouseMove); |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) { |
||||||
|
var c = plot.getOptions().crosshair; |
||||||
|
if (!c.mode) |
||||||
|
return; |
||||||
|
|
||||||
|
var plotOffset = plot.getPlotOffset(); |
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.translate(plotOffset.left, plotOffset.top); |
||||||
|
|
||||||
|
if (crosshair.x != -1) { |
||||||
|
var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; |
||||||
|
|
||||||
|
ctx.strokeStyle = c.color; |
||||||
|
ctx.lineWidth = c.lineWidth; |
||||||
|
ctx.lineJoin = "round"; |
||||||
|
|
||||||
|
ctx.beginPath(); |
||||||
|
if (c.mode.indexOf("x") != -1) { |
||||||
|
var drawX = Math.floor(crosshair.x) + adj; |
||||||
|
ctx.moveTo(drawX, 0); |
||||||
|
ctx.lineTo(drawX, plot.height()); |
||||||
|
} |
||||||
|
if (c.mode.indexOf("y") != -1) { |
||||||
|
var drawY = Math.floor(crosshair.y) + adj; |
||||||
|
ctx.moveTo(0, drawY); |
||||||
|
ctx.lineTo(plot.width(), drawY); |
||||||
|
} |
||||||
|
ctx.stroke(); |
||||||
|
} |
||||||
|
ctx.restore(); |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.shutdown.push(function (plot, eventHolder) { |
||||||
|
eventHolder.unbind("mouseout", onMouseOut); |
||||||
|
eventHolder.unbind("mousemove", onMouseMove); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: 'crosshair', |
||||||
|
version: '1.0' |
||||||
|
}); |
||||||
|
})(jQuery); |
@ -0,0 +1,236 @@ |
|||||||
|
/* |
||||||
|
* jQuery.flot.dashes |
||||||
|
* |
||||||
|
* options = { |
||||||
|
* series: { |
||||||
|
* dashes: { |
||||||
|
* |
||||||
|
* // show
|
||||||
|
* // default: false
|
||||||
|
* // Whether to show dashes for the series.
|
||||||
|
* show: <boolean>, |
||||||
|
* |
||||||
|
* // lineWidth
|
||||||
|
* // default: 2
|
||||||
|
* // The width of the dashed line in pixels.
|
||||||
|
* lineWidth: <number>, |
||||||
|
* |
||||||
|
* // dashLength
|
||||||
|
* // default: 10
|
||||||
|
* // Controls the length of the individual dashes and the amount of
|
||||||
|
* // space between them.
|
||||||
|
* // If this is a number, the dashes and spaces will have that length.
|
||||||
|
* // If this is an array, it is read as [ dashLength, spaceLength ]
|
||||||
|
* dashLength: <number> or <array[2]> |
||||||
|
* } |
||||||
|
* } |
||||||
|
* } |
||||||
|
*/ |
||||||
|
(function($){ |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(function(plot, series, datapoints) { |
||||||
|
|
||||||
|
if (!series.dashes.show) return; |
||||||
|
|
||||||
|
plot.hooks.draw.push(function(plot, ctx) { |
||||||
|
|
||||||
|
var plotOffset = plot.getPlotOffset(), |
||||||
|
axisx = series.xaxis, |
||||||
|
axisy = series.yaxis; |
||||||
|
|
||||||
|
function plotDashes(xoffset, yoffset) { |
||||||
|
|
||||||
|
var points = datapoints.points, |
||||||
|
ps = datapoints.pointsize, |
||||||
|
prevx = null, |
||||||
|
prevy = null, |
||||||
|
dashRemainder = 0, |
||||||
|
dashOn = true, |
||||||
|
dashOnLength, |
||||||
|
dashOffLength; |
||||||
|
|
||||||
|
if (series.dashes.dashLength[0]) { |
||||||
|
dashOnLength = series.dashes.dashLength[0]; |
||||||
|
if (series.dashes.dashLength[1]) { |
||||||
|
dashOffLength = series.dashes.dashLength[1]; |
||||||
|
} else { |
||||||
|
dashOffLength = dashOnLength; |
||||||
|
} |
||||||
|
} else { |
||||||
|
dashOffLength = dashOnLength = series.dashes.dashLength; |
||||||
|
} |
||||||
|
|
||||||
|
ctx.beginPath(); |
||||||
|
|
||||||
|
for (var i = ps; i < points.length; i += ps) { |
||||||
|
|
||||||
|
var x1 = points[i - ps], |
||||||
|
y1 = points[i - ps + 1], |
||||||
|
x2 = points[i], |
||||||
|
y2 = points[i + 1]; |
||||||
|
|
||||||
|
if (x1 == null || x2 == null) continue; |
||||||
|
|
||||||
|
// clip with ymin
|
||||||
|
if (y1 <= y2 && y1 < axisy.min) { |
||||||
|
if (y2 < axisy.min) continue; // line segment is outside
|
||||||
|
// compute new intersection point
|
||||||
|
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; |
||||||
|
y1 = axisy.min; |
||||||
|
} else if (y2 <= y1 && y2 < axisy.min) { |
||||||
|
if (y1 < axisy.min) continue; |
||||||
|
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; |
||||||
|
y2 = axisy.min; |
||||||
|
} |
||||||
|
|
||||||
|
// clip with ymax
|
||||||
|
if (y1 >= y2 && y1 > axisy.max) { |
||||||
|
if (y2 > axisy.max) continue; |
||||||
|
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; |
||||||
|
y1 = axisy.max; |
||||||
|
} else if (y2 >= y1 && y2 > axisy.max) { |
||||||
|
if (y1 > axisy.max) continue; |
||||||
|
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; |
||||||
|
y2 = axisy.max; |
||||||
|
} |
||||||
|
|
||||||
|
// clip with xmin
|
||||||
|
if (x1 <= x2 && x1 < axisx.min) { |
||||||
|
if (x2 < axisx.min) continue; |
||||||
|
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; |
||||||
|
x1 = axisx.min; |
||||||
|
} else if (x2 <= x1 && x2 < axisx.min) { |
||||||
|
if (x1 < axisx.min) continue; |
||||||
|
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; |
||||||
|
x2 = axisx.min; |
||||||
|
} |
||||||
|
|
||||||
|
// clip with xmax
|
||||||
|
if (x1 >= x2 && x1 > axisx.max) { |
||||||
|
if (x2 > axisx.max) continue; |
||||||
|
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; |
||||||
|
x1 = axisx.max; |
||||||
|
} else if (x2 >= x1 && x2 > axisx.max) { |
||||||
|
if (x1 > axisx.max) continue; |
||||||
|
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; |
||||||
|
x2 = axisx.max; |
||||||
|
} |
||||||
|
|
||||||
|
if (x1 != prevx || y1 != prevy) { |
||||||
|
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); |
||||||
|
} |
||||||
|
|
||||||
|
var ax1 = axisx.p2c(x1) + xoffset, |
||||||
|
ay1 = axisy.p2c(y1) + yoffset, |
||||||
|
ax2 = axisx.p2c(x2) + xoffset, |
||||||
|
ay2 = axisy.p2c(y2) + yoffset, |
||||||
|
dashOffset; |
||||||
|
|
||||||
|
function lineSegmentOffset(segmentLength) { |
||||||
|
|
||||||
|
var c = Math.sqrt(Math.pow(ax2 - ax1, 2) + Math.pow(ay2 - ay1, 2)); |
||||||
|
|
||||||
|
if (c <= segmentLength) { |
||||||
|
return { |
||||||
|
deltaX: ax2 - ax1, |
||||||
|
deltaY: ay2 - ay1, |
||||||
|
distance: c, |
||||||
|
remainder: segmentLength - c |
||||||
|
} |
||||||
|
} else { |
||||||
|
var xsign = ax2 > ax1 ? 1 : -1, |
||||||
|
ysign = ay2 > ay1 ? 1 : -1; |
||||||
|
return { |
||||||
|
deltaX: xsign * Math.sqrt(Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), |
||||||
|
deltaY: ysign * Math.sqrt(Math.pow(segmentLength, 2) - Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))), |
||||||
|
distance: segmentLength, |
||||||
|
remainder: 0 |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
//-end lineSegmentOffset
|
||||||
|
|
||||||
|
do { |
||||||
|
|
||||||
|
dashOffset = lineSegmentOffset( |
||||||
|
dashRemainder > 0 ? dashRemainder : |
||||||
|
dashOn ? dashOnLength : dashOffLength); |
||||||
|
|
||||||
|
if (dashOffset.deltaX != 0 || dashOffset.deltaY != 0) { |
||||||
|
if (dashOn) { |
||||||
|
ctx.lineTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); |
||||||
|
} else { |
||||||
|
ctx.moveTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
dashOn = !dashOn; |
||||||
|
dashRemainder = dashOffset.remainder; |
||||||
|
ax1 += dashOffset.deltaX; |
||||||
|
ay1 += dashOffset.deltaY; |
||||||
|
|
||||||
|
} while (dashOffset.distance > 0); |
||||||
|
|
||||||
|
prevx = x2; |
||||||
|
prevy = y2; |
||||||
|
} |
||||||
|
|
||||||
|
ctx.stroke(); |
||||||
|
} |
||||||
|
//-end plotDashes
|
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.translate(plotOffset.left, plotOffset.top); |
||||||
|
ctx.lineJoin = 'round'; |
||||||
|
|
||||||
|
var lw = series.dashes.lineWidth, |
||||||
|
sw = series.shadowSize; |
||||||
|
|
||||||
|
// FIXME: consider another form of shadow when filling is turned on
|
||||||
|
if (lw > 0 && sw > 0) { |
||||||
|
// draw shadow as a thick and thin line with transparency
|
||||||
|
ctx.lineWidth = sw; |
||||||
|
ctx.strokeStyle = "rgba(0,0,0,0.1)"; |
||||||
|
// position shadow at angle from the mid of line
|
||||||
|
var angle = Math.PI/18; |
||||||
|
plotDashes(Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2)); |
||||||
|
ctx.lineWidth = sw/2; |
||||||
|
plotDashes(Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4)); |
||||||
|
} |
||||||
|
|
||||||
|
ctx.lineWidth = lw; |
||||||
|
ctx.strokeStyle = series.color; |
||||||
|
|
||||||
|
if (lw > 0) { |
||||||
|
plotDashes(0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
ctx.restore(); |
||||||
|
|
||||||
|
}); |
||||||
|
//-end draw hook
|
||||||
|
|
||||||
|
}); |
||||||
|
//-end processDatapoints hook
|
||||||
|
|
||||||
|
} |
||||||
|
//-end init
|
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: { |
||||||
|
series: { |
||||||
|
dashes: { |
||||||
|
show: false, |
||||||
|
lineWidth: 2, |
||||||
|
dashLength: 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
name: 'dashes', |
||||||
|
version: '0.1' |
||||||
|
}); |
||||||
|
|
||||||
|
})(jQuery) |
@ -0,0 +1,604 @@ |
|||||||
|
define([ |
||||||
|
'jquery', |
||||||
|
'lodash', |
||||||
|
'angular', |
||||||
|
'tether-drop', |
||||||
|
], |
||||||
|
function ($, _, angular, Drop) { |
||||||
|
'use strict'; |
||||||
|
|
||||||
|
function createAnnotationToolip(element, event, plot) { |
||||||
|
var injector = angular.element(document).injector(); |
||||||
|
var content = document.createElement('div'); |
||||||
|
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>'; |
||||||
|
|
||||||
|
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) { |
||||||
|
var eventManager = plot.getOptions().events.manager; |
||||||
|
var tmpScope = $rootScope.$new(true); |
||||||
|
tmpScope.event = event; |
||||||
|
tmpScope.onEdit = function() { |
||||||
|
eventManager.editEvent(event); |
||||||
|
}; |
||||||
|
|
||||||
|
$compile(content)(tmpScope); |
||||||
|
tmpScope.$digest(); |
||||||
|
tmpScope.$destroy(); |
||||||
|
|
||||||
|
var drop = new Drop({ |
||||||
|
target: element[0], |
||||||
|
content: content, |
||||||
|
position: "bottom center", |
||||||
|
classes: 'drop-popover drop-popover--annotation', |
||||||
|
openOn: 'hover', |
||||||
|
hoverCloseDelay: 200, |
||||||
|
tetherOptions: { |
||||||
|
constraints: [{to: 'window', pin: true, attachment: "both"}] |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
drop.open(); |
||||||
|
|
||||||
|
drop.on('close', function() { |
||||||
|
setTimeout(function() { |
||||||
|
drop.destroy(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}]); |
||||||
|
} |
||||||
|
|
||||||
|
var markerElementToAttachTo = null; |
||||||
|
|
||||||
|
function createEditPopover(element, event, plot) { |
||||||
|
var eventManager = plot.getOptions().events.manager; |
||||||
|
if (eventManager.editorOpen) { |
||||||
|
// update marker element to attach to (needed in case of legend on the right
|
||||||
|
// when there is a double render pass and the inital marker element is removed)
|
||||||
|
markerElementToAttachTo = element; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// mark as openend
|
||||||
|
eventManager.editorOpened(); |
||||||
|
// set marker elment to attache to
|
||||||
|
markerElementToAttachTo = element; |
||||||
|
|
||||||
|
// wait for element to be attached and positioned
|
||||||
|
setTimeout(function() { |
||||||
|
|
||||||
|
var injector = angular.element(document).injector(); |
||||||
|
var content = document.createElement('div'); |
||||||
|
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>'; |
||||||
|
|
||||||
|
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) { |
||||||
|
var scope = $rootScope.$new(true); |
||||||
|
var drop; |
||||||
|
|
||||||
|
scope.event = event; |
||||||
|
scope.panelCtrl = eventManager.panelCtrl; |
||||||
|
scope.close = function() { |
||||||
|
drop.close(); |
||||||
|
}; |
||||||
|
|
||||||
|
$compile(content)(scope); |
||||||
|
scope.$digest(); |
||||||
|
|
||||||
|
drop = new Drop({ |
||||||
|
target: markerElementToAttachTo[0], |
||||||
|
content: content, |
||||||
|
position: "bottom center", |
||||||
|
classes: 'drop-popover drop-popover--form', |
||||||
|
openOn: 'click', |
||||||
|
tetherOptions: { |
||||||
|
constraints: [{to: 'window', pin: true, attachment: "both"}] |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
drop.open(); |
||||||
|
eventManager.editorOpened(); |
||||||
|
|
||||||
|
drop.on('close', function() { |
||||||
|
// need timeout here in order call drop.destroy
|
||||||
|
setTimeout(function() { |
||||||
|
eventManager.editorClosed(); |
||||||
|
scope.$destroy(); |
||||||
|
drop.destroy(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}]); |
||||||
|
|
||||||
|
}, 100); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* jquery.flot.events |
||||||
|
* |
||||||
|
* description: Flot plugin for adding events/markers to the plot |
||||||
|
* version: 0.2.5 |
||||||
|
* authors: |
||||||
|
* Alexander Wunschik <alex@wunschik.net> |
||||||
|
* Joel Oughton <joeloughton@gmail.com> |
||||||
|
* Nicolas Joseph <www.nicolasjoseph.com> |
||||||
|
* |
||||||
|
* website: https://github.com/mojoaxel/flot-events
|
||||||
|
* |
||||||
|
* released under MIT License and GPLv2+ |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* A class that allows for the drawing an remove of some object |
||||||
|
*/ |
||||||
|
var DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) { |
||||||
|
var _object = object; |
||||||
|
var _drawFunc = drawFunc; |
||||||
|
var _clearFunc = clearFunc; |
||||||
|
var _moveFunc = moveFunc; |
||||||
|
var _position = { left: left, top: top }; |
||||||
|
var _width = width; |
||||||
|
var _height = height; |
||||||
|
|
||||||
|
this.width = function() { return _width; }; |
||||||
|
this.height = function() { return _height; }; |
||||||
|
this.position = function() { return _position; }; |
||||||
|
this.draw = function() { _drawFunc(_object); }; |
||||||
|
this.clear = function() { _clearFunc(_object); }; |
||||||
|
this.getObject = function() { return _object; }; |
||||||
|
this.moveTo = function(position) { |
||||||
|
_position = position; |
||||||
|
_moveFunc(_object, _position); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Event class that stores options (eventType, min, max, title, description) and the object to draw. |
||||||
|
*/ |
||||||
|
var VisualEvent = function(options, drawableEvent) { |
||||||
|
var _parent; |
||||||
|
var _options = options; |
||||||
|
var _drawableEvent = drawableEvent; |
||||||
|
var _hidden = false; |
||||||
|
|
||||||
|
this.visual = function() { return _drawableEvent; }; |
||||||
|
this.getOptions = function() { return _options; }; |
||||||
|
this.getParent = function() { return _parent; }; |
||||||
|
this.isHidden = function() { return _hidden; }; |
||||||
|
this.hide = function() { _hidden = true; }; |
||||||
|
this.unhide = function() { _hidden = false; }; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Class that handles the event-markers inside the given plot |
||||||
|
*/ |
||||||
|
var EventMarkers = function(plot) { |
||||||
|
var _events = []; |
||||||
|
|
||||||
|
this._types = []; |
||||||
|
this._plot = plot; |
||||||
|
this.eventsEnabled = false; |
||||||
|
|
||||||
|
this.getEvents = function() { |
||||||
|
return _events; |
||||||
|
}; |
||||||
|
|
||||||
|
this.setTypes = function(types) { |
||||||
|
return this._types = types; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* create internal objects for the given events |
||||||
|
*/ |
||||||
|
this.setupEvents = function(events) { |
||||||
|
var that = this; |
||||||
|
var parts = _.partition(events, 'isRegion'); |
||||||
|
var regions = parts[0]; |
||||||
|
events = parts[1]; |
||||||
|
|
||||||
|
$.each(events, function(index, event) { |
||||||
|
var ve = new VisualEvent(event, that._buildDiv(event)); |
||||||
|
_events.push(ve); |
||||||
|
}); |
||||||
|
|
||||||
|
$.each(regions, function (index, event) { |
||||||
|
var vre = new VisualEvent(event, that._buildRegDiv(event)); |
||||||
|
_events.push(vre); |
||||||
|
}); |
||||||
|
|
||||||
|
_events.sort(function(a, b) { |
||||||
|
var ao = a.getOptions(), bo = b.getOptions(); |
||||||
|
if (ao.min > bo.min) { return 1; } |
||||||
|
if (ao.min < bo.min) { return -1; } |
||||||
|
return 0; |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the events to the plot |
||||||
|
*/ |
||||||
|
this.drawEvents = function() { |
||||||
|
var that = this; |
||||||
|
// var o = this._plot.getPlotOffset();
|
||||||
|
|
||||||
|
$.each(_events, function(index, event) { |
||||||
|
// check event is inside the graph range
|
||||||
|
if (that._insidePlot(event.getOptions().min) && !event.isHidden()) { |
||||||
|
event.visual().draw(); |
||||||
|
} else { |
||||||
|
event.visual().getObject().hide(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* update the position of the event-markers (e.g. after scrolling or zooming) |
||||||
|
*/ |
||||||
|
this.updateEvents = function() { |
||||||
|
var that = this; |
||||||
|
var o = this._plot.getPlotOffset(), left, top; |
||||||
|
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; |
||||||
|
|
||||||
|
$.each(_events, function(index, event) { |
||||||
|
top = o.top + that._plot.height() - event.visual().height(); |
||||||
|
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2; |
||||||
|
event.visual().moveTo({ top: top, left: left }); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* remove all events from the plot |
||||||
|
*/ |
||||||
|
this._clearEvents = function() { |
||||||
|
$.each(_events, function(index, val) { |
||||||
|
val.visual().clear(); |
||||||
|
}); |
||||||
|
_events = []; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* create a DOM element for the given event |
||||||
|
*/ |
||||||
|
this._buildDiv = function(event) { |
||||||
|
var that = this; |
||||||
|
|
||||||
|
var container = this._plot.getPlaceholder(); |
||||||
|
var o = this._plot.getPlotOffset(); |
||||||
|
var axes = this._plot.getAxes(); |
||||||
|
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; |
||||||
|
var yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth; |
||||||
|
var markerTooltip; |
||||||
|
|
||||||
|
// determine the y axis used
|
||||||
|
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; } |
||||||
|
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; } |
||||||
|
|
||||||
|
// map the eventType to a types object
|
||||||
|
var eventTypeId = event.eventType; |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { |
||||||
|
color = '#666'; |
||||||
|
} else { |
||||||
|
color = this._types[eventTypeId].color; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) { |
||||||
|
markerSize = 8; //default marker size
|
||||||
|
} else { |
||||||
|
markerSize = this._types[eventTypeId].markerSize; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) { |
||||||
|
markerShow = true; |
||||||
|
} else { |
||||||
|
markerShow = this._types[eventTypeId].markerShow; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) { |
||||||
|
markerTooltip = true; |
||||||
|
} else { |
||||||
|
markerTooltip = this._types[eventTypeId].markerTooltip; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) { |
||||||
|
lineStyle = 'dashed'; //default line style
|
||||||
|
} else { |
||||||
|
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase(); |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) { |
||||||
|
lineWidth = 1; //default line width
|
||||||
|
} else { |
||||||
|
lineWidth = this._types[eventTypeId].lineWidth; |
||||||
|
} |
||||||
|
|
||||||
|
var topOffset = xaxis.options.eventSectionHeight || 0; |
||||||
|
topOffset = topOffset / 3; |
||||||
|
|
||||||
|
top = o.top + this._plot.height() + topOffset; |
||||||
|
left = xaxis.p2c(event.min) + o.left; |
||||||
|
|
||||||
|
var line = $('<div class="events_line flot-temp-elem"></div>').css({ |
||||||
|
"position": "absolute", |
||||||
|
"opacity": 0.8, |
||||||
|
"left": left + 'px', |
||||||
|
"top": 8, |
||||||
|
"width": lineWidth + "px", |
||||||
|
"height": this._plot.height() + topOffset * 0.8, |
||||||
|
"border-left-width": lineWidth + "px", |
||||||
|
"border-left-style": lineStyle, |
||||||
|
"border-left-color": color, |
||||||
|
"color": color |
||||||
|
}) |
||||||
|
.appendTo(container); |
||||||
|
|
||||||
|
if (markerShow) { |
||||||
|
var marker = $('<div class="events_marker"></div>').css({ |
||||||
|
"position": "absolute", |
||||||
|
"left": (-markerSize - Math.round(lineWidth / 2)) + "px", |
||||||
|
"font-size": 0, |
||||||
|
"line-height": 0, |
||||||
|
"width": 0, |
||||||
|
"height": 0, |
||||||
|
"border-left": markerSize+"px solid transparent", |
||||||
|
"border-right": markerSize+"px solid transparent" |
||||||
|
}); |
||||||
|
|
||||||
|
marker.appendTo(line); |
||||||
|
|
||||||
|
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') { |
||||||
|
marker.css({ |
||||||
|
"top": top-markerSize-8 +"px", |
||||||
|
"border-top": "none", |
||||||
|
"border-bottom": markerSize+"px solid " + color |
||||||
|
}); |
||||||
|
} else { |
||||||
|
marker.css({ |
||||||
|
"top": "0px", |
||||||
|
"border-top": markerSize+"px solid " + color, |
||||||
|
"border-bottom": "none" |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
marker.data({ |
||||||
|
"event": event |
||||||
|
}); |
||||||
|
|
||||||
|
var mouseenter = function() { |
||||||
|
createAnnotationToolip(marker, $(this).data("event"), that._plot); |
||||||
|
}; |
||||||
|
|
||||||
|
if (event.editModel) { |
||||||
|
createEditPopover(marker, event.editModel, that._plot); |
||||||
|
} |
||||||
|
|
||||||
|
var mouseleave = function() { |
||||||
|
that._plot.clearSelection(); |
||||||
|
}; |
||||||
|
|
||||||
|
if (markerTooltip) { |
||||||
|
marker.css({ "cursor": "help" }); |
||||||
|
marker.hover(mouseenter, mouseleave); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var drawableEvent = new DrawableEvent( |
||||||
|
line, |
||||||
|
function drawFunc(obj) { obj.show(); }, |
||||||
|
function(obj) { obj.remove(); }, |
||||||
|
function(obj, position) { |
||||||
|
obj.css({ |
||||||
|
top: position.top, |
||||||
|
left: position.left |
||||||
|
}); |
||||||
|
}, |
||||||
|
left, |
||||||
|
top, |
||||||
|
line.width(), |
||||||
|
line.height() |
||||||
|
); |
||||||
|
|
||||||
|
return drawableEvent; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* create a DOM element for the given region |
||||||
|
*/ |
||||||
|
this._buildRegDiv = function (event) { |
||||||
|
var that = this; |
||||||
|
|
||||||
|
var container = this._plot.getPlaceholder(); |
||||||
|
var o = this._plot.getPlotOffset(); |
||||||
|
var axes = this._plot.getAxes(); |
||||||
|
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; |
||||||
|
var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip; |
||||||
|
|
||||||
|
// determine the y axis used
|
||||||
|
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; } |
||||||
|
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; } |
||||||
|
|
||||||
|
// map the eventType to a types object
|
||||||
|
var eventTypeId = event.eventType; |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { |
||||||
|
color = '#666'; |
||||||
|
} else { |
||||||
|
color = this._types[eventTypeId].color; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) { |
||||||
|
markerTooltip = true; |
||||||
|
} else { |
||||||
|
markerTooltip = this._types[eventTypeId].markerTooltip; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) { |
||||||
|
lineWidth = 1; //default line width
|
||||||
|
} else { |
||||||
|
lineWidth = this._types[eventTypeId].lineWidth; |
||||||
|
} |
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) { |
||||||
|
lineStyle = 'dashed'; //default line style
|
||||||
|
} else { |
||||||
|
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase(); |
||||||
|
} |
||||||
|
|
||||||
|
var topOffset = 2; |
||||||
|
top = o.top + this._plot.height() + topOffset; |
||||||
|
|
||||||
|
var timeFrom = Math.min(event.min, event.timeEnd); |
||||||
|
var timeTo = Math.max(event.min, event.timeEnd); |
||||||
|
left = xaxis.p2c(timeFrom) + o.left; |
||||||
|
var right = xaxis.p2c(timeTo) + o.left; |
||||||
|
regionWidth = right - left; |
||||||
|
|
||||||
|
_.each([left, right], function(position) { |
||||||
|
var line = $('<div class="events_line flot-temp-elem"></div>').css({ |
||||||
|
"position": "absolute", |
||||||
|
"opacity": 0.8, |
||||||
|
"left": position + 'px', |
||||||
|
"top": 8, |
||||||
|
"width": lineWidth + "px", |
||||||
|
"height": that._plot.height() + topOffset, |
||||||
|
"border-left-width": lineWidth + "px", |
||||||
|
"border-left-style": lineStyle, |
||||||
|
"border-left-color": color, |
||||||
|
"color": color |
||||||
|
}); |
||||||
|
line.appendTo(container); |
||||||
|
}); |
||||||
|
|
||||||
|
var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({ |
||||||
|
"position": "absolute", |
||||||
|
"opacity": 0.5, |
||||||
|
"left": left + 'px', |
||||||
|
"top": top, |
||||||
|
"width": Math.round(regionWidth + lineWidth) + "px", |
||||||
|
"height": "0.5rem", |
||||||
|
"border-left-color": color, |
||||||
|
"color": color, |
||||||
|
"background-color": color |
||||||
|
}); |
||||||
|
region.appendTo(container); |
||||||
|
|
||||||
|
region.data({ |
||||||
|
"event": event |
||||||
|
}); |
||||||
|
|
||||||
|
var mouseenter = function () { |
||||||
|
createAnnotationToolip(region, $(this).data("event"), that._plot); |
||||||
|
}; |
||||||
|
|
||||||
|
if (event.editModel) { |
||||||
|
createEditPopover(region, event.editModel, that._plot); |
||||||
|
} |
||||||
|
|
||||||
|
var mouseleave = function () { |
||||||
|
that._plot.clearSelection(); |
||||||
|
}; |
||||||
|
|
||||||
|
if (markerTooltip) { |
||||||
|
region.css({ "cursor": "help" }); |
||||||
|
region.hover(mouseenter, mouseleave); |
||||||
|
} |
||||||
|
|
||||||
|
var drawableEvent = new DrawableEvent( |
||||||
|
region, |
||||||
|
function drawFunc(obj) { obj.show(); }, |
||||||
|
function (obj) { obj.remove(); }, |
||||||
|
function (obj, position) { |
||||||
|
obj.css({ |
||||||
|
top: position.top, |
||||||
|
left: position.left |
||||||
|
}); |
||||||
|
}, |
||||||
|
left, |
||||||
|
top, |
||||||
|
region.width(), |
||||||
|
region.height() |
||||||
|
); |
||||||
|
|
||||||
|
return drawableEvent; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* check if the event is inside visible range |
||||||
|
*/ |
||||||
|
this._insidePlot = function(x) { |
||||||
|
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; |
||||||
|
var xc = xaxis.p2c(x); |
||||||
|
return xc > 0 && xc < xaxis.p2c(xaxis.max); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* initialize the plugin for the given plot |
||||||
|
*/ |
||||||
|
function init(plot) { |
||||||
|
/*jshint validthis:true */ |
||||||
|
var that = this; |
||||||
|
var eventMarkers = new EventMarkers(plot); |
||||||
|
|
||||||
|
plot.getEvents = function() { |
||||||
|
return eventMarkers._events; |
||||||
|
}; |
||||||
|
|
||||||
|
plot.hideEvents = function() { |
||||||
|
$.each(eventMarkers._events, function(index, event) { |
||||||
|
event.visual().getObject().hide(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
plot.showEvents = function() { |
||||||
|
plot.hideEvents(); |
||||||
|
$.each(eventMarkers._events, function(index, event) { |
||||||
|
event.hide(); |
||||||
|
}); |
||||||
|
|
||||||
|
that.eventMarkers.drawEvents(); |
||||||
|
}; |
||||||
|
|
||||||
|
// change events on an existing plot
|
||||||
|
plot.setEvents = function(events) { |
||||||
|
if (eventMarkers.eventsEnabled) { |
||||||
|
eventMarkers.setupEvents(events); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
plot.hooks.processOptions.push(function(plot, options) { |
||||||
|
// enable the plugin
|
||||||
|
if (options.events.data != null) { |
||||||
|
eventMarkers.eventsEnabled = true; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.draw.push(function(plot) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
|
||||||
|
if (eventMarkers.eventsEnabled) { |
||||||
|
// check for first run
|
||||||
|
if (eventMarkers.getEvents().length < 1) { |
||||||
|
eventMarkers.setTypes(options.events.types); |
||||||
|
eventMarkers.setupEvents(options.events.data); |
||||||
|
} else { |
||||||
|
eventMarkers.updateEvents(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
eventMarkers.drawEvents(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
var defaultOptions = { |
||||||
|
events: { |
||||||
|
data: null, |
||||||
|
types: null, |
||||||
|
xaxis: 1, |
||||||
|
position: 'BOTTOM' |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: defaultOptions, |
||||||
|
name: "events", |
||||||
|
version: "0.2.5" |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,288 @@ |
|||||||
|
(function($) { |
||||||
|
"use strict"; |
||||||
|
|
||||||
|
var options = { |
||||||
|
series: { |
||||||
|
fillBelowTo: null |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
function findBelowSeries( series, allseries ) { |
||||||
|
|
||||||
|
var i; |
||||||
|
|
||||||
|
for ( i = 0; i < allseries.length; ++i ) { |
||||||
|
if ( allseries[ i ].id === series.fillBelowTo ) { |
||||||
|
return allseries[ i ]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/* top and bottom doesn't actually matter for this, we're just using it to help make this easier to think about */ |
||||||
|
/* this is a vector cross product operation */ |
||||||
|
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) { |
||||||
|
var top_delta_x, top_delta_y, bottom_delta_x, bottom_delta_y, |
||||||
|
s, t; |
||||||
|
|
||||||
|
top_delta_x = top_right_x - top_left_x; |
||||||
|
top_delta_y = top_right_y - top_left_y; |
||||||
|
bottom_delta_x = bottom_right_x - bottom_left_x; |
||||||
|
bottom_delta_y = bottom_right_y - bottom_left_y; |
||||||
|
|
||||||
|
s = ( |
||||||
|
(-top_delta_y * (top_left_x - bottom_left_x)) + (top_delta_x * (top_left_y - bottom_left_y)) |
||||||
|
) / ( |
||||||
|
-bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y |
||||||
|
); |
||||||
|
|
||||||
|
t = ( |
||||||
|
(bottom_delta_x * (top_left_y - bottom_left_y)) - (bottom_delta_y * (top_left_x - bottom_left_x)) |
||||||
|
) / ( |
||||||
|
-bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y |
||||||
|
); |
||||||
|
|
||||||
|
// Collision detected
|
||||||
|
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { |
||||||
|
return [ |
||||||
|
top_left_x + (t * top_delta_x), // X
|
||||||
|
top_left_y + (t * top_delta_y) // Y
|
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
// No collision
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function plotDifferenceArea(plot, ctx, series) { |
||||||
|
if ( series.fillBelowTo === null ) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var otherseries, |
||||||
|
|
||||||
|
ps, |
||||||
|
points, |
||||||
|
|
||||||
|
otherps, |
||||||
|
otherpoints, |
||||||
|
|
||||||
|
plotOffset, |
||||||
|
fillStyle; |
||||||
|
|
||||||
|
function openPolygon(x, y) { |
||||||
|
ctx.beginPath(); |
||||||
|
ctx.moveTo( |
||||||
|
series.xaxis.p2c(x) + plotOffset.left, |
||||||
|
series.yaxis.p2c(y) + plotOffset.top |
||||||
|
); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
function closePolygon() { |
||||||
|
ctx.closePath(); |
||||||
|
ctx.fill(); |
||||||
|
} |
||||||
|
|
||||||
|
function validateInput() { |
||||||
|
if (points.length/ps !== otherpoints.length/otherps) { |
||||||
|
console.error("Refusing to graph inconsistent number of points"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
var i; |
||||||
|
for (i = 0; i < (points.length / ps); i++) { |
||||||
|
if ( |
||||||
|
points[i * ps] !== null && |
||||||
|
otherpoints[i * otherps] !== null && |
||||||
|
points[i * ps] !== otherpoints[i * otherps] |
||||||
|
) { |
||||||
|
console.error("Refusing to graph points without matching value"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
function findNextStart(start_i, end_i) { |
||||||
|
console.assert(end_i > start_i, "expects the end index to be greater than the start index"); |
||||||
|
|
||||||
|
var start = ( |
||||||
|
start_i === 0 || |
||||||
|
points[start_i - 1] === null || |
||||||
|
otherpoints[start_i - 1] === null |
||||||
|
), |
||||||
|
equal = false, |
||||||
|
i, |
||||||
|
intersect; |
||||||
|
|
||||||
|
for (i = start_i; i < end_i; i++) { |
||||||
|
// Take note of null points
|
||||||
|
if ( |
||||||
|
points[(i * ps) + 1] === null || |
||||||
|
otherpoints[(i * ps) + 1] === null |
||||||
|
) { |
||||||
|
equal = false; |
||||||
|
start = true; |
||||||
|
} |
||||||
|
|
||||||
|
// Take note of equal points
|
||||||
|
else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { |
||||||
|
equal = true; |
||||||
|
start = false; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
else if (points[(i * ps) + 1] > otherpoints[(i * otherps) + 1]) { |
||||||
|
// If we begin above the desired point
|
||||||
|
if (start) { |
||||||
|
openPolygon(points[i * ps], points[(i * ps) + 1]); |
||||||
|
} |
||||||
|
|
||||||
|
// If an equal point preceeds this, start the polygon at that equal point
|
||||||
|
else if (equal) { |
||||||
|
openPolygon(points[(i - 1) * ps], points[((i - 1) * ps) + 1]); |
||||||
|
} |
||||||
|
|
||||||
|
// Otherwise, find the intersection point, and start it there
|
||||||
|
else { |
||||||
|
intersect = intersectionPoint(i); |
||||||
|
openPolygon(intersect[0], intersect[1]); |
||||||
|
} |
||||||
|
|
||||||
|
topTraversal(i, end_i); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// If we go below equal, equal at any preceeding point is irrelevant
|
||||||
|
else { |
||||||
|
start = false; |
||||||
|
equal = false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function intersectionPoint(right_i) { |
||||||
|
console.assert(right_i > 0, "expects the second point in the series line segment"); |
||||||
|
|
||||||
|
var i, intersect; |
||||||
|
|
||||||
|
for (i = 1; i < (otherpoints.length/otherps); i++) { |
||||||
|
intersect = segmentIntersection( |
||||||
|
points[(right_i - 1) * ps], points[((right_i - 1) * ps) + 1], |
||||||
|
points[right_i * ps], points[(right_i * ps) + 1], |
||||||
|
|
||||||
|
otherpoints[(i - 1) * otherps], otherpoints[((i - 1) * otherps) + 1], |
||||||
|
otherpoints[i * otherps], otherpoints[(i * otherps) + 1] |
||||||
|
); |
||||||
|
|
||||||
|
if (intersect !== null) { |
||||||
|
return intersect; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.error("intersectionPoint() should only be called when an intersection happens"); |
||||||
|
} |
||||||
|
|
||||||
|
function bottomTraversal(start_i, end_i) { |
||||||
|
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)"); |
||||||
|
|
||||||
|
var i; |
||||||
|
|
||||||
|
for (i = start_i; i >= end_i; i--) { |
||||||
|
ctx.lineTo( |
||||||
|
otherseries.xaxis.p2c(otherpoints[i * otherps]) + plotOffset.left, |
||||||
|
otherseries.yaxis.p2c(otherpoints[(i * otherps) + 1]) + plotOffset.top |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
closePolygon(); |
||||||
|
} |
||||||
|
|
||||||
|
function topTraversal(start_i, end_i) { |
||||||
|
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)"); |
||||||
|
|
||||||
|
var i, |
||||||
|
intersect; |
||||||
|
|
||||||
|
for (i = start_i; i < end_i; i++) { |
||||||
|
if (points[(i * ps) + 1] === null && i > start_i) { |
||||||
|
bottomTraversal(i - 1, start_i); |
||||||
|
findNextStart(i, end_i); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { |
||||||
|
bottomTraversal(i, start_i); |
||||||
|
findNextStart(i, end_i); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
else if (points[(i * ps) + 1] < otherpoints[(i * otherps) + 1]) { |
||||||
|
intersect = intersectionPoint(i); |
||||||
|
ctx.lineTo( |
||||||
|
series.xaxis.p2c(intersect[0]) + plotOffset.left, |
||||||
|
series.yaxis.p2c(intersect[1]) + plotOffset.top |
||||||
|
); |
||||||
|
bottomTraversal(i, start_i); |
||||||
|
findNextStart(i, end_i); |
||||||
|
return; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
else { |
||||||
|
ctx.lineTo( |
||||||
|
series.xaxis.p2c(points[i * ps]) + plotOffset.left, |
||||||
|
series.yaxis.p2c(points[(i * ps) + 1]) + plotOffset.top |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bottomTraversal(end_i, start_i); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Begin processing
|
||||||
|
|
||||||
|
otherseries = findBelowSeries( series, plot.getData() ); |
||||||
|
|
||||||
|
if ( !otherseries ) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
ps = series.datapoints.pointsize; |
||||||
|
points = series.datapoints.points; |
||||||
|
otherps = otherseries.datapoints.pointsize; |
||||||
|
otherpoints = otherseries.datapoints.points; |
||||||
|
plotOffset = plot.getPlotOffset(); |
||||||
|
|
||||||
|
if (!validateInput()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Flot's getFillStyle() should probably be exposed somewhere
|
||||||
|
fillStyle = $.color.parse(series.color); |
||||||
|
fillStyle.a = 0.4; |
||||||
|
fillStyle.normalize(); |
||||||
|
ctx.fillStyle = fillStyle.toString(); |
||||||
|
|
||||||
|
|
||||||
|
// Begin recursive bi-directional traversal
|
||||||
|
findNextStart(0, points.length/ps); |
||||||
|
} |
||||||
|
|
||||||
|
plot.hooks.drawSeries.push(plotDifferenceArea); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: "fillbelow", |
||||||
|
version: "0.1.0" |
||||||
|
}); |
||||||
|
|
||||||
|
})(jQuery); |
@ -0,0 +1,225 @@ |
|||||||
|
/* Flot plugin for computing bottoms for filled line and bar charts. |
||||||
|
|
||||||
|
Copyright (c) 2007-2013 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
The case: you've got two series that you want to fill the area between. In Flot |
||||||
|
terms, you need to use one as the fill bottom of the other. You can specify the |
||||||
|
bottom of each data point as the third coordinate manually, or you can use this |
||||||
|
plugin to compute it for you. |
||||||
|
|
||||||
|
In order to name the other series, you need to give it an id, like this: |
||||||
|
|
||||||
|
var dataset = [ |
||||||
|
{ data: [ ... ], id: "foo" } , // use default bottom
|
||||||
|
{ data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
|
||||||
|
]; |
||||||
|
|
||||||
|
$.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); |
||||||
|
|
||||||
|
As a convenience, if the id given is a number that doesn't appear as an id in |
||||||
|
the series, it is interpreted as the index in the array instead (so fillBetween: |
||||||
|
0 can also mean the first series). |
||||||
|
|
||||||
|
Internally, the plugin modifies the datapoints in each series. For line series, |
||||||
|
extra data points might be inserted through interpolation. Note that at points |
||||||
|
where the bottom line is not defined (due to a null point or start/end of line), |
||||||
|
the current line will show a gap too. The algorithm comes from the |
||||||
|
jquery.flot.stack.js plugin, possibly some code could be shared. |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
(function ( $ ) { |
||||||
|
|
||||||
|
var options = { |
||||||
|
series: { |
||||||
|
fillBetween: null // or number
|
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
function init( plot ) { |
||||||
|
|
||||||
|
function findBottomSeries( s, allseries ) { |
||||||
|
|
||||||
|
var i; |
||||||
|
|
||||||
|
for ( i = 0; i < allseries.length; ++i ) { |
||||||
|
if ( allseries[ i ].id === s.fillBetween ) { |
||||||
|
return allseries[ i ]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ( typeof s.fillBetween === "number" ) { |
||||||
|
if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return allseries[ s.fillBetween ]; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function computeFillBottoms( plot, s, datapoints ) { |
||||||
|
if ( s.fillBetween == null ) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var other = findBottomSeries( s, plot.getData() ); |
||||||
|
|
||||||
|
if ( !other ) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var ps = datapoints.pointsize, |
||||||
|
points = datapoints.points, |
||||||
|
otherps = other.datapoints.pointsize, |
||||||
|
otherpoints = other.datapoints.points, |
||||||
|
newpoints = [], |
||||||
|
px, py, intery, qx, qy, bottom, |
||||||
|
withlines = s.lines.show, |
||||||
|
withbottom = ps > 2 && datapoints.format[2].y, |
||||||
|
withsteps = withlines && s.lines.steps, |
||||||
|
fromgap = true, |
||||||
|
i = 0, |
||||||
|
j = 0, |
||||||
|
l, m; |
||||||
|
|
||||||
|
while ( true ) { |
||||||
|
|
||||||
|
if ( i >= points.length ) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
l = newpoints.length; |
||||||
|
|
||||||
|
if ( points[ i ] == null ) { |
||||||
|
|
||||||
|
// copy gaps
|
||||||
|
|
||||||
|
for ( m = 0; m < ps; ++m ) { |
||||||
|
newpoints.push( points[ i + m ] ); |
||||||
|
} |
||||||
|
|
||||||
|
i += ps; |
||||||
|
|
||||||
|
} else if ( j >= otherpoints.length ) { |
||||||
|
|
||||||
|
// for lines, we can't use the rest of the points
|
||||||
|
|
||||||
|
if ( !withlines ) { |
||||||
|
for ( m = 0; m < ps; ++m ) { |
||||||
|
newpoints.push( points[ i + m ] ); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
i += ps; |
||||||
|
|
||||||
|
} else if ( otherpoints[ j ] == null ) { |
||||||
|
|
||||||
|
// oops, got a gap
|
||||||
|
|
||||||
|
for ( m = 0; m < ps; ++m ) { |
||||||
|
newpoints.push( null ); |
||||||
|
} |
||||||
|
|
||||||
|
fromgap = true; |
||||||
|
j += otherps; |
||||||
|
|
||||||
|
} else { |
||||||
|
|
||||||
|
// cases where we actually got two points
|
||||||
|
|
||||||
|
px = points[ i ]; |
||||||
|
py = points[ i + 1 ]; |
||||||
|
qx = otherpoints[ j ]; |
||||||
|
qy = otherpoints[ j + 1 ]; |
||||||
|
bottom = 0; |
||||||
|
|
||||||
|
if ( px === qx ) { |
||||||
|
|
||||||
|
for ( m = 0; m < ps; ++m ) { |
||||||
|
newpoints.push( points[ i + m ] ); |
||||||
|
} |
||||||
|
|
||||||
|
//newpoints[ l + 1 ] += qy;
|
||||||
|
bottom = qy; |
||||||
|
|
||||||
|
i += ps; |
||||||
|
j += otherps; |
||||||
|
|
||||||
|
} else if ( px > qx ) { |
||||||
|
|
||||||
|
// we got past point below, might need to
|
||||||
|
// insert interpolated extra point
|
||||||
|
|
||||||
|
if ( withlines && i > 0 && points[ i - ps ] != null ) { |
||||||
|
intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); |
||||||
|
newpoints.push( qx ); |
||||||
|
newpoints.push( intery ); |
||||||
|
for ( m = 2; m < ps; ++m ) { |
||||||
|
newpoints.push( points[ i + m ] ); |
||||||
|
} |
||||||
|
bottom = qy; |
||||||
|
} |
||||||
|
|
||||||
|
j += otherps; |
||||||
|
|
||||||
|
} else { // px < qx
|
||||||
|
|
||||||
|
// if we come from a gap, we just skip this point
|
||||||
|
|
||||||
|
if ( fromgap && withlines ) { |
||||||
|
i += ps; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
for ( m = 0; m < ps; ++m ) { |
||||||
|
newpoints.push( points[ i + m ] ); |
||||||
|
} |
||||||
|
|
||||||
|
// we might be able to interpolate a point below,
|
||||||
|
// this can give us a better y
|
||||||
|
|
||||||
|
if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { |
||||||
|
bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); |
||||||
|
} |
||||||
|
|
||||||
|
//newpoints[l + 1] += bottom;
|
||||||
|
|
||||||
|
i += ps; |
||||||
|
} |
||||||
|
|
||||||
|
fromgap = false; |
||||||
|
|
||||||
|
if ( l !== newpoints.length && withbottom ) { |
||||||
|
newpoints[ l + 2 ] = bottom; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// maintain the line steps invariant
|
||||||
|
|
||||||
|
if ( withsteps && l !== newpoints.length && l > 0 && |
||||||
|
newpoints[ l ] !== null && |
||||||
|
newpoints[ l ] !== newpoints[ l - ps ] && |
||||||
|
newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { |
||||||
|
for (m = 0; m < ps; ++m) { |
||||||
|
newpoints[ l + ps + m ] = newpoints[ l + m ]; |
||||||
|
} |
||||||
|
newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
datapoints.points = newpoints; |
||||||
|
} |
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push( computeFillBottoms ); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: "fillbetween", |
||||||
|
version: "1.0" |
||||||
|
}); |
||||||
|
|
||||||
|
})(jQuery); |
@ -0,0 +1,960 @@ |
|||||||
|
/*! |
||||||
|
* jquery.flot.gauge v1.1.0 * |
||||||
|
* |
||||||
|
* Flot plugin for rendering gauge charts. |
||||||
|
* |
||||||
|
* Copyright (c) 2015 @toyoty99. |
||||||
|
* Licensed under the MIT license. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @module flot.gauge |
||||||
|
*/ |
||||||
|
(function($) { |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Gauge class |
||||||
|
* |
||||||
|
* @class Gauge |
||||||
|
*/ |
||||||
|
var Gauge = (function() { |
||||||
|
/** |
||||||
|
* context of canvas |
||||||
|
* |
||||||
|
* @property context |
||||||
|
* @type Object |
||||||
|
*/ |
||||||
|
var context; |
||||||
|
/** |
||||||
|
* placeholder of canvas |
||||||
|
* |
||||||
|
* @property placeholder |
||||||
|
* @type Object |
||||||
|
*/ |
||||||
|
var placeholder; |
||||||
|
/** |
||||||
|
* options of plot |
||||||
|
* |
||||||
|
* @property options |
||||||
|
* @type Object |
||||||
|
*/ |
||||||
|
var options; |
||||||
|
/** |
||||||
|
* options of gauge |
||||||
|
* |
||||||
|
* @property gaugeOptions |
||||||
|
* @type Object |
||||||
|
*/ |
||||||
|
var gaugeOptions; |
||||||
|
/** |
||||||
|
* data series |
||||||
|
* |
||||||
|
* @property series |
||||||
|
* @type Array |
||||||
|
*/ |
||||||
|
var series; |
||||||
|
/** |
||||||
|
* logger |
||||||
|
* |
||||||
|
* @property logger |
||||||
|
* @type Object |
||||||
|
*/ |
||||||
|
var logger; |
||||||
|
|
||||||
|
/** |
||||||
|
* constructor |
||||||
|
* |
||||||
|
* @class Gauge |
||||||
|
* @constructor |
||||||
|
* @param {Object} gaugeOptions gauge options |
||||||
|
*/ |
||||||
|
var Gauge = function(plot, ctx) { |
||||||
|
context = ctx; |
||||||
|
placeholder = plot.getPlaceholder(); |
||||||
|
options = plot.getOptions(); |
||||||
|
gaugeOptions = options.series.gauges; |
||||||
|
series = plot.getData(); |
||||||
|
logger = getLogger(gaugeOptions.debug); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate layout |
||||||
|
* |
||||||
|
* @method calculateLayout |
||||||
|
* @return the calculated layout properties |
||||||
|
*/ |
||||||
|
Gauge.prototype.calculateLayout = function() { |
||||||
|
|
||||||
|
var canvasWidth = placeholder.width(); |
||||||
|
var canvasHeight = placeholder.height(); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// calculate cell size
|
||||||
|
var columns = Math.min(series.length, gaugeOptions.layout.columns); |
||||||
|
var rows = Math.ceil(series.length / columns); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var margin = gaugeOptions.layout.margin; |
||||||
|
var hMargin = gaugeOptions.layout.hMargin; |
||||||
|
var vMargin = gaugeOptions.layout.vMargin; |
||||||
|
var cellWidth = (canvasWidth - (margin * 2) - (hMargin * (columns - 1))) / columns; |
||||||
|
var cellHeight = (canvasHeight - (margin * 2) - (vMargin * (rows - 1))) / rows; |
||||||
|
if (gaugeOptions.layout.square) { |
||||||
|
var cell = Math.min(cellWidth, cellHeight); |
||||||
|
cellWidth = cell; |
||||||
|
cellHeight = cell; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// calculate 'auto' values
|
||||||
|
calculateAutoValues(gaugeOptions, cellWidth); |
||||||
|
|
||||||
|
// calculate maximum radius
|
||||||
|
var cellMargin = gaugeOptions.cell.margin; |
||||||
|
var labelMargin = 0; |
||||||
|
var labelFontSize = 0; |
||||||
|
if (gaugeOptions.label.show) { |
||||||
|
labelMargin = gaugeOptions.label.margin; |
||||||
|
labelFontSize = gaugeOptions.label.font.size; |
||||||
|
} |
||||||
|
var valueMargin = 0; |
||||||
|
var valueFontSize = 0; |
||||||
|
if (gaugeOptions.value.show) { |
||||||
|
valueMargin = gaugeOptions.value.margin; |
||||||
|
valueFontSize = gaugeOptions.value.font.size; |
||||||
|
} |
||||||
|
var thresholdWidth = 0; |
||||||
|
if (gaugeOptions.threshold.show) { |
||||||
|
thresholdWidth = gaugeOptions.threshold.width; |
||||||
|
} |
||||||
|
var thresholdLabelMargin = 0; |
||||||
|
var thresholdLabelFontSize = 0; |
||||||
|
if (gaugeOptions.threshold.label.show) { |
||||||
|
thresholdLabelMargin = gaugeOptions.threshold.label.margin; |
||||||
|
thresholdLabelFontSize = gaugeOptions.threshold.label.font.size; |
||||||
|
} |
||||||
|
|
||||||
|
var maxRadiusH = (cellWidth / 2) - cellMargin - thresholdWidth - (thresholdLabelMargin * 2) - thresholdLabelFontSize; |
||||||
|
|
||||||
|
var startAngle = gaugeOptions.gauge.startAngle; |
||||||
|
var endAngle = gaugeOptions.gauge.endAngle; |
||||||
|
var dAngle = (endAngle - startAngle) / 100; |
||||||
|
var heightRatioV = -1; |
||||||
|
for (var a = startAngle; a < endAngle; a += dAngle) { |
||||||
|
heightRatioV = Math.max(heightRatioV, Math.sin(toRad(a))); |
||||||
|
} |
||||||
|
heightRatioV = Math.max(heightRatioV, Math.sin(toRad(endAngle))); |
||||||
|
var outerRadiusV = (cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize) / (1 + heightRatioV); |
||||||
|
if (outerRadiusV * heightRatioV < valueMargin + (valueFontSize / 2)) { |
||||||
|
outerRadiusV = cellHeight - (cellMargin * 2) - (labelMargin * 2) - labelFontSize - valueMargin - (valueFontSize / 2); |
||||||
|
} |
||||||
|
var maxRadiusV = outerRadiusV - (thresholdLabelMargin * 2) - thresholdLabelFontSize - thresholdWidth; |
||||||
|
|
||||||
|
var radius = Math.min(maxRadiusH, maxRadiusV); |
||||||
|
|
||||||
|
|
||||||
|
var width = gaugeOptions.gauge.width; |
||||||
|
if (width >= radius) { |
||||||
|
width = Math.max(3, radius / 3); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
var outerRadius = (thresholdLabelMargin * 2) + thresholdLabelFontSize + thresholdWidth + radius; |
||||||
|
var gaugeOuterHeight = Math.max(outerRadius * (1 + heightRatioV), outerRadius + valueMargin + (valueFontSize / 2)); |
||||||
|
|
||||||
|
return { |
||||||
|
canvasWidth: canvasWidth, |
||||||
|
canvasHeight: canvasHeight, |
||||||
|
margin: margin, |
||||||
|
hMargin: hMargin, |
||||||
|
vMargin: vMargin, |
||||||
|
columns: columns, |
||||||
|
rows: rows, |
||||||
|
cellWidth: cellWidth, |
||||||
|
cellHeight: cellHeight, |
||||||
|
cellMargin: cellMargin, |
||||||
|
labelMargin: labelMargin, |
||||||
|
labelFontSize: labelFontSize, |
||||||
|
valueMargin: valueMargin, |
||||||
|
valueFontSize: valueFontSize, |
||||||
|
width: width, |
||||||
|
radius: radius, |
||||||
|
thresholdWidth: thresholdWidth, |
||||||
|
thresholdLabelMargin: thresholdLabelMargin, |
||||||
|
thresholdLabelFontSize: thresholdLabelFontSize, |
||||||
|
gaugeOuterHeight: gaugeOuterHeight |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the values which are set as 'auto' |
||||||
|
* |
||||||
|
* @method calculateAutoValues |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Number} cellWidth the width of cell |
||||||
|
*/ |
||||||
|
function calculateAutoValues(gaugeOptionsi, cellWidth) { |
||||||
|
|
||||||
|
if (gaugeOptionsi.gauge.width === "auto") { |
||||||
|
gaugeOptionsi.gauge.width = Math.max(5, cellWidth / 8); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.label.margin === "auto") { |
||||||
|
gaugeOptionsi.label.margin = Math.max(1, cellWidth / 20); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.label.font.size === "auto") { |
||||||
|
gaugeOptionsi.label.font.size = Math.max(5, cellWidth / 8); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.value.margin === "auto") { |
||||||
|
gaugeOptionsi.value.margin = Math.max(1, cellWidth / 30); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.value.font.size === "auto") { |
||||||
|
gaugeOptionsi.value.font.size = Math.max(5, cellWidth / 9); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.threshold.width === "auto") { |
||||||
|
gaugeOptionsi.threshold.width = Math.max(3, cellWidth / 100); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.threshold.label.margin === "auto") { |
||||||
|
gaugeOptionsi.threshold.label.margin = Math.max(3, cellWidth / 40); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.threshold.label.font.size === "auto") { |
||||||
|
gaugeOptionsi.threshold.label.font.size = Math.max(5, cellWidth / 15); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
Gauge.prototype.calculateAutoValues = calculateAutoValues; |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the layout of the cell inside |
||||||
|
* |
||||||
|
* @method calculateCellLayout |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Number} cellWidth the width of cell |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @return the calculated cell layout properties |
||||||
|
*/ |
||||||
|
Gauge.prototype.calculateCellLayout = function(gaugeOptionsi, layout, i) { |
||||||
|
|
||||||
|
// calculate top, left and center
|
||||||
|
var c = col(layout.columns, i); |
||||||
|
var r = row(layout.columns, i); |
||||||
|
var x = layout.margin + (layout.cellWidth + layout.hMargin) * c; |
||||||
|
var y = layout.margin + (layout.cellHeight + layout.vMargin) * r; |
||||||
|
var cx = x + (layout.cellWidth / 2); |
||||||
|
var cy = y + layout.cellMargin + (layout.labelMargin * 2) + layout.labelFontSize + layout.thresholdWidth |
||||||
|
+ layout.thresholdLabelFontSize + (layout.thresholdLabelMargin * 2) + layout.radius; |
||||||
|
var blank = layout.cellHeight - (layout.cellMargin * 2) - (layout.labelMargin * 2) - layout.labelFontSize - layout.gaugeOuterHeight; |
||||||
|
var offsetY = 0; |
||||||
|
if (gaugeOptionsi.cell.vAlign === "middle") { |
||||||
|
offsetY = (blank / 2); |
||||||
|
} else if (gaugeOptionsi.cell.vAlign === "bottom") { |
||||||
|
offsetY = blank; |
||||||
|
} |
||||||
|
cy += offsetY; |
||||||
|
|
||||||
|
return { |
||||||
|
col: c, |
||||||
|
row: r, |
||||||
|
x: x, |
||||||
|
y: y, |
||||||
|
offsetY: offsetY, |
||||||
|
cellWidth: layout.cellWidth, |
||||||
|
cellHeight: layout.cellHeight, |
||||||
|
cellMargin: layout.cellMargin, |
||||||
|
cx: cx, |
||||||
|
cy: cy |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the background of chart |
||||||
|
* |
||||||
|
* @method drawBackground |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawBackground = function(layout) { |
||||||
|
|
||||||
|
if (!gaugeOptions.frame.show) { |
||||||
|
return; |
||||||
|
} |
||||||
|
context.save(); |
||||||
|
context.strokeStyle = options.grid.borderColor; |
||||||
|
context.lineWidth = options.grid.borderWidth; |
||||||
|
context.strokeRect(0, 0, layout.canvasWidth, layout.canvasHeight); |
||||||
|
if (options.grid.backgroundColor) { |
||||||
|
context.fillStyle = options.grid.backgroundColor; |
||||||
|
context.fillRect(0, 0, layout.canvasWidth, layout.canvasHeight); |
||||||
|
} |
||||||
|
context.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the background of cell |
||||||
|
* |
||||||
|
* @method drawCellBackground |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawCellBackground = function(gaugeOptionsi, cellLayout) { |
||||||
|
|
||||||
|
context.save(); |
||||||
|
if (gaugeOptionsi.cell.border && gaugeOptionsi.cell.border.show && gaugeOptionsi.cell.border.color && gaugeOptionsi.cell.border.width) { |
||||||
|
context.strokeStyle = gaugeOptionsi.cell.border.color; |
||||||
|
context.lineWidth = gaugeOptionsi.cell.border.width; |
||||||
|
context.strokeRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.cell.background && gaugeOptionsi.cell.background.color) { |
||||||
|
context.fillStyle = gaugeOptionsi.cell.background.color; |
||||||
|
context.fillRect(cellLayout.x, cellLayout.y, cellLayout.cellWidth, cellLayout.cellHeight); |
||||||
|
} |
||||||
|
context.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the gauge |
||||||
|
* |
||||||
|
* @method drawGauge |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
* @param {String} label the label of data |
||||||
|
* @param {Number} data the value of the gauge |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawGauge = function(gaugeOptionsi, layout, cellLayout, label, data) { |
||||||
|
|
||||||
|
|
||||||
|
var blur = gaugeOptionsi.gauge.shadow.show ? gaugeOptionsi.gauge.shadow.blur : 0; |
||||||
|
|
||||||
|
|
||||||
|
// draw gauge frame
|
||||||
|
drawArcWithShadow( |
||||||
|
cellLayout.cx, // center x
|
||||||
|
cellLayout.cy, // center y
|
||||||
|
layout.radius, |
||||||
|
layout.width, |
||||||
|
toRad(gaugeOptionsi.gauge.startAngle), |
||||||
|
toRad(gaugeOptionsi.gauge.endAngle), |
||||||
|
gaugeOptionsi.gauge.border.color, // line color
|
||||||
|
gaugeOptionsi.gauge.border.width, // line width
|
||||||
|
gaugeOptionsi.gauge.background.color, // fill color
|
||||||
|
blur); |
||||||
|
|
||||||
|
// draw gauge
|
||||||
|
var c1 = getColor(gaugeOptionsi, data); |
||||||
|
var a2 = calculateAngle(gaugeOptionsi, layout, data); |
||||||
|
drawArcWithShadow( |
||||||
|
cellLayout.cx, // center x
|
||||||
|
cellLayout.cy, // center y
|
||||||
|
layout.radius - 1, |
||||||
|
layout.width - 2, |
||||||
|
toRad(gaugeOptionsi.gauge.startAngle), |
||||||
|
toRad(a2), |
||||||
|
c1, // line color
|
||||||
|
1, // line width
|
||||||
|
c1, // fill color
|
||||||
|
blur); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* decide the color of the data from the threshold options |
||||||
|
* |
||||||
|
* @method getColor |
||||||
|
* @private |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Number} data the value of the gauge |
||||||
|
*/ |
||||||
|
function getColor(gaugeOptionsi, data) { |
||||||
|
var color; |
||||||
|
for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { |
||||||
|
var threshold = gaugeOptionsi.threshold.values[i]; |
||||||
|
color = threshold.color; |
||||||
|
if (data < threshold.value) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return color; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the angle of the data |
||||||
|
* |
||||||
|
* @method calculateAngle |
||||||
|
* @private |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Number} data the value of the gauge |
||||||
|
*/ |
||||||
|
function calculateAngle(gaugeOptionsi, layout, data) { |
||||||
|
var a = |
||||||
|
gaugeOptionsi.gauge.startAngle |
||||||
|
+ (gaugeOptionsi.gauge.endAngle - gaugeOptionsi.gauge.startAngle) |
||||||
|
* ((data - gaugeOptionsi.gauge.min) / (gaugeOptionsi.gauge.max - gaugeOptionsi.gauge.min)); |
||||||
|
|
||||||
|
if (a < gaugeOptionsi.gauge.startAngle) { |
||||||
|
a = gaugeOptionsi.gauge.startAngle; |
||||||
|
} else if (a > gaugeOptionsi.gauge.endAngle) { |
||||||
|
a = gaugeOptionsi.gauge.endAngle; |
||||||
|
} |
||||||
|
return a; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the arc of the threshold |
||||||
|
* |
||||||
|
* @method drawThreshold |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawThreshold = function(gaugeOptionsi, layout, cellLayout) { |
||||||
|
|
||||||
|
var a1 = gaugeOptionsi.gauge.startAngle; |
||||||
|
for (var i = 0; i < gaugeOptionsi.threshold.values.length; i++) { |
||||||
|
var threshold = gaugeOptionsi.threshold.values[i]; |
||||||
|
c1 = threshold.color; |
||||||
|
a2 = calculateAngle(gaugeOptionsi, layout, threshold.value); |
||||||
|
drawArc( |
||||||
|
context, |
||||||
|
cellLayout.cx, // center x
|
||||||
|
cellLayout.cy, // center y
|
||||||
|
layout.radius + layout.thresholdWidth, |
||||||
|
layout.thresholdWidth - 2, |
||||||
|
toRad(a1), |
||||||
|
toRad(a2), |
||||||
|
c1, // line color
|
||||||
|
1, // line width
|
||||||
|
c1); // fill color
|
||||||
|
a1 = a2; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw an arc with a shadow |
||||||
|
* |
||||||
|
* @method drawArcWithShadow |
||||||
|
* @private |
||||||
|
* @param {Number} cx the x position of the center |
||||||
|
* @param {Number} cy the y position of the center |
||||||
|
* @param {Number} r the radius of an arc |
||||||
|
* @param {Number} w the width of an arc |
||||||
|
* @param {Number} rd1 the start angle of an arc in radians |
||||||
|
* @param {Number} rd2 the end angle of an arc in radians |
||||||
|
* @param {String} lc the color of a line |
||||||
|
* @param {Number} lw the widht of a line |
||||||
|
* @param {String} fc the fill color of an arc |
||||||
|
* @param {Number} blur the shdow blur |
||||||
|
*/ |
||||||
|
function drawArcWithShadow(cx, cy, r, w, rd1, rd2, lc, lw, fc, blur) { |
||||||
|
if (rd1 === rd2) { |
||||||
|
return; |
||||||
|
} |
||||||
|
context.save(); |
||||||
|
|
||||||
|
drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc); |
||||||
|
|
||||||
|
if (blur) { |
||||||
|
drawArc(context, cx, cy, r, w, rd1, rd2); |
||||||
|
context.clip(); |
||||||
|
context.shadowOffsetX = 0; |
||||||
|
context.shadowOffsetY = 0; |
||||||
|
context.shadowBlur = 10; |
||||||
|
context.shadowColor = "gray"; |
||||||
|
drawArc(context, cx, cy, r + 1, w + 2, rd1, rd2, lc, 1); |
||||||
|
} |
||||||
|
context.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the label of the gauge |
||||||
|
* |
||||||
|
* @method drawLable |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @param {Object} item the item of the series |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawLable = function(gaugeOptionsi, layout, cellLayout, i, item) { |
||||||
|
|
||||||
|
drawText( |
||||||
|
cellLayout.cx, |
||||||
|
cellLayout.y + cellLayout.cellMargin + layout.labelMargin + cellLayout.offsetY, |
||||||
|
"flotGagueLabel" + i, |
||||||
|
gaugeOptionsi.label.formatter ? gaugeOptionsi.label.formatter(item.label, item.data[0][1]) : text, |
||||||
|
gaugeOptionsi.label); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the value of the gauge |
||||||
|
* |
||||||
|
* @method drawValue |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @param {Object} item the item of the series |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawValue = function(gaugeOptionsi, layout, cellLayout, i, item) { |
||||||
|
|
||||||
|
drawText( |
||||||
|
cellLayout.cx, |
||||||
|
cellLayout.cy - (gaugeOptionsi.value.font.size / 2), |
||||||
|
"flotGagueValue" + i, |
||||||
|
gaugeOptionsi.value.formatter ? gaugeOptionsi.value.formatter(item.label, item.data[0][1]) : text, |
||||||
|
gaugeOptionsi.value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the values of the threshold |
||||||
|
* |
||||||
|
* @method drawThresholdValues |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
* @param {Number} i the index of the series |
||||||
|
*/ |
||||||
|
Gauge.prototype.drawThresholdValues = function(gaugeOptionsi, layout, cellLayout, i) { |
||||||
|
|
||||||
|
// min, max
|
||||||
|
drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Min" + i, gaugeOptionsi.gauge.min, gaugeOptionsi.gauge.startAngle); |
||||||
|
drawThresholdValue(gaugeOptionsi, layout, cellLayout, "Max" + i, gaugeOptionsi.gauge.max, gaugeOptionsi.gauge.endAngle); |
||||||
|
// threshold values
|
||||||
|
for (var j = 0; j < gaugeOptionsi.threshold.values.length; j++) { |
||||||
|
var threshold = gaugeOptionsi.threshold.values[j]; |
||||||
|
if (threshold.value > gaugeOptionsi.gauge.min && threshold.value < gaugeOptionsi.gauge.max) { |
||||||
|
var a = calculateAngle(gaugeOptionsi, layout, threshold.value); |
||||||
|
drawThresholdValue(gaugeOptionsi, layout, cellLayout, i + "_" + j, threshold.value, a); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw the value of the threshold |
||||||
|
* |
||||||
|
* @method drawThresholdValue |
||||||
|
* @param {Object} gaugeOptionsi the options of the gauge |
||||||
|
* @param {Object} layout the layout properties |
||||||
|
* @param {Object} cellLayout the cell layout properties |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @param {Number} value the value of the threshold |
||||||
|
* @param {Number} a the angle of the value drawn |
||||||
|
*/ |
||||||
|
function drawThresholdValue(gaugeOptionsi, layout, cellLayout, i, value, a) { |
||||||
|
drawText( |
||||||
|
cellLayout.cx |
||||||
|
+ ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) |
||||||
|
* Math.cos(toRad(a))), |
||||||
|
cellLayout.cy |
||||||
|
+ ((layout.thresholdLabelMargin + (layout.thresholdLabelFontSize / 2) + layout.radius) |
||||||
|
* Math.sin(toRad(a))), |
||||||
|
"flotGagueThresholdValue" + i, |
||||||
|
gaugeOptionsi.threshold.label.formatter ? gaugeOptionsi.threshold.label.formatter(value) : value, |
||||||
|
gaugeOptionsi.threshold.label, |
||||||
|
a); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw a text |
||||||
|
* |
||||||
|
* the textOptions is assumed as follows: |
||||||
|
* |
||||||
|
* textOptions: { |
||||||
|
* background: { |
||||||
|
* color: null, |
||||||
|
* opacity: 0 |
||||||
|
* }, |
||||||
|
* font: { |
||||||
|
* size: "auto" |
||||||
|
* family: "\"MS ゴシック\",sans-serif" |
||||||
|
* }, |
||||||
|
* color: null |
||||||
|
* } |
||||||
|
* |
||||||
|
* @method drawText |
||||||
|
* @private |
||||||
|
* @param {Number} x the x position of the text drawn (left top) |
||||||
|
* @param {Number} y the y position of the text drawn (left top) |
||||||
|
* @param {String} id the id of the dom element |
||||||
|
* @param {String} text the text drawn |
||||||
|
* @param {Object} textOptions the option of the text |
||||||
|
* @param {Number} [a] the angle of the value drawn |
||||||
|
*/ |
||||||
|
function drawText(x, y, id, text, textOptions, a) { |
||||||
|
var span = $("." + id, placeholder); |
||||||
|
var exists = span.length; |
||||||
|
if (!exists) { |
||||||
|
span = $("<span></span>") |
||||||
|
span.attr("id", id); |
||||||
|
span.css("position", "absolute"); |
||||||
|
span.css("top", y + "px"); |
||||||
|
if (textOptions.font.size) { |
||||||
|
span.css("font-size", textOptions.font.size + "px"); |
||||||
|
} |
||||||
|
if (textOptions.font.family) { |
||||||
|
span.css("font-family", textOptions.font.family); |
||||||
|
} |
||||||
|
if (textOptions.color) { |
||||||
|
span.css("color", textOptions.color); |
||||||
|
} |
||||||
|
if (textOptions.background.color) { |
||||||
|
span.css("background-color", textOptions.background.color); |
||||||
|
} |
||||||
|
if (textOptions.background.opacity) { |
||||||
|
span.css("opacity", textOptions.background.opacity); |
||||||
|
} |
||||||
|
placeholder.append(span); |
||||||
|
} |
||||||
|
span.text(text); |
||||||
|
// after append, readjust the left position
|
||||||
|
span.css("left", x + "px"); // for redraw, resetting the left position is needed here
|
||||||
|
span.css("left", (parseInt(span.css("left")) - (span.width()/ 2)) + "px"); |
||||||
|
|
||||||
|
// at last, set angle
|
||||||
|
if (!exists && a) { |
||||||
|
span.css("top", (parseInt(span.css("top")) - (span.height()/ 2)) + "px"); |
||||||
|
span.css("transform", "rotate(" + ((180 * a) + 90) + "deg)"); // not supported for ie8
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return Gauge; |
||||||
|
})(); |
||||||
|
/** |
||||||
|
* get a instance of Logger |
||||||
|
* |
||||||
|
* @method getLogger |
||||||
|
* @for flot.gauge |
||||||
|
* @private |
||||||
|
* @param {Object} debugOptions the options of debug |
||||||
|
*/ |
||||||
|
function getLogger(debugOptions) { |
||||||
|
return typeof Logger !== "undefined" ? new Logger(debugOptions) : null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the index of columns for the specified data |
||||||
|
* |
||||||
|
* @method col |
||||||
|
* @for flot.gauge |
||||||
|
* @param {Number} columns the number of columns |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @return the index of columns |
||||||
|
*/ |
||||||
|
function col(columns, i) { |
||||||
|
return i % columns; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the index of rows for the specified data |
||||||
|
* |
||||||
|
* @method row |
||||||
|
* @for flot.gauge |
||||||
|
* @param {Number} columns the number of rows |
||||||
|
* @param {Number} i the index of the series |
||||||
|
* @return the index of rows |
||||||
|
*/ |
||||||
|
function row(columns, i) { |
||||||
|
return Math.floor(i / columns); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* calculate the angle in radians |
||||||
|
* |
||||||
|
* internally, use a number without PI (0 - 2). |
||||||
|
* so, in this function, multiply PI |
||||||
|
* |
||||||
|
* @method toRad |
||||||
|
* @for flot.gauge |
||||||
|
* @param {Number} a the number of angle without PI |
||||||
|
* @return the angle in radians |
||||||
|
*/ |
||||||
|
function toRad(a) { |
||||||
|
return a * Math.PI; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* draw an arc |
||||||
|
* |
||||||
|
* @method drawArc |
||||||
|
* @for flot.gauge |
||||||
|
* @param {Object} context the context of canvas |
||||||
|
* @param {Number} cx the x position of the center |
||||||
|
* @param {Number} cy the y position of the center |
||||||
|
* @param {Number} r the radius of an arc |
||||||
|
* @param {Number} w the width of an arc |
||||||
|
* @param {Number} rd1 the start angle of an arc in radians |
||||||
|
* @param {Number} rd2 the end angle of an arc in radians |
||||||
|
* @param {String} lc the color of a line |
||||||
|
* @param {Number} lw the widht of a line |
||||||
|
* @param {String} fc the fill color of an arc |
||||||
|
*/ |
||||||
|
function drawArc(context, cx, cy, r, w, rd1, rd2, lc, lw, fc) { |
||||||
|
if (rd1 === rd2) { |
||||||
|
return; |
||||||
|
} |
||||||
|
var counterClockwise = false; |
||||||
|
context.save(); |
||||||
|
context.beginPath(); |
||||||
|
context.arc(cx, cy, r, rd1, rd2, counterClockwise); |
||||||
|
context.lineTo(cx + (r - w) * Math.cos(rd2), |
||||||
|
cy + (r - w) * Math.sin(rd2)); |
||||||
|
context.arc(cx, cy, r - w, rd2, rd1, !counterClockwise); |
||||||
|
context.closePath(); |
||||||
|
if (lw) { |
||||||
|
context.lineWidth = lw; |
||||||
|
} |
||||||
|
if (lc) { |
||||||
|
context.strokeStyle = lc; |
||||||
|
context.stroke(); |
||||||
|
} |
||||||
|
if (fc) { |
||||||
|
context.fillStyle = fc; |
||||||
|
context.fill(); |
||||||
|
} |
||||||
|
context.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* initialize plugin |
||||||
|
* |
||||||
|
* @method init |
||||||
|
* @for flot.gauge |
||||||
|
* @private |
||||||
|
* @param {Object} plot a instance of plot |
||||||
|
*/ |
||||||
|
function init (plot) { |
||||||
|
// add processOptions hook
|
||||||
|
plot.hooks.processOptions.push(function(plot, options) { |
||||||
|
var logger = getLogger(options.series.gauges.debug); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// turn 'grid' and 'legend' off
|
||||||
|
if (options.series.gauges.show) { |
||||||
|
options.grid.show = false; |
||||||
|
options.legend.show = false; |
||||||
|
} |
||||||
|
|
||||||
|
// sort threshold
|
||||||
|
var thresholds = options.series.gauges.threshold.values; |
||||||
|
|
||||||
|
thresholds.sort(function(a, b) { |
||||||
|
if (a.value < b.value) { |
||||||
|
return -1; |
||||||
|
} else if (a.value > b.value) { |
||||||
|
return 1; |
||||||
|
} else { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
// add draw hook
|
||||||
|
plot.hooks.draw.push(function(plot, context) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
var gaugeOptions = options.series.gauges; |
||||||
|
|
||||||
|
var logger = getLogger(gaugeOptions.debug); |
||||||
|
|
||||||
|
|
||||||
|
if (!gaugeOptions.show) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var series = plot.getData(); |
||||||
|
|
||||||
|
if (!series || !series.length) { |
||||||
|
return; // if no series were passed
|
||||||
|
} |
||||||
|
|
||||||
|
var gauge = new Gauge(plot, context); |
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
var layout = gauge.calculateLayout(); |
||||||
|
|
||||||
|
// debug layout
|
||||||
|
if (gaugeOptions.debug.layout) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// draw background
|
||||||
|
gauge.drawBackground(layout) |
||||||
|
|
||||||
|
// draw cells (label, gauge, value, threshold)
|
||||||
|
for (var i = 0; i < series.length; i++) { |
||||||
|
var item = series[i]; |
||||||
|
|
||||||
|
var gaugeOptionsi = $.extend({}, gaugeOptions, item.gauges); |
||||||
|
if (item.gauges) { |
||||||
|
// re-calculate 'auto' values
|
||||||
|
gauge.calculateAutoValues(gaugeOptionsi, layout.cellWidth); |
||||||
|
} |
||||||
|
|
||||||
|
// calculate cell layout
|
||||||
|
var cellLayout = gauge.calculateCellLayout(gaugeOptionsi, layout, i); |
||||||
|
|
||||||
|
// draw cell background
|
||||||
|
gauge.drawCellBackground(gaugeOptionsi, cellLayout) |
||||||
|
// debug layout
|
||||||
|
if (gaugeOptionsi.debug.layout) { |
||||||
|
|
||||||
|
} |
||||||
|
// draw label
|
||||||
|
if (gaugeOptionsi.label.show) { |
||||||
|
gauge.drawLable(gaugeOptionsi, layout, cellLayout, i, item); |
||||||
|
} |
||||||
|
// draw gauge
|
||||||
|
gauge.drawGauge(gaugeOptionsi, layout, cellLayout, item.label, item.data[0][1]); |
||||||
|
// draw threshold
|
||||||
|
if (gaugeOptionsi.threshold.show) { |
||||||
|
gauge.drawThreshold(gaugeOptionsi, layout, cellLayout); |
||||||
|
} |
||||||
|
if (gaugeOptionsi.threshold.label.show) { |
||||||
|
gauge.drawThresholdValues(gaugeOptionsi, layout, cellLayout, i) |
||||||
|
} |
||||||
|
// draw value
|
||||||
|
if (gaugeOptionsi.value.show) { |
||||||
|
gauge.drawValue(gaugeOptionsi, layout, cellLayout, i, item); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* [defaults description] |
||||||
|
* |
||||||
|
* @property defaults |
||||||
|
* @type {Object} |
||||||
|
*/ |
||||||
|
var defaults = { |
||||||
|
series: { |
||||||
|
gauges: { |
||||||
|
debug: { |
||||||
|
log: false, |
||||||
|
layout: false, |
||||||
|
alert: false |
||||||
|
}, |
||||||
|
show: false, |
||||||
|
layout: { |
||||||
|
margin: 5, |
||||||
|
columns: 3, |
||||||
|
hMargin: 5, |
||||||
|
vMargin: 5, |
||||||
|
square: false |
||||||
|
}, |
||||||
|
frame: { |
||||||
|
show: true |
||||||
|
}, |
||||||
|
cell: { |
||||||
|
background: { |
||||||
|
color: null |
||||||
|
}, |
||||||
|
border: { |
||||||
|
show: true, |
||||||
|
color: "black", |
||||||
|
width: 1 |
||||||
|
}, |
||||||
|
margin: 5, |
||||||
|
vAlign: "middle" // 'top' or 'middle' or 'bottom'
|
||||||
|
}, |
||||||
|
gauge: { |
||||||
|
width: "auto", // a specified number, or 'auto'
|
||||||
|
startAngle: 0.9, // 0 - 2 factor of the radians
|
||||||
|
endAngle: 2.1, // 0 - 2 factor of the radians
|
||||||
|
min: 0, |
||||||
|
max: 100, |
||||||
|
background: { |
||||||
|
color: "white" |
||||||
|
}, |
||||||
|
border: { |
||||||
|
color: "lightgray", |
||||||
|
width: 2 |
||||||
|
}, |
||||||
|
shadow: { |
||||||
|
show: true, |
||||||
|
blur: 5 |
||||||
|
} |
||||||
|
}, |
||||||
|
label: { |
||||||
|
show: true, |
||||||
|
margin: "auto", // a specified number, or 'auto'
|
||||||
|
background: { |
||||||
|
color: null, |
||||||
|
opacity: 0 |
||||||
|
}, |
||||||
|
font: { |
||||||
|
size: "auto", // a specified number, or 'auto'
|
||||||
|
family: "sans-serif" |
||||||
|
}, |
||||||
|
color: null, |
||||||
|
formatter: function(label, value) { |
||||||
|
return label; |
||||||
|
} |
||||||
|
}, |
||||||
|
value: { |
||||||
|
show: true, |
||||||
|
margin: "auto", // a specified number, or 'auto'
|
||||||
|
background: { |
||||||
|
color: null, |
||||||
|
opacity: 0 |
||||||
|
}, |
||||||
|
font: { |
||||||
|
size: "auto", // a specified number, or 'auto'
|
||||||
|
family: "sans-serif" |
||||||
|
}, |
||||||
|
color: null, |
||||||
|
formatter: function(label, value) { |
||||||
|
return parseInt(value); |
||||||
|
} |
||||||
|
}, |
||||||
|
threshold: { |
||||||
|
show: true, |
||||||
|
width: "auto", // a specified number, or 'auto'
|
||||||
|
label: { |
||||||
|
show: true, |
||||||
|
margin: "auto", // a specified number, or 'auto'
|
||||||
|
background: { |
||||||
|
color: null, |
||||||
|
opacity: 0 |
||||||
|
}, |
||||||
|
font: { |
||||||
|
size: "auto", // a specified number, or 'auto'
|
||||||
|
family: ",sans-serif" |
||||||
|
}, |
||||||
|
color: null, |
||||||
|
formatter: function(value) { |
||||||
|
return value; |
||||||
|
} |
||||||
|
}, |
||||||
|
values: [ |
||||||
|
{ |
||||||
|
value: 50, |
||||||
|
color: "lightgreen" |
||||||
|
}, { |
||||||
|
value: 80, |
||||||
|
color: "yellow" |
||||||
|
}, { |
||||||
|
value: 100, |
||||||
|
color: "red" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// register the gauge plugin
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: defaults, |
||||||
|
name: "gauge", |
||||||
|
version: "1.1.0" |
||||||
|
}); |
||||||
|
|
||||||
|
})(jQuery); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,817 @@ |
|||||||
|
/* Flot plugin for rendering pie charts. |
||||||
|
|
||||||
|
Copyright (c) 2007-2013 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
The plugin assumes that each series has a single data value, and that each |
||||||
|
value is a positive integer or zero. Negative numbers don't make sense for a |
||||||
|
pie chart, and have unpredictable results. The values do NOT need to be |
||||||
|
passed in as percentages; the plugin will calculate the total and per-slice |
||||||
|
percentages internally. |
||||||
|
|
||||||
|
* Created by Brian Medendorp |
||||||
|
|
||||||
|
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars |
||||||
|
|
||||||
|
The plugin supports these options: |
||||||
|
|
||||||
|
series: { |
||||||
|
pie: { |
||||||
|
show: true/false |
||||||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' |
||||||
|
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect |
||||||
|
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result |
||||||
|
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) |
||||||
|
offset: { |
||||||
|
top: integer value to move the pie up or down |
||||||
|
left: integer value to move the pie left or right, or 'auto' |
||||||
|
}, |
||||||
|
stroke: { |
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') |
||||||
|
width: integer pixel width of the stroke |
||||||
|
}, |
||||||
|
label: { |
||||||
|
show: true/false, or 'auto' |
||||||
|
formatter: a user-defined function that modifies the text/style of the label text |
||||||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length |
||||||
|
background: { |
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') |
||||||
|
opacity: 0-1 |
||||||
|
}, |
||||||
|
threshold: 0-1 for the percentage value at which to hide labels (if they're too small) |
||||||
|
}, |
||||||
|
combine: { |
||||||
|
threshold: 0-1 for the percentage value at which to combine slices (if they're too small) |
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined |
||||||
|
label: any text value of what the combined slice should be labeled |
||||||
|
} |
||||||
|
highlight: { |
||||||
|
opacity: 0-1 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
More detail and specific examples can be found in the included HTML file. |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
(function($) { |
||||||
|
|
||||||
|
// Maximum redraw attempts when fitting labels within the plot
|
||||||
|
|
||||||
|
var REDRAW_ATTEMPTS = 10; |
||||||
|
|
||||||
|
// Factor by which to shrink the pie when fitting labels within the plot
|
||||||
|
|
||||||
|
var REDRAW_SHRINK = 0.95; |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
|
||||||
|
var canvas = null, |
||||||
|
target = null, |
||||||
|
maxRadius = null, |
||||||
|
centerLeft = null, |
||||||
|
centerTop = null, |
||||||
|
processed = false, |
||||||
|
ctx = null; |
||||||
|
|
||||||
|
// interactive variables
|
||||||
|
|
||||||
|
var highlights = []; |
||||||
|
|
||||||
|
// add hook to determine if pie plugin in enabled, and then perform necessary operations
|
||||||
|
|
||||||
|
plot.hooks.processOptions.push(function(plot, options) { |
||||||
|
if (options.series.pie.show) { |
||||||
|
|
||||||
|
options.grid.show = false; |
||||||
|
|
||||||
|
// set labels.show
|
||||||
|
|
||||||
|
if (options.series.pie.label.show == "auto") { |
||||||
|
if (options.legend.show) { |
||||||
|
options.series.pie.label.show = false; |
||||||
|
} else { |
||||||
|
options.series.pie.label.show = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// set radius
|
||||||
|
|
||||||
|
if (options.series.pie.radius == "auto") { |
||||||
|
if (options.series.pie.label.show) { |
||||||
|
options.series.pie.radius = 3/4; |
||||||
|
} else { |
||||||
|
options.series.pie.radius = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ensure sane tilt
|
||||||
|
|
||||||
|
if (options.series.pie.tilt > 1) { |
||||||
|
options.series.pie.tilt = 1; |
||||||
|
} else if (options.series.pie.tilt < 0) { |
||||||
|
options.series.pie.tilt = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
if (options.series.pie.show) { |
||||||
|
if (options.grid.hoverable) { |
||||||
|
eventHolder.unbind("mousemove").mousemove(onMouseMove); |
||||||
|
} |
||||||
|
if (options.grid.clickable) { |
||||||
|
eventHolder.unbind("click").click(onClick); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
if (options.series.pie.show) { |
||||||
|
processDatapoints(plot, series, data, datapoints); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function(plot, octx) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
if (options.series.pie.show) { |
||||||
|
drawOverlay(plot, octx); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.draw.push(function(plot, newCtx) { |
||||||
|
var options = plot.getOptions(); |
||||||
|
if (options.series.pie.show) { |
||||||
|
draw(plot, newCtx); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
function processDatapoints(plot, series, datapoints) { |
||||||
|
if (!processed) { |
||||||
|
processed = true; |
||||||
|
canvas = plot.getCanvas(); |
||||||
|
target = $(canvas).parent(); |
||||||
|
options = plot.getOptions(); |
||||||
|
plot.setData(combine(plot.getData())); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function combine(data) { |
||||||
|
|
||||||
|
var total = 0, |
||||||
|
combined = 0, |
||||||
|
numCombined = 0, |
||||||
|
color = options.series.pie.combine.color, |
||||||
|
newdata = []; |
||||||
|
|
||||||
|
// Fix up the raw data from Flot, ensuring the data is numeric
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; ++i) { |
||||||
|
|
||||||
|
var value = data[i].data; |
||||||
|
|
||||||
|
// If the data is an array, we'll assume that it's a standard
|
||||||
|
// Flot x-y pair, and are concerned only with the second value.
|
||||||
|
|
||||||
|
// Note how we use the original array, rather than creating a
|
||||||
|
// new one; this is more efficient and preserves any extra data
|
||||||
|
// that the user may have stored in higher indexes.
|
||||||
|
|
||||||
|
if ($.isArray(value) && value.length == 1) { |
||||||
|
value = value[0]; |
||||||
|
} |
||||||
|
|
||||||
|
if ($.isArray(value)) { |
||||||
|
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
|
||||||
|
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { |
||||||
|
value[1] = +value[1]; |
||||||
|
} else { |
||||||
|
value[1] = 0; |
||||||
|
} |
||||||
|
} else if (!isNaN(parseFloat(value)) && isFinite(value)) { |
||||||
|
value = [1, +value]; |
||||||
|
} else { |
||||||
|
value = [1, 0]; |
||||||
|
} |
||||||
|
|
||||||
|
data[i].data = [value]; |
||||||
|
} |
||||||
|
|
||||||
|
// Sum up all the slices, so we can calculate percentages for each
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; ++i) { |
||||||
|
total += data[i].data[0][1]; |
||||||
|
} |
||||||
|
|
||||||
|
// Count the number of slices with percentages below the combine
|
||||||
|
// threshold; if it turns out to be just one, we won't combine.
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; ++i) { |
||||||
|
var value = data[i].data[0][1]; |
||||||
|
if (value / total <= options.series.pie.combine.threshold) { |
||||||
|
combined += value; |
||||||
|
numCombined++; |
||||||
|
if (!color) { |
||||||
|
color = data[i].color; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < data.length; ++i) { |
||||||
|
var value = data[i].data[0][1]; |
||||||
|
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { |
||||||
|
newdata.push({ |
||||||
|
data: [[1, value]], |
||||||
|
color: data[i].color, |
||||||
|
label: data[i].label, |
||||||
|
angle: value * Math.PI * 2 / total, |
||||||
|
percent: value / (total / 100) |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (numCombined > 1) { |
||||||
|
newdata.push({ |
||||||
|
data: [[1, combined]], |
||||||
|
color: color, |
||||||
|
label: options.series.pie.combine.label, |
||||||
|
angle: combined * Math.PI * 2 / total, |
||||||
|
percent: combined / (total / 100) |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return newdata; |
||||||
|
} |
||||||
|
|
||||||
|
function draw(plot, newCtx) { |
||||||
|
|
||||||
|
if (!target) { |
||||||
|
return; // if no series were passed
|
||||||
|
} |
||||||
|
|
||||||
|
var canvasWidth = plot.getPlaceholder().width(), |
||||||
|
canvasHeight = plot.getPlaceholder().height(), |
||||||
|
legendWidth = target.children().filter(".legend").children().width() || 0; |
||||||
|
|
||||||
|
ctx = newCtx; |
||||||
|
|
||||||
|
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
|
||||||
|
|
||||||
|
// When combining smaller slices into an 'other' slice, we need to
|
||||||
|
// add a new series. Since Flot gives plugins no way to modify the
|
||||||
|
// list of series, the pie plugin uses a hack where the first call
|
||||||
|
// to processDatapoints results in a call to setData with the new
|
||||||
|
// list of series, then subsequent processDatapoints do nothing.
|
||||||
|
|
||||||
|
// The plugin-global 'processed' flag is used to control this hack;
|
||||||
|
// it starts out false, and is set to true after the first call to
|
||||||
|
// processDatapoints.
|
||||||
|
|
||||||
|
// Unfortunately this turns future setData calls into no-ops; they
|
||||||
|
// call processDatapoints, the flag is true, and nothing happens.
|
||||||
|
|
||||||
|
// To fix this we'll set the flag back to false here in draw, when
|
||||||
|
// all series have been processed, so the next sequence of calls to
|
||||||
|
// processDatapoints once again starts out with a slice-combine.
|
||||||
|
// This is really a hack; in 0.9 we need to give plugins a proper
|
||||||
|
// way to modify series before any processing begins.
|
||||||
|
|
||||||
|
processed = false; |
||||||
|
|
||||||
|
// calculate maximum radius and center point
|
||||||
|
|
||||||
|
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; |
||||||
|
centerTop = canvasHeight / 2 + options.series.pie.offset.top; |
||||||
|
centerLeft = canvasWidth / 2; |
||||||
|
|
||||||
|
if (options.series.pie.offset.left == "auto") { |
||||||
|
if (options.legend.position.match("w")) { |
||||||
|
centerLeft += legendWidth / 2; |
||||||
|
} else { |
||||||
|
centerLeft -= legendWidth / 2; |
||||||
|
} |
||||||
|
} else { |
||||||
|
centerLeft += options.series.pie.offset.left; |
||||||
|
} |
||||||
|
|
||||||
|
if (centerLeft < maxRadius) { |
||||||
|
centerLeft = maxRadius; |
||||||
|
} else if (centerLeft > canvasWidth - maxRadius) { |
||||||
|
centerLeft = canvasWidth - maxRadius; |
||||||
|
} |
||||||
|
|
||||||
|
var slices = plot.getData(), |
||||||
|
attempts = 0; |
||||||
|
|
||||||
|
// Keep shrinking the pie's radius until drawPie returns true,
|
||||||
|
// indicating that all the labels fit, or we try too many times.
|
||||||
|
|
||||||
|
do { |
||||||
|
if (attempts > 0) { |
||||||
|
maxRadius *= REDRAW_SHRINK; |
||||||
|
} |
||||||
|
attempts += 1; |
||||||
|
clear(); |
||||||
|
if (options.series.pie.tilt <= 0.8) { |
||||||
|
drawShadow(); |
||||||
|
} |
||||||
|
} while (!drawPie() && attempts < REDRAW_ATTEMPTS) |
||||||
|
|
||||||
|
if (attempts >= REDRAW_ATTEMPTS) { |
||||||
|
clear(); |
||||||
|
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>"); |
||||||
|
} |
||||||
|
|
||||||
|
if (plot.setSeries && plot.insertLegend) { |
||||||
|
plot.setSeries(slices); |
||||||
|
plot.insertLegend(); |
||||||
|
} |
||||||
|
|
||||||
|
// we're actually done at this point, just defining internal functions at this point
|
||||||
|
|
||||||
|
function clear() { |
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
||||||
|
target.children().filter(".pieLabel, .pieLabelBackground").remove(); |
||||||
|
} |
||||||
|
|
||||||
|
function drawShadow() { |
||||||
|
|
||||||
|
var shadowLeft = options.series.pie.shadow.left; |
||||||
|
var shadowTop = options.series.pie.shadow.top; |
||||||
|
var edge = 10; |
||||||
|
var alpha = options.series.pie.shadow.alpha; |
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; |
||||||
|
|
||||||
|
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { |
||||||
|
return; // shadow would be outside canvas, so don't draw it
|
||||||
|
} |
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.translate(shadowLeft,shadowTop); |
||||||
|
ctx.globalAlpha = alpha; |
||||||
|
ctx.fillStyle = "#000"; |
||||||
|
|
||||||
|
// center and rotate to starting position
|
||||||
|
|
||||||
|
ctx.translate(centerLeft,centerTop); |
||||||
|
ctx.scale(1, options.series.pie.tilt); |
||||||
|
|
||||||
|
//radius -= edge;
|
||||||
|
|
||||||
|
for (var i = 1; i <= edge; i++) { |
||||||
|
ctx.beginPath(); |
||||||
|
ctx.arc(0, 0, radius, 0, Math.PI * 2, false); |
||||||
|
ctx.fill(); |
||||||
|
radius -= i; |
||||||
|
} |
||||||
|
|
||||||
|
ctx.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
function drawPie() { |
||||||
|
|
||||||
|
var startAngle = Math.PI * options.series.pie.startAngle; |
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; |
||||||
|
|
||||||
|
// center and rotate to starting position
|
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.translate(centerLeft,centerTop); |
||||||
|
ctx.scale(1, options.series.pie.tilt); |
||||||
|
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
|
||||||
|
|
||||||
|
// draw slices
|
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
var currentAngle = startAngle; |
||||||
|
for (var i = 0; i < slices.length; ++i) { |
||||||
|
slices[i].startAngle = currentAngle; |
||||||
|
drawSlice(slices[i].angle, slices[i].color, true); |
||||||
|
} |
||||||
|
ctx.restore(); |
||||||
|
|
||||||
|
// draw slice outlines
|
||||||
|
|
||||||
|
if (options.series.pie.stroke.width > 0) { |
||||||
|
ctx.save(); |
||||||
|
ctx.lineWidth = options.series.pie.stroke.width; |
||||||
|
currentAngle = startAngle; |
||||||
|
for (var i = 0; i < slices.length; ++i) { |
||||||
|
drawSlice(slices[i].angle, options.series.pie.stroke.color, false); |
||||||
|
} |
||||||
|
ctx.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
// draw donut hole
|
||||||
|
|
||||||
|
drawDonutHole(ctx); |
||||||
|
|
||||||
|
ctx.restore(); |
||||||
|
|
||||||
|
// Draw the labels, returning true if they fit within the plot
|
||||||
|
|
||||||
|
if (options.series.pie.label.show) { |
||||||
|
return drawLabels(); |
||||||
|
} else return true; |
||||||
|
|
||||||
|
function drawSlice(angle, color, fill) { |
||||||
|
|
||||||
|
if (angle <= 0 || isNaN(angle)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (fill) { |
||||||
|
ctx.fillStyle = color; |
||||||
|
} else { |
||||||
|
ctx.strokeStyle = color; |
||||||
|
ctx.lineJoin = "round"; |
||||||
|
} |
||||||
|
|
||||||
|
ctx.beginPath(); |
||||||
|
if (Math.abs(angle - Math.PI * 2) > 0.000000001) { |
||||||
|
ctx.moveTo(0, 0); // Center of the pie
|
||||||
|
} |
||||||
|
|
||||||
|
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
|
||||||
|
ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); |
||||||
|
ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); |
||||||
|
ctx.closePath(); |
||||||
|
//ctx.rotate(angle); // This doesn't work properly in Opera
|
||||||
|
currentAngle += angle; |
||||||
|
|
||||||
|
if (fill) { |
||||||
|
ctx.fill(); |
||||||
|
} else { |
||||||
|
ctx.stroke(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function drawLabels() { |
||||||
|
|
||||||
|
var currentAngle = startAngle; |
||||||
|
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; |
||||||
|
|
||||||
|
for (var i = 0; i < slices.length; ++i) { |
||||||
|
if (slices[i].percent >= options.series.pie.label.threshold * 100) { |
||||||
|
if (!drawLabel(slices[i], currentAngle, i)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
currentAngle += slices[i].angle; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
|
||||||
|
function drawLabel(slice, startAngle, index) { |
||||||
|
|
||||||
|
if (slice.data[0][1] == 0) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// format label text
|
||||||
|
|
||||||
|
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; |
||||||
|
|
||||||
|
if (lf) { |
||||||
|
text = lf(slice.label, slice); |
||||||
|
} else { |
||||||
|
text = slice.label; |
||||||
|
} |
||||||
|
|
||||||
|
if (plf) { |
||||||
|
text = plf(text, slice); |
||||||
|
} |
||||||
|
|
||||||
|
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; |
||||||
|
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); |
||||||
|
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; |
||||||
|
|
||||||
|
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>"; |
||||||
|
target.append(html); |
||||||
|
|
||||||
|
var label = target.children("#pieLabel" + index); |
||||||
|
var labelTop = (y - label.height() / 2); |
||||||
|
var labelLeft = (x - label.width() / 2); |
||||||
|
|
||||||
|
label.css("top", labelTop); |
||||||
|
label.css("left", labelLeft); |
||||||
|
|
||||||
|
// check to make sure that the label is not outside the canvas
|
||||||
|
|
||||||
|
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (options.series.pie.label.background.opacity != 0) { |
||||||
|
|
||||||
|
// put in the transparent background separately to avoid blended labels and label boxes
|
||||||
|
|
||||||
|
var c = options.series.pie.label.background.color; |
||||||
|
|
||||||
|
if (c == null) { |
||||||
|
c = slice.color; |
||||||
|
} |
||||||
|
|
||||||
|
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; |
||||||
|
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>") |
||||||
|
.css("opacity", options.series.pie.label.background.opacity) |
||||||
|
.insertBefore(label); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} // end individual label function
|
||||||
|
} // end drawLabels function
|
||||||
|
} // end drawPie function
|
||||||
|
} // end draw function
|
||||||
|
|
||||||
|
// Placed here because it needs to be accessed from multiple locations
|
||||||
|
|
||||||
|
function drawDonutHole(layer) { |
||||||
|
if (options.series.pie.innerRadius > 0) { |
||||||
|
|
||||||
|
// subtract the center
|
||||||
|
|
||||||
|
layer.save(); |
||||||
|
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; |
||||||
|
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
|
||||||
|
layer.beginPath(); |
||||||
|
layer.fillStyle = options.series.pie.stroke.color; |
||||||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); |
||||||
|
layer.fill(); |
||||||
|
layer.closePath(); |
||||||
|
layer.restore(); |
||||||
|
|
||||||
|
// add inner stroke
|
||||||
|
|
||||||
|
layer.save(); |
||||||
|
layer.beginPath(); |
||||||
|
layer.strokeStyle = options.series.pie.stroke.color; |
||||||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); |
||||||
|
layer.stroke(); |
||||||
|
layer.closePath(); |
||||||
|
layer.restore(); |
||||||
|
|
||||||
|
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//-- Additional Interactive related functions --
|
||||||
|
|
||||||
|
function isPointInPoly(poly, pt) { |
||||||
|
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) |
||||||
|
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) |
||||||
|
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) |
||||||
|
&& (c = !c); |
||||||
|
return c; |
||||||
|
} |
||||||
|
|
||||||
|
function findNearbySlice(mouseX, mouseY) { |
||||||
|
|
||||||
|
var slices = plot.getData(), |
||||||
|
options = plot.getOptions(), |
||||||
|
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, |
||||||
|
x, y; |
||||||
|
|
||||||
|
for (var i = 0; i < slices.length; ++i) { |
||||||
|
|
||||||
|
var s = slices[i]; |
||||||
|
|
||||||
|
if (s.pie.show) { |
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.beginPath(); |
||||||
|
ctx.moveTo(0, 0); // Center of the pie
|
||||||
|
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
|
||||||
|
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); |
||||||
|
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); |
||||||
|
ctx.closePath(); |
||||||
|
x = mouseX - centerLeft; |
||||||
|
y = mouseY - centerTop; |
||||||
|
|
||||||
|
if (ctx.isPointInPath) { |
||||||
|
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { |
||||||
|
ctx.restore(); |
||||||
|
return { |
||||||
|
datapoint: [s.percent, s.data], |
||||||
|
dataIndex: 0, |
||||||
|
series: s, |
||||||
|
seriesIndex: i |
||||||
|
}; |
||||||
|
} |
||||||
|
} else { |
||||||
|
|
||||||
|
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
|
||||||
|
|
||||||
|
var p1X = radius * Math.cos(s.startAngle), |
||||||
|
p1Y = radius * Math.sin(s.startAngle), |
||||||
|
p2X = radius * Math.cos(s.startAngle + s.angle / 4), |
||||||
|
p2Y = radius * Math.sin(s.startAngle + s.angle / 4), |
||||||
|
p3X = radius * Math.cos(s.startAngle + s.angle / 2), |
||||||
|
p3Y = radius * Math.sin(s.startAngle + s.angle / 2), |
||||||
|
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), |
||||||
|
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), |
||||||
|
p5X = radius * Math.cos(s.startAngle + s.angle), |
||||||
|
p5Y = radius * Math.sin(s.startAngle + s.angle), |
||||||
|
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], |
||||||
|
arrPoint = [x, y]; |
||||||
|
|
||||||
|
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
|
||||||
|
|
||||||
|
if (isPointInPoly(arrPoly, arrPoint)) { |
||||||
|
ctx.restore(); |
||||||
|
return { |
||||||
|
datapoint: [s.percent, s.data], |
||||||
|
dataIndex: 0, |
||||||
|
series: s, |
||||||
|
seriesIndex: i |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ctx.restore(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseMove(e) { |
||||||
|
triggerClickHoverEvent("plothover", e); |
||||||
|
} |
||||||
|
|
||||||
|
function onClick(e) { |
||||||
|
triggerClickHoverEvent("plotclick", e); |
||||||
|
} |
||||||
|
|
||||||
|
// trigger click or hover event (they send the same parameters so we share their code)
|
||||||
|
|
||||||
|
function triggerClickHoverEvent(eventname, e) { |
||||||
|
|
||||||
|
var offset = plot.offset(); |
||||||
|
var canvasX = parseInt(e.pageX - offset.left); |
||||||
|
var canvasY = parseInt(e.pageY - offset.top); |
||||||
|
var item = findNearbySlice(canvasX, canvasY); |
||||||
|
|
||||||
|
if (options.grid.autoHighlight) { |
||||||
|
|
||||||
|
// clear auto-highlights
|
||||||
|
|
||||||
|
for (var i = 0; i < highlights.length; ++i) { |
||||||
|
var h = highlights[i]; |
||||||
|
if (h.auto == eventname && !(item && h.series == item.series)) { |
||||||
|
unhighlight(h.series); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// highlight the slice
|
||||||
|
|
||||||
|
if (item) { |
||||||
|
highlight(item.series, eventname); |
||||||
|
} |
||||||
|
|
||||||
|
// trigger any hover bind events
|
||||||
|
|
||||||
|
var pos = { pageX: e.pageX, pageY: e.pageY }; |
||||||
|
target.trigger(eventname, [pos, item]); |
||||||
|
} |
||||||
|
|
||||||
|
function highlight(s, auto) { |
||||||
|
//if (typeof s == "number") {
|
||||||
|
// s = series[s];
|
||||||
|
//}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s); |
||||||
|
|
||||||
|
if (i == -1) { |
||||||
|
highlights.push({ series: s, auto: auto }); |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} else if (!auto) { |
||||||
|
highlights[i].auto = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function unhighlight(s) { |
||||||
|
if (s == null) { |
||||||
|
highlights = []; |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} |
||||||
|
|
||||||
|
//if (typeof s == "number") {
|
||||||
|
// s = series[s];
|
||||||
|
//}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s); |
||||||
|
|
||||||
|
if (i != -1) { |
||||||
|
highlights.splice(i, 1); |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function indexOfHighlight(s) { |
||||||
|
for (var i = 0; i < highlights.length; ++i) { |
||||||
|
var h = highlights[i]; |
||||||
|
if (h.series == s) |
||||||
|
return i; |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
function drawOverlay(plot, octx) { |
||||||
|
|
||||||
|
var options = plot.getOptions(); |
||||||
|
|
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; |
||||||
|
|
||||||
|
octx.save(); |
||||||
|
octx.translate(centerLeft, centerTop); |
||||||
|
octx.scale(1, options.series.pie.tilt); |
||||||
|
|
||||||
|
for (var i = 0; i < highlights.length; ++i) { |
||||||
|
drawHighlight(highlights[i].series); |
||||||
|
} |
||||||
|
|
||||||
|
drawDonutHole(octx); |
||||||
|
|
||||||
|
octx.restore(); |
||||||
|
|
||||||
|
function drawHighlight(series) { |
||||||
|
|
||||||
|
if (series.angle <= 0 || isNaN(series.angle)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
|
||||||
|
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
|
||||||
|
octx.beginPath(); |
||||||
|
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { |
||||||
|
octx.moveTo(0, 0); // Center of the pie
|
||||||
|
} |
||||||
|
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); |
||||||
|
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); |
||||||
|
octx.closePath(); |
||||||
|
octx.fill(); |
||||||
|
} |
||||||
|
} |
||||||
|
} // end init (plugin body)
|
||||||
|
|
||||||
|
// define pie specific options and their default values
|
||||||
|
|
||||||
|
var options = { |
||||||
|
series: { |
||||||
|
pie: { |
||||||
|
show: false, |
||||||
|
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
|
||||||
|
innerRadius: 0, /* for donut */ |
||||||
|
startAngle: 3/2, |
||||||
|
tilt: 1, |
||||||
|
shadow: { |
||||||
|
left: 5, // shadow left offset
|
||||||
|
top: 15, // shadow top offset
|
||||||
|
alpha: 0.02 // shadow alpha
|
||||||
|
}, |
||||||
|
offset: { |
||||||
|
top: 0, |
||||||
|
left: "auto" |
||||||
|
}, |
||||||
|
stroke: { |
||||||
|
color: "#fff", |
||||||
|
width: 1 |
||||||
|
}, |
||||||
|
label: { |
||||||
|
show: "auto", |
||||||
|
formatter: function(label, slice) { |
||||||
|
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>"; |
||||||
|
}, // formatter function
|
||||||
|
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
|
||||||
|
background: { |
||||||
|
color: null, |
||||||
|
opacity: 0 |
||||||
|
}, |
||||||
|
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
|
||||||
|
}, |
||||||
|
combine: { |
||||||
|
threshold: -1, // percentage at which to combine little slices into one larger slice
|
||||||
|
color: null, // color to give the new slice (auto-generated if null)
|
||||||
|
label: "Other" // label to give the new slice
|
||||||
|
}, |
||||||
|
highlight: { |
||||||
|
//color: "#fff", // will add this functionality once parseColor is available
|
||||||
|
opacity: 0.5 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: "pie", |
||||||
|
version: "1.1" |
||||||
|
}); |
||||||
|
|
||||||
|
})(jQuery); |
@ -0,0 +1,380 @@ |
|||||||
|
/* Flot plugin for selecting regions of a plot. |
||||||
|
|
||||||
|
Copyright (c) 2007-2013 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
The plugin supports these options: |
||||||
|
|
||||||
|
selection: { |
||||||
|
mode: null or "x" or "y" or "xy", |
||||||
|
color: color, |
||||||
|
shape: "round" or "miter" or "bevel", |
||||||
|
minSize: number of pixels |
||||||
|
} |
||||||
|
|
||||||
|
Selection support is enabled by setting the mode to one of "x", "y" or "xy". |
||||||
|
In "x" mode, the user will only be able to specify the x range, similarly for |
||||||
|
"y" mode. For "xy", the selection becomes a rectangle where both ranges can be |
||||||
|
specified. "color" is color of the selection (if you need to change the color |
||||||
|
later on, you can get to it with plot.getOptions().selection.color). "shape" |
||||||
|
is the shape of the corners of the selection. |
||||||
|
|
||||||
|
"minSize" is the minimum size a selection can be in pixels. This value can |
||||||
|
be customized to determine the smallest size a selection can be and still |
||||||
|
have the selection rectangle be displayed. When customizing this value, the |
||||||
|
fact that it refers to pixels, not axis units must be taken into account. |
||||||
|
Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 |
||||||
|
minute, setting "minSize" to 1 will not make the minimum selection size 1 |
||||||
|
minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent |
||||||
|
"plotunselected" events from being fired when the user clicks the mouse without |
||||||
|
dragging. |
||||||
|
|
||||||
|
When selection support is enabled, a "plotselected" event will be emitted on |
||||||
|
the DOM element you passed into the plot function. The event handler gets a |
||||||
|
parameter with the ranges selected on the axes, like this: |
||||||
|
|
||||||
|
placeholder.bind( "plotselected", function( event, ranges ) { |
||||||
|
alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) |
||||||
|
// similar for yaxis - with multiple axes, the extra ones are in
|
||||||
|
// x2axis, x3axis, ...
|
||||||
|
}); |
||||||
|
|
||||||
|
The "plotselected" event is only fired when the user has finished making the |
||||||
|
selection. A "plotselecting" event is fired during the process with the same |
||||||
|
parameters as the "plotselected" event, in case you want to know what's |
||||||
|
happening while it's happening, |
||||||
|
|
||||||
|
A "plotunselected" event with no arguments is emitted when the user clicks the |
||||||
|
mouse to remove the selection. As stated above, setting "minSize" to 0 will |
||||||
|
destroy this behavior. |
||||||
|
|
||||||
|
The plugin allso adds the following methods to the plot object: |
||||||
|
|
||||||
|
- setSelection( ranges, preventEvent ) |
||||||
|
|
||||||
|
Set the selection rectangle. The passed in ranges is on the same form as |
||||||
|
returned in the "plotselected" event. If the selection mode is "x", you |
||||||
|
should put in either an xaxis range, if the mode is "y" you need to put in |
||||||
|
an yaxis range and both xaxis and yaxis if the selection mode is "xy", like |
||||||
|
this: |
||||||
|
|
||||||
|
setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); |
||||||
|
|
||||||
|
setSelection will trigger the "plotselected" event when called. If you don't |
||||||
|
want that to happen, e.g. if you're inside a "plotselected" handler, pass |
||||||
|
true as the second parameter. If you are using multiple axes, you can |
||||||
|
specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of |
||||||
|
xaxis, the plugin picks the first one it sees. |
||||||
|
|
||||||
|
- clearSelection( preventEvent ) |
||||||
|
|
||||||
|
Clear the selection rectangle. Pass in true to avoid getting a |
||||||
|
"plotunselected" event. |
||||||
|
|
||||||
|
- getSelection() |
||||||
|
|
||||||
|
Returns the current selection in the same format as the "plotselected" |
||||||
|
event. If there's currently no selection, the function returns null. |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
(function ($) { |
||||||
|
function init(plot) { |
||||||
|
var selection = { |
||||||
|
first: { x: -1, y: -1}, second: { x: -1, y: -1}, |
||||||
|
show: false, |
||||||
|
active: false |
||||||
|
}; |
||||||
|
|
||||||
|
// FIXME: The drag handling implemented here should be
|
||||||
|
// abstracted out, there's some similar code from a library in
|
||||||
|
// the navigation plugin, this should be massaged a bit to fit
|
||||||
|
// the Flot cases here better and reused. Doing this would
|
||||||
|
// make this plugin much slimmer.
|
||||||
|
var savedhandlers = {}; |
||||||
|
|
||||||
|
var mouseUpHandler = null; |
||||||
|
|
||||||
|
function onMouseMove(e) { |
||||||
|
if (selection.active) { |
||||||
|
updateSelection(e); |
||||||
|
|
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseDown(e) { |
||||||
|
if (e.which != 1) // only accept left-click
|
||||||
|
return; |
||||||
|
|
||||||
|
// cancel out any text selections
|
||||||
|
document.body.focus(); |
||||||
|
|
||||||
|
// prevent text selection and drag in old-school browsers
|
||||||
|
if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { |
||||||
|
savedhandlers.onselectstart = document.onselectstart; |
||||||
|
document.onselectstart = function () { return false; }; |
||||||
|
} |
||||||
|
if (document.ondrag !== undefined && savedhandlers.ondrag == null) { |
||||||
|
savedhandlers.ondrag = document.ondrag; |
||||||
|
document.ondrag = function () { return false; }; |
||||||
|
} |
||||||
|
|
||||||
|
setSelectionPos(selection.first, e); |
||||||
|
|
||||||
|
selection.active = true; |
||||||
|
|
||||||
|
// this is a bit silly, but we have to use a closure to be
|
||||||
|
// able to whack the same handler again
|
||||||
|
mouseUpHandler = function (e) { onMouseUp(e); }; |
||||||
|
|
||||||
|
$(document).one("mouseup", mouseUpHandler); |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseUp(e) { |
||||||
|
mouseUpHandler = null; |
||||||
|
|
||||||
|
// revert drag stuff for old-school browsers
|
||||||
|
if (document.onselectstart !== undefined) |
||||||
|
document.onselectstart = savedhandlers.onselectstart; |
||||||
|
if (document.ondrag !== undefined) |
||||||
|
document.ondrag = savedhandlers.ondrag; |
||||||
|
|
||||||
|
// no more dragging
|
||||||
|
selection.active = false; |
||||||
|
updateSelection(e); |
||||||
|
|
||||||
|
if (selectionIsSane()) |
||||||
|
triggerSelectedEvent(e); |
||||||
|
else { |
||||||
|
// this counts as a clear
|
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]); |
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ null ]); |
||||||
|
} |
||||||
|
|
||||||
|
setTimeout(function() { |
||||||
|
plot.isSelecting = false; |
||||||
|
}, 10); |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
function getSelection() { |
||||||
|
if (!selectionIsSane()) |
||||||
|
return null; |
||||||
|
|
||||||
|
if (!selection.show) return null; |
||||||
|
|
||||||
|
var r = {}, c1 = selection.first, c2 = selection.second; |
||||||
|
var axes = plot.getAxes(); |
||||||
|
// look if no axis is used
|
||||||
|
var noAxisInUse = true; |
||||||
|
$.each(axes, function (name, axis) { |
||||||
|
if (axis.used) { |
||||||
|
anyUsed = false; |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
$.each(axes, function (name, axis) { |
||||||
|
if (axis.used || noAxisInUse) { |
||||||
|
var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); |
||||||
|
r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; |
||||||
|
} |
||||||
|
}); |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
function triggerSelectedEvent(event) { |
||||||
|
var r = getSelection(); |
||||||
|
|
||||||
|
// Add ctrlKey and metaKey to event
|
||||||
|
r.ctrlKey = event.ctrlKey; |
||||||
|
r.metaKey = event.metaKey; |
||||||
|
|
||||||
|
plot.getPlaceholder().trigger("plotselected", [ r ]); |
||||||
|
|
||||||
|
// backwards-compat stuff, to be removed in future
|
||||||
|
if (r.xaxis && r.yaxis) |
||||||
|
plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); |
||||||
|
} |
||||||
|
|
||||||
|
function clamp(min, value, max) { |
||||||
|
return value < min ? min: (value > max ? max: value); |
||||||
|
} |
||||||
|
|
||||||
|
function setSelectionPos(pos, e) { |
||||||
|
var o = plot.getOptions(); |
||||||
|
var offset = plot.getPlaceholder().offset(); |
||||||
|
var plotOffset = plot.getPlotOffset(); |
||||||
|
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); |
||||||
|
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); |
||||||
|
|
||||||
|
if (o.selection.mode == "y") |
||||||
|
pos.x = pos == selection.first ? 0 : plot.width(); |
||||||
|
|
||||||
|
if (o.selection.mode == "x") |
||||||
|
pos.y = pos == selection.first ? 0 : plot.height(); |
||||||
|
} |
||||||
|
|
||||||
|
function updateSelection(pos) { |
||||||
|
if (pos.pageX == null) |
||||||
|
return; |
||||||
|
|
||||||
|
setSelectionPos(selection.second, pos); |
||||||
|
if (selectionIsSane()) { |
||||||
|
plot.isSelecting = true; |
||||||
|
selection.show = true; |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
} |
||||||
|
else |
||||||
|
clearSelection(true); |
||||||
|
} |
||||||
|
|
||||||
|
function clearSelection(preventEvent) { |
||||||
|
if (selection.show) { |
||||||
|
selection.show = false; |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
if (!preventEvent) |
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// function taken from markings support in Flot
|
||||||
|
function extractRange(ranges, coord) { |
||||||
|
var axis, from, to, key, axes = plot.getAxes(); |
||||||
|
|
||||||
|
for (var k in axes) { |
||||||
|
axis = axes[k]; |
||||||
|
if (axis.direction == coord) { |
||||||
|
key = coord + axis.n + "axis"; |
||||||
|
if (!ranges[key] && axis.n == 1) |
||||||
|
key = coord + "axis"; // support x1axis as xaxis
|
||||||
|
if (ranges[key]) { |
||||||
|
from = ranges[key].from; |
||||||
|
to = ranges[key].to; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// backwards-compat stuff - to be removed in future
|
||||||
|
if (!ranges[key]) { |
||||||
|
axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; |
||||||
|
from = ranges[coord + "1"]; |
||||||
|
to = ranges[coord + "2"]; |
||||||
|
} |
||||||
|
|
||||||
|
// auto-reverse as an added bonus
|
||||||
|
if (from != null && to != null && from > to) { |
||||||
|
var tmp = from; |
||||||
|
from = to; |
||||||
|
to = tmp; |
||||||
|
} |
||||||
|
|
||||||
|
return { from: from, to: to, axis: axis }; |
||||||
|
} |
||||||
|
|
||||||
|
function setSelection(ranges, preventEvent) { |
||||||
|
var axis, range, o = plot.getOptions(); |
||||||
|
|
||||||
|
if (o.selection.mode == "y") { |
||||||
|
selection.first.x = 0; |
||||||
|
selection.second.x = plot.width(); |
||||||
|
} |
||||||
|
else { |
||||||
|
range = extractRange(ranges, "x"); |
||||||
|
|
||||||
|
selection.first.x = range.axis.p2c(range.from); |
||||||
|
selection.second.x = range.axis.p2c(range.to); |
||||||
|
} |
||||||
|
|
||||||
|
if (o.selection.mode == "x") { |
||||||
|
selection.first.y = 0; |
||||||
|
selection.second.y = plot.height(); |
||||||
|
} |
||||||
|
else { |
||||||
|
range = extractRange(ranges, "y"); |
||||||
|
|
||||||
|
selection.first.y = range.axis.p2c(range.from); |
||||||
|
selection.second.y = range.axis.p2c(range.to); |
||||||
|
} |
||||||
|
|
||||||
|
selection.show = true; |
||||||
|
plot.triggerRedrawOverlay(); |
||||||
|
if (!preventEvent && selectionIsSane()) |
||||||
|
triggerSelectedEvent(); |
||||||
|
} |
||||||
|
|
||||||
|
function selectionIsSane() { |
||||||
|
var minSize = plot.getOptions().selection.minSize; |
||||||
|
return Math.abs(selection.second.x - selection.first.x) >= minSize && |
||||||
|
Math.abs(selection.second.y - selection.first.y) >= minSize; |
||||||
|
} |
||||||
|
|
||||||
|
plot.clearSelection = clearSelection; |
||||||
|
plot.setSelection = setSelection; |
||||||
|
plot.getSelection = getSelection; |
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) { |
||||||
|
var o = plot.getOptions(); |
||||||
|
if (o.selection.mode != null) { |
||||||
|
eventHolder.mousemove(onMouseMove); |
||||||
|
eventHolder.mousedown(onMouseDown); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) { |
||||||
|
// draw selection
|
||||||
|
if (selection.show && selectionIsSane()) { |
||||||
|
var plotOffset = plot.getPlotOffset(); |
||||||
|
var o = plot.getOptions(); |
||||||
|
|
||||||
|
ctx.save(); |
||||||
|
ctx.translate(plotOffset.left, plotOffset.top); |
||||||
|
|
||||||
|
var c = $.color.parse(o.selection.color); |
||||||
|
|
||||||
|
ctx.strokeStyle = c.scale('a', o.selection.strokeAlpha).toString(); |
||||||
|
ctx.lineWidth = 1; |
||||||
|
ctx.lineJoin = o.selection.shape; |
||||||
|
ctx.fillStyle = c.scale('a', o.selection.fillAlpha).toString(); |
||||||
|
|
||||||
|
var x = Math.min(selection.first.x, selection.second.x) + 0.5, |
||||||
|
y = Math.min(selection.first.y, selection.second.y) + 0.5, |
||||||
|
w = Math.abs(selection.second.x - selection.first.x) - 1, |
||||||
|
h = Math.abs(selection.second.y - selection.first.y) - 1; |
||||||
|
|
||||||
|
ctx.fillRect(x, y, w, h); |
||||||
|
ctx.strokeRect(x, y, w, h); |
||||||
|
|
||||||
|
ctx.restore(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
plot.hooks.shutdown.push(function (plot, eventHolder) { |
||||||
|
eventHolder.unbind("mousemove", onMouseMove); |
||||||
|
eventHolder.unbind("mousedown", onMouseDown); |
||||||
|
|
||||||
|
if (mouseUpHandler) |
||||||
|
$(document).unbind("mouseup", mouseUpHandler); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: { |
||||||
|
selection: { |
||||||
|
mode: null, // one of null, "x", "y" or "xy"
|
||||||
|
color: "#e8cfac", |
||||||
|
shape: "round", // one of "round", "miter", or "bevel"
|
||||||
|
minSize: 5, // minimum number of pixels
|
||||||
|
strokeAlpha: 0.8, |
||||||
|
fillAlpha: 0.4, |
||||||
|
} |
||||||
|
}, |
||||||
|
name: 'selection', |
||||||
|
version: '1.1' |
||||||
|
}); |
||||||
|
})(jQuery); |
@ -0,0 +1,190 @@ |
|||||||
|
/* Flot plugin for stacking data sets rather than overlyaing them. |
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
The plugin assumes the data is sorted on x (or y if stacking horizontally). |
||||||
|
For line charts, it is assumed that if a line has an undefined gap (from a |
||||||
|
null point), then the line above it should have the same gap - insert zeros |
||||||
|
instead of "null" if you want another behaviour. This also holds for the start |
||||||
|
and end of the chart. Note that stacking a mix of positive and negative values |
||||||
|
in most instances doesn't make sense (so it looks weird). |
||||||
|
|
||||||
|
Two or more series are stacked when their "stack" attribute is set to the same |
||||||
|
key (which can be any number or string or just "true"). To specify the default |
||||||
|
stack, you can set the stack option like this: |
||||||
|
|
||||||
|
series: { |
||||||
|
stack: null/false, true, or a key (number/string) |
||||||
|
} |
||||||
|
|
||||||
|
You can also specify it for a single series, like this: |
||||||
|
|
||||||
|
$.plot( $("#placeholder"), [{ |
||||||
|
data: [ ... ], |
||||||
|
stack: true |
||||||
|
}]) |
||||||
|
|
||||||
|
The stacking order is determined by the order of the data series in the array |
||||||
|
(later series end up on top of the previous). |
||||||
|
|
||||||
|
Internally, the plugin modifies the datapoints in each series, adding an |
||||||
|
offset to the y value. For line series, extra data points are inserted through |
||||||
|
interpolation. If there's a second y value, it's also adjusted (e.g for bar |
||||||
|
charts or filled areas). |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
(function ($) { |
||||||
|
var options = { |
||||||
|
series: { stack: null } // or number/string
|
||||||
|
}; |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
function findMatchingSeries(s, allseries) { |
||||||
|
var res = null; |
||||||
|
for (var i = 0; i < allseries.length; ++i) { |
||||||
|
if (s == allseries[i]) |
||||||
|
break; |
||||||
|
|
||||||
|
if (allseries[i].stack == s.stack) |
||||||
|
res = allseries[i]; |
||||||
|
} |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
function stackData(plot, s, datapoints) { |
||||||
|
if (s.stack == null || s.stack === false) |
||||||
|
return; |
||||||
|
|
||||||
|
var other = findMatchingSeries(s, plot.getData()); |
||||||
|
if (!other) |
||||||
|
return; |
||||||
|
|
||||||
|
var ps = datapoints.pointsize, |
||||||
|
points = datapoints.points, |
||||||
|
otherps = other.datapoints.pointsize, |
||||||
|
otherpoints = other.datapoints.points, |
||||||
|
newpoints = [], |
||||||
|
px, py, intery, qx, qy, bottom, |
||||||
|
withlines = s.lines.show, |
||||||
|
horizontal = s.bars.horizontal, |
||||||
|
withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), |
||||||
|
withsteps = withlines && s.lines.steps, |
||||||
|
keyOffset = horizontal ? 1 : 0, |
||||||
|
accumulateOffset = horizontal ? 0 : 1, |
||||||
|
i = 0, j = 0, l, m; |
||||||
|
|
||||||
|
while (true) { |
||||||
|
if (i >= points.length && j >= otherpoints.length) |
||||||
|
break; |
||||||
|
|
||||||
|
l = newpoints.length; |
||||||
|
|
||||||
|
if (i < points.length && points[i] == null) { |
||||||
|
// copy gaps
|
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(points[i + m]); |
||||||
|
i += ps; |
||||||
|
} |
||||||
|
else if (i >= points.length) { |
||||||
|
// take the remaining points from the previous series
|
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(otherpoints[j + m]); |
||||||
|
if (withbottom) |
||||||
|
newpoints[l + 2] = otherpoints[j + accumulateOffset]; |
||||||
|
j += otherps; |
||||||
|
} |
||||||
|
else if (j >= otherpoints.length) { |
||||||
|
// take the remaining points from the current series
|
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(points[i + m]); |
||||||
|
i += ps; |
||||||
|
} |
||||||
|
else if (j < otherpoints.length && otherpoints[j] == null) { |
||||||
|
// ignore point
|
||||||
|
j += otherps; |
||||||
|
} |
||||||
|
else { |
||||||
|
// cases where we actually got two points
|
||||||
|
px = points[i + keyOffset]; |
||||||
|
py = points[i + accumulateOffset]; |
||||||
|
qx = otherpoints[j + keyOffset]; |
||||||
|
qy = otherpoints[j + accumulateOffset]; |
||||||
|
bottom = 0; |
||||||
|
|
||||||
|
if (px == qx) { |
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(points[i + m]); |
||||||
|
|
||||||
|
newpoints[l + accumulateOffset] += qy; |
||||||
|
bottom = qy; |
||||||
|
|
||||||
|
i += ps; |
||||||
|
j += otherps; |
||||||
|
} |
||||||
|
else if (px > qx) { |
||||||
|
// take the point from the previous series so that next series will correctly stack
|
||||||
|
if (i == 0) { |
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(otherpoints[j + m]); |
||||||
|
bottom = qy; |
||||||
|
} |
||||||
|
// we got past point below, might need to
|
||||||
|
// insert interpolated extra point
|
||||||
|
if (i > 0 && points[i - ps] != null) { |
||||||
|
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); |
||||||
|
newpoints.push(qx); |
||||||
|
newpoints.push(intery + qy); |
||||||
|
for (m = 2; m < ps; ++m) |
||||||
|
newpoints.push(points[i + m]); |
||||||
|
bottom = qy; |
||||||
|
} |
||||||
|
|
||||||
|
j += otherps; |
||||||
|
} |
||||||
|
else { // px < qx
|
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints.push(points[i + m]); |
||||||
|
|
||||||
|
// we might be able to interpolate a point below,
|
||||||
|
// this can give us a better y
|
||||||
|
if (j > 0 && otherpoints[j - otherps] != null) |
||||||
|
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); |
||||||
|
|
||||||
|
newpoints[l + accumulateOffset] += bottom; |
||||||
|
|
||||||
|
i += ps; |
||||||
|
} |
||||||
|
|
||||||
|
fromgap = false; |
||||||
|
|
||||||
|
if (l != newpoints.length && withbottom) |
||||||
|
newpoints[l + 2] = bottom; |
||||||
|
} |
||||||
|
|
||||||
|
// maintain the line steps invariant
|
||||||
|
if (withsteps && l != newpoints.length && l > 0 |
||||||
|
&& newpoints[l] != null |
||||||
|
&& newpoints[l] != newpoints[l - ps] |
||||||
|
&& newpoints[l + 1] != newpoints[l - ps + 1]) { |
||||||
|
for (m = 0; m < ps; ++m) |
||||||
|
newpoints[l + ps + m] = newpoints[l + m]; |
||||||
|
newpoints[l + 1] = newpoints[l - ps + 1]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
datapoints.points = newpoints; |
||||||
|
} |
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(stackData); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: 'stack', |
||||||
|
version: '1.2' |
||||||
|
}); |
||||||
|
})(jQuery); |
@ -0,0 +1,126 @@ |
|||||||
|
(function ($) { |
||||||
|
var options = { |
||||||
|
series: { |
||||||
|
stackpercent: null |
||||||
|
} // or number/string
|
||||||
|
}; |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
|
||||||
|
// will be built up dynamically as a hash from x-value, or y-value if horizontal
|
||||||
|
var stackBases = {}; |
||||||
|
var processed = false; |
||||||
|
var stackSums = {}; |
||||||
|
|
||||||
|
//set percentage for stacked chart
|
||||||
|
function processRawData(plot, series, data, datapoints) { |
||||||
|
if (!processed) { |
||||||
|
processed = true; |
||||||
|
stackSums = getStackSums(plot.getData()); |
||||||
|
} |
||||||
|
if (series.stackpercent == true) { |
||||||
|
var num = data.length; |
||||||
|
series.percents = []; |
||||||
|
var key_idx = 0; |
||||||
|
var value_idx = 1; |
||||||
|
if (series.bars && series.bars.horizontal && series.bars.horizontal === true) { |
||||||
|
key_idx = 1; |
||||||
|
value_idx = 0; |
||||||
|
} |
||||||
|
for (var j = 0; j < num; j++) { |
||||||
|
var sum = stackSums[data[j][key_idx] + ""]; |
||||||
|
if (sum > 0) { |
||||||
|
series.percents.push(data[j][value_idx] * 100 / sum); |
||||||
|
} else { |
||||||
|
series.percents.push(0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//calculate summary
|
||||||
|
function getStackSums(_data) { |
||||||
|
var data_len = _data.length; |
||||||
|
var sums = {}; |
||||||
|
if (data_len > 0) { |
||||||
|
//caculate summary
|
||||||
|
for (var i = 0; i < data_len; i++) { |
||||||
|
if (_data[i].stackpercent) { |
||||||
|
var key_idx = 0; |
||||||
|
var value_idx = 1; |
||||||
|
if (_data[i].bars && _data[i].bars.horizontal && _data[i].bars.horizontal === true) { |
||||||
|
key_idx = 1; |
||||||
|
value_idx = 0; |
||||||
|
} |
||||||
|
var num = _data[i].data.length; |
||||||
|
for (var j = 0; j < num; j++) { |
||||||
|
var value = 0; |
||||||
|
if (_data[i].data[j][1] != null) { |
||||||
|
value = _data[i].data[j][value_idx]; |
||||||
|
} |
||||||
|
if (sums[_data[i].data[j][key_idx] + ""]) { |
||||||
|
sums[_data[i].data[j][key_idx] + ""] += value; |
||||||
|
} else { |
||||||
|
sums[_data[i].data[j][key_idx] + ""] = value; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return sums; |
||||||
|
} |
||||||
|
|
||||||
|
function stackData(plot, s, datapoints) { |
||||||
|
if (!s.stackpercent) return; |
||||||
|
if (!processed) { |
||||||
|
stackSums = getStackSums(plot.getData()); |
||||||
|
} |
||||||
|
var newPoints = []; |
||||||
|
|
||||||
|
|
||||||
|
var key_idx = 0; |
||||||
|
var value_idx = 1; |
||||||
|
if (s.bars && s.bars.horizontal && s.bars.horizontal === true) { |
||||||
|
key_idx = 1; |
||||||
|
value_idx = 0; |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < datapoints.points.length; i += 3) { |
||||||
|
// note that the values need to be turned into absolute y-values.
|
||||||
|
// in other words, if you were to stack (x, y1), (x, y2), and (x, y3),
|
||||||
|
// (each from different series, which is where stackBases comes in),
|
||||||
|
// you'd want the new points to be (x, y1, 0), (x, y1+y2, y1), (x, y1+y2+y3, y1+y2)
|
||||||
|
// generally, (x, thisValue + (base up to this point), + (base up to this point))
|
||||||
|
if (!stackBases[datapoints.points[i + key_idx]]) { |
||||||
|
stackBases[datapoints.points[i + key_idx]] = 0; |
||||||
|
} |
||||||
|
newPoints[i + key_idx] = datapoints.points[i + key_idx]; |
||||||
|
newPoints[i + value_idx] = datapoints.points[i + value_idx] + stackBases[datapoints.points[i + key_idx]]; |
||||||
|
newPoints[i + 2] = stackBases[datapoints.points[i + key_idx]]; |
||||||
|
stackBases[datapoints.points[i + key_idx]] += datapoints.points[i + value_idx]; |
||||||
|
// change points to percentage values
|
||||||
|
// you may need to set yaxis:{ max = 100 }
|
||||||
|
if ( stackSums[newPoints[i+key_idx]+""] > 0 ){ |
||||||
|
newPoints[i + value_idx] = newPoints[i + value_idx] * 100 / stackSums[newPoints[i + key_idx] + ""]; |
||||||
|
newPoints[i + 2] = newPoints[i + 2] * 100 / stackSums[newPoints[i + key_idx] + ""]; |
||||||
|
} else { |
||||||
|
newPoints[i + value_idx] = 0; |
||||||
|
newPoints[i + 2] = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
datapoints.points = newPoints; |
||||||
|
} |
||||||
|
|
||||||
|
plot.hooks.processRawData.push(processRawData); |
||||||
|
plot.hooks.processDatapoints.push(stackData); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: 'stackpercent', |
||||||
|
version: '0.1' |
||||||
|
}); |
||||||
|
})(jQuery); |
@ -0,0 +1,431 @@ |
|||||||
|
/* Pretty handling of time axes. |
||||||
|
|
||||||
|
Copyright (c) 2007-2013 IOLA and Ole Laursen. |
||||||
|
Licensed under the MIT license. |
||||||
|
|
||||||
|
Set axis.mode to "time" to enable. See the section "Time series data" in |
||||||
|
API.txt for details. |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
(function($) { |
||||||
|
|
||||||
|
var options = { |
||||||
|
xaxis: { |
||||||
|
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||||
|
timeformat: null, // format string to use
|
||||||
|
twelveHourClock: false, // 12 or 24 time in time mode
|
||||||
|
monthNames: null // list of names of months
|
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// round to nearby lower multiple of base
|
||||||
|
|
||||||
|
function floorInBase(n, base) { |
||||||
|
return base * Math.floor(n / base); |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a string with the date d formatted according to fmt.
|
||||||
|
// A subset of the Open Group's strftime format is supported.
|
||||||
|
|
||||||
|
function formatDate(d, fmt, monthNames, dayNames) { |
||||||
|
|
||||||
|
if (typeof d.strftime == "function") { |
||||||
|
return d.strftime(fmt); |
||||||
|
} |
||||||
|
|
||||||
|
var leftPad = function(n, pad) { |
||||||
|
n = "" + n; |
||||||
|
pad = "" + (pad == null ? "0" : pad); |
||||||
|
return n.length == 1 ? pad + n : n; |
||||||
|
}; |
||||||
|
|
||||||
|
var r = []; |
||||||
|
var escape = false; |
||||||
|
var hours = d.getHours(); |
||||||
|
var isAM = hours < 12; |
||||||
|
|
||||||
|
if (monthNames == null) { |
||||||
|
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; |
||||||
|
} |
||||||
|
|
||||||
|
if (dayNames == null) { |
||||||
|
dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
||||||
|
} |
||||||
|
|
||||||
|
var hours12; |
||||||
|
|
||||||
|
if (hours > 12) { |
||||||
|
hours12 = hours - 12; |
||||||
|
} else if (hours == 0) { |
||||||
|
hours12 = 12; |
||||||
|
} else { |
||||||
|
hours12 = hours; |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < fmt.length; ++i) { |
||||||
|
|
||||||
|
var c = fmt.charAt(i); |
||||||
|
|
||||||
|
if (escape) { |
||||||
|
switch (c) { |
||||||
|
case 'a': c = "" + dayNames[d.getDay()]; break; |
||||||
|
case 'b': c = "" + monthNames[d.getMonth()]; break; |
||||||
|
case 'd': c = leftPad(d.getDate(), ""); break; |
||||||
|
case 'e': c = leftPad(d.getDate(), " "); break; |
||||||
|
case 'h': // For back-compat with 0.7; remove in 1.0
|
||||||
|
case 'H': c = leftPad(hours); break; |
||||||
|
case 'I': c = leftPad(hours12); break; |
||||||
|
case 'l': c = leftPad(hours12, " "); break; |
||||||
|
case 'm': c = leftPad(d.getMonth() + 1, ""); break; |
||||||
|
case 'M': c = leftPad(d.getMinutes()); break; |
||||||
|
// quarters not in Open Group's strftime specification
|
||||||
|
case 'q': |
||||||
|
c = "" + (Math.floor(d.getMonth() / 3) + 1); break; |
||||||
|
case 'S': c = leftPad(d.getSeconds()); break; |
||||||
|
case 'y': c = leftPad(d.getFullYear() % 100); break; |
||||||
|
case 'Y': c = "" + d.getFullYear(); break; |
||||||
|
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; |
||||||
|
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; |
||||||
|
case 'w': c = "" + d.getDay(); break; |
||||||
|
} |
||||||
|
r.push(c); |
||||||
|
escape = false; |
||||||
|
} else { |
||||||
|
if (c == "%") { |
||||||
|
escape = true; |
||||||
|
} else { |
||||||
|
r.push(c); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return r.join(""); |
||||||
|
} |
||||||
|
|
||||||
|
// To have a consistent view of time-based data independent of which time
|
||||||
|
// zone the client happens to be in we need a date-like object independent
|
||||||
|
// of time zones. This is done through a wrapper that only calls the UTC
|
||||||
|
// versions of the accessor methods.
|
||||||
|
|
||||||
|
function makeUtcWrapper(d) { |
||||||
|
|
||||||
|
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { |
||||||
|
sourceObj[sourceMethod] = function() { |
||||||
|
return targetObj[targetMethod].apply(targetObj, arguments); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
var utc = { |
||||||
|
date: d |
||||||
|
}; |
||||||
|
|
||||||
|
// support strftime, if found
|
||||||
|
|
||||||
|
if (d.strftime != undefined) { |
||||||
|
addProxyMethod(utc, "strftime", d, "strftime"); |
||||||
|
} |
||||||
|
|
||||||
|
addProxyMethod(utc, "getTime", d, "getTime"); |
||||||
|
addProxyMethod(utc, "setTime", d, "setTime"); |
||||||
|
|
||||||
|
var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; |
||||||
|
|
||||||
|
for (var p = 0; p < props.length; p++) { |
||||||
|
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); |
||||||
|
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); |
||||||
|
} |
||||||
|
|
||||||
|
return utc; |
||||||
|
}; |
||||||
|
|
||||||
|
// select time zone strategy. This returns a date-like object tied to the
|
||||||
|
// desired timezone
|
||||||
|
|
||||||
|
function dateGenerator(ts, opts) { |
||||||
|
if (opts.timezone == "browser") { |
||||||
|
return new Date(ts); |
||||||
|
} else if (!opts.timezone || opts.timezone == "utc") { |
||||||
|
return makeUtcWrapper(new Date(ts)); |
||||||
|
} else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { |
||||||
|
var d = new timezoneJS.Date(); |
||||||
|
// timezone-js is fickle, so be sure to set the time zone before
|
||||||
|
// setting the time.
|
||||||
|
d.setTimezone(opts.timezone); |
||||||
|
d.setTime(ts); |
||||||
|
return d; |
||||||
|
} else { |
||||||
|
return makeUtcWrapper(new Date(ts)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// map of app. size of time units in milliseconds
|
||||||
|
|
||||||
|
var timeUnitSize = { |
||||||
|
"second": 1000, |
||||||
|
"minute": 60 * 1000, |
||||||
|
"hour": 60 * 60 * 1000, |
||||||
|
"day": 24 * 60 * 60 * 1000, |
||||||
|
"month": 30 * 24 * 60 * 60 * 1000, |
||||||
|
"quarter": 3 * 30 * 24 * 60 * 60 * 1000, |
||||||
|
"year": 365.2425 * 24 * 60 * 60 * 1000 |
||||||
|
}; |
||||||
|
|
||||||
|
// the allowed tick sizes, after 1 year we use
|
||||||
|
// an integer algorithm
|
||||||
|
|
||||||
|
var baseSpec = [ |
||||||
|
[1, "second"], [2, "second"], [5, "second"], [10, "second"], |
||||||
|
[30, "second"], |
||||||
|
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], |
||||||
|
[30, "minute"], |
||||||
|
[1, "hour"], [2, "hour"], [4, "hour"], |
||||||
|
[8, "hour"], [12, "hour"], |
||||||
|
[1, "day"], [2, "day"], [3, "day"], |
||||||
|
[0.25, "month"], [0.5, "month"], [1, "month"], |
||||||
|
[2, "month"] |
||||||
|
]; |
||||||
|
|
||||||
|
// we don't know which variant(s) we'll need yet, but generating both is
|
||||||
|
// cheap
|
||||||
|
|
||||||
|
var specMonths = baseSpec.concat([[3, "month"], [6, "month"], |
||||||
|
[1, "year"]]); |
||||||
|
var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], |
||||||
|
[1, "year"]]); |
||||||
|
|
||||||
|
function init(plot) { |
||||||
|
plot.hooks.processOptions.push(function (plot, options) { |
||||||
|
$.each(plot.getAxes(), function(axisName, axis) { |
||||||
|
|
||||||
|
var opts = axis.options; |
||||||
|
|
||||||
|
if (opts.mode == "time") { |
||||||
|
axis.tickGenerator = function(axis) { |
||||||
|
|
||||||
|
var ticks = []; |
||||||
|
var d = dateGenerator(axis.min, opts); |
||||||
|
var minSize = 0; |
||||||
|
|
||||||
|
// make quarter use a possibility if quarters are
|
||||||
|
// mentioned in either of these options
|
||||||
|
|
||||||
|
var spec = (opts.tickSize && opts.tickSize[1] === |
||||||
|
"quarter") || |
||||||
|
(opts.minTickSize && opts.minTickSize[1] === |
||||||
|
"quarter") ? specQuarters : specMonths; |
||||||
|
|
||||||
|
if (opts.minTickSize != null) { |
||||||
|
if (typeof opts.tickSize == "number") { |
||||||
|
minSize = opts.tickSize; |
||||||
|
} else { |
||||||
|
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < spec.length - 1; ++i) { |
||||||
|
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] |
||||||
|
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 |
||||||
|
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var size = spec[i][0]; |
||||||
|
var unit = spec[i][1]; |
||||||
|
|
||||||
|
// special-case the possibility of several years
|
||||||
|
|
||||||
|
if (unit == "year") { |
||||||
|
|
||||||
|
// if given a minTickSize in years, just use it,
|
||||||
|
// ensuring that it's an integer
|
||||||
|
|
||||||
|
if (opts.minTickSize != null && opts.minTickSize[1] == "year") { |
||||||
|
size = Math.floor(opts.minTickSize[0]); |
||||||
|
} else { |
||||||
|
|
||||||
|
var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); |
||||||
|
var norm = (axis.delta / timeUnitSize.year) / magn; |
||||||
|
|
||||||
|
if (norm < 1.5) { |
||||||
|
size = 1; |
||||||
|
} else if (norm < 3) { |
||||||
|
size = 2; |
||||||
|
} else if (norm < 7.5) { |
||||||
|
size = 5; |
||||||
|
} else { |
||||||
|
size = 10; |
||||||
|
} |
||||||
|
|
||||||
|
size *= magn; |
||||||
|
} |
||||||
|
|
||||||
|
// minimum size for years is 1
|
||||||
|
|
||||||
|
if (size < 1) { |
||||||
|
size = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
axis.tickSize = opts.tickSize || [size, unit]; |
||||||
|
var tickSize = axis.tickSize[0]; |
||||||
|
unit = axis.tickSize[1]; |
||||||
|
|
||||||
|
var step = tickSize * timeUnitSize[unit]; |
||||||
|
|
||||||
|
if (unit == "second") { |
||||||
|
d.setSeconds(floorInBase(d.getSeconds(), tickSize)); |
||||||
|
} else if (unit == "minute") { |
||||||
|
d.setMinutes(floorInBase(d.getMinutes(), tickSize)); |
||||||
|
} else if (unit == "hour") { |
||||||
|
d.setHours(floorInBase(d.getHours(), tickSize)); |
||||||
|
} else if (unit == "month") { |
||||||
|
d.setMonth(floorInBase(d.getMonth(), tickSize)); |
||||||
|
} else if (unit == "quarter") { |
||||||
|
d.setMonth(3 * floorInBase(d.getMonth() / 3, |
||||||
|
tickSize)); |
||||||
|
} else if (unit == "year") { |
||||||
|
d.setFullYear(floorInBase(d.getFullYear(), tickSize)); |
||||||
|
} |
||||||
|
|
||||||
|
// reset smaller components
|
||||||
|
|
||||||
|
d.setMilliseconds(0); |
||||||
|
|
||||||
|
if (step >= timeUnitSize.minute) { |
||||||
|
d.setSeconds(0); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.hour) { |
||||||
|
d.setMinutes(0); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.day) { |
||||||
|
d.setHours(0); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.day * 4) { |
||||||
|
d.setDate(1); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.month * 2) { |
||||||
|
d.setMonth(floorInBase(d.getMonth(), 3)); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.quarter * 2) { |
||||||
|
d.setMonth(floorInBase(d.getMonth(), 6)); |
||||||
|
} |
||||||
|
if (step >= timeUnitSize.year) { |
||||||
|
d.setMonth(0); |
||||||
|
} |
||||||
|
|
||||||
|
var carry = 0; |
||||||
|
var v = Number.NaN; |
||||||
|
var prev; |
||||||
|
|
||||||
|
do { |
||||||
|
|
||||||
|
prev = v; |
||||||
|
v = d.getTime(); |
||||||
|
ticks.push(v); |
||||||
|
|
||||||
|
if (unit == "month" || unit == "quarter") { |
||||||
|
if (tickSize < 1) { |
||||||
|
|
||||||
|
// a bit complicated - we'll divide the
|
||||||
|
// month/quarter up but we need to take
|
||||||
|
// care of fractions so we don't end up in
|
||||||
|
// the middle of a day
|
||||||
|
|
||||||
|
d.setDate(1); |
||||||
|
var start = d.getTime(); |
||||||
|
d.setMonth(d.getMonth() + |
||||||
|
(unit == "quarter" ? 3 : 1)); |
||||||
|
var end = d.getTime(); |
||||||
|
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); |
||||||
|
carry = d.getHours(); |
||||||
|
d.setHours(0); |
||||||
|
} else { |
||||||
|
d.setMonth(d.getMonth() + |
||||||
|
tickSize * (unit == "quarter" ? 3 : 1)); |
||||||
|
} |
||||||
|
} else if (unit == "year") { |
||||||
|
d.setFullYear(d.getFullYear() + tickSize); |
||||||
|
} else { |
||||||
|
d.setTime(v + step); |
||||||
|
} |
||||||
|
} while (v < axis.max && v != prev); |
||||||
|
|
||||||
|
return ticks; |
||||||
|
}; |
||||||
|
|
||||||
|
axis.tickFormatter = function (v, axis) { |
||||||
|
|
||||||
|
var d = dateGenerator(v, axis.options); |
||||||
|
|
||||||
|
// first check global format
|
||||||
|
|
||||||
|
if (opts.timeformat != null) { |
||||||
|
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); |
||||||
|
} |
||||||
|
|
||||||
|
// possibly use quarters if quarters are mentioned in
|
||||||
|
// any of these places
|
||||||
|
|
||||||
|
var useQuarters = (axis.options.tickSize && |
||||||
|
axis.options.tickSize[1] == "quarter") || |
||||||
|
(axis.options.minTickSize && |
||||||
|
axis.options.minTickSize[1] == "quarter"); |
||||||
|
|
||||||
|
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; |
||||||
|
var span = axis.max - axis.min; |
||||||
|
var suffix = (opts.twelveHourClock) ? " %p" : ""; |
||||||
|
var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; |
||||||
|
var fmt; |
||||||
|
|
||||||
|
if (t < timeUnitSize.minute) { |
||||||
|
fmt = hourCode + ":%M:%S" + suffix; |
||||||
|
} else if (t < timeUnitSize.day) { |
||||||
|
if (span < 2 * timeUnitSize.day) { |
||||||
|
fmt = hourCode + ":%M" + suffix; |
||||||
|
} else { |
||||||
|
fmt = "%b %d " + hourCode + ":%M" + suffix; |
||||||
|
} |
||||||
|
} else if (t < timeUnitSize.month) { |
||||||
|
fmt = "%b %d"; |
||||||
|
} else if ((useQuarters && t < timeUnitSize.quarter) || |
||||||
|
(!useQuarters && t < timeUnitSize.year)) { |
||||||
|
if (span < timeUnitSize.year) { |
||||||
|
fmt = "%b"; |
||||||
|
} else { |
||||||
|
fmt = "%b %Y"; |
||||||
|
} |
||||||
|
} else if (useQuarters && t < timeUnitSize.year) { |
||||||
|
if (span < timeUnitSize.year) { |
||||||
|
fmt = "Q%q"; |
||||||
|
} else { |
||||||
|
fmt = "Q%q %Y"; |
||||||
|
} |
||||||
|
} else { |
||||||
|
fmt = "%Y"; |
||||||
|
} |
||||||
|
|
||||||
|
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); |
||||||
|
|
||||||
|
return rt; |
||||||
|
}; |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
$.plot.plugins.push({ |
||||||
|
init: init, |
||||||
|
options: options, |
||||||
|
name: 'time', |
||||||
|
version: '1.0' |
||||||
|
}); |
||||||
|
|
||||||
|
// Time-axis support used to be in Flot core, which exposed the
|
||||||
|
// formatDate function on the plot object. Various plugins depend
|
||||||
|
// on the function, so we need to re-expose it here.
|
||||||
|
|
||||||
|
$.plot.formatDate = formatDate; |
||||||
|
|
||||||
|
})(jQuery); |
@ -0,0 +1,33 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"moduleResolution": "node", |
||||||
|
"outDir": "dist", |
||||||
|
"target": "es5", |
||||||
|
"lib": [ |
||||||
|
"es6", |
||||||
|
"dom" |
||||||
|
], |
||||||
|
"rootDir": "./src", |
||||||
|
"module": "esnext", |
||||||
|
"declaration": false, |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"inlineSourceMap": false, |
||||||
|
"sourceMap": true, |
||||||
|
"noEmitOnError": false, |
||||||
|
"emitDecoratorMetadata": false, |
||||||
|
"experimentalDecorators": true, |
||||||
|
"noImplicitReturns": true, |
||||||
|
"noImplicitThis": false, |
||||||
|
"noImplicitUseStrict": false, |
||||||
|
"noImplicitAny": false, |
||||||
|
"noUnusedLocals": false, |
||||||
|
"baseUrl": "./src", |
||||||
|
"allowUnreachableCode": true, |
||||||
|
"paths": { |
||||||
|
"@": ["."] |
||||||
|
} |
||||||
|
}, |
||||||
|
"include": [ |
||||||
|
"./src/**/*.ts" |
||||||
|
] |
||||||
|
} |
Loading…
Reference in new issue