diff --git a/src/kibana/components/filter_bar/filter_bar_click_handler.js b/src/kibana/components/filter_bar/filter_bar_click_handler.js index 908777f585e43..ca0af22b268bd 100644 --- a/src/kibana/components/filter_bar/filter_bar_click_handler.js +++ b/src/kibana/components/filter_bar/filter_bar_click_handler.js @@ -6,16 +6,42 @@ define(function (require) { return function (Notifier) { return function ($state) { return function (event) { - // Hierarchical and tabular data set their aggConfigResult parameter - // differently because of how the point is rewritten between the two. So - // we need to check if the point.orig is set, if not use try the point.aggConfigResult - var aggConfigResult = event.point.orig && event.point.orig.aggConfigResult || event.point.aggConfigResult; var notify = new Notifier({ location: 'Filter bar' }); + var aggConfigResult; + + // Hierarchical and tabular data set their aggConfigResult parameter + // differently because of how the point is rewritten between the two. So + // we need to check if the point.orig is set, if not use try the point.aggConfigResult + if (event.point.orig) { + aggConfigResult = event.point.orig.aggConfigResult; + } else if (event.point.values) { + aggConfigResult = findAggConfig(event.point.values); + } else { + aggConfigResult = event.point.aggConfigResult; + } + + function findAggConfig(values) { + if (_.isArray(values)) { // point series chart + var index = _.findIndex(values, 'aggConfigResult'); + return values[index].aggConfigResult; + } + return values.aggConfigResult; // pie chart + } + + function findLabel(obj) { + // TODO: find out if there is always a fieldFormatter + var formatter = obj.aggConfig.fieldFormatter(); + return formatter(obj.key) === event.label; + } if (aggConfigResult) { + var isLegendLabel = !!event.point.values; var results = _.filter(aggConfigResult.getPath(), { type: 'bucket' }); + + if (isLegendLabel) results = _.filter(results, findLabel); // filter results array by legend label + var filters = _(results) .map(function (result) { try { diff --git a/src/kibana/components/vislib/components/labels/data_array.js b/src/kibana/components/vislib/components/labels/data_array.js index b5efd59fd7c47..de2f41976ae3f 100644 --- a/src/kibana/components/vislib/components/labels/data_array.js +++ b/src/kibana/components/vislib/components/labels/data_array.js @@ -1,22 +1,17 @@ define(function (require) { return function GetArrayUtilService(Private) { var _ = require('lodash'); - var flattenSeries = Private(require('components/vislib/components/labels/flatten_series')); /* * Accepts a Kibana data object and returns an array of values objects. */ - return function (obj) { if (!_.isObject(obj) || !obj.rows && !obj.columns && !obj.series) { throw new TypeError('GetArrayUtilService expects an object with a series, rows, or columns key'); } - if (!obj.series) { - return flattenSeries(obj); - } - + if (!obj.series) return flattenSeries(obj); return obj.series; }; }; diff --git a/src/kibana/components/vislib/components/labels/flatten_series.js b/src/kibana/components/vislib/components/labels/flatten_series.js index 8a50f9f88b1fc..862112ea09a17 100644 --- a/src/kibana/components/vislib/components/labels/flatten_series.js +++ b/src/kibana/components/vislib/components/labels/flatten_series.js @@ -6,7 +6,6 @@ define(function (require) { * Accepts a Kibana data object with a rows or columns key * and returns an array of flattened series values. */ - return function (obj) { if (!_.isObject(obj) || !obj.rows && !obj.columns) { throw new TypeError('GetSeriesUtilService expects an object with either a rows or columns key'); diff --git a/src/kibana/components/vislib/components/labels/labels.js b/src/kibana/components/vislib/components/labels/labels.js index 264c82754eb4c..3e2acc99d360a 100644 --- a/src/kibana/components/vislib/components/labels/labels.js +++ b/src/kibana/components/vislib/components/labels/labels.js @@ -4,6 +4,7 @@ define(function (require) { var createArr = Private(require('components/vislib/components/labels/data_array')); var getArrOfUniqLabels = Private(require('components/vislib/components/labels/uniq_labels')); + var getPieLabels = Private(require('components/vislib/components/labels/pie/pie_labels')); /* * Accepts a Kibana data object and returns an array of unique labels (strings). @@ -12,12 +13,9 @@ define(function (require) { * * Currently, this service is only used for vertical bar charts and line charts. */ - - return function (obj) { - if (!_.isObject(obj)) { - throw new TypeError('LabelUtil expects an object'); - } - + return function (obj, chartType) { + if (!_.isObject(obj)) { throw new TypeError('LabelUtil expects an object'); } + if (chartType === 'pie') { return getPieLabels(obj); } return getArrOfUniqLabels(createArr(obj)); }; }; diff --git a/src/kibana/components/vislib/components/labels/pie/get_pie_names.js b/src/kibana/components/vislib/components/labels/pie/get_pie_names.js new file mode 100644 index 0000000000000..01cf2fc2f12fc --- /dev/null +++ b/src/kibana/components/vislib/components/labels/pie/get_pie_names.js @@ -0,0 +1,21 @@ +define(function (require) { + var _ = require('lodash'); + + return function GetPieNames(Private) { + var returnNames = Private(require('components/vislib/components/labels/pie/return_pie_names')); + + return function (data, columns) { + var slices = data.slices; + + if (slices.children) { + return _(returnNames(slices.children, 0, columns)) + .sortBy(function (obj) { + return obj.index; + }) + .pluck('key') + .unique() + .value(); + } + }; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/vislib/components/labels/pie/pie_labels.js b/src/kibana/components/vislib/components/labels/pie/pie_labels.js new file mode 100644 index 0000000000000..9f75f623bd76b --- /dev/null +++ b/src/kibana/components/vislib/components/labels/pie/pie_labels.js @@ -0,0 +1,26 @@ +define(function (require) { + var _ = require('lodash'); + + return function PieLabels(Private) { + var removeZeroSlices = Private(require('components/vislib/components/labels/pie/remove_zero_slices')); + var getNames = Private(require('components/vislib/components/labels/pie/get_pie_names')); + + return function (obj) { + if (!_.isObject(obj)) { throw new TypeError('PieLabel expects an object'); } + + var data = obj.columns || obj.rows || [obj]; + var names = []; + + data.forEach(function (obj) { + var columns = obj.raw ? obj.raw.columns : undefined; + obj.slices = removeZeroSlices(obj.slices); + + getNames(obj, columns).forEach(function (name) { + names.push(name); + }); + }); + + return _.uniq(names); + }; + }; +}); diff --git a/src/kibana/components/vislib/components/labels/pie/remove_zero_slices.js b/src/kibana/components/vislib/components/labels/pie/remove_zero_slices.js new file mode 100644 index 0000000000000..d2be485a6b9fe --- /dev/null +++ b/src/kibana/components/vislib/components/labels/pie/remove_zero_slices.js @@ -0,0 +1,17 @@ +define(function (require) { + var _ = require('lodash'); + + return function RemoveZeroSlices() { + return function removeZeroSlices(slices) { + if (!slices.children) return slices; + + slices = _.clone(slices); + slices.children = slices.children.reduce(function (children, child) { + if (child.size !== 0) { children.push(removeZeroSlices(child)); } + return children; + }, []); + + return slices; + }; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/vislib/components/labels/pie/return_pie_names.js b/src/kibana/components/vislib/components/labels/pie/return_pie_names.js new file mode 100644 index 0000000000000..36aa21d82f7cc --- /dev/null +++ b/src/kibana/components/vislib/components/labels/pie/return_pie_names.js @@ -0,0 +1,19 @@ +define(function () { + return function ReturnPieNames() { + return function returnNames(array, index, columns) { + var names = []; + + array.forEach(function (obj) { + names.push({ key: obj.name, index: index }); + + if (obj.children) { + returnNames(obj.children, (index + 1), columns).forEach(function (namedObj) { + names.push(namedObj); + }); + } + }); + + return names; + }; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/vislib/components/labels/uniq_labels.js b/src/kibana/components/vislib/components/labels/uniq_labels.js index d0c2b160aa16f..699bbd401ad22 100644 --- a/src/kibana/components/vislib/components/labels/uniq_labels.js +++ b/src/kibana/components/vislib/components/labels/uniq_labels.js @@ -6,7 +6,6 @@ define(function (require) { * Accepts an array of data objects and a formatter function. * Returns a unique list of formatted labels (strings). */ - return function (arr) { if (!_.isArray(arr)) { throw new TypeError('UniqLabelUtil expects an array of objects'); diff --git a/src/kibana/components/vislib/lib/data.js b/src/kibana/components/vislib/lib/data.js index 5d312043f62de..0ca3d96e7cd88 100644 --- a/src/kibana/components/vislib/lib/data.js +++ b/src/kibana/components/vislib/lib/data.js @@ -37,21 +37,8 @@ define(function (require) { this.data = data; this.type = this.getDataType(); - - this.labels; - - if (this.type === 'series') { - if (getLabels(data).length === 1 && getLabels(data)[0] === '') { - this.labels = [(this.get('yAxisLabel'))]; - } else { - this.labels = getLabels(data); - } - } else if (this.type === 'slices') { - this.labels = this.pieNames(); - } - + this.labels = this._getLabels(this.data); this.color = this.labels ? color(this.labels) : undefined; - this._normalizeOrdered(); this._attr = _.defaults(attr || {}, { @@ -73,6 +60,15 @@ define(function (require) { } } + Data.prototype._getLabels = function (data) { + if (this.type === 'series') { + var noLabel = getLabels(data).length === 1 && getLabels(data)[0] === ''; + if (noLabel) return [(this.get('yAxisLabel'))]; + return getLabels(data); + } + return this.pieNames(); + }; + /** * Returns true for positive numbers */ @@ -485,7 +481,7 @@ define(function (require) { var self = this; _.forEach(array, function (obj) { - names.push({ key: obj.name, index: index }); + names.push({ label: obj.name, values: obj, index: index }); if (obj.children) { var plusIndex = index + 1; @@ -519,8 +515,9 @@ define(function (require) { .sortBy(function (obj) { return obj.index; }) - .pluck('key') - .unique() + .unique(function (d) { + return d.label; + }) .value(); } }; @@ -553,9 +550,8 @@ define(function (require) { * @method pieNames * @returns {Array} Array of unique names (strings) */ - Data.prototype.pieNames = function () { + Data.prototype.pieNames = function (data) { var self = this; - var data = this.getVisData(); var names = []; _.forEach(data, function (obj) { @@ -567,7 +563,7 @@ define(function (require) { }); }); - return _.uniq(names); + return _.uniq(names, 'label'); }; /** @@ -619,7 +615,9 @@ define(function (require) { * @returns {Function} Performs lookup on string and returns hex color */ Data.prototype.getPieColorFunc = function () { - return color(this.pieNames()); + return color(this.pieNames(this.getVisData()).map(function (d) { + return d.label; + })); }; /** diff --git a/src/kibana/components/vislib/lib/dispatch.js b/src/kibana/components/vislib/lib/dispatch.js index 39e52109488e2..3977d8499a795 100644 --- a/src/kibana/components/vislib/lib/dispatch.js +++ b/src/kibana/components/vislib/lib/dispatch.js @@ -35,26 +35,27 @@ define(function (require) { */ Dispatch.prototype.eventResponse = function (d, i) { var datum = d._input || d; - var data = d3.event.target.nearestViewportElement.__data__; + 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.series); - var isSlices = !!(data.slices); + 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 = handler.data.color; - var isPercentage = (handler._attr.mode === 'percentage'); + 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(label), + color: color ? color(label) : undefined, pointIndex: i, series: series, slices: slices, - config: handler._attr, + config: handler && handler._attr, data: data, e: d3.event, handler: handler @@ -231,7 +232,7 @@ define(function (require) { .select('.legend-ul') .selectAll('li.color') .filter(function (d, i) { - return this.getAttribute('data-label') !== label; + return String(d.label) !== label; }) .classed('blur_shape', true); }; diff --git a/src/kibana/components/vislib/lib/handler/handler.js b/src/kibana/components/vislib/lib/handler/handler.js index bbc96c66df781..b091b41b856f2 100644 --- a/src/kibana/components/vislib/lib/handler/handler.js +++ b/src/kibana/components/vislib/lib/handler/handler.js @@ -4,6 +4,7 @@ define(function (require) { var errors = require('errors'); var Data = Private(require('components/vislib/lib/data')); + var Legend = Private(require('components/vislib/lib/legend')); var Layout = Private(require('components/vislib/lib/layout/layout')); /** @@ -93,6 +94,12 @@ define(function (require) { this._validateData(); this.renderArray.forEach(function (property) { + if (property instanceof Legend) { + self.vis.activeEvents().forEach(function (event) { + self.enable(event, property); + }); + } + if (typeof property.render === 'function') { property.render(); } diff --git a/src/kibana/components/vislib/lib/handler/types/pie.js b/src/kibana/components/vislib/lib/handler/types/pie.js index 4bcb4a3c7d923..10293e4bdda56 100644 --- a/src/kibana/components/vislib/lib/handler/types/pie.js +++ b/src/kibana/components/vislib/lib/handler/types/pie.js @@ -10,10 +10,8 @@ define(function (require) { */ return function (vis) { - var data = new Data(vis.data, vis._attr); - return new Handler(vis, { - legend: new Legend(vis, vis.el, data.pieNames(), data.getPieColorFunc(), vis._attr), + legend: new Legend(vis), chartTitle: new ChartTitle(vis.el) }); }; diff --git a/src/kibana/components/vislib/lib/handler/types/point_series.js b/src/kibana/components/vislib/lib/handler/types/point_series.js index 70b7a774bbcb8..2fbb9c13be0f3 100644 --- a/src/kibana/components/vislib/lib/handler/types/point_series.js +++ b/src/kibana/components/vislib/lib/handler/types/point_series.js @@ -29,7 +29,7 @@ define(function (require) { return new Handler(vis, { data: data, - legend: new Legend(vis, vis.el, data.labels, data.color, vis._attr), + legend: new Legend(vis, vis.data), axisTitle: new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')), chartTitle: new ChartTitle(vis.el), xAxis: new XAxis({ diff --git a/src/kibana/components/vislib/lib/legend.js b/src/kibana/components/vislib/lib/legend.js index 8e374e3aef02b..ea40440acb271 100644 --- a/src/kibana/components/vislib/lib/legend.js +++ b/src/kibana/components/vislib/lib/legend.js @@ -1,8 +1,11 @@ define(function (require) { - return function LegendFactory(d3) { + return function LegendFactory(d3, Private) { var _ = require('lodash'); + var Dispatch = Private(require('components/vislib/lib/dispatch')); + var Data = Private(require('components/vislib/lib/data')); var legendHeaderTemplate = _.template(require('text!components/vislib/partials/legend_header.html')); var dataLabel = require('components/vislib/lib/_data_label'); + var color = Private(require('components/vislib/components/color/color')); require('css!components/vislib/styles/main'); @@ -12,21 +15,22 @@ define(function (require) { * @class Legend * @constructor * @param vis {Object} Reference to Vis Constructor - * @param el {HTMLElement} Reference to DOM element - * @param labels {Array} Array of chart labels - * @param color {Function} Color function - * @param _attr {Object|*} Reference to Vis options */ - function Legend(vis, el, labels, color, _attr) { + function Legend(vis) { if (!(this instanceof Legend)) { - return new Legend(vis, el, labels, color, _attr); + return new Legend(vis); } + var data = vis.data.columns || vis.data.rows || [vis.data]; + var type = vis._attr.type; + var labels = this.labels = this._getLabels(data, type); + var labelsArray = labels.map(function (obj) { return obj.label; }); + + this.events = new Dispatch(); this.vis = vis; - this.el = el; - this.labels = labels; - this.color = color; - this._attr = _.defaults(_attr || {}, { + this.el = vis.el; + this.color = color(labelsArray); + this._attr = _.defaults({}, vis._attr || {}, { 'legendClass' : 'legend-col-wrapper', 'blurredOpacity' : 0.3, 'focusOpacity' : 1, @@ -36,6 +40,35 @@ define(function (require) { }); } + Legend.prototype._getPieLabels = function (data) { + return Data.prototype.pieNames(data); + }; + + Legend.prototype._getSeriesLabels = function (data) { + var isOneSeries = data.every(function (chart) { + return chart.series.length === 1; + }); + + var values = data.map(function (chart) { + var yLabel = isOneSeries ? chart.yAxisLabel : undefined; + + return chart.series.map(function (series) { + if (yLabel) series.label = yLabel; + return series; + }); + }) + .reduce(function (a, b) { + return a.concat(b); + }, []); + + return _.uniq(values, 'label'); + }; + + Legend.prototype._getLabels = function (data, type) { + if (type === 'pie') return this._getPieLabels(data); + return this._getSeriesLabels(data); + }; + /** * Adds legend header * @@ -44,7 +77,7 @@ define(function (require) { * @param args {Object|*} Legend options * @returns {*} HTML element */ - Legend.prototype.header = function (el, args) { + Legend.prototype._header = function (el, args) { return el.append('div') .attr('class', 'header') .append('div') @@ -63,30 +96,28 @@ define(function (require) { * @param args {Object|*} Legend options * @returns {D3.Selection} HTML element with list of labels attached */ - Legend.prototype.list = function (el, arrOfLabels, args) { + Legend.prototype._list = function (el, data, args) { var self = this; return el.append('ul') .attr('class', function () { - if (args._attr.isOpen) { - return 'legend-ul'; - } + if (args._attr.isOpen) { return 'legend-ul'; } return 'legend-ul hidden'; }) .selectAll('li') - .data(arrOfLabels) + .data(data) .enter() .append('li') .attr('class', 'color') - .each(function (label) { + .each(function (d) { var li = d3.select(this); - self._addIdentifier.call(this, label); + self._addIdentifier.call(this, d); li.append('i') .attr('class', 'fa fa-circle dots') - .attr('style', 'color:' + args.color(label)); + .attr('style', 'color:' + args.color(d.label)); - li.append('span').text(label); + li.append('span').text(d.label); }); }; @@ -96,11 +127,10 @@ define(function (require) { * @method _addIdentifier * @param label {string} label to use */ - Legend.prototype._addIdentifier = function (label) { - dataLabel(this, label); + Legend.prototype._addIdentifier = function (d) { + dataLabel(this, d.label); }; - /** * Renders legend * @@ -112,8 +142,8 @@ define(function (require) { var visEl = d3.select(this.el); var legendDiv = visEl.select('.' + this._attr.legendClass); var items = this.labels; - this.header(legendDiv, this); - this.list(legendDiv, items, this); + this._header(legendDiv, this); + this._list(legendDiv, items, this); var headerIcon = visEl.select('.legend-toggle'); @@ -138,21 +168,27 @@ define(function (require) { }); legendDiv.select('.legend-ul').selectAll('li') - .on('mouseover', function (label) { + .on('mouseover', function (d) { + var label = d.label; var charts = visEl.selectAll('.chart'); + function filterLabel() { + var pointLabel = this.getAttribute('data-label'); + return pointLabel !== label.toString(); + } + + if (label && label !== '_all') { + d3.select(this).style('cursor', 'pointer'); + } + // legend legendDiv.selectAll('li') - .filter(function (d) { - return this.getAttribute('data-label') !== label.toString(); - }) + .filter(filterLabel) .classed('blur_shape', true); // all data-label attribute charts.selectAll('[data-label]') - .filter(function (d) { - return this.getAttribute('data-label') !== label.toString(); - }) + .filter(filterLabel) .classed('blur_shape', true); var eventEl = d3.select(this); @@ -179,6 +215,13 @@ define(function (require) { eventEl.style('white-space', 'nowrap'); eventEl.style('word-break', 'inherit'); }); + + legendDiv.selectAll('li.color').each(function (d) { + var label = d.label; + if (label !== undefined && label !== '_all') { + d3.select(this).call(self.events.addClickEvent()); + } + }); }; return Legend; diff --git a/test/unit/specs/vislib/lib/data.js b/test/unit/specs/vislib/lib/data.js index 96a72a13848a5..2f941fbdc45f7 100644 --- a/test/unit/specs/vislib/lib/data.js +++ b/test/unit/specs/vislib/lib/data.js @@ -136,11 +136,10 @@ define(function (require) { beforeEach(function () { data = new Data(pieData, {}); - data._removeZeroSlices(pieData.slices); }); it('should remove zero values', function () { - var slices = data.data.slices; + var slices = data._removeZeroSlices(data.data.slices); expect(slices.children.length).to.be(2); }); }); diff --git a/test/unit/specs/vislib/lib/legend.js b/test/unit/specs/vislib/lib/legend.js index e4f08f46174cb..f21bb7f56aee5 100644 --- a/test/unit/specs/vislib/lib/legend.js +++ b/test/unit/specs/vislib/lib/legend.js @@ -38,16 +38,20 @@ define(function (require) { type: chartTypes[i], addLegend: true }; + var Legend; var vis; + var $el; beforeEach(function () { module('LegendFactory'); }); beforeEach(function () { - inject(function (Private) { + inject(function (Private, d3) { vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams); + Legend = Private(require('components/vislib/lib/legend')); require('css!components/vislib/styles/main'); + $el = d3.select('body').append('div').attr('class', 'fake-legend'); vis.render(data); }); @@ -55,6 +59,7 @@ define(function (require) { afterEach(function () { $(vis.el).remove(); + $('.fake-legend').remove(); vis = null; }); @@ -64,9 +69,9 @@ define(function (require) { var paths = $(vis.el).find(chartSelectors[chartType]).toArray(); var items = vis.handler.legend.labels; - items.forEach(function (label) { + items.forEach(function (d) { var path = _.find(paths, function (path) { - return path.getAttribute('data-label') === String(label); + return path.getAttribute('data-label') === String(d.label); }); expect(path).to.be.ok(); @@ -88,6 +93,22 @@ define(function (require) { it('should contain a list of items', function () { expect($(vis.el).find('li').length).to.be.greaterThan(1); }); + it('should not return an undefined value', function () { + var emptyObject = { + label: '' + }; + var labels = [emptyObject, emptyObject, emptyObject]; + var args = { + _attr: {isOpen: true}, + color: function () { return 'blue'; } + }; + + Legend.prototype._list($el, labels, args); + + $el.selectAll('li').each(function (d) { + expect(d.label).not.to.be(undefined); + }); + }); }); describe('render method', function () { @@ -97,6 +118,7 @@ define(function (require) { it('should have an onclick listener', function () { expect(!!$('.legend-toggle')[0].__onclick).to.be(true); + expect(!!$('li.color')[0].__onclick).to.be(true); }); it('should attach onmouseover listener', function () {