UNPKG

kibana-123

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

353 lines (304 loc) 9.69 kB
import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; import SimpleEmitter from 'ui/utils/simple_emitter'; export default function DispatchClass(Private, config) { /** * Handles event responses * * @class Dispatch * @constructor * @param handler {Object} Reference to Handler Class Object */ class Dispatch extends SimpleEmitter { constructor(handler) { super(); 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 */ eventResponse(d, i) { const datum = d._input || d; const data = d3.event.target.nearestViewportElement ? d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__; const label = d.label ? d.label : (d.series || 'Count'); const isSeries = !!(data && data.series); const isSlices = !!(data && data.slices); const series = isSeries ? data.series : undefined; const slices = isSlices ? data.slices : undefined; const handler = this.handler; const color = _.get(handler, 'data.color'); const isPercentage = (handler && handler.visConfig.get('mode', 'normal') === 'percentage'); const eventData = { value: d.y, point: datum, datum: datum, label: label, color: color ? color(label) : undefined, pointIndex: i, series: series, slices: slices, config: handler && handler.visConfig, data: data, e: d3.event, handler: handler }; if (isSeries) { // Find object with the actual d value and add it to the point object const object = _.find(series, {'label': label}); if (object) { 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} */ addEvent(event, callback) { return function (selection) { selection.each(function () { const element = d3.select(this); if (typeof callback === 'function') { return element.on(event, callback); } }); }; }; /** * * @method addHoverEvent * @returns {Function} */ addHoverEvent() { const self = this; const isClickable = this.listenerCount('click') > 0; const addEvent = this.addEvent; const $el = this.handler.el; if (!this.handler.highlight) { this.handler.highlight = self.highlight; } function hover(d, i) { // Add pointer if item is clickable if (isClickable) { self.addMousePointer.call(this, arguments); } self.handler.highlight.call(this, $el); self.emit('hover', self.eventResponse(d, i)); } return addEvent('mouseover', hover); }; /** * * @method addMouseoutEvent * @returns {Function} */ addMouseoutEvent() { const self = this; const addEvent = this.addEvent; const $el = this.handler.el; if (!this.handler.unHighlight) { this.handler.unHighlight = self.unHighlight; } function mouseout() { self.handler.unHighlight.call(this, $el); } return addEvent('mouseout', mouseout); }; /** * * @method addClickEvent * @returns {Function} */ addClickEvent() { const self = this; const 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} */ allowBrushing() { const xAxis = this.handler.categoryAxes[0]; //Allow brushing for ordered axis - date histogram and histogram return Boolean(xAxis.ordered); }; /** * Determine if brushing is currently enabled * * @method isBrushable * @returns {Boolean} */ isBrushable() { return this.allowBrushing() && this.listenerCount('brush') > 0; }; /** * * @param svg * @returns {Function} */ addBrushEvent(svg) { if (!this.isBrushable()) return; const self = this; const xScale = this.handler.categoryAxes[0].getScale(); const brush = this.createBrush(xScale, svg); function simulateClickWithBrushEnabled(d, i) { if (!validBrushClick(d3.event)) return; if (isQuantitativeScale(xScale)) { const bar = d3.select(this); const startX = d3.mouse(svg.node()); const 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); } else { self.emit('click', self.eventResponse(d, i)); } } return this.addEvent('mousedown', simulateClickWithBrushEnabled); }; /** * Mouseover Behavior * * @method addMousePointer * @returns {d3.Selection} */ addMousePointer() { return d3.select(this).style('cursor', 'pointer'); }; /** * Highlight the element that is under the cursor * by reducing the opacity of all the elements on the graph. * @param element {d3.Selection} * @method highlight */ highlight(element) { const label = this.getAttribute('data-label'); if (!label) return; const dimming = config.get('visualization:dimmingOpacity'); $(element).parent().find('[data-label]') .css('opacity', 1)//Opacity 1 is needed to avoid the css application .not((els, el) => String($(el).data('label')) === label) .css('opacity', justifyOpacity(dimming)); } /** * Mouseout Behavior * * @param element {d3.Selection} * @method unHighlight */ unHighlight(element) { $('[data-label]', element.parentNode).css('opacity', 1); }; /** * 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 */ createBrush(xScale, svg) { const self = this; const visConfig = self.handler.visConfig; const {width, height} = svg.node().getBBox(); const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal(); // Brush scale const brush = d3.svg.brush(); if (isHorizontal) { brush.x(xScale); } else { brush.y(xScale); } brush.on('brushend', function brushEnd() { // Assumes data is selected at the chart level // In this case, the number of data objects should always be 1 const data = d3.select(this).data()[0]; const isTimeSeries = (data.ordered && data.ordered.date); // Allows for brushing on d3.scale.ordinal() const selected = xScale.domain().filter(function (d) { return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]); }); const range = isTimeSeries ? brush.extent() : selected; return self.emit('brush', { range: range, config: visConfig, e: d3.event, data: data }); }); // if `addBrushing` is true, add brush canvas if (self.listenerCount('brush')) { const rect = svg.insert('g', 'g') .attr('class', 'brush') .call(brush) .call(function (brushG) { // hijack the brush start event to filter out right/middle clicks const 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'); if (isHorizontal) { rect.attr('height', height); } else { rect.attr('width', width); } return brush; } }; } /** * Determine if d3.Scale is quantitative * * @param element {d3.Scale} * @method isQuantitativeScale * @returns {boolean} */ function isQuantitativeScale(scale) { //Invert is a method that only exists on quantitative scales if (scale.invert) { return true; } else { return false; } } function validBrushClick(event) { return event.button === 0; } function justifyOpacity(opacity) { const decimalNumber = parseFloat(opacity, 10); const fallbackOpacity = 0.5; return (0 <= decimalNumber && decimalNumber <= 1) ? decimalNumber : fallbackOpacity; } return Dispatch; };