From 85913ab1fceca520f3daf531956e6cad04442ef9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 21 Nov 2016 14:50:35 -0700 Subject: [PATCH 01/11] [draggable] continue to work when debug info is disabled (#9153) --- .../public/draggable/draggable_container.js | 25 +++++++++++++------ src/ui/public/draggable/draggable_item.js | 3 ++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/ui/public/draggable/draggable_container.js b/src/ui/public/draggable/draggable_container.js index 1d5d6b6bd2804..ea1df9894210e 100644 --- a/src/ui/public/draggable/draggable_container.js +++ b/src/ui/public/draggable/draggable_container.js @@ -7,19 +7,26 @@ uiModules .get('kibana') .directive('draggableContainer', function () { + const $scopes = new WeakMap(); + return { restrict: 'A', scope: true, controllerAs: 'draggableContainerCtrl', - controller($scope, $attrs, $parse) { + controller($scope, $attrs, $parse, $element) { + $scopes.set($element.get(0), $scope); + this.linkDraggableItem = (el, $scope) => { + $scopes.set(el, $scope); + }; + this.getList = () => $parse($attrs.draggableContainer)($scope); }, link($scope, $el, attr) { const drake = dragula({ containers: $el.toArray(), moves(el, source, handle) { - const itemScope = $(el).scope(); - if (!('draggableItemCtrl' in itemScope)) { + const itemScope = $scopes.get(el); + if (!itemScope || !('draggableItemCtrl' in itemScope)) { return; // only [draggable-item] is draggable } return itemScope.draggableItemCtrl.moves(handle); @@ -53,7 +60,8 @@ uiModules function markDragging(isDragging) { return el => { - const scope = $(el).scope(); + const scope = $scopes.get(el); + if (!scope) return; scope.isDragging = isDragging; scope.$apply(); }; @@ -61,13 +69,15 @@ uiModules function forwardEvent(type, el, ...args) { const name = `drag-${prettifiedDrakeEvents[type] || type}`; - const scope = $(el).scope(); + const scope = $scopes.get(el); + if (!scope) return; scope.$broadcast(name, el, ...args); } function drop(el, target, source, sibling) { const list = $scope.draggableContainerCtrl.getList(); - const itemScope = $(el).scope(); + const itemScope = $scopes.get(el); + if (!itemScope) return; const item = itemScope.draggableItemCtrl.getItem(); const fromIndex = list.indexOf(item); const siblingIndex = getItemIndexFromElement(list, sibling); @@ -91,7 +101,8 @@ uiModules function getItemIndexFromElement(list, element) { if (!element) return -1; - const scope = $(element).scope(); + const scope = $scopes.get(element); + if (!scope) return; const item = scope.draggableItemCtrl.getItem(); const index = list.indexOf(item); diff --git a/src/ui/public/draggable/draggable_item.js b/src/ui/public/draggable/draggable_item.js index ff46c7a5d4de2..2c11c3b315455 100644 --- a/src/ui/public/draggable/draggable_item.js +++ b/src/ui/public/draggable/draggable_item.js @@ -23,7 +23,8 @@ uiModules return movable; }; }, - link($scope, $el, attr) { + link($scope, $el, attr, draggableController) { + draggableController.linkDraggableItem($el.get(0), $scope); } }; }); From 31a6cbab6d536d8130cb11e0ffdccaa0b0d47471 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 21 Nov 2016 17:03:25 -0500 Subject: [PATCH 02/11] Add Tag Cloud Visualization A tag cloud visualization is a visual representation of text data, typically used to visualize free form text. Tags are usually single words. The font size of word corresponds with its importance. --- docs/visualize.asciidoc | 2 + docs/visualize/tagcloud.asciidoc | 44 +++ package.json | 2 + src/core_plugins/tagcloud/index.js | 8 + src/core_plugins/tagcloud/package.json | 4 + .../tagcloud/public/__tests__/tag_cloud.js | 181 ++++++++++ src/core_plugins/tagcloud/public/tag_cloud.js | 319 ++++++++++++++++++ .../tagcloud/public/tag_cloud.less | 47 +++ .../tagcloud/public/tag_cloud_controller.html | 7 + .../tagcloud/public/tag_cloud_controller.js | 101 ++++++ .../tagcloud/public/tag_cloud_vis.js | 57 ++++ .../tagcloud/public/tag_cloud_vis_params.html | 21 ++ .../tagcloud/public/tag_cloud_vis_params.js | 32 ++ src/ui/public/agg_types/metrics/median.js | 4 +- .../functional/apps/visualize/_chart_types.js | 18 +- 15 files changed, 834 insertions(+), 13 deletions(-) create mode 100644 docs/visualize/tagcloud.asciidoc create mode 100644 src/core_plugins/tagcloud/index.js create mode 100644 src/core_plugins/tagcloud/package.json create mode 100644 src/core_plugins/tagcloud/public/__tests__/tag_cloud.js create mode 100644 src/core_plugins/tagcloud/public/tag_cloud.js create mode 100644 src/core_plugins/tagcloud/public/tag_cloud.less create mode 100644 src/core_plugins/tagcloud/public/tag_cloud_controller.html create mode 100644 src/core_plugins/tagcloud/public/tag_cloud_controller.js create mode 100644 src/core_plugins/tagcloud/public/tag_cloud_vis.js create mode 100644 src/core_plugins/tagcloud/public/tag_cloud_vis_params.html create mode 100644 src/core_plugins/tagcloud/public/tag_cloud_vis_params.js diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index 1aa8076d578ea..5412591685701 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -118,3 +118,5 @@ include::visualize/pie.asciidoc[] include::visualize/tilemap.asciidoc[] include::visualize/vertbar.asciidoc[] + +include::visualize/tagcloud.asciidoc[] diff --git a/docs/visualize/tagcloud.asciidoc b/docs/visualize/tagcloud.asciidoc new file mode 100644 index 0000000000000..b06e57a722e47 --- /dev/null +++ b/docs/visualize/tagcloud.asciidoc @@ -0,0 +1,44 @@ +[[tagcloud-chart]] +== Cloud Tag Charts + +A tag cloud visualization is a visual representation of text data, typically used to visualize free form text. +Tags are usually single words, and the importance of each tag is shown with font size or color. + +The font size for each word is determined by the _metrics_ aggregation. The following aggregations are available for +this chart: + +include::y-axis-aggs.asciidoc[] + + +The _buckets_ aggregations determine what information is being retrieved from your data set. + +Before you choose a buckets aggregation, select the *Split Tags* option. + +You can specify the following bucket aggregations for tag cloud visualization: + +*Terms*:: A {es-ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top +or bottom _n_ elements of a given field to display, ordered by count or a custom metric. + +You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation: + +*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation +definition, as in the following example: + +[source,shell] +{ "script" : "doc['grade'].value * 1.2" } + +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +{es-ref}modules-scripting.html[dynamic Groovy scripting]. + + +Select the *Options* tab to change the following aspects of the chart: + +*Text Scale*:: You can select *linear*, *log*, or *square root* scales for the text scale. You can use a log +scale to display data that varies exponentially or a square root scale to +regularize the display of data sets with variabilities that are themselves highly variable. +*Orientation*:: You can select how to orientate your text in the tag cloud. You can choose one of the following options: +Single, right angles and multiple. +*Font Size*:: Allows you to set minimum and maximum font size to use for this visualization. + + +include::visualization-raw-data.asciidoc[] \ No newline at end of file diff --git a/package.json b/package.json index 1380678cc5d74..b26563945552c 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "commander": "2.8.1", "css-loader": "0.17.0", "d3": "3.5.6", + "d3-cloud": "1.2.1", "dragula": "3.7.0", "elasticsearch": "12.0.0-rc5", "elasticsearch-browser": "12.0.0-rc5", @@ -138,6 +139,7 @@ "mkdirp": "0.5.1", "moment": "2.13.0", "moment-timezone": "0.5.4", + "no-ui-slider": "1.2.0", "node-fetch": "1.3.2", "node-uuid": "1.4.7", "pegjs": "0.9.0", diff --git a/src/core_plugins/tagcloud/index.js b/src/core_plugins/tagcloud/index.js new file mode 100644 index 0000000000000..fe00c2462e893 --- /dev/null +++ b/src/core_plugins/tagcloud/index.js @@ -0,0 +1,8 @@ +export default function (kibana) { + + return new kibana.Plugin({ + uiExports: { + visTypes: ['plugins/tagcloud/tag_cloud_vis'] + } + }); +}; diff --git a/src/core_plugins/tagcloud/package.json b/src/core_plugins/tagcloud/package.json new file mode 100644 index 0000000000000..4200ef264fece --- /dev/null +++ b/src/core_plugins/tagcloud/package.json @@ -0,0 +1,4 @@ +{ + "name": "tagcloud", + "version": "kibana" +} diff --git a/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js b/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js new file mode 100644 index 0000000000000..1b78b02b9aff3 --- /dev/null +++ b/src/core_plugins/tagcloud/public/__tests__/tag_cloud.js @@ -0,0 +1,181 @@ +import expect from 'expect.js'; +import _ from 'lodash'; +import TagCloud from 'plugins/tagcloud/tag_cloud'; + +describe('tag cloud', function () { + + let domNode; + + beforeEach(function () { + domNode = document.createElement('div'); + domNode.style.top = '0'; + domNode.style.left = '0'; + domNode.style.width = '512px'; + domNode.style.height = '512px'; + domNode.style.position = 'fixed'; + domNode.style['pointer-events'] = 'none'; + document.body.appendChild(domNode); + }); + + afterEach(function () { + document.body.removeChild(domNode); + }); + + + const baseTestConfig = { + data: [ + {text: 'foo', size: 1}, + {text: 'bar', size: 5}, + {text: 'foobar', size: 9}, + ], + options: { + orientation: 'single', + scale: 'linear', + minFontSize: 10, + maxFontSize: 36 + }, + expected: [ + { + text: 'foo', + fontSize: '10px' + }, + { + text: 'bar', + fontSize: '23px' + }, + { + text: 'foobar', + fontSize: '36px' + } + ] + }; + + const singleLayout = _.cloneDeep(baseTestConfig); + const rightAngleLayout = _.cloneDeep(baseTestConfig); + rightAngleLayout.options.orientation = 'right angled'; + const multiLayout = _.cloneDeep(baseTestConfig); + multiLayout.options.orientation = 'multiple'; + const logScale = _.cloneDeep(baseTestConfig); + logScale.options.scale = 'log'; + logScale.expected[1].fontSize = '31px'; + const sqrtScale = _.cloneDeep(baseTestConfig); + sqrtScale.options.scale = 'square root'; + sqrtScale.expected[1].fontSize = '27px'; + const biggerFont = _.cloneDeep(baseTestConfig); + biggerFont.options.minFontSize = 36; + biggerFont.options.maxFontSize = 72; + biggerFont.expected[0].fontSize = '36px'; + biggerFont.expected[1].fontSize = '54px'; + biggerFont.expected[2].fontSize = '72px'; + + [ + singleLayout, + rightAngleLayout, + multiLayout, + logScale, + sqrtScale, + biggerFont + ].forEach((test, index) => { + + it(`should position elements correctly: ${index}`, done => { + const tagCloud = new TagCloud(domNode); + tagCloud.setData(test.data); + tagCloud.setOptions(test.options); + tagCloud.on('renderComplete', function onRender() { + tagCloud.removeListener('renderComplete', onRender); + const textElements = domNode.querySelectorAll('text'); + verifyTagProperties(test.expected, textElements); + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + done(); + }); + }); + }); + + it(`should not put elements in view when container to small`, function (done) { + + domNode.style.width = '1px'; + domNode.style.height = '1px'; + + const tagCloud = new TagCloud(domNode); + tagCloud.setData(baseTestConfig.data); + tagCloud.setOptions(baseTestConfig.options); + tagCloud.on('renderComplete', function onRender() { + tagCloud.removeListener('renderComplete', onRender); + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + const textElements = domNode.querySelectorAll('text'); + for (let i = 0; i < textElements; i++) { + const bbox = textElements[i].getBoundingClientRect(); + verifyBbox(bbox, false); + } + done(); + }); + }); + + + it(`tags should fit after making container bigger`, function (done) { + + domNode.style.width = '1px'; + domNode.style.height = '1px'; + + const tagCloud = new TagCloud(domNode); + tagCloud.setData(baseTestConfig.data); + tagCloud.setOptions(baseTestConfig.options); + tagCloud.on('renderComplete', function onRender() { + tagCloud.removeListener('renderComplete', onRender); + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + + domNode.style.width = '512px'; + domNode.style.height = '512px'; + tagCloud.on('renderComplete', _ => { + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + done(); + }); + tagCloud.resize(); + + }); + }); + + it(`tags should no longer fit after making container smaller`, function (done) { + + const tagCloud = new TagCloud(domNode); + tagCloud.setData(baseTestConfig.data); + tagCloud.setOptions(baseTestConfig.options); + tagCloud.on('renderComplete', function onRender() { + tagCloud.removeListener('renderComplete', onRender); + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + + domNode.style.width = '1px'; + domNode.style.height = '1px'; + tagCloud.on('renderComplete', _ => { + expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + done(); + }); + tagCloud.resize(); + + }); + + }); + + function verifyTagProperties(expectedValues, actualElements) { + expect(actualElements.length).to.equal(expectedValues.length); + expectedValues.forEach((test, index) => { + expect(actualElements[index].style.fontSize).to.equal(test.fontSize); + expect(actualElements[index].innerHTML).to.equal(test.text); + isInsideContainer(actualElements[index]); + }); + } + + function isInsideContainer(actualElement) { + const bbox = actualElement.getBoundingClientRect(); + verifyBbox(bbox, true); + } + + function verifyBbox(bbox, shouldBeInside) { + expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).to.be(shouldBeInside); + expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).to.be(shouldBeInside); + } + + +}); diff --git a/src/core_plugins/tagcloud/public/tag_cloud.js b/src/core_plugins/tagcloud/public/tag_cloud.js new file mode 100644 index 0000000000000..712826f6e23f4 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud.js @@ -0,0 +1,319 @@ +import d3 from 'd3'; +import d3TagCloud from 'd3-cloud'; +import vislibComponentsSeedColorsProvider from 'ui/vislib/components/color/seed_colors'; +import {EventEmitter} from 'events'; + + +const ORIENTATIONS = { + 'single': () => 0, + 'right angled': (tag) => { + return hashCode(tag.text) % 2 * 90; + }, + 'multiple': (tag) => { + const hashcode = Math.abs(hashCode(tag.text)); + return ((hashcode % 12) * 15) - 90;//fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) + } +}; +const D3_SCALING_FUNCTIONS = { + 'linear': d3.scale.linear(), + 'log': d3.scale.log(), + 'square root': d3.scale.sqrt() +}; + + +class TagCloud extends EventEmitter { + + constructor(domNode) { + + super(); + + this._element = domNode; + this._d3SvgContainer = d3.select(this._element).append('svg'); + this._svgGroup = this._d3SvgContainer.append('g'); + this._size = [1, 1]; + this.resize(); + + this._fontFamily = 'Impact'; + this._fontStyle = 'normal'; + this._fontWeight = 'normal'; + this._orientation = 'single'; + this._minFontSize = 10; + this._maxFontSize = 36; + this._textScale = 'linear'; + this._spiral = 'archimedean';//layout shape + this._timeInterval = 1000;//time allowed for layout algorithm + this._padding = 5; + + } + + + setOptions(options) { + if (JSON.stringify(options) === this._optionsAsString) { + return; + } + + this._optionsAsString = JSON.stringify(options); + this._orientation = options.orientation; + this._minFontSize = Math.min(options.minFontSize, options.maxFontSize); + this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize); + this._textScale = options.scale; + this._invalidate(false); + } + + + resize() { + + const newWidth = this._element.offsetWidth; + const newHeight = this._element.offsetHeight; + + if (newWidth < 1 || newHeight < 1) { + return; + } + + if (newWidth === this._size[0] && newHeight === this._size[1]) { + return; + } + + const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight; + const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight; + + this._size[0] = newWidth; + this._size[1] = newHeight; + + if (wasInside && willBeInside && this._allInViewBox) { + this._invalidate(true); + } else { + this._invalidate(false); + } + + } + + setData(data) { + this._words = data.map(toWordTag); + this._makeTextSizeMapper(); + this._invalidate(false); + } + + + destroy() { + clearTimeout(this._timeoutHandle); + this._element.innerHTML = ''; + } + + getStatus() { + return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE; + } + + _updateContainerSize() { + this._d3SvgContainer.attr('width', this._size[0]); + this._d3SvgContainer.attr('height', this._size[1]); + this._svgGroup.attr('width', this._size[0]); + this._svgGroup.attr('height', this._size[1]); + } + + _washWords() { + if (!this._words) { + return; + } + + //the tagCloudLayoutGenerator clobbers the word-object with metadata about positioning. + //This can causes corrupt states in the layout-generator + //where words get collapsed to the same location and do not reposition correctly. + //=> we recreate an empty word object without the metadata + this._words = this._words.map(toWordTag); + this._makeTextSizeMapper(); + } + + _onLayoutEnd() { + + const affineTransform = positionWord.bind(null, this._element.offsetWidth / 2, this._element.offsetHeight / 2); + const svgTextNodes = this._svgGroup.selectAll('text'); + const stage = svgTextNodes.data(this._words, getText); + + const enterSelection = stage.enter(); + const enteringTags = enterSelection.append('text'); + enteringTags.style('font-size', getSizeInPixels); + enteringTags.style('font-style', this._fontStyle); + enteringTags.style('font-weight', () => this._fontWeight); + enteringTags.style('font-family', () => this._fontFamily); + enteringTags.style('fill', getFill); + enteringTags.attr('text-anchor', () => 'middle'); + enteringTags.attr('transform', affineTransform); + enteringTags.text(getText); + + const self = this; + enteringTags.on({ + click: function (event) { + self.emit('select', event.text); + }, + mouseover: function (d) { + d3.select(this).style('cursor', 'pointer'); + }, + mouseout: function (d) { + d3.select(this).style('cursor', 'default'); + } + }); + + const movingTags = stage.transition(); + movingTags.duration(600); + movingTags.style('font-size', getSizeInPixels); + movingTags.style('font-style', this._fontStyle); + movingTags.style('font-weight', () => this._fontWeight); + movingTags.style('font-family', () => this._fontFamily); + movingTags.attr('transform', affineTransform); + + const exitingTags = stage.exit(); + const exitTransition = exitingTags.transition(); + exitTransition.duration(200); + exitingTags.style('fill-opacity', 1e-6); + exitingTags.attr('font-size', 1); + exitingTags.remove(); + + let exits = 0; + let moves = 0; + const resolveWhenDone = () => { + if (exits === 0 && moves === 0) { + const cloudBBox = this._svgGroup[0][0].getBBox(); + this._cloudWidth = cloudBBox.width; + this._cloudHeight = cloudBBox.height; + this._allInViewBox = cloudBBox.x >= 0 && cloudBBox.y >= 0 && + cloudBBox.x + cloudBBox.width <= this._element.offsetWidth && + cloudBBox.y + cloudBBox.height <= this._element.offsetHeight; + + this._dirtyPromise = null; + this._resolve(true); + this.emit('renderComplete'); + } + }; + exitTransition.each(_ => exits++); + exitTransition.each('end', () => { + exits--; + resolveWhenDone(); + }); + movingTags.each(_ => moves++); + movingTags.each('end', () => { + moves--; + resolveWhenDone(); + }); + + }; + + + _makeTextSizeMapper() { + this._mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale]; + if (this._words.length === 1) { + this._mapSizeToFontSize.range([this._maxFontSize, this._maxFontSize]); + } else { + this._mapSizeToFontSize.range([this._minFontSize, this._maxFontSize]); + } + + if (this._words) { + this._mapSizeToFontSize.domain(d3.extent(this._words, getSize)); + } + } + + _invalidate(keepLayout) { + + + if (!this._words) { + return; + } + + if (!this._dirtyPromise) { + this._dirtyPromise = new Promise((resolve, reject) => { + this._resolve = resolve; + }); + } + + clearTimeout(this._timeoutHandle); + this._timeoutHandle = requestAnimationFrame(() => { + this._timeoutHandle = null; + this._updateContainerSize(); + if (keepLayout) { + this._onLayoutEnd(); + } else { + this._washWords(); + this._updateLayout(); + } + }); + } + + _updateLayout() { + + const tagCloudLayoutGenerator = d3TagCloud(); + tagCloudLayoutGenerator.size(this._size); + tagCloudLayoutGenerator.padding(this._padding); + tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]); + tagCloudLayoutGenerator.font(this._fontFamily); + tagCloudLayoutGenerator.fontStyle(this._fontStyle); + tagCloudLayoutGenerator.fontWeight(this._fontWeight); + tagCloudLayoutGenerator.fontSize(tag => this._mapSizeToFontSize(tag.size)); + tagCloudLayoutGenerator.random(seed); + tagCloudLayoutGenerator.spiral(this._spiral); + tagCloudLayoutGenerator.words(this._words); + tagCloudLayoutGenerator.text(getText); + tagCloudLayoutGenerator.timeInterval(this._timeInterval); + tagCloudLayoutGenerator.on('end', this._onLayoutEnd.bind(this)); + tagCloudLayoutGenerator.start(); + + } + +} + +TagCloud.STATUS = {COMPLETE: 0, INCOMPLETE: 1}; + +function seed() { + return 0.5;//constant random seed to ensure constant layouts for identical data +} + +function toWordTag(word) { + return {size: word.size, text: word.text}; +} + + +function getText(word) { + return word.text; +} + +function positionWord(xTranslate, yTranslate, word) { + + if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) { + return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`; + } + + return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`; +} + +function getSize(tag) { + return tag.size; +} + +function getSizeInPixels(tag) { + return `${tag.size}px`; +} + +const colorScale = d3.scale.ordinal().range(vislibComponentsSeedColorsProvider()); +function getFill(tag) { + return colorScale(tag.text); +} + +/** + * Hash a string to a number. Ensures there is no random element in positioning strings + * Retrieved from http://stackoverflow.com/questions/26057572/string-to-unique-hash-in-javascript-jquery + * @param string + */ +function hashCode(string) { + string = JSON.stringify(string); + let hash = 0; + if (string.length === 0) { + return hash; + } + for (let i = 0; i < string.length; i++) { + let char = string.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} + +export default TagCloud; diff --git a/src/core_plugins/tagcloud/public/tag_cloud.less b/src/core_plugins/tagcloud/public/tag_cloud.less new file mode 100644 index 0000000000000..c17a378d573c6 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud.less @@ -0,0 +1,47 @@ +.tagcloud-vis { + position: relative; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +.tagcloud-notifications { + position: absolute; + left: 0; + top: 0; + width: 100%; + text-align: center; + color: inherit; + font: inherit; +} + +.tagcloud-incomplete-message { + display: none; +} + +.tagcloud-truncated-message { + display: none; +} + +.tagcloud-custom-label { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + text-align: center; + color: inherit; + font-weight: bold; +} + +.tag-cloud-fontsize-slider { + margin-top: 38px; + margin-bottom: 36px; + margin-left: 12px; + margin-right: 24px; +} + +.tag-cloud-fontsize-slider .noUi-connect { + background: #e4e4e4; +} diff --git a/src/core_plugins/tagcloud/public/tag_cloud_controller.html b/src/core_plugins/tagcloud/public/tag_cloud_controller.html new file mode 100644 index 0000000000000..bb667aad52883 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud_controller.html @@ -0,0 +1,7 @@ +
+
+
+
The number of tags has been truncated to avoid long draw times.
+
The container is too small to display the entire cloud. Tags may appear cropped or be ommitted.
+
+
diff --git a/src/core_plugins/tagcloud/public/tag_cloud_controller.js b/src/core_plugins/tagcloud/public/tag_cloud_controller.js new file mode 100644 index 0000000000000..b41a364bee1d6 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud_controller.js @@ -0,0 +1,101 @@ +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import TagCloud from 'plugins/tagcloud/tag_cloud'; +import AggConfigResult from 'ui/vis/agg_config_result'; +import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler'; + +const module = uiModules.get('kibana/tagcloud', ['kibana']); +module.controller('KbnTagCloudController', function ($scope, $element, Private, getAppState) { + + const containerNode = $element[0]; + const filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider); + const maxTagCount = 200; + let truncated = false; + + const tagCloud = new TagCloud(containerNode); + tagCloud.on('select', (event) => { + const appState = getAppState(); + const clickHandler = filterBarClickHandler(appState); + const aggs = $scope.vis.aggs.getResponseAggs(); + const aggConfigResult = new AggConfigResult(aggs[0], false, event, event); + clickHandler({point: {aggConfigResult: aggConfigResult}}); + }); + tagCloud.on('renderComplete', () => { + + const bucketName = containerNode.querySelector('.tagcloud-custom-label'); + bucketName.innerHTML = `${$scope.vis.aggs[0].makeLabel()} - ${$scope.vis.aggs[1].makeLabel()}`; + + const truncatedMessage = containerNode.querySelector('.tagcloud-truncated-message'); + truncatedMessage.style.display = truncated ? 'block' : 'none'; + + const incompleteMessage = containerNode.querySelector('.tagcloud-incomplete-message'); + const status = tagCloud.getStatus(); + + if (TagCloud.STATUS.COMPLETE === status) { + incompleteMessage.style.display = 'none'; + } else if (TagCloud.STATUS.INCOMPLETE === status) { + incompleteMessage.style.display = 'block'; + } + if (typeof $scope.vis.emit === 'function') { + $scope.vis.emit('renderComplete'); + } + }); + + $scope.$watch('esResponse', async function (response) { + + if (!response) { + return; + } + + const tagsAggId = _.first(_.pluck($scope.vis.aggs.bySchemaName.segment, 'id')); + if (!tagsAggId || !response.aggregations) { + return; + } + + const metricsAgg = _.first($scope.vis.aggs.bySchemaName.metric); + const buckets = response.aggregations[tagsAggId].buckets; + + const tags = buckets.map((bucket) => { + return { + text: bucket.key, + size: getValue(metricsAgg, bucket) + }; + }); + + + if (tags.length > maxTagCount) { + tags.length = maxTagCount; + truncated = true; + } else { + truncated = false; + } + + tagCloud.setData(tags); + }); + + + $scope.$watch('vis.params', (options) => tagCloud.setOptions(options)); + + $scope.$watch(getContainerSize, _.debounce(() => { + tagCloud.resize(); + }, 1000, {trailing: true}), true); + + + function getContainerSize() { + return {width: $element.width(), height: $element.height()}; + } + + function getValue(metricsAgg, bucket) { + let size = metricsAgg.getValue(bucket); + if (typeof size !== 'number' || isNaN(size)) { + try { + size = bucket[1].values[0].value;//lift out first value (e.g. median aggregations return as array) + } catch (e) { + size = 1;//punt + } + } + return size; + } + + +}); diff --git a/src/core_plugins/tagcloud/public/tag_cloud_vis.js b/src/core_plugins/tagcloud/public/tag_cloud_vis.js new file mode 100644 index 0000000000000..6c026ae54eb9e --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud_vis.js @@ -0,0 +1,57 @@ +import 'plugins/tagcloud/tag_cloud.less'; +import 'plugins/tagcloud/tag_cloud_controller'; +import 'plugins/tagcloud/tag_cloud_vis_params'; +import TemplateVisTypeTemplateVisTypeProvider from 'ui/template_vis_type/template_vis_type'; +import VisSchemasProvider from 'ui/vis/schemas'; +import tagCloudTemplate from 'plugins/tagcloud/tag_cloud_controller.html'; +import visTypes from 'ui/registry/vis_types'; + +visTypes.register(function TagCloudProvider(Private) { + const TemplateVisType = Private(TemplateVisTypeTemplateVisTypeProvider); + const Schemas = Private(VisSchemasProvider); + + return new TemplateVisType({ + name: 'tagcloud', + title: 'Tag cloud', + description: 'A tag cloud visualization is a visual representation of text data, ' + + 'typically used to visualize free form text. Tags are usually single words. The font size of word corresponds' + + 'with its importance.', + icon: 'fa-cloud', + template: tagCloudTemplate, + params: { + defaults: { + scale: 'linear', + orientation: 'single', + minFontSize: 18, + maxFontSize: 72 + }, + scales: ['linear', 'log', 'square root'], + orientations: ['single', 'right angled', 'multiple'], + editor: '' + }, + schemas: new Schemas([ + { + group: 'metrics', + name: 'metric', + title: 'Tag Size', + min: 1, + max: 1, + aggFilter: ['!std_dev', '!percentiles', '!percentile_ranks'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: 'buckets', + name: 'segment', + icon: 'fa fa-cloud', + title: 'Tags', + min: 1, + max: 1, + aggFilter: ['terms'] + } + ]) + }); +}); + + diff --git a/src/core_plugins/tagcloud/public/tag_cloud_vis_params.html b/src/core_plugins/tagcloud/public/tag_cloud_vis_params.html new file mode 100644 index 0000000000000..7c466480b22a5 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud_vis_params.html @@ -0,0 +1,21 @@ +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
diff --git a/src/core_plugins/tagcloud/public/tag_cloud_vis_params.js b/src/core_plugins/tagcloud/public/tag_cloud_vis_params.js new file mode 100644 index 0000000000000..f6d127931d619 --- /dev/null +++ b/src/core_plugins/tagcloud/public/tag_cloud_vis_params.js @@ -0,0 +1,32 @@ +import uiModules from 'ui/modules'; +import tagCloudVisParamsTemplate from 'plugins/tagcloud/tag_cloud_vis_params.html'; +import noUiSlider from 'no-ui-slider'; +import 'no-ui-slider/css/nouislider.css'; +import 'no-ui-slider/css/nouislider.pips.css'; +import 'no-ui-slider/css/nouislider.tooltips.css'; + +uiModules.get('kibana/table_vis') + .directive('tagcloudVisParams', function () { + return { + restrict: 'E', + template: tagCloudVisParamsTemplate, + link: function ($scope, $element) { + const sliderContainer = $element[0]; + var slider = sliderContainer.querySelector('.tag-cloud-fontsize-slider'); + noUiSlider.create(slider, { + start: [$scope.vis.params.minFontSize, $scope.vis.params.maxFontSize], + connect: true, + tooltips: true, + step: 1, + range: {'min': 1, 'max': 100}, + format: {to: (value) => parseInt(value) + 'px', from: value => parseInt(value)} + }); + slider.noUiSlider.on('change', function () { + const fontSize = slider.noUiSlider.get(); + $scope.vis.params.minFontSize = parseInt(fontSize[0], 10); + $scope.vis.params.maxFontSize = parseInt(fontSize[1], 10); + $scope.$apply(); + }); + } + }; + }); diff --git a/src/ui/public/agg_types/metrics/median.js b/src/ui/public/agg_types/metrics/median.js index 0b01fbbd1b116..4c36b0d9a4de2 100644 --- a/src/ui/public/agg_types/metrics/median.js +++ b/src/ui/public/agg_types/metrics/median.js @@ -1,10 +1,8 @@ -import _ from 'lodash'; import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; -import AggTypesMetricsGetResponseAggConfigClassProvider from 'ui/agg_types/metrics/get_response_agg_config_class'; import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles'; export default function AggTypeMetricMedianProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); - let getResponseAggConfigClass = Private(AggTypesMetricsGetResponseAggConfigClassProvider); let percentiles = Private(AggTypesMetricsPercentilesProvider); return new MetricAggType({ diff --git a/test/functional/apps/visualize/_chart_types.js b/test/functional/apps/visualize/_chart_types.js index 081f1a3b01e66..e62fcee08811a 100644 --- a/test/functional/apps/visualize/_chart_types.js +++ b/test/functional/apps/visualize/_chart_types.js @@ -1,9 +1,7 @@ - import expect from 'expect.js'; import { - bdd, - scenarioManager, + bdd } from '../../../support'; import PageObjects from '../../../support/page_objects'; @@ -19,16 +17,16 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.it('should show the correct chart types', function () { var expectedChartTypes = [ 'Area chart', 'Data table', 'Line chart', 'Markdown widget', - 'Metric', 'Pie chart', 'Tile map', 'Timeseries', 'Vertical bar chart' + 'Metric', 'Pie chart', 'Tag cloud', 'Tile map', 'Timeseries', 'Vertical bar chart' ]; // find all the chart types and make sure there all there return PageObjects.visualize.getChartTypes() - .then(function testChartTypes(chartTypes) { - PageObjects.common.debug('returned chart types = ' + chartTypes); - PageObjects.common.debug('expected chart types = ' + expectedChartTypes); - PageObjects.common.saveScreenshot('Visualize-chart-types'); - expect(chartTypes).to.eql(expectedChartTypes); - }); + .then(function testChartTypes(chartTypes) { + PageObjects.common.debug('returned chart types = ' + chartTypes); + PageObjects.common.debug('expected chart types = ' + expectedChartTypes); + PageObjects.common.saveScreenshot('Visualize-chart-types'); + expect(chartTypes).to.eql(expectedChartTypes); + }); }); }); }); From 4556ac3b94b4dd23eeb3f0d6111a28543dd2b357 Mon Sep 17 00:00:00 2001 From: Joe Fleming Date: Tue, 22 Nov 2016 09:45:06 -0700 Subject: [PATCH 03/11] Trigger renderComplete on DOM instead of the Vis (#9176) * correctly use implementsRenderComplete from vis config * set implementsRenderComplete on all vis types * emit on DOM instead of the vis * vis doesn't need to be an EventEmitter * listen for renderComplete on visualize set and update element attributes as the event(s) fire, and indicate if they ever will * [vislib/handler] fall through to lower return * [vislibRenderbot/tests] reduce expected call count * [vis/tests] add $element to test injectors * [markdownVis] fix test --- .../kbn_vislib_vis_types/public/area.js | 1 + .../kbn_vislib_vis_types/public/histogram.js | 1 + .../kbn_vislib_vis_types/public/line.js | 1 + .../kbn_vislib_vis_types/public/pie.js | 1 + .../kbn_vislib_vis_types/public/tile_map.js | 1 + .../public/__tests__/markdown_vis_controller.js | 14 +++++++------- .../markdown_vis/public/markdown_vis_controller.js | 4 ++-- .../public/__tests__/metric_vis_controller.js | 4 +++- .../metric_vis/public/metric_vis_controller.js | 4 ++-- .../table_vis/public/table_vis_controller.js | 4 ++-- .../tagcloud/public/tag_cloud_controller.js | 5 ++--- .../timelion/public/vis/timelion_vis_controller.js | 13 +++++++++++-- .../public/template_vis_type/template_vis_type.js | 2 +- src/ui/public/vis/vis.js | 13 ------------- src/ui/public/vis/vis_type.js | 2 +- src/ui/public/vislib/lib/handler/handler.js | 9 +++++---- .../vislib_vis_type/__tests__/_vislib_renderbot.js | 2 +- src/ui/public/vislib_vis_type/vislib_renderbot.js | 4 ---- src/ui/public/vislib_vis_type/vislib_vis_type.js | 5 +---- src/ui/public/visualize/visualize.js | 13 ++++++++++++- 20 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/core_plugins/kbn_vislib_vis_types/public/area.js b/src/core_plugins/kbn_vislib_vis_types/public/area.js index 9e8e80d5f89cd..cf33aaef09a04 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/area.js @@ -47,6 +47,7 @@ export default function HistogramVisType(Private) { modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'], editor: areaTemplate }, + implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js index 1b2d803ac644b..3b3db63fddcdc 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -43,6 +43,7 @@ export default function HistogramVisType(Private) { modes: ['stacked', 'percentage', 'grouped'], editor: histogramTemplate }, + implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/line.js b/src/core_plugins/kbn_vislib_vis_types/public/line.js index a8b4eedff6535..c061d6183237d 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/line.js @@ -46,6 +46,7 @@ export default function HistogramVisType(Private) { scales: ['linear', 'log', 'square root'], editor: lineTemplate }, + implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/core_plugins/kbn_vislib_vis_types/public/pie.js index 64b4b4688156f..9a66fd9924ccb 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -37,6 +37,7 @@ export default function HistogramVisType(Private) { }, responseConverter: false, hierarchicalData: true, + implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js index 875c079a2558e..1a79dfd35ac19 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js @@ -81,6 +81,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) { } }, responseConverter: geoJsonConverter, + implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', diff --git a/src/core_plugins/markdown_vis/public/__tests__/markdown_vis_controller.js b/src/core_plugins/markdown_vis/public/__tests__/markdown_vis_controller.js index b85bf484f99f7..f9dc317de5f2b 100644 --- a/src/core_plugins/markdown_vis/public/__tests__/markdown_vis_controller.js +++ b/src/core_plugins/markdown_vis/public/__tests__/markdown_vis_controller.js @@ -1,24 +1,24 @@ import ngMock from 'ng_mock'; import expect from 'expect.js'; +import $ from 'jquery'; describe('markdown vis controller', function () { let $scope; - let $el; beforeEach(ngMock.module('kibana/markdown_vis')); beforeEach(ngMock.inject(function ($rootScope, $controller) { $scope = $rootScope.$new(); - $scope.vis = { - emit: function () {} - }; - $controller('KbnMarkdownVisController', {$scope: $scope}); + const $element = $('
'); + $controller('KbnMarkdownVisController', { $scope, $element }); $scope.$digest(); })); it('should set html from markdown params', function () { expect($scope).to.not.have.property('html'); - $scope.vis.params = { - markdown: 'This is a test of the [markdown](http://daringfireball.net/projects/markdown) vis.' + $scope.vis = { + params: { + markdown: 'This is a test of the [markdown](http://daringfireball.net/projects/markdown) vis.' + } }; $scope.$digest(); diff --git a/src/core_plugins/markdown_vis/public/markdown_vis_controller.js b/src/core_plugins/markdown_vis/public/markdown_vis_controller.js index 11e44ca55727b..6913833640540 100644 --- a/src/core_plugins/markdown_vis/public/markdown_vis_controller.js +++ b/src/core_plugins/markdown_vis/public/markdown_vis_controller.js @@ -9,11 +9,11 @@ marked.setOptions({ const module = uiModules.get('kibana/markdown_vis', ['kibana', 'ngSanitize']); -module.controller('KbnMarkdownVisController', function ($scope) { +module.controller('KbnMarkdownVisController', function ($scope, $element) { $scope.$watch('vis.params.markdown', function (html) { if (html) { $scope.html = marked(html); } - $scope.vis.emit('renderComplete'); + $element.trigger('renderComplete'); }); }); diff --git a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js index a745d90c7f841..af11e615e94a8 100644 --- a/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js @@ -1,5 +1,6 @@ import ngMock from 'ng_mock'; import expect from 'expect.js'; +import $ from 'jquery'; describe('metric vis', function () { let $scope; @@ -11,7 +12,8 @@ describe('metric vis', function () { beforeEach(ngMock.module('kibana/metric_vis')); beforeEach(ngMock.inject(function ($rootScope, $controller) { $scope = $rootScope.$new(); - $controller('KbnMetricVisController', {$scope: $scope}); + const $element = $('
'); + $controller('KbnMetricVisController', { $scope, $element }); $scope.$digest(); })); diff --git a/src/core_plugins/metric_vis/public/metric_vis_controller.js b/src/core_plugins/metric_vis/public/metric_vis_controller.js index 2597b49e3e0c5..c9efb01d417a9 100644 --- a/src/core_plugins/metric_vis/public/metric_vis_controller.js +++ b/src/core_plugins/metric_vis/public/metric_vis_controller.js @@ -5,7 +5,7 @@ import uiModules from 'ui/modules'; // didn't already const module = uiModules.get('kibana/metric_vis', ['kibana']); -module.controller('KbnMetricVisController', function ($scope, Private) { +module.controller('KbnMetricVisController', function ($scope, $element, Private) { const tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); const metrics = $scope.metrics = []; @@ -34,7 +34,7 @@ module.controller('KbnMetricVisController', function ($scope, Private) { if (resp) { metrics.length = 0; $scope.processTableGroups(tabifyAggResponse($scope.vis, resp)); - $scope.vis.emit('renderComplete'); + $element.trigger('renderComplete'); } }); }); diff --git a/src/core_plugins/table_vis/public/table_vis_controller.js b/src/core_plugins/table_vis/public/table_vis_controller.js index b87a915910a6a..c32839a8347af 100644 --- a/src/core_plugins/table_vis/public/table_vis_controller.js +++ b/src/core_plugins/table_vis/public/table_vis_controller.js @@ -8,7 +8,7 @@ const module = uiModules.get('kibana/table_vis', ['kibana']); // add a controller to tha module, which will transform the esResponse into a // tabular format that we can pass to the table directive -module.controller('KbnTableVisController', function ($scope, Private) { +module.controller('KbnTableVisController', function ($scope, $element, Private) { const tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); var uiStateSort = ($scope.uiState) ? $scope.uiState.get('vis.params.sort') : {}; @@ -44,7 +44,7 @@ module.controller('KbnTableVisController', function ($scope, Private) { return table.rows.length > 0; }); - $scope.vis.emit('renderComplete'); + $element.trigger('renderComplete'); } $scope.hasSomeRows = hasSomeRows; diff --git a/src/core_plugins/tagcloud/public/tag_cloud_controller.js b/src/core_plugins/tagcloud/public/tag_cloud_controller.js index b41a364bee1d6..c745831b68f12 100644 --- a/src/core_plugins/tagcloud/public/tag_cloud_controller.js +++ b/src/core_plugins/tagcloud/public/tag_cloud_controller.js @@ -36,9 +36,8 @@ module.controller('KbnTagCloudController', function ($scope, $element, Private, } else if (TagCloud.STATUS.INCOMPLETE === status) { incompleteMessage.style.display = 'block'; } - if (typeof $scope.vis.emit === 'function') { - $scope.vis.emit('renderComplete'); - } + + $element.trigger('renderComplete'); }); $scope.$watch('esResponse', async function (response) { diff --git a/src/core_plugins/timelion/public/vis/timelion_vis_controller.js b/src/core_plugins/timelion/public/vis/timelion_vis_controller.js index 8dc120c7053e6..0850b86d59561 100644 --- a/src/core_plugins/timelion/public/vis/timelion_vis_controller.js +++ b/src/core_plugins/timelion/public/vis/timelion_vis_controller.js @@ -6,7 +6,16 @@ define(function (require) { var _ = require('lodash'); var module = require('ui/modules').get('kibana/timelion_vis', ['kibana']); - module.controller('TimelionVisController', function ($scope, Private, Notifier, $http, $rootScope, timefilter, getAppState) { + module.controller('TimelionVisController', function ( + $scope, + $element, + Private, + Notifier, + $http, + $rootScope, + timefilter, + getAppState + ) { var queryFilter = Private(require('ui/filter_bar/query_filter')); var timezone = Private(require('plugins/timelion/services/timezone'))(); var dashboardContext = Private(require('plugins/timelion/services/dashboard_context')); @@ -61,7 +70,7 @@ define(function (require) { $scope.$on('renderComplete', event => { event.stopPropagation(); - $scope.vis.emit('renderComplete'); + $element.trigger('renderComplete'); }); }); diff --git a/src/ui/public/template_vis_type/template_vis_type.js b/src/ui/public/template_vis_type/template_vis_type.js index 3203b047ac583..70509d53da677 100644 --- a/src/ui/public/template_vis_type/template_vis_type.js +++ b/src/ui/public/template_vis_type/template_vis_type.js @@ -6,7 +6,7 @@ export default function TemplateVisTypeFactory(Private) { let TemplateRenderbot = Private(TemplateVisTypeTemplateRenderbotProvider); _.class(TemplateVisType).inherits(VisType); - function TemplateVisType(opts) { + function TemplateVisType(opts = {}) { TemplateVisType.Super.call(this, opts); this.template = opts.template; diff --git a/src/ui/public/vis/vis.js b/src/ui/public/vis/vis.js index ecfc7e730a62d..b92735498ba48 100644 --- a/src/ui/public/vis/vis.js +++ b/src/ui/public/vis/vis.js @@ -20,14 +20,12 @@ export default function VisFactory(Notifier, Private) { let visTypes = Private(RegistryVisTypesProvider); let AggConfigs = Private(VisAggConfigsProvider); const PersistedState = Private(PersistedStateProvider); - const EventEmitter = Private(EventsProvider); let notify = new Notifier({ location: 'Vis' }); function Vis(indexPattern, state, uiState) { - EventEmitter.call(this); state = state || {}; if (_.isString(state)) { @@ -40,8 +38,6 @@ export default function VisFactory(Notifier, Private) { this.setState(state); this.setUiState(uiState); - - this.on('renderComplete', () => this._renderComplete = true); } Vis.convertOldState = function (type, oldState) { @@ -85,7 +81,6 @@ export default function VisFactory(Notifier, Private) { }; }; - Vis.prototype = Object.create(EventEmitter.prototype); Vis.prototype.type = 'histogram'; Vis.prototype.setState = function (state) { @@ -173,14 +168,6 @@ export default function VisFactory(Notifier, Private) { return this.type.implementsRenderComplete; }; - Vis.prototype.onRenderComplete = function (cb) { - this.on('renderComplete', cb); - if (this._renderComplete) { - // guarantees that caller receives event in case visualization already finished rendering - this.emit('renderComplete'); - } - }; - /** * Currently this is only used to extract map-specific information * (e.g. mapZoom, mapCenter). diff --git a/src/ui/public/vis/vis_type.js b/src/ui/public/vis/vis_type.js index 03f963734cc61..7faf37b5dea66 100644 --- a/src/ui/public/vis/vis_type.js +++ b/src/ui/public/vis/vis_type.js @@ -15,7 +15,7 @@ export default function VisTypeFactory(Private) { this.schemas = opts.schemas || new VisTypeSchemas(); this.params = opts.params || {}; this.requiresSearch = opts.requiresSearch == null ? true : opts.requiresSearch; // Default to true unless otherwise specified - this.implementsRenderComplete = false; + this.implementsRenderComplete = opts.implementsRenderComplete || false; } VisType.prototype.createRenderbot = function (vis, $el, uiState) { diff --git a/src/ui/public/vislib/lib/handler/handler.js b/src/ui/public/vislib/lib/handler/handler.js index 68b9987164745..6d5885020f024 100644 --- a/src/ui/public/vislib/lib/handler/handler.js +++ b/src/ui/public/vislib/lib/handler/handler.js @@ -1,5 +1,6 @@ import d3 from 'd3'; import _ from 'lodash'; +import $ from 'jquery'; import errors from 'ui/errors'; import Binder from 'ui/binder'; import VislibLibDataProvider from 'ui/vislib/lib/data'; @@ -126,8 +127,8 @@ export default function HandlerBaseClass(Private) { binder.on(chart.events, 'rendered', () => { loadedCount++; if (loadedCount === chartSelection.length) { - // events from all charts are propagated to vis, we only need to fire renderComplete on one (first) - charts[0].events.emit('renderComplete'); + // events from all charts are propagated to vis, we only need to fire renderComplete once they all finish + $(self.el).trigger('renderComplete'); } }); @@ -184,11 +185,11 @@ export default function HandlerBaseClass(Private) { .append('h4').text(message); div.append('div').attr('class', 'item bottom'); - return div; } else { div.append('h4').text(message); } - this.vis.emit('renderComplete'); + + $(this.el).trigger('renderComplete'); return div; }; diff --git a/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js b/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js index 5e1e1f8bc6988..654bb5f01d5f9 100644 --- a/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js +++ b/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js @@ -77,7 +77,7 @@ describe('renderbot', function exportWrapper() { }); it('should attach listeners and set vislibVis', function () { - expect(listenerSpy.callCount).to.be(4); + expect(listenerSpy.callCount).to.be(3); expect(listenerSpy.calledWith('test', _.noop)).to.be(true); expect(renderbot.vislibVis).to.be.a(Vis); }); diff --git a/src/ui/public/vislib_vis_type/vislib_renderbot.js b/src/ui/public/vislib_vis_type/vislib_renderbot.js index dea21e72df926..20787334fb787 100644 --- a/src/ui/public/vislib_vis_type/vislib_renderbot.js +++ b/src/ui/public/vislib_vis_type/vislib_renderbot.js @@ -24,10 +24,6 @@ module.exports = function VislibRenderbotFactory(Private, $injector) { this.vislibVis.on(event, listener); }); - this.vislibVis.on('renderComplete', () => { - this.vis.emit('renderComplete'); - }); - if (this.chartData) { this.vislibVis.render(this.chartData, this.uiState); } diff --git a/src/ui/public/vislib_vis_type/vislib_vis_type.js b/src/ui/public/vislib_vis_type/vislib_vis_type.js index f16e768cdfd5f..40a58d5347166 100644 --- a/src/ui/public/vislib_vis_type/vislib_vis_type.js +++ b/src/ui/public/vislib_vis_type/vislib_vis_type.js @@ -16,9 +16,7 @@ export default function VislibVisTypeFactory(Private) { _.class(VislibVisType).inherits(VisType); - function VislibVisType(opts) { - opts = opts || {}; - + function VislibVisType(opts = {}) { VislibVisType.Super.call(this, opts); if (this.responseConverter == null) { @@ -26,7 +24,6 @@ export default function VislibVisTypeFactory(Private) { } this.listeners = opts.listeners || {}; - this.implementsRenderComplete = true; } VislibVisType.prototype.createRenderbot = function (vis, $el, uiState) { diff --git a/src/ui/public/visualize/visualize.js b/src/ui/public/visualize/visualize.js index a0c07675da311..29234e2b46cbf 100644 --- a/src/ui/public/visualize/visualize.js +++ b/src/ui/public/visualize/visualize.js @@ -72,6 +72,16 @@ uiModules return legendPositionToVisContainerClassMap[$scope.vis.params.legendPosition]; }; + if ($scope.vis.implementsRenderComplete()) { + $el.attr('has-render-count', 'true'); + let renderCount = 0; + $el.on('renderComplete', () => { + $el.attr('render-count', ++renderCount); + }); + } else { + $el.attr('has-render-count', 'false'); + } + $scope.spy = {}; $scope.spy.mode = ($scope.uiState) ? $scope.uiState.get('spy.mode', {}) : {}; @@ -168,7 +178,7 @@ uiModules }).catch(notify.fatal); searchSource.onError(e => { - $scope.vis.emit('renderComplete'); + $el.trigger('renderComplete'); if (isTermSizeZeroError(e)) { return notify.error( `Your visualization ('${$scope.vis.title}') has an error: it has a term ` + @@ -194,6 +204,7 @@ uiModules $scope.$on('$destroy', function () { if ($scope.renderbot) { + $el.off('renderComplete'); $scope.renderbot.destroy(); } }); From 11f5711aa28858673d73853f6a350d716171af5f Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 23 Nov 2016 15:02:31 +0100 Subject: [PATCH 04/11] fixing timelion Y axis #9114 (#9197) --- src/core_plugins/timelion/public/panels/timechart/schema.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core_plugins/timelion/public/panels/timechart/schema.js b/src/core_plugins/timelion/public/panels/timechart/schema.js index d5f1c19ced270..c80595082bafa 100644 --- a/src/core_plugins/timelion/public/panels/timechart/schema.js +++ b/src/core_plugins/timelion/public/panels/timechart/schema.js @@ -217,6 +217,7 @@ module.exports = function timechartFn(Private, config, $rootScope, timefilter, $ // This is kind of gross, it means that you can't replace a global value with a null // best you can do is an empty string. Deal with it. if (objVal == null) return srcVal; + if (srcVal == null) return objVal; }); } From a0a1fbda56156903abcd77186d98396b4f3e8872 Mon Sep 17 00:00:00 2001 From: Clinton Gormley Date: Wed, 23 Nov 2016 17:15:51 +0100 Subject: [PATCH 05/11] Fix UI link to filebeat getting started --- src/ui/public/documentation_links/documentation_links.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/public/documentation_links/documentation_links.js b/src/ui/public/documentation_links/documentation_links.js index 36f83e0e969cd..e1b9013a04915 100644 --- a/src/ui/public/documentation_links/documentation_links.js +++ b/src/ui/public/documentation_links/documentation_links.js @@ -12,7 +12,7 @@ export default { configuration: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/filebeat-configuration.html`, elasticsearchOutput: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/elasticsearch-output.html`, elasticsearchOutputAnchorParameters: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/elasticsearch-output.html#_parameters`, - startup: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/_step_5_starting_filebeat.html`, + startup: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/filebeat-starting.html`, exportedFields: `${baseUrl}guide/en/beats/filebeat/${urlVersion}/exported-fields.html` }, scriptedFields: { From 0e7b5c7b0678e78b3ed53c0941200cb5c536fa78 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 23 Nov 2016 13:18:05 -0600 Subject: [PATCH 06/11] Prevent crashing from incompatible nodes without http when logging (#9181) * Do not crash for warning and error nodes without http published address * [es version check] Prevent failure on incompatible nodes without http --- .../lib/__tests__/check_es_version.js | 24 +++++++++++++++++++ .../elasticsearch/lib/check_es_version.js | 7 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/core_plugins/elasticsearch/lib/__tests__/check_es_version.js b/src/core_plugins/elasticsearch/lib/__tests__/check_es_version.js index 74d43816d8384..cc36d350d6eb0 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/check_es_version.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/check_es_version.js @@ -56,6 +56,12 @@ describe('plugins/elasticsearch', () => { client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes })); } + function setNodeWithoutHTTP(version) { + const nodes = { 'node-without-http': { version, ip: 'ip' } }; + const client = server.plugins.elasticsearch.client; + client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes })); + } + it('returns true with single a node that matches', async () => { setNodes('5.1.0'); const result = await checkEsVersion(server, KIBANA_VERSION); @@ -99,6 +105,24 @@ describe('plugins/elasticsearch', () => { expect(server.log.getCall(1).args[0]).to.contain('warning'); }); + it('warns if a node is off by a patch version and without http publish address', async () => { + setNodeWithoutHTTP('5.1.1'); + await checkEsVersion(server, KIBANA_VERSION); + sinon.assert.callCount(server.log, 2); + expect(server.log.getCall(0).args[0]).to.contain('debug'); + expect(server.log.getCall(1).args[0]).to.contain('warning'); + }); + + it('errors if a node incompatible and without http publish address', async () => { + setNodeWithoutHTTP('6.1.1'); + try { + await checkEsVersion(server, KIBANA_VERSION); + } catch (e) { + expect(e.message).to.contain('incompatible nodes'); + expect(e).to.be.a(Error); + } + }); + it('only warns once per node list', async () => { setNodes('5.1.1'); diff --git a/src/core_plugins/elasticsearch/lib/check_es_version.js b/src/core_plugins/elasticsearch/lib/check_es_version.js index b9eddd0e0f249..a6cc89978ac9b 100644 --- a/src/core_plugins/elasticsearch/lib/check_es_version.js +++ b/src/core_plugins/elasticsearch/lib/check_es_version.js @@ -44,7 +44,8 @@ module.exports = function checkEsVersion(server, kibanaVersion) { function getHumanizedNodeNames(nodes) { return nodes.map(node => { - return 'v' + node.version + ' @ ' + node.http.publish_address + ' (' + node.ip + ')'; + const publishAddress = _.get(node, 'http.publish_address') ? (_.get(node, 'http.publish_address') + ' ') : ''; + return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; }); } @@ -52,7 +53,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) { const simplifiedNodes = warningNodes.map(node => ({ version: node.version, http: { - publish_address: node.http.publish_address, + publish_address: _.get(node, 'http.publish_address') }, ip: node.ip, })); @@ -78,7 +79,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) { throw new Error( `This version of Kibana requires Elasticsearch v` + `${kibanaVersion} on all nodes. I found ` + - `the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(',')}` + `the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(', ')}` ); } From 9527a8f2cafdc9723de489f90bf64331679db2f3 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 23 Nov 2016 15:03:47 -0700 Subject: [PATCH 07/11] `render-counter` directive (#9206) * [docTable/row] delay first render until watchers initialize * [docTable/row] emit a renderComplete event, count in table directive * [renderCounter] unify render counting login into directive * [docTable] remove unused var --- .../dashboard/components/panel/panel.html | 5 ++- .../kibana/public/discover/index.html | 3 +- .../public/visualize/editor/editor.html | 1 + src/ui/public/directives/render_counter.js | 31 +++++++++++++++++++ .../public/doc_table/components/table_row.js | 17 ++++------ src/ui/public/doc_table/doc_table.html | 2 +- src/ui/public/visualize/visualize.js | 13 +++----- 7 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 src/ui/public/directives/render_counter.js diff --git a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html index 6665034471980..874a62b75522a 100644 --- a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html +++ b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html @@ -28,13 +28,16 @@ search-source="savedObj.searchSource" show-spy-panel="chrome.getVisible()" ui-state="uiState" + render-counter class="panel-content"> - diff --git a/src/core_plugins/kibana/public/discover/index.html b/src/core_plugins/kibana/public/discover/index.html index 1a60d56b6b641..0f2faa466bc67 100644 --- a/src/core_plugins/kibana/public/discover/index.html +++ b/src/core_plugins/kibana/public/discover/index.html @@ -125,7 +125,8 @@

Searching

sorting="state.sort" columns="state.columns" infinite-scroll="true" - filter="filterQuery"> + filter="filterQuery" + render-counter>