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