mirror of https://github.com/grafana/grafana
commit
86c83d7903
@ -0,0 +1,43 @@ |
||||
<h4>Charted</h4> |
||||
<div ng-include src="'app/partials/querySelect.html'"></div> |
||||
|
||||
<div class="editor-row"> |
||||
<h4>Markers</h4> |
||||
|
||||
<div class="small"> |
||||
Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed. |
||||
</div> |
||||
<style> |
||||
.querySelect .query { |
||||
margin-right: 5px; |
||||
} |
||||
.querySelect .selected { |
||||
border: 3px solid; |
||||
} |
||||
.querySelect .unselected { |
||||
border: 0px solid; |
||||
} |
||||
</style> |
||||
<p> |
||||
<div class="editor-option"> |
||||
<label class="small">Enable</label> |
||||
<input type="checkbox" ng-change="set_refresh(true)" ng-model="panel.annotate.enable" ng-checked="panel.annotate.enable"> |
||||
</div> |
||||
<div class="editor-option" ng-show="panel.annotate.enable"> |
||||
<label class="small">Marker Query</label> |
||||
<input type="text" ng-change="set_refresh(true)" class="input-large" ng-model="panel.annotate.query"/> |
||||
</div> |
||||
<div class="editor-option" ng-show="panel.annotate.enable"> |
||||
<label class="small">Tooltip field</label> |
||||
<input type="text" class="input-small" ng-model="panel.annotate.field" bs-typeahead="fields.list"/> |
||||
</div> |
||||
<div class="editor-option" ng-show="panel.annotate.enable"> |
||||
<label class="small">Limit <tip>Max markers on the chart</tip></label> |
||||
<input type="number" class="input-mini" ng-model="panel.annotate.size" ng-change="set_refresh(true)"/> |
||||
</div> |
||||
<div class="editor-option" ng-show="panel.annotate.enable"> |
||||
<label class="small">Sort <tip>Determine the most relevant markers using this field</tip></label> |
||||
<input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.annotate.sort[0]" ng-change="set_refresh(true)" /> |
||||
<i ng-click="panel.annotate.sort[1] = _.toggle(panel.annotate.sort[1],'desc','asc');set_refresh(true)" ng-class="{'icon-chevron-up': panel.annotate.sort[1] == 'asc','icon-chevron-down': panel.annotate.sort[1] == 'desc'}"></i> |
||||
</div> |
||||
</div> |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,641 @@ |
||||
/** |
||||
* Flot plugin for adding 'events' to the plot. |
||||
* |
||||
* Events are small icons drawn onto the graph that represent something happening at that time. |
||||
* |
||||
* This plugin adds the following options to flot: |
||||
* |
||||
* options = { |
||||
* events: { |
||||
* levels: int // number of hierarchy levels
|
||||
* data: [], // array of event objects
|
||||
* types: [] // array of icons
|
||||
* xaxis: int // the x axis to attach events to
|
||||
* } |
||||
* }; |
||||
* |
||||
* |
||||
* An event is a javascript object in the following form: |
||||
* |
||||
* { |
||||
* min: startTime, |
||||
* max: endTime, |
||||
* eventType: "type", |
||||
* title: "event title", |
||||
* description: "event description" |
||||
* } |
||||
* |
||||
* Types is an array of javascript objects in the following form: |
||||
* |
||||
* types: [ |
||||
* { |
||||
* eventType: "eventType", |
||||
* level: hierarchicalLevel, |
||||
* icon: { |
||||
image: "eventImage1.png", |
||||
* width: 10, |
||||
* height: 10 |
||||
* } |
||||
* } |
||||
* ] |
||||
* |
||||
* @author Joel Oughton |
||||
*/ |
||||
(function($){ |
||||
function init(plot){ |
||||
var DEFAULT_ICON = { |
||||
icon: "icon-caret-up", |
||||
size: 20, |
||||
width: 19, |
||||
height: 10 |
||||
}; |
||||
|
||||
var _events = [], _types, _eventsEnabled = false, lastRange; |
||||
|
||||
plot.getEvents = function(){ |
||||
return _events; |
||||
}; |
||||
|
||||
plot.hideEvents = function(levelRange){ |
||||
|
||||
$.each(_events, function(index, event){ |
||||
if (_withinHierarchy(event.level(), levelRange)) { |
||||
event.visual().getObject().hide(); |
||||
} |
||||
}); |
||||
|
||||
}; |
||||
|
||||
plot.showEvents = function(levelRange){ |
||||
plot.hideEvents(); |
||||
|
||||
$.each(_events, function(index, event){ |
||||
if (!_withinHierarchy(event.level(), levelRange)) { |
||||
event.hide(); |
||||
} |
||||
}); |
||||
|
||||
_drawEvents(); |
||||
}; |
||||
|
||||
plot.hooks.processOptions.push(function(plot, options){ |
||||
// enable the plugin
|
||||
if (options.events.data != null) { |
||||
_eventsEnabled = true; |
||||
} |
||||
}); |
||||
|
||||
plot.hooks.draw.push(function(plot, canvascontext){ |
||||
var options = plot.getOptions(); |
||||
var xaxis = plot.getXAxes()[options.events.xaxis - 1]; |
||||
|
||||
if (_eventsEnabled) { |
||||
|
||||
// check for first run
|
||||
if (_events.length < 1) { |
||||
|
||||
_lastRange = xaxis.max - xaxis.min; |
||||
|
||||
// check for clustering
|
||||
if (options.events.clustering) { |
||||
var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min); |
||||
_types = ed.types; |
||||
_setupEvents(ed.data); |
||||
} else { |
||||
_types = options.events.types; |
||||
_setupEvents(options.events.data); |
||||
} |
||||
|
||||
} else { |
||||
/*if (options.events.clustering) { |
||||
_clearEvents(); |
||||
var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min); |
||||
_types = ed.types; |
||||
_setupEvents(ed.data); |
||||
}*/ |
||||
_updateEvents(); |
||||
} |
||||
} |
||||
|
||||
_drawEvents(); |
||||
}); |
||||
|
||||
var _drawEvents = function() { |
||||
var o = plot.getPlotOffset(); |
||||
var pleft = o.left, pright = plot.width() - o.right; |
||||
|
||||
$.each(_events, function(index, event){ |
||||
|
||||
// check event is inside the graph range and inside the hierarchy level
|
||||
if (_insidePlot(event.getOptions().min) && |
||||
!event.isHidden()) { |
||||
event.visual().draw(); |
||||
} else { |
||||
event.visual().getObject().hide(); |
||||
} |
||||
}); |
||||
|
||||
_identicalStarts(); |
||||
_overlaps(); |
||||
}; |
||||
|
||||
var _withinHierarchy = function(level, levelRange){ |
||||
var range = {}; |
||||
|
||||
if (!levelRange) { |
||||
range.start = 0; |
||||
range.end = _events.length - 1; |
||||
} else { |
||||
range.start = (levelRange.min == undefined) ? 0 : levelRange.min; |
||||
range.end = (levelRange.max == undefined) ? _events.length - 1 : levelRange.max; |
||||
} |
||||
|
||||
if (level >= range.start && level <= range.end) { |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
var _clearEvents = function(){ |
||||
$.each(_events, function(index, val) { |
||||
val.visual().clear(); |
||||
}); |
||||
|
||||
_events = []; |
||||
}; |
||||
|
||||
var _updateEvents = function() { |
||||
var o = plot.getPlotOffset(), left, top; |
||||
var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1]; |
||||
|
||||
$.each(_events, function(index, event) { |
||||
top = o.top + plot.height() - event.visual().height(); |
||||
left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2; |
||||
|
||||
event.visual().moveTo({ top: top, left: left }); |
||||
}); |
||||
}; |
||||
|
||||
var _showTooltip = function(x, y, event){ |
||||
/* |
||||
var tooltip = $('<div id="tooltip" class=""></div>').appendTo('body').fadeIn(200); |
||||
|
||||
$('<div id="title">' + event.title + '</div>').appendTo(tooltip); |
||||
$('<div id="type">Type: ' + event.eventType + '</div>').appendTo(tooltip); |
||||
$('<div id="description">' + event.description + '</div>').appendTo(tooltip); |
||||
|
||||
tooltip.css({ |
||||
top: y - tooltip.height() - 5, |
||||
left: x |
||||
}); |
||||
console.log(tooltip); |
||||
*/ |
||||
|
||||
// @rashidkpc - hack to work with our normal tooltip placer
|
||||
var $tooltip = $('<div id="tooltip">'); |
||||
if (event) { |
||||
$tooltip |
||||
.html(event.description) |
||||
.place_tt(x, y, { |
||||
offset: 10 |
||||
}); |
||||
} else { |
||||
$tooltip.remove(); |
||||
} |
||||
}; |
||||
|
||||
var _setupEvents = function(events){ |
||||
|
||||
$.each(events, function(index, event){ |
||||
var level = (plot.getOptions().events.levels == null || !_types || !_types[event.eventType]) ? 0 : _types[event.eventType].level; |
||||
|
||||
if (level > plot.getOptions().events.levels) { |
||||
throw "A type's level has exceeded the maximum. Level=" + |
||||
level + |
||||
", Max levels:" + |
||||
(plot.getOptions().events.levels); |
||||
} |
||||
|
||||
_events.push(new VisualEvent(event, _buildDiv(event), level)); |
||||
}); |
||||
|
||||
_events.sort(compareEvents); |
||||
}; |
||||
|
||||
var _identicalStarts = function() { |
||||
var ranges = [], range = {}, event, prev, offset = 0; |
||||
|
||||
$.each(_events, function(index, val) { |
||||
|
||||
if (prev) { |
||||
if (val.getOptions().min == prev.getOptions().min) { |
||||
|
||||
if (!range.min) { |
||||
range.min = index; |
||||
} |
||||
range.max = index; |
||||
} else { |
||||
if (range.min) { |
||||
ranges.push(range); |
||||
range = {}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
prev = val; |
||||
}); |
||||
|
||||
if (range.min) { |
||||
ranges.push(range); |
||||
} |
||||
|
||||
$.each(ranges, function(index, val) { |
||||
var removed = _events.splice(val.min - offset, val.max - val.min + 1); |
||||
|
||||
$.each(removed, function(index, val) { |
||||
val.visual().clear(); |
||||
}); |
||||
|
||||
offset += val.max - val.min + 1; |
||||
}); |
||||
}; |
||||
|
||||
var _overlaps = function() { |
||||
var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1]; |
||||
var range, diff, cmid, pmid, left = 0, right = -1; |
||||
pright = plot.width() - plot.getPlotOffset().right; |
||||
|
||||
// coverts a clump of events into a single vertical line
|
||||
var processClump = function() { |
||||
// find the middle x value
|
||||
pmid = _events[right].getOptions().min - |
||||
(_events[right].getOptions().min - _events[left].getOptions().min) / 2; |
||||
|
||||
cmid = xaxis.p2c(pmid); |
||||
|
||||
// hide the events between the discovered range
|
||||
while (left <= right) { |
||||
_events[left++].visual().getObject().hide(); |
||||
} |
||||
|
||||
// draw a vertical line in the middle of where they are
|
||||
if (_insidePlot(pmid)) { |
||||
_drawLine('#000', 1, { x: cmid, y: 0 }, { x: cmid, y: plot.height() }); |
||||
|
||||
} |
||||
}; |
||||
|
||||
if (xaxis.min && xaxis.max) { |
||||
range = xaxis.max - xaxis.min; |
||||
|
||||
for (var i = 1; i < _events.length; i++) { |
||||
diff = _events[i].getOptions().min - _events[i - 1].getOptions().min; |
||||
|
||||
if (diff / range > 0.007) { //enough variance
|
||||
// has a clump has been found
|
||||
if (right != -1) { |
||||
//processClump();
|
||||
} |
||||
right = -1; |
||||
left = i; |
||||
} else { // not enough variance
|
||||
right = i; |
||||
// handle to final case
|
||||
if (i == _events.length - 1) { |
||||
//processClump();
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
var _buildDiv = function(event){ |
||||
//var po = plot.pointOffset({ x: 450, y: 1});
|
||||
var container = plot.getPlaceholder(), o = plot.getPlotOffset(), yaxis, |
||||
xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1], axes = plot.getAxes(); |
||||
var top, left, div, icon, level, drawableEvent; |
||||
|
||||
// determine the y axis used
|
||||
if (axes.yaxis && axes.yaxis.used) yaxis = axes.yaxis; |
||||
if (axes.yaxis2 && axes.yaxis2.used) yaxis = axes.yaxis2; |
||||
|
||||
// use the default icon and level
|
||||
if (_types == null || !_types[event.eventType] || !_types[event.eventType].icon) { |
||||
icon = DEFAULT_ICON; |
||||
level = 0; |
||||
} else { |
||||
icon = _types[event.eventType].icon; |
||||
level = _types[event.eventType].level; |
||||
} |
||||
|
||||
div = $('<i style="position:absolute" class="'+icon.icon+'"></i>').appendTo(container); |
||||
|
||||
top = o.top + plot.height() - icon.size + 1; |
||||
left = xaxis.p2c(event.min) + o.left - icon.size / 2; |
||||
|
||||
div.css({ |
||||
left: left + 'px', |
||||
top: top, |
||||
color: icon.color, |
||||
"text-shadow" : "1px 1px "+icon.outline+", -1px -1px "+icon.outline+", -1px 1px "+icon.outline+", 1px -1px "+icon.outline, |
||||
'font-size': icon['size']+'px', |
||||
}); |
||||
div.hide(); |
||||
div.data({ |
||||
"event": event |
||||
}); |
||||
div.hover( |
||||
// mouseenter
|
||||
function(){ |
||||
var pos = $(this).offset(); |
||||
|
||||
/*// check if the mouse is not already over the event |
||||
if ($(this).data("bouncing") == false || $(this).data("bouncing") == undefined) { |
||||
|
||||
// check the div is not already bouncing
|
||||
if ($(this).position().top == $(this).data("top")) { |
||||
$(this).effect("bounce", { |
||||
times: 3 |
||||
}, 300); |
||||
} |
||||
|
||||
$(this).data("bouncing", true); |
||||
_showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event")); |
||||
}*/ |
||||
|
||||
_showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event")); |
||||
|
||||
if (event.min != event.max) { |
||||
plot.setSelection({ |
||||
xaxis: { |
||||
from: event.min, |
||||
to: event.max |
||||
}, |
||||
yaxis: { |
||||
from: yaxis.min, |
||||
to: yaxis.max |
||||
} |
||||
}); |
||||
} |
||||
}, |
||||
// mouseleave
|
||||
function(){ |
||||
//$(this).data("bouncing", false);
|
||||
$('#tooltip').remove(); |
||||
plot.clearSelection(); |
||||
}); |
||||
|
||||
drawableEvent = new DrawableEvent( |
||||
div, |
||||
function(obj){ |
||||
obj.show(); |
||||
}, |
||||
function(obj){ |
||||
obj.remove(); |
||||
}, |
||||
function(obj, position){ |
||||
obj.css({ |
||||
top: position.top, |
||||
left: position.left |
||||
}); |
||||
}, |
||||
left, top, div.width(), div.height()); |
||||
|
||||
return drawableEvent; |
||||
}; |
||||
|
||||
var _getEventsAtPos = function(x, y){ |
||||
var found = [], left, top, width, height; |
||||
|
||||
$.each(_events, function(index, val){ |
||||
|
||||
left = val.div.offset().left; |
||||
top = val.div.offset().top; |
||||
width = val.div.width(); |
||||
height = val.div.height(); |
||||
|
||||
if (x >= left && x <= left + width && y >= top && y <= top + height) { |
||||
found.push(val); |
||||
} |
||||
|
||||
return found; |
||||
}); |
||||
}; |
||||
|
||||
var _insidePlot = function(x) { |
||||
var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1]; |
||||
var xc = xaxis.p2c(x); |
||||
|
||||
return xc > 0 && xc < xaxis.p2c(xaxis.max); |
||||
}; |
||||
|
||||
var _drawLine = function(color, lineWidth, from, to) { |
||||
var ctx = plot.getCanvas().getContext("2d"); |
||||
var plotOffset = plot.getPlotOffset(); |
||||
|
||||
ctx.save(); |
||||
ctx.translate(plotOffset.left, plotOffset.top); |
||||
|
||||
ctx.beginPath(); |
||||
ctx.strokeStyle = color; |
||||
ctx.lineWidth = lineWidth; |
||||
ctx.moveTo(from.x, from.y); |
||||
ctx.lineTo(to.x, to.y); |
||||
ctx.stroke(); |
||||
|
||||
ctx.restore(); |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Runs over the given 2d array of event objects and returns an object |
||||
* containing: |
||||
* |
||||
* { |
||||
* types {}, // An array containing all the different event types
|
||||
* data [], // An array of the clustered events
|
||||
* } |
||||
* |
||||
* @param {Object} types |
||||
* an object containing event types |
||||
* @param {Object} events |
||||
* an array of event to cluster |
||||
* @param {Object} range |
||||
* the current graph range |
||||
*/ |
||||
var _clusterEvents = function(types, events, range) { |
||||
//TODO: support custom types
|
||||
var groups, clusters = [], newEvents = []; |
||||
|
||||
// split into same evenType groups
|
||||
groups = _groupEvents(events); |
||||
|
||||
$.each(groups.eventTypes, function(index, val) { |
||||
clusters.push(_varianceAlgorithm(groups.groupedEvents[val], 1, range)); |
||||
}); |
||||
|
||||
// summarise clusters
|
||||
$.each(clusters, function(index, eventType) { |
||||
|
||||
// each cluser of each event type
|
||||
$.each(eventType, function(index, cluster) { |
||||
|
||||
var newEvent = { |
||||
min: cluster[0].min, |
||||
max: cluster[cluster.length - 1].min, //TODO: needs to be max of end event if it exists
|
||||
eventType: cluster[0].eventType + ",cluster", |
||||
title: "Cluster of: " + cluster[0].title, |
||||
description: cluster[0].description + ", Number of events in the cluster: " + cluster.length |
||||
}; |
||||
|
||||
newEvents.push(newEvent); |
||||
}); |
||||
}); |
||||
|
||||
return { types: types, data: newEvents }; |
||||
}; |
||||
|
||||
/** |
||||
* Runs over the given 2d array of event objects and returns an object |
||||
* containing: |
||||
* |
||||
* { |
||||
* eventTypes [], // An array containing all the different event types
|
||||
* groupedEvents {}, // An object containing all the grouped events
|
||||
* } |
||||
* |
||||
* @param {Object} events |
||||
* an array of event objects |
||||
*/ |
||||
var _groupEvents = function(events) { |
||||
var eventTypes = [], groupedEvents = {}; |
||||
|
||||
$.each(events, function(index, val) { |
||||
if (!groupedEvents[val.eventType]) { |
||||
groupedEvents[val.eventType] = []; |
||||
eventTypes.push(val.eventType); |
||||
} |
||||
|
||||
groupedEvents[val.eventType].push(val); |
||||
}); |
||||
|
||||
return { eventTypes: eventTypes, groupedEvents: groupedEvents }; |
||||
}; |
||||
|
||||
/** |
||||
* Runs over the given 2d array of event objects and returns a 3d array of |
||||
* the same events,but clustered into groups with similar x deltas. |
||||
* |
||||
* This function assumes that the events are related. So it must be run on |
||||
* each set of related events. |
||||
* |
||||
* @param {Object} events |
||||
* an array of event objects |
||||
* @param {Object} sens |
||||
* a measure of the level of grouping tolerance |
||||
* @param {Object} space |
||||
* the size of the space we have to place clusters within |
||||
*/ |
||||
var _varianceAlgorithm = function(events, sens, space) { |
||||
var cluster, clusters = [], sum = 0, avg, density; |
||||
|
||||
// find the average x delta
|
||||
for (var i = 1; i < events.length - 1; i++) { |
||||
sum += events[i].min - events[i - 1].min; |
||||
} |
||||
avg = sum / (events.length - 2); |
||||
|
||||
// first point
|
||||
cluster = [ events[0] ]; |
||||
|
||||
// middle points
|
||||
for (var i = 1; i < events.length; i++) { |
||||
var leftDiff = events[i].min - events[i - 1].min; |
||||
|
||||
density = leftDiff / space; |
||||
|
||||
if (leftDiff > avg * sens && density > 0.05) { |
||||
clusters.push(cluster); |
||||
cluster = [ events[i] ]; |
||||
} else { |
||||
cluster.push(events[i]); |
||||
} |
||||
} |
||||
|
||||
clusters.push(cluster); |
||||
|
||||
return clusters; |
||||
}; |
||||
} |
||||
|
||||
var options = { |
||||
events: { |
||||
levels: null, |
||||
data: null, |
||||
types: null, |
||||
xaxis: 1, |
||||
clustering: false |
||||
} |
||||
}; |
||||
|
||||
$.plot.plugins.push({ |
||||
init: init, |
||||
options: options, |
||||
name: "events", |
||||
version: "0.20" |
||||
}); |
||||
|
||||
/** |
||||
* A class that allows for the drawing an remove of some object |
||||
* |
||||
* @param {Object} object |
||||
* the drawable object |
||||
* @param {Object} drawFunc |
||||
* the draw function |
||||
* @param {Object} clearFunc |
||||
* the clear function |
||||
*/ |
||||
function DrawableEvent(object, drawFunc, clearFunc, moveFunc, left, top, width, height){ |
||||
var _object = object, _drawFunc = drawFunc, _clearFunc = clearFunc, _moveFunc = moveFunc, |
||||
_position = { left: left, top: top }, _width = width, _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. |
||||
* |
||||
* @param {Object} options |
||||
* @param {Object} drawableEvent |
||||
*/ |
||||
function VisualEvent(options, drawableEvent, level){ |
||||
var _parent, _options = options, _drawableEvent = drawableEvent, |
||||
_level = level, _hidden = false; |
||||
|
||||
this.visual = function() { return _drawableEvent; } |
||||
this.level = function() { return _level; }; |
||||
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; }; |
||||
} |
||||
|
||||
function compareEvents(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; |
||||
}; |
||||
})(jQuery); |
Loading…
Reference in new issue