Browse Source

Add src

master
rozetko 6 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.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);
}
}
}

310
src/graph_tooltip.ts

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

54
src/histogram.ts

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

186
src/img/icn-graph-panel.svg

@ -0,0 +1,186 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<polyline style="fill:none;stroke:#898989;stroke-width:2;stroke-miterlimit:10;" points="4.734,34.349 36.05,19.26 64.876,36.751
96.308,6.946 "/>
<circle style="fill:#898989;" cx="4.885" cy="33.929" r="4.885"/>
<circle style="fill:#898989;" cx="35.95" cy="19.545" r="4.885"/>
<circle style="fill:#898989;" cx="65.047" cy="36.046" r="4.885"/>
<circle style="fill:#898989;" cx="94.955" cy="7.135" r="4.885"/>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5" y1="103.7019" x2="5" y2="32.0424">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_1_);" d="M9.001,48.173H0.999C0.447,48.173,0,48.62,0,49.172V100h10V49.172
C10,48.62,9.553,48.173,9.001,48.173z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5" y1="98.9423" x2="5" y2="53.1961">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_2_);" d="M0,69.173v30.563h10V69.173"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="5" y1="99.4343" x2="5" y2="74.4359">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_3_);" d="M0,83.166v16.701h10V83.166"/>
</g>
<g>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="20" y1="103.7019" x2="20" y2="32.0424">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_4_);" d="M24.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
C25,41.216,24.553,40.769,24.001,40.769z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="20" y1="98.9423" x2="20" y2="53.1961">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_5_);" d="M15,64.716v35.02h10v-35.02"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="20" y1="99.4343" x2="20" y2="74.4359">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_6_);" d="M15,80.731v19.137h10V80.731"/>
</g>
<g>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="35" y1="103.7019" x2="35" y2="32.0424">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_7_);" d="M39.001,34.423h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V35.422
C40,34.87,39.553,34.423,39.001,34.423z"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="35" y1="98.9423" x2="35" y2="53.1961">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_8_);" d="M30,60.895v38.84h10v-38.84"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="35" y1="99.4343" x2="35" y2="74.4359">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_9_);" d="M30,78.643v21.225h10V78.643"/>
</g>
<g>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="50" y1="103.7019" x2="50" y2="32.0424">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_10_);" d="M54.001,41.827h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V42.826
C55,42.274,54.553,41.827,54.001,41.827z"/>
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="50" y1="98.9423" x2="50" y2="53.1961">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_11_);" d="M45,65.352v34.383h10V65.352"/>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="50" y1="99.4343" x2="50" y2="74.4359">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_12_);" d="M45,81.079v18.789h10V81.079"/>
</g>
<g>
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="65" y1="103.8575" x2="65" y2="29.1875">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_13_);" d="M69.001,50.404h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V51.403
C70,50.851,69.553,50.404,69.001,50.404z"/>
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="65" y1="98.8979" x2="65" y2="51.2298">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_14_);" d="M60,70.531v29.193h10V70.531"/>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="65" y1="99.4105" x2="65" y2="73.3619">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_15_);" d="M60,83.909v15.953h10V83.909"/>
</g>
<g>
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="80" y1="104.4108" x2="80" y2="19.0293">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_16_);" d="M84.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
C85,41.216,84.553,40.769,84.001,40.769z"/>
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="80" y1="98.9423" x2="80" y2="53.1961">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_17_);" d="M75,64.716v35.02h10v-35.02"/>
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="80" y1="99.4343" x2="80" y2="74.4359">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_18_);" d="M75,80.731v19.137h10V80.731"/>
</g>
<g>
<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="95" y1="103.5838" x2="95" y2="34.2115">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="0" style="stop-color:#FFD53F"/>
<stop offset="0" style="stop-color:#FBBC40"/>
<stop offset="0" style="stop-color:#F7A840"/>
<stop offset="0" style="stop-color:#F59B40"/>
<stop offset="0" style="stop-color:#F3933F"/>
<stop offset="0" style="stop-color:#F3903F"/>
<stop offset="0.8423" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path style="fill:url(#SVGID_19_);" d="M99.001,21.157h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V22.156
C100,21.604,99.553,21.157,99.001,21.157z"/>
<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="95" y1="98.9761" x2="95" y2="54.69">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#F99B1C"/>
</linearGradient>
<path style="fill:url(#SVGID_20_);" d="M90,52.898v46.846h10V52.898"/>
<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="95" y1="99.4524" x2="95" y2="75.2518">
<stop offset="0" style="stop-color:#FEBC11"/>
<stop offset="1" style="stop-color:#FFDE17"/>
</linearGradient>
<path style="fill:url(#SVGID_21_);" d="M90,74.272v25.6h10v-25.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

159
src/model/anomaly.ts

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

58
src/model/metric.ts

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

39
src/model/segment.ts

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

101
src/model/segment_array.ts

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

14
src/model/segment_set.ts

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

547
src/module.ts

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

72
src/partials/axes_editor.html

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

144
src/partials/tab_analytics.html

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

135
src/partials/tab_display.html

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

73
src/partials/tab_legend.html

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

21
src/plugin.json

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

160
src/series_overrides_ctrl.ts

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

100
src/services/anomaly_service.ts

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

8
src/template.ts

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

238
src/threshold_manager.ts

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

142
src/thresholds_form.ts

@ -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>&nbsp;Add Threshold
</button>
</div>
</div>
</div>
`;
coreModule.directive('grafalysGraphThresholdForm', function() {
return {
restrict: 'E',
template: template,
controller: ThresholdFormCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
panelCtrl: '=',
},
};
});

176
src/vendor/flot/jquery.flot.crosshair.js vendored

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

236
src/vendor/flot/jquery.flot.dashes.js vendored

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

604
src/vendor/flot/jquery.flot.events.js vendored

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

288
src/vendor/flot/jquery.flot.fillbelow.js vendored

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

225
src/vendor/flot/jquery.flot.fillbetween.js vendored

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

960
src/vendor/flot/jquery.flot.gauge.js vendored

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

3198
src/vendor/flot/jquery.flot.js vendored

File diff suppressed because it is too large Load Diff

817
src/vendor/flot/jquery.flot.pie.js vendored

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

380
src/vendor/flot/jquery.flot.selection.js vendored

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

190
src/vendor/flot/jquery.flot.stack.js vendored

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

126
src/vendor/flot/jquery.flot.stackpercent.js vendored

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

431
src/vendor/flot/jquery.flot.time.js vendored

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

33
tsconfig.json

@ -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…
Cancel
Save