mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Tagcloud (#9204)
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:
parent
3891b0e6c2
commit
867ff100df
15 changed files with 834 additions and 13 deletions
|
@ -118,3 +118,5 @@ include::visualize/pie.asciidoc[]
|
|||
include::visualize/tilemap.asciidoc[]
|
||||
|
||||
include::visualize/vertbar.asciidoc[]
|
||||
|
||||
include::visualize/tagcloud.asciidoc[]
|
||||
|
|
44
docs/visualize/tagcloud.asciidoc
Normal file
44
docs/visualize/tagcloud.asciidoc
Normal 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[]
|
|
@ -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",
|
||||
|
|
8
src/core_plugins/tagcloud/index.js
Normal file
8
src/core_plugins/tagcloud/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function (kibana) {
|
||||
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
visTypes: ['plugins/tagcloud/tag_cloud_vis']
|
||||
}
|
||||
});
|
||||
};
|
4
src/core_plugins/tagcloud/package.json
Normal file
4
src/core_plugins/tagcloud/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "tagcloud",
|
||||
"version": "kibana"
|
||||
}
|
181
src/core_plugins/tagcloud/public/__tests__/tag_cloud.js
Normal file
181
src/core_plugins/tagcloud/public/__tests__/tag_cloud.js
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
});
|
319
src/core_plugins/tagcloud/public/tag_cloud.js
Normal file
319
src/core_plugins/tagcloud/public/tag_cloud.js
Normal 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;
|
47
src/core_plugins/tagcloud/public/tag_cloud.less
Normal file
47
src/core_plugins/tagcloud/public/tag_cloud.less
Normal 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;
|
||||
}
|
|
@ -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>
|
101
src/core_plugins/tagcloud/public/tag_cloud_controller.js
Normal file
101
src/core_plugins/tagcloud/public/tag_cloud_controller.js
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
});
|
57
src/core_plugins/tagcloud/public/tag_cloud_vis.js
Normal file
57
src/core_plugins/tagcloud/public/tag_cloud_vis.js
Normal 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']
|
||||
}
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
|
21
src/core_plugins/tagcloud/public/tag_cloud_vis_params.html
Normal file
21
src/core_plugins/tagcloud/public/tag_cloud_vis_params.html
Normal 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>
|
32
src/core_plugins/tagcloud/public/tag_cloud_vis_params.js
Normal file
32
src/core_plugins/tagcloud/public/tag_cloud_vis_params.js
Normal 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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue