Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

Commit

Permalink
fix(plugin-chart-word-cloud): ensure top results are always displayed (
Browse files Browse the repository at this point in the history
…#841)

While resizing chart sometimes top results were filtered out because their sizes were too big. This
solution makes sure that top 10% of results will always be displayed by gradually scaling down the
chart if needed.
  • Loading branch information
agatapst authored Jan 12, 2021
1 parent 9fd7b38 commit 286bee7
Showing 1 changed file with 52 additions and 6 deletions.
58 changes: 52 additions & 6 deletions plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import cloudLayout, { Word } from 'd3-cloud';
import { PlainObject, createEncoderFactory, DeriveEncoding } from 'encodable';
import { PlainObject, createEncoderFactory, DeriveEncoding, Encoder } from 'encodable';
import { SupersetThemeProps, withTheme, seedRandom } from '@superset-ui/core';

export const ROTATION = {
Expand Down Expand Up @@ -38,6 +38,7 @@ export interface WordCloudProps extends WordCloudVisualProps {

export interface WordCloudState {
words: Word[];
scaleFactor: number;
}

const defaultProps: Required<WordCloudVisualProps> = {
Expand All @@ -47,6 +48,12 @@ const defaultProps: Required<WordCloudVisualProps> = {

type FullWordCloudProps = WordCloudProps & typeof defaultProps & SupersetThemeProps;

const SCALE_FACTOR_STEP = 0.5;
const MAX_SCALE_FACTOR = 3;
// Percentage of top results that will always be displayed.
// Needed to avoid clutter when shrinking a chart with many records.
const TOP_RESULTS_PERCENTAGE = 0.1;

class WordCloud extends React.PureComponent<FullWordCloudProps, WordCloudState> {
static defaultProps = defaultProps;

Expand Down Expand Up @@ -77,6 +84,7 @@ class WordCloud extends React.PureComponent<FullWordCloudProps, WordCloudState>
super(props);
this.state = {
words: [],
scaleFactor: 1,
};
this.setWords = this.setWords.bind(this);
}
Expand Down Expand Up @@ -111,13 +119,35 @@ class WordCloud extends React.PureComponent<FullWordCloudProps, WordCloudState>
}

update() {
const { data, width, height, rotation, encoding } = this.props;
const { data, encoding } = this.props;

const encoder = this.createEncoder(encoding);
encoder.setDomainFromDataset(data);

const sortedData = [...data].sort(
(a, b) =>
encoder.channels.fontSize.encodeDatum(b, 0) - encoder.channels.fontSize.encodeDatum(a, 0),
);
const topResultsCount = Math.max(sortedData.length * TOP_RESULTS_PERCENTAGE, 10);
const topResults = sortedData.slice(0, topResultsCount);

// Ensure top results are always included in the final word cloud by scaling chart down if needed
this.generateCloud(encoder, 1, (words: Word[]) =>
topResults.every((d: PlainObject) =>
words.find(({ text }) => encoder.channels.text.getValueFromDatum(d) === text),
),
);
}

generateCloud(
encoder: Encoder<WordCloudEncodingConfig>,
scaleFactor: number,
isValid: (word: Word[]) => boolean,
) {
const { data, width, height, rotation } = this.props;

cloudLayout()
.size([width, height])
.size([width * scaleFactor, height * scaleFactor])
// clone the data because cloudLayout mutates input
.words(data.map(d => ({ ...d })))
.padding(5)
Expand All @@ -128,20 +158,36 @@ class WordCloud extends React.PureComponent<FullWordCloudProps, WordCloudState>
)
.fontWeight(d => encoder.channels.fontWeight.encodeDatum(d, 'normal'))
.fontSize(d => encoder.channels.fontSize.encodeDatum(d, 0))
.on('end', this.setWords)
.on('end', (words: Word[]) => {
if (isValid(words) || scaleFactor > MAX_SCALE_FACTOR) {
if (this.isComponentMounted) {
this.setState({ words, scaleFactor });
}
} else {
this.generateCloud(encoder, scaleFactor + SCALE_FACTOR_STEP, isValid);
}
})
.start();
}

render() {
const { scaleFactor } = this.state;
const { width, height, encoding } = this.props;
const { words } = this.state;

const encoder = this.createEncoder(encoding);
encoder.channels.color.setDomainFromDataset(words);

const viewBoxWidth = width * scaleFactor;
const viewBoxHeight = height * scaleFactor;

return (
<svg width={width} height={height}>
<g transform={`translate(${width / 2},${height / 2})`}>
<svg
width={width}
height={height}
viewBox={`-${viewBoxWidth / 2} -${viewBoxHeight / 2} ${viewBoxWidth} ${viewBoxHeight}`}
>
<g>
{words.map(w => (
<text
key={w.text}
Expand Down

1 comment on commit 286bee7

@vercel
Copy link

@vercel vercel bot commented on 286bee7 Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.