diff --git a/README.md b/README.md index dfdaf11fe0..5e6b049bc0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/superset-ui-chart/src/models/ChartPlugin.ts b/packages/superset-ui-chart/src/models/ChartPlugin.ts index 4f55a8477a..6b08f5f2cc 100644 --- a/packages/superset-ui-chart/src/models/ChartPlugin.ts +++ b/packages/superset-ui-chart/src/models/ChartPlugin.ts @@ -13,7 +13,7 @@ const IDENTITY = (x: any) => x; type PromiseOrValue = Promise | T; type PromiseOrValueLoader = () => PromiseOrValue | PromiseOrValue<{ default: T }>; -export type BuildQueryFunction = (formData: FormData) => QueryContext; +export type BuildQueryFunction = (formData: T) => QueryContext; export type TransformPropsFunction = ( chartProps: ChartProps, @@ -21,12 +21,12 @@ export type TransformPropsFunction = ( [key: string]: any; }; -export interface ChartPluginConfig { +export interface ChartPluginConfig { metadata: ChartMetadata; // use buildQuery for immediate value - buildQuery?: BuildQueryFunction; + buildQuery?: BuildQueryFunction; // use loadBuildQuery for dynamic import (lazy-loading) - loadBuildQuery?: PromiseOrValueLoader; + loadBuildQuery?: PromiseOrValueLoader>; // use transformProps for immediate value transformProps?: TransformPropsFunction; // use loadTransformProps for dynamic import (lazy-loading) @@ -37,13 +37,13 @@ export interface ChartPluginConfig { loadChart?: PromiseOrValueLoader; } -export default class ChartPlugin extends Plugin { +export default class ChartPlugin extends Plugin { metadata: ChartMetadata; - loadBuildQuery?: PromiseOrValueLoader; + loadBuildQuery?: PromiseOrValueLoader>; loadTransformProps: PromiseOrValueLoader; loadChart: PromiseOrValueLoader; - constructor(config: ChartPluginConfig) { + constructor(config: ChartPluginConfig) { super(); const { metadata, diff --git a/packages/superset-ui-chart/src/models/ChartProps.ts b/packages/superset-ui-chart/src/models/ChartProps.ts index 331f8feed5..448768dbdd 100644 --- a/packages/superset-ui-chart/src/models/ChartProps.ts +++ b/packages/superset-ui-chart/src/models/ChartProps.ts @@ -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; diff --git a/packages/superset-ui-chart/test/components/SuperChart.test.tsx b/packages/superset-ui-chart/test/components/SuperChart.test.tsx index 9217491709..9c7f9f1270 100644 --- a/packages/superset-ui-chart/test/components/SuperChart.test.tsx +++ b/packages/superset-ui-chart/test/components/SuperChart.test.tsx @@ -1,6 +1,6 @@ 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) => ( @@ -8,7 +8,7 @@ describe('SuperChart', () => { ); const chartProps = new ChartProps(); - class MyChartPlugin extends ChartPlugin { + class MyChartPlugin extends ChartPlugin { constructor() { super({ metadata: new ChartMetadata({ @@ -21,7 +21,7 @@ describe('SuperChart', () => { } } - class SecondChartPlugin extends ChartPlugin { + class SecondChartPlugin extends ChartPlugin { constructor() { super({ metadata: new ChartMetadata({ @@ -34,7 +34,7 @@ describe('SuperChart', () => { } } - class SlowChartPlugin extends ChartPlugin { + class SlowChartPlugin extends ChartPlugin { constructor() { super({ metadata: new ChartMetadata({ diff --git a/packages/superset-ui-chart/test/components/createLoadableRenderer.test.tsx b/packages/superset-ui-chart/test/components/createLoadableRenderer.test.tsx index da0f6691a8..a6b8efbed1 100644 --- a/packages/superset-ui-chart/test/components/createLoadableRenderer.test.tsx +++ b/packages/superset-ui-chart/test/components/createLoadableRenderer.test.tsx @@ -9,8 +9,8 @@ describe('createLoadableRenderer', () => { return
test
; } 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(() => { diff --git a/packages/superset-ui-chart/test/components/reactify.test.tsx b/packages/superset-ui-chart/test/components/reactify.test.tsx index dedfb2e503..629779f7b8 100644 --- a/packages/superset-ui-chart/test/components/reactify.test.tsx +++ b/packages/superset-ui-chart/test/components/reactify.test.tsx @@ -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); }); diff --git a/packages/superset-ui-chart/test/models/ChartPlugin.test.ts b/packages/superset-ui-chart/test/models/ChartPlugin.test.ts index 2c475aaa37..7c49024796 100644 --- a/packages/superset-ui-chart/test/models/ChartPlugin.test.ts +++ b/packages/superset-ui-chart/test/models/ChartPlugin.test.ts @@ -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; expect(fn(FORM_DATA).queries[0]).toEqual({ granularity: 'day' }); } }); @@ -65,7 +65,7 @@ describe('ChartPlugin', () => { buildQuery, }); if (typeof plugin.loadBuildQuery === 'function') { - const fn = plugin.loadBuildQuery() as BuildQueryFunction; + const fn = plugin.loadBuildQuery() as BuildQueryFunction; expect(fn(FORM_DATA).queries[0]).toEqual({ granularity: 'day' }); } }); diff --git a/packages/superset-ui-plugin-chart-word-cloud/README.md b/packages/superset-ui-plugin-chart-word-cloud/README.md new file mode 100644 index 0000000000..8abadb2f0d --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/README.md @@ -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 + +``` \ No newline at end of file diff --git a/packages/superset-ui-plugin-chart-word-cloud/package.json b/packages/superset-ui-plugin-chart-word-cloud/package.json new file mode 100644 index 0000000000..ee8609baf4 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/package.json @@ -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" + } +} diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/FormData.ts b/packages/superset-ui-plugin-chart-word-cloud/src/FormData.ts new file mode 100644 index 0000000000..78496d9fec --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/FormData.ts @@ -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; diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/ReactWordCloud.ts b/packages/superset-ui-plugin-chart-word-cloud/src/ReactWordCloud.ts new file mode 100644 index 0000000000..7b77fbf2a4 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/ReactWordCloud.ts @@ -0,0 +1,4 @@ +import { reactify } from '@superset-ui/chart'; +import Component, { Props } from './WordCloud'; + +export default reactify(Component); diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/WordCloud.ts b/packages/superset-ui-plugin-chart-word-cloud/src/WordCloud.ts new file mode 100644 index 0000000000..049ae5e78b --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/WordCloud.ts @@ -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() + .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; diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/buildQuery.ts b/packages/superset-ui-plugin-chart-word-cloud/src/buildQuery.ts new file mode 100644 index 0000000000..3c75e4bc51 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/buildQuery.ts @@ -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], + }, + ]); +} diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnail.png b/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnail.png new file mode 100644 index 0000000000..1829a2f560 Binary files /dev/null and b/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnail.png differ diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnailLarge.png b/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnailLarge.png new file mode 100644 index 0000000000..03936e1253 Binary files /dev/null and b/packages/superset-ui-plugin-chart-word-cloud/src/images/thumbnailLarge.png differ diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/index.ts b/packages/superset-ui-plugin-chart-word-cloud/src/index.ts new file mode 100644 index 0000000000..0ca05994ec --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/index.ts @@ -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 { + constructor() { + super({ + buildQuery, + loadChart: () => import('./ReactWordCloud.js'), + metadata, + transformProps, + }); + } +} diff --git a/packages/superset-ui-plugin-chart-word-cloud/src/transformProps.ts b/packages/superset-ui-plugin-chart-word-cloud/src/transformProps.ts new file mode 100644 index 0000000000..0186322cd4 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/src/transformProps.ts @@ -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, + }; +} diff --git a/packages/superset-ui-plugin-chart-word-cloud/test/buildQuery.test.ts b/packages/superset-ui-plugin-chart-word-cloud/test/buildQuery.test.ts new file mode 100644 index 0000000000..6c1d10d6e2 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/test/buildQuery.test.ts @@ -0,0 +1,16 @@ +import buildQuery from '../src/buildQuery'; + +describe('WordCloud buildQuery', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + series: 'foo', + viz_type: 'word_cloud', + }; + + it('should build groupby with series in form data', () => { + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.groupby).toEqual(['foo']); + }); +}); diff --git a/packages/superset-ui-plugin-chart-word-cloud/test/index.test.ts b/packages/superset-ui-plugin-chart-word-cloud/test/index.test.ts new file mode 100644 index 0000000000..457263a03f --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/test/index.test.ts @@ -0,0 +1,5 @@ +describe('My Test', () => { + it('tests something', () => { + expect(1).toEqual(1); + }); +}); diff --git a/packages/superset-ui-plugin-chart-word-cloud/test/transformProps.test.ts b/packages/superset-ui-plugin-chart-word-cloud/test/transformProps.test.ts new file mode 100644 index 0000000000..eca284a282 --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/test/transformProps.test.ts @@ -0,0 +1,34 @@ +import { ChartProps } from '@superset-ui/chart'; +import transformProps from '../src/transformProps'; + +describe('WordCloud tranformProps', () => { + const formData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'sum__num', + rotation: 'square', + series: 'name', + sizeFrom: 10, + sizeTo: 70, + }; + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + payload: { + data: [{ name: 'Hulk', sum__num: 1 }], + }, + }); + + it('should tranform chart props for word cloud viz', () => { + expect(transformProps(chartProps)).toEqual({ + colorScheme: 'bnbColors', + width: 800, + height: 600, + rotation: 'square', + sizeRange: [10, 70], + data: [{ size: 1, text: 'Hulk' }], + }); + }); +}); diff --git a/packages/superset-ui-plugin-chart-word-cloud/types/external.d.ts b/packages/superset-ui-plugin-chart-word-cloud/types/external.d.ts new file mode 100644 index 0000000000..e2937d470e --- /dev/null +++ b/packages/superset-ui-plugin-chart-word-cloud/types/external.d.ts @@ -0,0 +1 @@ +declare module '*.png';