Browse Source

Add src

master
rozetko 7 years ago
parent
commit
d607d7857c
  1. 38
      .gitignore
  2. 26
      README.md
  3. 51
      build/webpack.base.conf.js
  4. 9
      build/webpack.dev.conf.js
  5. 6
      build/webpack.prod.conf.js
  6. 158
      examples/cpu_util.json
  7. 8159
      package-lock.json
  8. 40
      package.json
  9. 88
      src/axes_editor.ts
  10. 81
      src/colors.ts
  11. 348
      src/controllers/anomaly_controller.ts
  12. 214
      src/data_processor.ts
  13. 253
      src/graph_legend.ts
  14. 843
      src/graph_renderer.ts
  15. 310
      src/graph_tooltip.ts
  16. 54
      src/histogram.ts
  17. 186
      src/img/icn-graph-panel.svg
  18. 159
      src/model/anomaly.ts
  19. 58
      src/model/metric.ts
  20. 39
      src/model/segment.ts
  21. 101
      src/model/segment_array.ts
  22. 14
      src/model/segment_set.ts
  23. 547
      src/module.ts
  24. 72
      src/partials/axes_editor.html
  25. 144
      src/partials/tab_analytics.html
  26. 135
      src/partials/tab_display.html
  27. 73
      src/partials/tab_legend.html
  28. 21
      src/plugin.json
  29. 160
      src/series_overrides_ctrl.ts
  30. 100
      src/services/anomaly_service.ts
  31. 8
      src/template.ts
  32. 238
      src/threshold_manager.ts
  33. 142
      src/thresholds_form.ts
  34. 176
      src/vendor/flot/jquery.flot.crosshair.js
  35. 236
      src/vendor/flot/jquery.flot.dashes.js
  36. 604
      src/vendor/flot/jquery.flot.events.js
  37. 288
      src/vendor/flot/jquery.flot.fillbelow.js
  38. 225
      src/vendor/flot/jquery.flot.fillbetween.js
  39. 960
      src/vendor/flot/jquery.flot.gauge.js
  40. 3198
      src/vendor/flot/jquery.flot.js
  41. 817
      src/vendor/flot/jquery.flot.pie.js
  42. 380
      src/vendor/flot/jquery.flot.selection.js
  43. 190
      src/vendor/flot/jquery.flot.stack.js
  44. 126
      src/vendor/flot/jquery.flot.stackpercent.js
  45. 431
      src/vendor/flot/jquery.flot.time.js
  46. 33
      tsconfig.json

38
.gitignore vendored

@ -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/

26
README.md

@ -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)

51
build/webpack.base.conf.js

@ -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/,
}
]
}
}

9
build/webpack.dev.conf.js

@ -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;

6
build/webpack.prod.conf.js

@ -0,0 +1,6 @@
const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.mode = 'development'; // cuz production wont work
module.exports = baseWebpackConfig;

158
examples/cpu_util.json

@ -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
}
]
}

8159
package-lock.json generated

File diff suppressed because it is too large Load Diff

40
package.json

@ -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"
}
}

88
src/axes_editor.ts

@ -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,
};
}

81
src/colors.ts

@ -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;

348
src/controllers/anomaly_controller.ts

@ -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;
}
}

214
src/data_processor.ts

@ -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;
}
}

253
src/graph_legend.ts

@ -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();
}
}
}

843
src/graph_renderer.ts

@ -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.<