Backports PR #8104

**Commit 1:**
tagcloud squashed

* Original sha: 1282011fab
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-18T21:08:29Z

**Commit 2:**
ensure tags are not turned upside down

* Original sha: 68e09f477a
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T03:43:41Z

**Commit 3:**
remove unnecessary background

* Original sha: dba79b87ad
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T03:55:54Z

**Commit 4:**
add option to hide the label

* Original sha: 7f32544fa0
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T04:08:45Z

**Commit 5:**
use double-ende slider for font-size selection

* Original sha: 00c9ea8291
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T15:26:14Z

**Commit 6:**
give slider some more space

* Original sha: b282084cc5
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T16:38:55Z

**Commit 7:**
do not allow 0-sized tags

* Original sha: 42bbc39110
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T16:44:26Z

**Commit 8:**
fix typo

* Original sha: f753e1a90d
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T16:48:24Z

**Commit 9:**
make capitalization consistent

* Original sha: 63101d2c36
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T19:47:44Z

**Commit 10:**
doc checkin

* Original sha: fefa40a4c0
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T21:21:17Z

**Commit 11:**
minor doc edits

* Original sha: 84a8fc3abe
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T21:34:32Z

**Commit 12:**
doc build issues

* Original sha: cd2374a052
* Authored by Thomas Neirynck <thomas@elastic.co> on 2016-11-21T21:46:59Z
This commit is contained in:
jasper 2016-11-23 18:06:25 -05:00 committed by Court Ewing
parent 76897e1eb6
commit 3ca49d089a
15 changed files with 834 additions and 13 deletions

View file

@ -118,3 +118,5 @@ include::visualize/pie.asciidoc[]
include::visualize/tilemap.asciidoc[]
include::visualize/vertbar.asciidoc[]
include::visualize/tagcloud.asciidoc[]

View file

@ -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[]

View file

@ -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",

View file

@ -0,0 +1,8 @@
export default function (kibana) {
return new kibana.Plugin({
uiExports: {
visTypes: ['plugins/tagcloud/tag_cloud_vis']
}
});
};

View file

@ -0,0 +1,4 @@
{
"name": "tagcloud",
"version": "kibana"
}

View file

@ -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);
}
});

View file

@ -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;

View file

@ -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;
}

View file

@ -0,0 +1,7 @@
<div ng-controller="KbnTagCloudController" class="tagcloud-vis">
<div ng-show="vis.params.hideLabel" class="tagcloud-custom-label"></div>
<div class="tagcloud-notifications">
<div class="tagcloud-truncated-message">The number of tags has been truncated to avoid long draw times.</div>
<div class="tagcloud-incomplete-message">The container is too small to display the entire cloud. Tags may appear cropped or be ommitted.</div>
</div>
</div>

View file

@ -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;
}
});

View file

@ -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: '<tagcloud-vis-params></tagcloud-vis-params>'
},
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']
}
])
});
});

View file

@ -0,0 +1,21 @@
<div class="form-group">
<div class="form-group">
<label>Text Scale</label>
<select class="form-control" ng-model="vis.params.scale" ng-options="mode for mode in vis.type.params.scales"></select>
</div>
<div class="form-group">
<label>Orientations</label>
<select class="form-control" ng-model="vis.params.orientation" ng-options="mode for mode in vis.type.params.orientations"></select>
</div>
<div class="form-group">
<label>Font Size</label>
<div class="tag-cloud-fontsize-slider"></div>
</div>
<div>
<label>
<input type="checkbox" value="{{hideLabel}}" ng-model="vis.params.hideLabel" name="hideLabel"
ng-checked="vis.params.hideLabel">
Show Label
</label>
</div>
</div>

View file

@ -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();
});
}
};
});

View file

@ -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({

View file

@ -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);
});
});
});
});