@spalger/kibana
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
319 lines (273 loc) • 8.82 kB
JavaScript
define(function (require) {
return function DispatchClass(Private) {
var d3 = require('d3');
var _ = require('lodash');
var $ = require('jquery');
var Tooltip = Private(require('ui/vislib/components/Tooltip'));
var SimpleEmitter = require('ui/utils/SimpleEmitter');
/**
* Handles event responses
*
* @class Dispatch
* @constructor
* @param handler {Object} Reference to Handler Class Object
*/
_.class(Dispatch).inherits(SimpleEmitter);
function Dispatch(handler) {
if (!(this instanceof Dispatch)) {
return new Dispatch(handler);
}
Dispatch.Super.call(this);
this.handler = handler;
this._listeners = {};
}
/**
* Response to click and hover events
*
* @param d {Object} Data point
* @param i {Number} Index number of data point
* @returns {{value: *, point: *, label: *, color: *, pointIndex: *,
* series: *, config: *, data: (Object|*),
* e: (d3.event|*), handler: (Object|*)}} Event response object
*/
Dispatch.prototype.eventResponse = function (d, i) {
var datum = d._input || d;
var data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
var label = d.label ? d.label : d.name;
var isSeries = !!(data && data.series);
var isSlices = !!(data && data.slices);
var series = isSeries ? data.series : undefined;
var slices = isSlices ? data.slices : undefined;
var handler = this.handler;
var color = _.get(handler, 'data.color');
var isPercentage = (handler && handler._attr.mode === 'percentage');
var eventData = {
value: d.y,
point: datum,
datum: datum,
label: label,
color: color ? color(label) : undefined,
pointIndex: i,
series: series,
slices: slices,
config: handler && handler._attr,
data: data,
e: d3.event,
handler: handler
};
if (isSeries) {
// Find object with the actual d value and add it to the point object
var object = _.find(series, { 'label': d.label });
eventData.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
eventData.percent = (100 * d.y).toFixed(1) + '%';
}
}
return eventData;
};
/**
* Returns a function that adds events and listeners to a D3 selection
*
* @method addEvent
* @param event {String}
* @param callback {Function}
* @returns {Function}
*/
Dispatch.prototype.addEvent = function (event, callback) {
return function (selection) {
selection.each(function () {
var element = d3.select(this);
if (typeof callback === 'function') {
return element.on(event, callback);
}
});
};
};
/**
*
* @method addHoverEvent
* @returns {Function}
*/
Dispatch.prototype.addHoverEvent = function () {
var self = this;
var isClickable = this.listenerCount('click') > 0;
var addEvent = this.addEvent;
var $el = this.handler.el;
function hover(d, i) {
// Add pointer if item is clickable
if (isClickable) {
self.addMousePointer.call(this, arguments);
}
self.highlightLegend.call(this, $el);
self.emit('hover', self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
};
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
Dispatch.prototype.addMouseoutEvent = function () {
var self = this;
var addEvent = this.addEvent;
var $el = this.handler.el;
function mouseout() {
self.unHighlightLegend.call(this, $el);
}
return addEvent('mouseout', mouseout);
};
/**
*
* @method addClickEvent
* @returns {Function}
*/
Dispatch.prototype.addClickEvent = function () {
var self = this;
var addEvent = this.addEvent;
function click(d, i) {
self.emit('click', self.eventResponse(d, i));
}
return addEvent('click', click);
};
/**
* Determine if we will allow brushing
*
* @method allowBrushing
* @returns {Boolean}
*/
Dispatch.prototype.allowBrushing = function () {
var xAxis = this.handler.xAxis;
return Boolean(xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert));
};
/**
* Determine if brushing is currently enabled
*
* @method isBrushable
* @returns {Boolean}
*/
Dispatch.prototype.isBrushable = function () {
return this.allowBrushing() && this.listenerCount('brush') > 0;
};
/**
*
* @param svg
* @returns {Function}
*/
Dispatch.prototype.addBrushEvent = function (svg) {
if (!this.isBrushable()) return;
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.xAxis.yScale;
var brush = this.createBrush(xScale, svg);
function brushEnd() {
if (!validBrushClick(d3.event)) return;
var bar = d3.select(this);
var startX = d3.mouse(svg.node());
var startXInv = xScale.invert(startX[0]);
// Reset the brush value
brush.extent([startXInv, startXInv]);
// Magic!
// Need to call brush on svg to see brush when brushing
// while on top of bars.
// Need to call brush on bar to allow the click event to be registered
svg.call(brush);
bar.call(brush);
}
return this.addEvent('mousedown', brushEnd);
};
/**
* Mouseover Behavior
*
* @method addMousePointer
* @returns {D3.Selection}
*/
Dispatch.prototype.addMousePointer = function () {
return d3.select(this).style('cursor', 'pointer');
};
/**
* Mouseover Behavior
*
* @param element {D3.Selection}
* @method highlightLegend
*/
Dispatch.prototype.highlightLegend = function (element) {
var label = this.getAttribute('data-label');
if (!label) return;
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.filter(function (d, i) {
return String(d.label) !== label;
})
.classed('blur_shape', true);
};
/**
* Mouseout Behavior
*
* @param element {D3.Selection}
* @method unHighlightLegend
*/
Dispatch.prototype.unHighlightLegend = function (element) {
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.classed('blur_shape', false);
};
/**
* Adds D3 brush to SVG and returns the brush function
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
*/
Dispatch.prototype.createBrush = function (xScale, svg) {
var self = this;
var attr = self.handler._attr;
var height = attr.height;
var margin = attr.margin;
// Brush scale
var brush = d3.svg.brush()
.x(xScale)
.on('brushend', function brushEnd() {
// Assumes data is selected at the chart level
// In this case, the number of data objects should always be 1
var data = d3.select(this).data()[0];
var isTimeSeries = (data.ordered && data.ordered.date);
// Allows for brushing on d3.scale.ordinal()
var selected = xScale.domain().filter(function (d) {
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
});
var range = isTimeSeries ? brush.extent() : selected;
return self.emit('brush', {
range: range,
config: attr,
e: d3.event,
data: data
});
});
// if `addBrushing` is true, add brush canvas
if (self.listenerCount('brush')) {
svg.insert('g', 'g')
.attr('class', 'brush')
.call(brush)
.call(function (brushG) {
// hijack the brush start event to filter out right/middle clicks
var brushHandler = brushG.on('mousedown.brush');
if (!brushHandler) return; // touch events in use
brushG.on('mousedown.brush', function () {
if (validBrushClick(d3.event)) brushHandler.apply(this, arguments);
});
})
.selectAll('rect')
.attr('height', height - margin.top - margin.bottom);
return brush;
}
};
function validBrushClick(event) {
return event.button === 0;
}
return Dispatch;
};
});