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

Commit

Permalink
Add WordCloud plugin (#50)
Browse files Browse the repository at this point in the history
* scaffold

* resolve dependency issues

* use d3 v4

* - Update peer dependencies
- Getting src to build

* Type word cloud

* Add tests for transformProps

* Update and simplify d3-cloud typing as the fix we've been waiting was merged.

* resolve all tests

* update unit tests

* fix unit test

* update readme
  • Loading branch information
kristw authored Feb 5, 2019
1 parent b7b9524 commit 5afc9b6
Show file tree
Hide file tree
Showing 21 changed files with 321 additions and 18 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ applications that leverage a Superset backend :chart_with_upwards_trend:
| [@superset-ui/number-format](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-number-format) | [![Version](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat-square) |
| [@superset-ui/time-format](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-time-format) | [![Version](https://img.shields.io/npm/v/@superset-ui/time-format.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/time-format.svg?style=flat-square) |
| [@superset-ui/translation](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-translation) | [![Version](https://img.shields.io/npm/v/@superset-ui/translation.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/translation.svg?style=flat-square) |
| [@superset-ui/plugin-chart-word-cloud](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-plugin-chart-word-cloud) | [![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-word-cloud.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-word-cloud.svg?style=flat-square) |

#### Coming :soon:

- Data providers
- Embeddable charts
- Chart collections
- Chart plugins

### Development

Expand Down
14 changes: 7 additions & 7 deletions packages/superset-ui-chart/src/models/ChartPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ const IDENTITY = (x: any) => x;
type PromiseOrValue<T> = Promise<T> | T;
type PromiseOrValueLoader<T> = () => PromiseOrValue<T> | PromiseOrValue<{ default: T }>;

export type BuildQueryFunction = (formData: FormData) => QueryContext;
export type BuildQueryFunction<T extends FormData> = (formData: T) => QueryContext;

export type TransformPropsFunction = (
chartProps: ChartProps,
) => {
[key: string]: any;
};

export interface ChartPluginConfig {
export interface ChartPluginConfig<T extends FormData> {
metadata: ChartMetadata;
// use buildQuery for immediate value
buildQuery?: BuildQueryFunction;
buildQuery?: BuildQueryFunction<T>;
// use loadBuildQuery for dynamic import (lazy-loading)
loadBuildQuery?: PromiseOrValueLoader<BuildQueryFunction>;
loadBuildQuery?: PromiseOrValueLoader<BuildQueryFunction<T>>;
// use transformProps for immediate value
transformProps?: TransformPropsFunction;
// use loadTransformProps for dynamic import (lazy-loading)
Expand All @@ -37,13 +37,13 @@ export interface ChartPluginConfig {
loadChart?: PromiseOrValueLoader<Function>;
}

export default class ChartPlugin extends Plugin {
export default class ChartPlugin<T extends FormData = FormData> extends Plugin {
metadata: ChartMetadata;
loadBuildQuery?: PromiseOrValueLoader<BuildQueryFunction>;
loadBuildQuery?: PromiseOrValueLoader<BuildQueryFunction<T>>;
loadTransformProps: PromiseOrValueLoader<TransformPropsFunction>;
loadChart: PromiseOrValueLoader<Function>;

constructor(config: ChartPluginConfig) {
constructor(config: ChartPluginConfig<T>) {
super();
const {
metadata,
Expand Down
1 change: 1 addition & 0 deletions packages/superset-ui-chart/src/models/ChartProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface PlainObject {
[key: string]: any;
}

// TODO: more specific typing for these fields of ChartProps
type AnnotationData = PlainObject;
type CamelCaseDatasource = PlainObject;
type SnakeCaseDatasource = PlainObject;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { ChartProps, ChartMetadata, ChartPlugin, SuperChart } from '../../src';
import { ChartProps, ChartMetadata, ChartPlugin, FormData, SuperChart } from '../../src';

describe('SuperChart', () => {
const TestComponent = (props: any) => (
<div className="test-component">{props.character || 'test-component'}</div>
);
const chartProps = new ChartProps();

class MyChartPlugin extends ChartPlugin {
class MyChartPlugin extends ChartPlugin<FormData> {
constructor() {
super({
metadata: new ChartMetadata({
Expand All @@ -21,7 +21,7 @@ describe('SuperChart', () => {
}
}

class SecondChartPlugin extends ChartPlugin {
class SecondChartPlugin extends ChartPlugin<FormData> {
constructor() {
super({
metadata: new ChartMetadata({
Expand All @@ -34,7 +34,7 @@ describe('SuperChart', () => {
}
}

class SlowChartPlugin extends ChartPlugin {
class SlowChartPlugin extends ChartPlugin<FormData> {
constructor() {
super({
metadata: new ChartMetadata({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('createLoadableRenderer', () => {
return <div className="test-component">test</div>;
}
let loadChartSuccess = jest.fn(() => Promise.resolve(TestComponent));
let render = () => null;
let loading = () => null;
let render: (loaded: { [key: string]: any }) => JSX.Element;
let loading: () => JSX.Element;
let LoadableRenderer: LoadableRendererType<{}, {}>;

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('reactify(renderFn)', () => {
const container = element;
container.innerHTML = '';
const child = document.createElement('b');
child.innerHTML = props.content;
child.innerHTML = props.content || '';
container.appendChild(child);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/superset-ui-chart/test/models/ChartPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('ChartPlugin', () => {
loadBuildQuery: () => buildQuery,
});
if (typeof plugin.loadBuildQuery === 'function') {
const fn = plugin.loadBuildQuery() as BuildQueryFunction;
const fn = plugin.loadBuildQuery() as BuildQueryFunction<FormData>;
expect(fn(FORM_DATA).queries[0]).toEqual({ granularity: 'day' });
}
});
Expand All @@ -65,7 +65,7 @@ describe('ChartPlugin', () => {
buildQuery,
});
if (typeof plugin.loadBuildQuery === 'function') {
const fn = plugin.loadBuildQuery() as BuildQueryFunction;
const fn = plugin.loadBuildQuery() as BuildQueryFunction<FormData>;
expect(fn(FORM_DATA).queries[0]).toEqual({ granularity: 'day' });
}
});
Expand Down
34 changes: 34 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## @superset-ui/plugin-chart-word-cloud

[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-word-cloud.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-word-cloud.svg?style=flat-square)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-chart-word-cloud&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-chart-word-cloud)

This plugin provides Word Cloud for Superset.

### Usage

Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.

```js
import WordCloudChartPlugin from '@superset-ui/legacy-plugin-chart-word-cloud';

new WordCloudChartPlugin()
.configure({ key: 'word-cloud' })
.register();
```

Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-legacy/?selectedKind=plugin-chart-word-cloud) for more details.

```js
<SuperChart
chartType="word-cloud"
chartProps={{
width: 600,
height: 600,
formData: {...},
payload: {
data: {...},
},
}}
/>
```
53 changes: 53 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@superset-ui/plugin-chart-word-cloud",
"version": "0.0.0",
"description": "Superset UI plugin-chart-word-cloud",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@types/d3-array": "^1.2.4",
"@types/d3-cloud": "^1.2.1",
"@types/d3-scale": "^2.0.2",
"@types/d3-selection": "^1.3.4",
"d3-array": "^2.0.2",
"d3-cloud": "^1.2.5",
"d3-scale": "^2.1.2",
"d3-selection": "^1.3.2",
"prop-types": "^15.6.2"
},
"devDependencies": {
"@superset-ui/chart": "^0.9.x",
"@superset-ui/color": "^0.9.x",
"@superset-ui/number-format": "^0.9.x",
"@superset-ui/time-format": "^0.9.x",
"@superset-ui/translation": "^0.9.x"
},
"peerDependencies": {
"@superset-ui/chart": "^0.9.x",
"@superset-ui/color": "^0.9.x",
"@superset-ui/number-format": "^0.9.x",
"@superset-ui/time-format": "^0.9.x",
"@superset-ui/translation": "^0.9.x"
}
}
12 changes: 12 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/src/FormData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FormData as GenericFormData } from '@superset-ui/chart';

// FormData specific to the wordcloud viz
interface WordCloudFormData {
series: string;
}

// FormData for wordcloud contains both common properties of all form data
// and properties specific to wordcloud vizzes
type FormData = GenericFormData & WordCloudFormData;

export default FormData;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { reactify } from '@superset-ui/chart';
import Component, { Props } from './WordCloud';

export default reactify<Props>(Component);
81 changes: 81 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/src/WordCloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { extent as d3Extent } from 'd3-array';
import { scaleLinear } from 'd3-scale';
import { select as d3Select } from 'd3-selection';
import cloudLayout from 'd3-cloud';
import { CategoricalColorNamespace } from '@superset-ui/color';

const ROTATION = {
flat: () => 0,
/* eslint-disable-next-line no-magic-numbers */
random: () => Math.floor(Math.random() * 6 - 3) * 30,
/* eslint-disable-next-line no-magic-numbers */
square: () => Math.floor(Math.random() * 2) * 90,
};

interface Datum {
size: number;
text: string;
}

export interface Props {
colorScheme: string;
data: Datum[];
height: number;
rotation: 'flat' | 'random' | 'square';
sizeRange: number[];
width: number;
}

function WordCloud(element: Element, props: Props) {
const { data, width, height, rotation, sizeRange, colorScheme } = props;

const chart = d3Select(element);
const size: [number, number] = [width, height];
const rotationFn = ROTATION[rotation] || ROTATION.flat;

const scale = scaleLinear()
.range(sizeRange)
.domain(d3Extent(data, d => d.size) as [number, number]);

const layout = cloudLayout<Datum>()
.size(size)
.words(data)
/* eslint-disable-next-line no-magic-numbers */
.padding(5)
.rotate(rotationFn)
.font('Helvetica')
.fontWeight('bold')
.fontSize(d => scale(d.size));

const colorFn = CategoricalColorNamespace.getScale(colorScheme);

function draw(words: d3.layout.cloud.Word[]) {
chart.selectAll('*').remove();

const [w, h] = layout.size();

chart
.append('svg')
.attr('width', w)
.attr('height', h)
.append('g')
.attr('transform', `translate(${w / 2},${h / 2})`)
.selectAll('text')
.data(words)
.enter()
.append('text')
.style('font-size', d => `${d.size}px`)
.style('font-weight', 'bold')
.style('font-family', 'Helvetica')
.style('fill', d => colorFn(d.text))
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${d.x}, ${d.y}) rotate(${d.rotate})`)
.text(d => d.text!);
}

layout.on('end', draw).start();
}

WordCloud.displayName = 'WordCloud';

export default WordCloud;
12 changes: 12 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/src/buildQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { buildQueryContext } from '@superset-ui/chart';
import FormData from './FormData';

export default function buildQuery(formData: FormData) {
// Set the single QueryObject's groupby field with series in formData
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
groupby: [formData.series],
},
]);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import buildQuery from './buildQuery';
import FormData from './FormData';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';

const metadata = new ChartMetadata({
credits: ['https://github.com/jasondavies/d3-cloud'],
description: '',
name: t('Word Cloud'),
thumbnail,
});

export default class WordCloudChartPlugin extends ChartPlugin<FormData> {
constructor() {
super({
buildQuery,
loadChart: () => import('./ReactWordCloud.js'),
metadata,
transformProps,
});
}
}
26 changes: 26 additions & 0 deletions packages/superset-ui-plugin-chart-word-cloud/src/transformProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ChartProps } from '@superset-ui/chart';

function transformData(data: ChartProps['payload'][], formData: ChartProps['formData']) {
const { metric, series } = formData;

const transformedData = data.map(datum => ({
size: datum[metric.label || metric],
text: datum[series],
}));

return transformedData;
}

export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, payload } = chartProps;
const { colorScheme, rotation, sizeTo, sizeFrom } = formData;

return {
colorScheme,
data: transformData(payload.data, formData),
height,
rotation,
sizeRange: [sizeFrom, sizeTo],
width,
};
}
Loading

0 comments on commit 5afc9b6

Please sign in to comment.