mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
Markers in histogram
This commit is contained in:
parent
113c6fc572
commit
b1aef03be2
@ -1,4 +1,4 @@
|
||||
If you have a bugfix or new feature that you would like to contribute to Kibana, please find or open an issue about it first. Talk about what you would like to do. It may be that somebody is already working on it, or that there are particular issues that you should know about before implementing the change.
|
||||
If you have a bugfix or new feature that you would like to contribute to Kibana, please **find or open an issue about it before you start working on it.** Talk about what you would like to do. It may be that somebody is already working on it, or that there are particular issues that you should know about before implementing the change.
|
||||
|
||||
We enjoy working with contributors to get their code accepted. There are many approaches to fixing a problem and it is important to find the best approach before writing too much code.
|
||||
|
||||
|
@ -32,6 +32,7 @@ require.config({
|
||||
|
||||
'jquery.flot': '../vendor/jquery/jquery.flot',
|
||||
'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie',
|
||||
'jquery.flot.events': '../vendor/jquery/jquery.flot.events',
|
||||
'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection',
|
||||
'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
|
||||
'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
|
||||
@ -66,6 +67,7 @@ require.config({
|
||||
'jquery-ui': ['jquery'],
|
||||
'jquery.flot': ['jquery'],
|
||||
'jquery.flot.pie': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.events': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.selection':['jquery', 'jquery.flot'],
|
||||
'jquery.flot.stack': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
|
||||
|
@ -37,7 +37,7 @@ define([
|
||||
'./timeSeries',
|
||||
|
||||
'jquery.flot',
|
||||
'jquery.flot.pie',
|
||||
'jquery.flot.events',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.stack',
|
||||
@ -67,7 +67,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
},
|
||||
{
|
||||
title:'Queries',
|
||||
src:'app/partials/querySelect.html'
|
||||
src:'app/panels/histogram/queriesEditor.html'
|
||||
},
|
||||
],
|
||||
status : "Stable",
|
||||
@ -84,6 +84,13 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
annotate : {
|
||||
enable : false,
|
||||
query : "*",
|
||||
size : 20,
|
||||
field : '@message',
|
||||
sort : ['_score','desc']
|
||||
},
|
||||
value_field : null,
|
||||
auto_int : true,
|
||||
resolution : 100,
|
||||
@ -121,6 +128,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
_.defaults($scope.panel.tooltip,_d.tooltip);
|
||||
_.defaults($scope.panel.annotate,_d.annotate);
|
||||
_.defaults($scope.panel.grid,_d.grid);
|
||||
|
||||
|
||||
@ -190,6 +198,16 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
* this call is made recursively for more segments
|
||||
*/
|
||||
$scope.get_data = function(segment, query_id) {
|
||||
var
|
||||
_range,
|
||||
_interval,
|
||||
request,
|
||||
queries,
|
||||
results;
|
||||
|
||||
$scope.panel.annotate.enable = true;
|
||||
$scope.panel.annotate.ids = [1];
|
||||
|
||||
if (_.isUndefined(segment)) {
|
||||
segment = 0;
|
||||
}
|
||||
@ -199,8 +217,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
var _range = $scope.get_time_range();
|
||||
var _interval = $scope.get_interval(_range);
|
||||
_range = $scope.get_time_range();
|
||||
_interval = $scope.get_interval(_range);
|
||||
|
||||
if ($scope.panel.auto_int) {
|
||||
$scope.panel.interval = kbn.secondsToHms(
|
||||
@ -208,11 +226,11 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
}
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices[segment]);
|
||||
request = $scope.ejs.Request().indices(dashboard.indices[segment]);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
|
||||
var queries = querySrv.getQueryObjs($scope.panel.queries.ids);
|
||||
queries = querySrv.getQueryObjs($scope.panel.queries.ids);
|
||||
|
||||
// Build the query
|
||||
_.each(queries, function(q) {
|
||||
@ -224,7 +242,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
var facet = $scope.ejs.DateHistogramFacet(q.id);
|
||||
|
||||
if($scope.panel.mode === 'count') {
|
||||
facet = facet.field($scope.panel.time_field);
|
||||
facet = facet.field($scope.panel.time_field).global(true);
|
||||
} else {
|
||||
if(_.isNull($scope.panel.value_field)) {
|
||||
$scope.panel.error = "In " + $scope.panel.mode + " mode a field must be specified";
|
||||
@ -233,14 +251,30 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field);
|
||||
}
|
||||
facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query));
|
||||
request = request.facet(facet).size(0);
|
||||
request = request.facet(facet)
|
||||
.size($scope.panel.annotate.enable ? $scope.panel.annotate.size : 0);
|
||||
});
|
||||
|
||||
if($scope.panel.annotate.enable) {
|
||||
var query = $scope.ejs.FilteredQuery(
|
||||
$scope.ejs.QueryStringQuery($scope.panel.annotate.query || '*'),
|
||||
filterSrv.getBoolFilter(filterSrv.idsByType('time'))
|
||||
);
|
||||
request = request.query(query);
|
||||
|
||||
// This is a hack proposed by @boaz to work around the fact that we can't get
|
||||
// to field data values directly, and we need timestamps as normalized longs
|
||||
request = request.sort([
|
||||
$scope.ejs.Sort($scope.panel.annotate.sort[0]).order($scope.panel.annotate.sort[1]),
|
||||
$scope.ejs.Sort($scope.panel.time_field).desc()
|
||||
]);
|
||||
}
|
||||
|
||||
// Populate the inspector panel
|
||||
$scope.populate_modal(request);
|
||||
|
||||
// Then run it
|
||||
var results = request.doSearch();
|
||||
results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
@ -249,6 +283,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
if(segment === 0) {
|
||||
$scope.hits = 0;
|
||||
$scope.data = [];
|
||||
$scope.annotations = [];
|
||||
query_id = $scope.query_id = new Date().getTime();
|
||||
}
|
||||
|
||||
@ -298,6 +333,30 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
i++;
|
||||
});
|
||||
|
||||
if($scope.panel.annotate.enable) {
|
||||
$scope.annotations = $scope.annotations.concat(_.map(results.hits.hits, function(hit) {
|
||||
var _p = _.omit(hit,'_source','sort','_score');
|
||||
var _h = _.extend(kbn.flatten_json(hit._source),_p);
|
||||
return {
|
||||
min: hit.sort[1],
|
||||
max: hit.sort[1],
|
||||
eventType: "annotation",
|
||||
title: null,
|
||||
description: "<small><i class='icon-tag icon-flip-vertical'></i> "+
|
||||
_h[$scope.panel.annotate.field]+"</small><br>"+
|
||||
moment(hit.sort[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
score: hit.sort[0]
|
||||
};
|
||||
}));
|
||||
// Sort the data
|
||||
$scope.annotations = _.sortBy($scope.annotations, function(v){
|
||||
// Sort in reverse
|
||||
return v.score*($scope.panel.annotate.sort[1] === 'desc' ? -1 : 1);
|
||||
});
|
||||
// And slice to the right size
|
||||
$scope.annotations = $scope.annotations.slice(0,$scope.panel.annotate.size);
|
||||
}
|
||||
|
||||
// Tell the histogram directive to render.
|
||||
$scope.$emit('render');
|
||||
|
||||
@ -429,7 +488,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
bars: {
|
||||
show: scope.panel.bars,
|
||||
fill: 1,
|
||||
barWidth: barwidth/1.8,
|
||||
barWidth: barwidth/1.5,
|
||||
zero: false,
|
||||
lineWidth: 0
|
||||
},
|
||||
@ -464,6 +523,25 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
}
|
||||
};
|
||||
|
||||
if(scope.panel.annotate.enable) {
|
||||
options.events = {
|
||||
levels: 1,
|
||||
data: scope.annotations,
|
||||
types: {
|
||||
'annotation': {
|
||||
level: 1,
|
||||
icon: {
|
||||
icon: "icon-tag icon-flip-vertical",
|
||||
size: 20,
|
||||
color: "#222",
|
||||
outline: "#bbb"
|
||||
}
|
||||
}
|
||||
}
|
||||
//xaxis: int // the x axis to attach events to
|
||||
};
|
||||
}
|
||||
|
||||
if(scope.panel.interactive) {
|
||||
options.selection = { mode: "x", color: '#666' };
|
||||
}
|
||||
@ -503,13 +581,13 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
function time_format(interval) {
|
||||
var _int = kbn.interval_to_seconds(interval);
|
||||
if(_int >= 2628000) {
|
||||
return "%m/%y";
|
||||
return "%Y-%m";
|
||||
}
|
||||
if(_int >= 86400) {
|
||||
return "%m/%d/%y";
|
||||
return "%Y-%m-%d";
|
||||
}
|
||||
if(_int >= 60) {
|
||||
return "%H:%M<br>%m/%d";
|
||||
return "%H:%M<br>%m-%d";
|
||||
}
|
||||
|
||||
return "%H:%M:%S";
|
||||
@ -534,7 +612,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
}
|
||||
$tooltip
|
||||
.html(
|
||||
group + value + " @ " + moment(item.datapoint[0]).format('MM/DD HH:mm:ss')
|
||||
group + value + " @ " + moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss')
|
||||
)
|
||||
.place_tt(pos.pageX, pos.pageY);
|
||||
} else {
|
||||
|
43
src/app/panels/histogram/queriesEditor.html
Normal file
43
src/app/panels/histogram/queriesEditor.html
Normal file
@ -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>
|
2
src/css/bootstrap.dark.min.css
vendored
2
src/css/bootstrap.dark.min.css
vendored
File diff suppressed because one or more lines are too long
2
src/css/bootstrap.light.min.css
vendored
2
src/css/bootstrap.light.min.css
vendored
File diff suppressed because one or more lines are too long
4
src/vendor/bootstrap/less/overrides.less
vendored
4
src/vendor/bootstrap/less/overrides.less
vendored
@ -256,7 +256,9 @@ a {
|
||||
.boolean {color:lighten(@warningText, 5%)}
|
||||
.key {color:lighten(@errorText, 5%)}
|
||||
|
||||
.typeahead { z-index: 1051; }
|
||||
.modal-body {
|
||||
overflow-y:visible;
|
||||
}
|
||||
|
||||
.btn-active {
|
||||
background-color: #E6E6E6;
|
||||
|
641
src/vendor/jquery/jquery.flot.events.js
vendored
Normal file
641
src/vendor/jquery/jquery.flot.events.js
vendored
Normal file
@ -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
Block a user