diff --git a/package.json b/package.json index 9bb308d4cdcf1..2cdad4c19cb8b 100644 --- a/package.json +++ b/package.json @@ -320,6 +320,7 @@ "@types/classnames": "^2.2.9", "@types/color": "^3.0.0", "@types/d3": "^3.5.43", + "@types/d3-cloud": "^1.2.3", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", "@types/delete-empty": "^2.0.0", diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.tsx similarity index 91% rename from src/plugins/vis_type_tagcloud/public/components/feedback_message.js rename to src/plugins/vis_type_tagcloud/public/components/feedback_message.tsx index 7b284911b739f..dad403245216d 100644 --- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js +++ b/src/plugins/vis_type_tagcloud/public/components/feedback_message.tsx @@ -20,10 +20,11 @@ import React, { Component, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiIconTip } from '@elastic/eui'; +import { FeedbackMessageComponentState } from '../types'; -export class FeedbackMessage extends Component { - constructor() { - super(); +export class FeedbackMessage extends Component<{}, FeedbackMessageComponentState> { + constructor(props: {}) { + super(props); this.state = { shouldShowTruncate: false, shouldShowIncomplete: false }; } diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.tsx similarity index 87% rename from src/plugins/vis_type_tagcloud/public/components/label.js rename to src/plugins/vis_type_tagcloud/public/components/label.tsx index 168ec4b270fde..0015dee2ca6c4 100644 --- a/src/plugins/vis_type_tagcloud/public/components/label.js +++ b/src/plugins/vis_type_tagcloud/public/components/label.tsx @@ -18,10 +18,11 @@ */ import React, { Component } from 'react'; +import { LabelComponentState } from '../types'; -export class Label extends Component { - constructor() { - super(); +export class Label extends Component<{}, LabelComponentState> { + constructor(props: {}) { + super(props); this.state = { label: '', shouldShowLabel: true }; } diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.tsx similarity index 73% rename from src/plugins/vis_type_tagcloud/public/components/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.tsx index fae7cdf797958..e290e3c273861 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.tsx @@ -20,61 +20,111 @@ import d3 from 'd3'; import d3TagCloud from 'd3-cloud'; import { EventEmitter } from 'events'; - -const ORIENTATIONS = { +import { + D3ScalingFunction, + OrientationsFunction, + TagCloudVisParams, + TagType, + JobType, +} from '../types'; + +const ORIENTATIONS: OrientationsFunction = { single: () => 0, - 'right angled': tag => { + 'right angled': (tag: TagType) => { return hashWithinRange(tag.text, 2) * 90; }, - multiple: tag => { - return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) + multiple: (tag: TagType) => { + return hashWithinRange(tag.text, 12) * 15 - 90; // fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) }, }; -const D3_SCALING_FUNCTIONS = { +const D3_SCALING_FUNCTIONS: D3ScalingFunction = { linear: () => d3.scale.linear(), log: () => d3.scale.log(), 'square root': () => d3.scale.sqrt(), }; export class TagCloud extends EventEmitter { - constructor(domNode, colorScale) { + _element: HTMLElement; + _d3SvgContainer: d3.Selection; + _svgGroup: any; + _size: [number, number]; + + _fontFamily: string; + _fontStyle: string; + _fontWeight: string; + _spiral: string; + _timeInterval: number; + _padding: number; + + _orientation: TagCloudVisParams['orientation']; + _minFontSize: TagCloudVisParams['minFontSize']; + _maxFontSize: TagCloudVisParams['maxFontSize']; + _textScale: TagCloudVisParams['scale']; + _optionsAsString: string | null; + + _words: string | null; + + _colorScale: string; + _setTimeoutId: any; + _pendingJob: any; + _layoutIsUpdating: boolean | null; + _allInViewBox: boolean; + _DOMisUpdating: boolean; + + _cloudWidth: number; + _cloudHeight: number; + + _completedJob: Record | null; + tag: any; + + STATUS = { COMPLETE: 0, INCOMPLETE: 1 }; + + constructor(domNode: HTMLElement, colorScale: string) { super(); - //DOM + // DOM this._element = domNode; this._d3SvgContainer = d3.select(this._element).append('svg'); this._svgGroup = this._d3SvgContainer.append('g'); this._size = [1, 1]; this.resize(); - //SETTING (non-configurable) + // SETTING (non-configurable) this._fontFamily = 'Open Sans, sans-serif'; this._fontStyle = 'normal'; this._fontWeight = 'normal'; - this._spiral = 'archimedean'; //layout shape - this._timeInterval = 1000; //time allowed for layout algorithm + this._spiral = 'archimedean'; // layout shape + this._timeInterval = 1000; // time allowed for layout algorithm this._padding = 5; - //OPTIONS + // OPTIONS this._orientation = 'single'; this._minFontSize = 10; this._maxFontSize = 36; this._textScale = 'linear'; this._optionsAsString = null; - //DATA + // DATA this._words = null; - //UTIL + // UTIL this._colorScale = colorScale; this._setTimeoutId = null; this._pendingJob = null; this._layoutIsUpdating = null; this._allInViewBox = false; this._DOMisUpdating = false; + + this._cloudWidth = 0; + this._cloudHeight = 0; + + this._completedJob = null; + + this.STATUS.COMPLETE = 0; + this.STATUS.INCOMPLETE = 0; } - setOptions(options) { + setOptions(options: Record) { if (JSON.stringify(options) === this._optionsAsString) { return; } @@ -105,7 +155,7 @@ export class TagCloud extends EventEmitter { } } - setData(data) { + setData(data: any) { this._words = data; this._invalidate(false); } @@ -116,7 +166,7 @@ export class TagCloud extends EventEmitter { } getStatus() { - return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE; + return this._allInViewBox ? this.STATUS.COMPLETE : this.STATUS.INCOMPLETE; } _updateContainerSize() { @@ -140,7 +190,7 @@ export class TagCloud extends EventEmitter { } this._completedJob = null; - const job = await this._pickPendingJob(); + const job: JobType = await this._pickPendingJob(); if (job.words.length) { if (job.refreshLayout) { await this._updateLayout(job); @@ -159,7 +209,7 @@ export class TagCloud extends EventEmitter { } if (this._pendingJob) { - this._processPendingJob(); //pick up next job + this._processPendingJob(); // pick up next job } else { this._completedJob = job; this.emit('renderComplete'); @@ -167,7 +217,7 @@ export class TagCloud extends EventEmitter { } async _pickPendingJob() { - return await new Promise(resolve => { + return await new Promise(resolve => { this._setTimeoutId = setTimeout(async () => { const job = this._pendingJob; this._pendingJob = null; @@ -185,7 +235,7 @@ export class TagCloud extends EventEmitter { this._DOMisUpdating = false; } - async _updateDOM(job) { + async _updateDOM(job: JobType) { const canSkipDomUpdate = this._pendingJob || this._setTimeoutId; if (canSkipDomUpdate) { this._DOMisUpdating = false; @@ -216,13 +266,13 @@ export class TagCloud extends EventEmitter { const self = this; enteringTags.on({ - click: function(event) { + click(event: MouseEvent) { self.emit('select', event); }, - mouseover: function() { + mouseover() { d3.select(this).style('cursor', 'pointer'); }, - mouseout: function() { + mouseout() { d3.select(this).style('cursor', 'default'); }, }); @@ -265,14 +315,17 @@ export class TagCloud extends EventEmitter { _makeTextSizeMapper() { const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale](); - const range = - this._words.length === 1 - ? [this._maxFontSize, this._maxFontSize] - : [this._minFontSize, this._maxFontSize]; - mapSizeToFontSize.range(range); - if (this._words) { - mapSizeToFontSize.domain(d3.extent(this._words, getValue)); + if (this._words != null) { + const range = + this._words.length === 1 + ? [this._maxFontSize, this._maxFontSize] + : [this._minFontSize, this._maxFontSize]; + mapSizeToFontSize.range(range); + if (this._words) { + mapSizeToFontSize.domain(d3.extent(this._words, getValue)); + } } + return mapSizeToFontSize; } @@ -285,24 +338,26 @@ export class TagCloud extends EventEmitter { } _makeJobPreservingLayout() { - return { - refreshLayout: false, - size: this._size.slice(), - words: this._completedJob.words.map(tag => { - return { - x: tag.x, - y: tag.y, - rotate: tag.rotate, - size: tag.size, - rawText: tag.rawText || tag.text, - displayText: tag.displayText, - meta: tag.meta, - }; - }), - }; + if (this._completedJob != null) { + return { + refreshLayout: false, + size: this._size.slice(), + words: this._completedJob.words.map((tag: TagType) => { + return { + x: tag.x, + y: tag.y, + rotate: tag.rotate, + size: tag.size, + rawText: tag.rawText || tag.text, + displayText: tag.displayText, + meta: tag.meta, + }; + }), + }; + } } - _invalidate(keepLayout) { + _invalidate(keepLayout: boolean) { if (!this._words) { return; } @@ -314,7 +369,7 @@ export class TagCloud extends EventEmitter { this._processPendingJob(); } - async _updateLayout(job) { + async _updateLayout(job: JobType) { if (job.size[0] <= 0 || job.size[1] <= 0) { // If either width or height isn't above 0 we don't relayout anything, // since the d3-cloud will be stuck in an infinite loop otherwise. @@ -329,7 +384,7 @@ export class TagCloud extends EventEmitter { tagCloudLayoutGenerator.font(this._fontFamily); tagCloudLayoutGenerator.fontStyle(this._fontStyle); tagCloudLayoutGenerator.fontWeight(this._fontWeight); - tagCloudLayoutGenerator.fontSize(tag => mapSizeToFontSize(tag.value)); + tagCloudLayoutGenerator.fontSize((tag: TagType): any => mapSizeToFontSize(tag.value)); tagCloudLayoutGenerator.random(seed); tagCloudLayoutGenerator.spiral(this._spiral); tagCloudLayoutGenerator.words(job.words); @@ -350,10 +405,10 @@ export class TagCloud extends EventEmitter { * Returns debug info. For debugging only. * @return {*} */ - getDebugInfo() { - const debug = {}; + getDebugInfo(): any { + const debug: any = {}; debug.positions = this._completedJob - ? this._completedJob.words.map(tag => { + ? this._completedJob.words.map((tag: TagType) => { return { displayText: tag.displayText, rawText: tag.rawText || tag.text, @@ -370,43 +425,41 @@ export class TagCloud extends EventEmitter { return debug; } - getFill(tag) { + getFill(tag: TagType): string { return this._colorScale(tag.text); } } -TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 }; - -function seed() { - return 0.5; //constant seed (not random) to ensure constant layouts for identical data +function seed(): number { + return 0.5; // constant seed (not random) to ensure constant layouts for identical data } -function getText(word) { +function getText(word: TagType): string { return word.rawText; } -function getDisplayText(word) { +function getDisplayText(word: TagType): string { return word.displayText; } -function positionWord(xTranslate, yTranslate, word) { +function positionWord(xTranslate: number, yTranslate: number, word: TagType): string { if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) { - //move off-screen + // move off-screen return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`; } return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`; } -function getValue(tag) { +function getValue(tag: TagType): string | number { return tag.value; } -function getSizeInPixels(tag) { +function getSizeInPixels(tag: TagType): string { return `${tag.size}px`; } -function hashWithinRange(str, max) { +function hashWithinRange(str: string, max: any): number { str = JSON.stringify(str); let hash = 0; for (const ch of str) { diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.tsx similarity index 78% rename from src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.tsx index 4094f2ab59158..0e485655e6a68 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.tsx @@ -22,20 +22,32 @@ import * as Rx from 'rxjs'; import { take } from 'rxjs/operators'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; - +import d3 from 'd3'; import { getFormatService } from '../services'; - import { Label } from './label'; import { TagCloud } from './tag_cloud'; import { FeedbackMessage } from './feedback_message'; -import d3 from 'd3'; +import { VisParams } from '../../../visualizations/public'; const MAX_TAG_COUNT = 200; -export function createTagCloudVisualization({ colors }) { - const colorScale = d3.scale.ordinal().range(colors.seedColors); +export function createTagCloudVisualization({ colors }: { colors: any }) { + const colorScale: d3.scale.Ordinal = d3.scale + .ordinal() + .range(colors.seedColors); return class TagCloudVisualization { - constructor(node, vis) { + _containerNode: HTMLElement; + _vis: any; + _truncated: boolean; + _tagCloud: TagCloud; + _visParams: any; + _renderComplete$: any; + _feedbackNode: HTMLElement; + _feedbackMessage: any; + _labelNode: HTMLElement; + _label: React.RefObject