Skip to content

Commit cc48de0

Browse files
Migrate Word Cloud visualization to React (#3930)
1 parent 300f3f6 commit cc48de0

File tree

13 files changed

+509
-201
lines changed

13 files changed

+509
-201
lines changed

client/app/directives/resize-event.js

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,13 @@
1-
import { findIndex } from 'lodash';
2-
3-
const items = new Map();
4-
5-
function checkItems() {
6-
items.forEach((item, node) => {
7-
const bounds = node.getBoundingClientRect();
8-
// convert to int (because these numbers needed for comparisons), but preserve 1 decimal point
9-
const width = Math.round(bounds.width * 10);
10-
const height = Math.round(bounds.height * 10);
11-
12-
if (
13-
(item.width !== width) ||
14-
(item.height !== height)
15-
) {
16-
item.width = width;
17-
item.height = height;
18-
item.callback(node);
19-
}
20-
});
21-
22-
setTimeout(checkItems, 100);
23-
}
24-
25-
checkItems(); // ensure it was called only once!
1+
import resizeObserver from '@/services/resizeObserver';
262

273
function resizeEvent() {
284
return {
295
restrict: 'A',
306
link($scope, $element, attrs) {
31-
const node = $element[0];
32-
if (!items.has(node)) {
33-
items.set(node, {
34-
callback: () => {
35-
$scope.$evalAsync(attrs.resizeEvent);
36-
},
37-
});
38-
39-
$scope.$on('$destroy', () => {
40-
const index = findIndex(items, item => item.node === node);
41-
if (index >= 0) {
42-
items.splice(index, 1); // remove item
43-
}
44-
});
45-
}
7+
const unwatch = resizeObserver($element[0], () => {
8+
$scope.$evalAsync(attrs.resizeEvent);
9+
});
10+
$scope.$on('$destroy', unwatch);
4611
},
4712
};
4813
}

client/app/lib/hooks/useQueryResult.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { useState, useEffect } from 'react';
22

33
function getQueryResultData(queryResult) {
44
return {
5-
columns: queryResult ? queryResult.getColumns() : [],
6-
rows: queryResult ? queryResult.getData() : [],
7-
filters: queryResult ? queryResult.getFilters() : [],
5+
columns: (queryResult && queryResult.getColumns()) || [],
6+
rows: (queryResult && queryResult.getData()) || [],
7+
filters: (queryResult && queryResult.getFilters()) || [],
88
};
99
}
1010

client/app/pages/dashboards/dashboard.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
.sunburst-visualization-container,
8484
.sankey-visualization-container,
8585
.map-visualization-container,
86+
.word-cloud-visualization-container,
8687
.plotly-chart-container {
8788
position: absolute;
8889
left: 0;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* global ResizeObserver */
2+
3+
function observeNative(node, callback) {
4+
if ((typeof ResizeObserver === 'function') && node) {
5+
const observer = new ResizeObserver(() => callback()); // eslint-disable-line compat/compat
6+
observer.observe(node);
7+
return () => observer.disconnect();
8+
}
9+
return null;
10+
}
11+
12+
const items = new Map();
13+
14+
function checkItems() {
15+
if (items.size > 0) {
16+
items.forEach((item, node) => {
17+
const bounds = node.getBoundingClientRect();
18+
// convert to int (because these numbers needed for comparisons), but preserve 1 decimal point
19+
const width = Math.round(bounds.width * 10);
20+
const height = Math.round(bounds.height * 10);
21+
22+
if (
23+
(item.width !== width) ||
24+
(item.height !== height)
25+
) {
26+
item.width = width;
27+
item.height = height;
28+
item.callback(node);
29+
}
30+
});
31+
32+
setTimeout(checkItems, 100);
33+
}
34+
}
35+
36+
function observeFallback(node, callback) {
37+
if (node && !items.has(node)) {
38+
const shouldTrigger = items.size === 0;
39+
items.set(node, { callback });
40+
if (shouldTrigger) {
41+
checkItems();
42+
}
43+
return () => items.delete(node);
44+
}
45+
return null;
46+
}
47+
48+
export default function observe(node, callback) {
49+
return observeNative(node, callback) || observeFallback(node, callback) || (() => {});
50+
}

client/app/visualizations/EditVisualizationDialog.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extend, map, findIndex, isEqual } from 'lodash';
1+
import { extend, map, sortBy, findIndex, isEqual } from 'lodash';
22
import React, { useState, useMemo } from 'react';
33
import PropTypes from 'prop-types';
44
import Modal from 'antd/lib/modal';
@@ -158,7 +158,7 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
158158
onChange={onTypeChanged}
159159
>
160160
{map(
161-
registeredVisualizations,
161+
sortBy(registeredVisualizations, ['type']),
162162
vis => <Select.Option key={vis.type} data-test={'VisualizationType.' + vis.type}>{vis.name}</Select.Option>,
163163
)}
164164
</Select>

client/app/visualizations/word-cloud/Editor.jsx

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,101 @@
1-
import { map } from 'lodash';
1+
import { map, merge } from 'lodash';
22
import React from 'react';
33
import Select from 'antd/lib/select';
4+
import InputNumber from 'antd/lib/input-number';
5+
import * as Grid from 'antd/lib/grid';
46
import { EditorPropTypes } from '@/visualizations';
57

6-
const { Option } = Select;
7-
88
export default function Editor({ options, data, onOptionsChange }) {
9-
const onColumnChanged = (column) => {
10-
const newOptions = { ...options, column };
11-
onOptionsChange(newOptions);
9+
const optionsChanged = (newOptions) => {
10+
onOptionsChange(merge({}, options, newOptions));
1211
};
1312

1413
return (
15-
<div className="form-group">
16-
<label className="control-label" htmlFor="word-cloud-column">Word Cloud Column Name</label>
17-
<Select
18-
id="word-cloud-column"
19-
className="w-100"
20-
value={options.column}
21-
onChange={onColumnChanged}
22-
>
23-
{map(data.columns, ({ name }) => (
24-
<Option key={name}>{name}</Option>
25-
))}
26-
</Select>
27-
</div>
14+
<React.Fragment>
15+
<div className="form-group">
16+
<label className="control-label" htmlFor="word-cloud-words-column">Words Column</label>
17+
<Select
18+
data-test="WordCloud.WordsColumn"
19+
id="word-cloud-words-column"
20+
className="w-100"
21+
value={options.column}
22+
onChange={column => optionsChanged({ column })}
23+
>
24+
{map(data.columns, ({ name }) => (
25+
<Select.Option key={name} data-test={'WordCloud.WordsColumn.' + name}>{name}</Select.Option>
26+
))}
27+
</Select>
28+
</div>
29+
<div className="form-group">
30+
<label className="control-label" htmlFor="word-cloud-frequencies-column">Frequencies Column</label>
31+
<Select
32+
data-test="WordCloud.FrequenciesColumn"
33+
id="word-cloud-frequencies-column"
34+
className="w-100"
35+
value={options.frequenciesColumn}
36+
onChange={frequenciesColumn => optionsChanged({ frequenciesColumn })}
37+
>
38+
<Select.Option key="none" value=""><i>(count word frequencies automatically)</i></Select.Option>
39+
{map(data.columns, ({ name }) => (
40+
<Select.Option key={'column-' + name} value={name} data-test={'WordCloud.FrequenciesColumn.' + name}>{name}</Select.Option>
41+
))}
42+
</Select>
43+
</div>
44+
<div className="form-group">
45+
<label className="control-label" htmlFor="word-cloud-word-length-limit">
46+
Words Length Limit
47+
</label>
48+
<Grid.Row gutter={15} type="flex">
49+
<Grid.Col span={12}>
50+
<InputNumber
51+
data-test="WordCloud.WordLengthLimit.Min"
52+
className="w-100"
53+
placeholder="Min"
54+
min={0}
55+
value={options.wordLengthLimit.min}
56+
onChange={value => optionsChanged({ wordLengthLimit: { min: value > 0 ? value : null } })}
57+
/>
58+
</Grid.Col>
59+
<Grid.Col span={12}>
60+
<InputNumber
61+
data-test="WordCloud.WordLengthLimit.Max"
62+
className="w-100"
63+
placeholder="Max"
64+
min={0}
65+
value={options.wordLengthLimit.max}
66+
onChange={value => optionsChanged({ wordLengthLimit: { max: value > 0 ? value : null } })}
67+
/>
68+
</Grid.Col>
69+
</Grid.Row>
70+
</div>
71+
<div className="form-group">
72+
<label className="control-label" htmlFor="word-cloud-word-length-limit">
73+
Frequencies Limit
74+
</label>
75+
<Grid.Row gutter={15} type="flex">
76+
<Grid.Col span={12}>
77+
<InputNumber
78+
data-test="WordCloud.WordCountLimit.Min"
79+
className="w-100"
80+
placeholder="Min"
81+
min={0}
82+
value={options.wordCountLimit.min}
83+
onChange={value => optionsChanged({ wordCountLimit: { min: value > 0 ? value : null } })}
84+
/>
85+
</Grid.Col>
86+
<Grid.Col span={12}>
87+
<InputNumber
88+
data-test="WordCloud.WordCountLimit.Max"
89+
className="w-100"
90+
placeholder="Max"
91+
min={0}
92+
value={options.wordCountLimit.max}
93+
onChange={value => optionsChanged({ wordCountLimit: { max: value > 0 ? value : null } })}
94+
/>
95+
</Grid.Col>
96+
</Grid.Row>
97+
</div>
98+
</React.Fragment>
2899
);
29100
}
30101

0 commit comments

Comments
 (0)