diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/constants.ts b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/constants.ts new file mode 100644 index 000000000..70083c631 --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/constants.ts @@ -0,0 +1,2 @@ +export const SCATTER_PLOT_PLUGIN_TYPE = 'v2-scatter-plot'; +export const SCATTER_PLOT_PLUGIN_LEGACY_TYPE = 'v2-scatter-plot/legacy'; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/data.js b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/data.js new file mode 100644 index 000000000..e9aff588c --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/data.js @@ -0,0 +1,178 @@ +/* eslint-disable sort-keys, no-magic-numbers */ +export default [ + { + country_name: 'China', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 1344130000, + sum__SP_RUR_TOTL_ZS: 49.427, + sum__SP_DYN_LE00_IN: 75.042, + }, + { + country_name: 'Indonesia', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 244808254, + sum__SP_RUR_TOTL_ZS: 49.288, + sum__SP_DYN_LE00_IN: 70.3915609756, + }, + { + country_name: 'Japan', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 127817277, + sum__SP_RUR_TOTL_ZS: 8.752, + sum__SP_DYN_LE00_IN: 82.5912195122, + }, + { + country_name: 'Philippines', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 94501233, + sum__SP_RUR_TOTL_ZS: 54.983, + sum__SP_DYN_LE00_IN: 68.3914878049, + }, + { + country_name: 'Vietnam', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 87840000, + sum__SP_RUR_TOTL_ZS: 68.971, + sum__SP_DYN_LE00_IN: 75.457902439, + }, + { + country_name: 'Thailand', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 66902958, + sum__SP_RUR_TOTL_ZS: 54.606, + sum__SP_DYN_LE00_IN: 74.008902439, + }, + { + country_name: 'Myanmar', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 52125411, + sum__SP_RUR_TOTL_ZS: 68.065, + sum__SP_DYN_LE00_IN: 64.7612439024, + }, + { + country_name: 'India', + region: 'South Asia', + sum__SP_POP_TOTL: 1247446011, + sum__SP_RUR_TOTL_ZS: 68.724, + sum__SP_DYN_LE00_IN: 65.9584878049, + }, + { + country_name: 'Pakistan', + region: 'South Asia', + sum__SP_POP_TOTL: 173669648, + sum__SP_RUR_TOTL_ZS: 62.993, + sum__SP_DYN_LE00_IN: 66.2838780488, + }, + { + country_name: 'Bangladesh', + region: 'South Asia', + sum__SP_POP_TOTL: 153405612, + sum__SP_RUR_TOTL_ZS: 68.775, + sum__SP_DYN_LE00_IN: 69.891804878, + }, + { + country_name: 'United States', + region: 'North America', + sum__SP_POP_TOTL: 311721632, + sum__SP_RUR_TOTL_ZS: 19.06, + sum__SP_DYN_LE00_IN: 78.6414634146, + }, + { + country_name: 'Brazil', + region: 'Latin America & Caribbean', + sum__SP_POP_TOTL: 200517584, + sum__SP_RUR_TOTL_ZS: 15.377, + sum__SP_DYN_LE00_IN: 73.3473658537, + }, + { + country_name: 'Mexico', + region: 'Latin America & Caribbean', + sum__SP_POP_TOTL: 120365271, + sum__SP_RUR_TOTL_ZS: 21.882, + sum__SP_DYN_LE00_IN: 76.9141707317, + }, + { + country_name: 'Nigeria', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 163770669, + sum__SP_RUR_TOTL_ZS: 55.638, + sum__SP_DYN_LE00_IN: 51.7102439024, + }, + { + country_name: 'Ethiopia', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 89858696, + sum__SP_RUR_TOTL_ZS: 82.265, + sum__SP_DYN_LE00_IN: 62.2528536585, + }, + { + country_name: 'Congo, Dem. Rep.', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 68087376, + sum__SP_RUR_TOTL_ZS: 59.558, + sum__SP_DYN_LE00_IN: 49.3007073171, + }, + { + country_name: 'South Africa', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 51553479, + sum__SP_RUR_TOTL_ZS: 37.254, + sum__SP_DYN_LE00_IN: 55.2956585366, + }, + { + country_name: 'Russian Federation', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 142960868, + sum__SP_RUR_TOTL_ZS: 26.268, + sum__SP_DYN_LE00_IN: 69.6585365854, + }, + { + country_name: 'Germany', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 81797673, + sum__SP_RUR_TOTL_ZS: 25.512, + sum__SP_DYN_LE00_IN: 80.7414634146, + }, + { + country_name: 'Turkey', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 73199372, + sum__SP_RUR_TOTL_ZS: 28.718, + sum__SP_DYN_LE00_IN: 74.5404878049, + }, + { + country_name: 'France', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 65342776, + sum__SP_RUR_TOTL_ZS: 21.416, + sum__SP_DYN_LE00_IN: 82.1146341463, + }, + { + country_name: 'United Kingdom', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 63258918, + sum__SP_RUR_TOTL_ZS: 18.43, + sum__SP_DYN_LE00_IN: 80.9512195122, + }, + { + country_name: 'Italy', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 59379449, + sum__SP_RUR_TOTL_ZS: 31.556, + sum__SP_DYN_LE00_IN: 82.187804878, + }, + { + country_name: 'Egypt, Arab Rep.', + region: 'Middle East & North Africa', + sum__SP_POP_TOTL: 83787634, + sum__SP_RUR_TOTL_ZS: 57, + sum__SP_DYN_LE00_IN: 70.6785609756, + }, + { + country_name: 'Iran, Islamic Rep.', + region: 'Middle East & North Africa', + sum__SP_POP_TOTL: 75184322, + sum__SP_RUR_TOTL_ZS: 28.8, + sum__SP_DYN_LE00_IN: 73.4493170732, + }, +]; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/legacyData.js b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/legacyData.js new file mode 100644 index 000000000..37a26d3ec --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/data/legacyData.js @@ -0,0 +1,338 @@ +/* eslint-disable sort-keys, no-magic-numbers */ +export default [ + { + key: 'East Asia & Pacific', + values: [ + { + country_name: 'China', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 1344130000.0, + sum__SP_RUR_TOTL_ZS: 49.427, + sum__SP_DYN_LE00_IN: 75.042, + x: 49.427, + y: 75.042, + size: 1344130000.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Indonesia', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 244808254.0, + sum__SP_RUR_TOTL_ZS: 49.288, + sum__SP_DYN_LE00_IN: 70.3915609756, + x: 49.288, + y: 70.3915609756, + size: 244808254.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Japan', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 127817277.0, + sum__SP_RUR_TOTL_ZS: 8.752, + sum__SP_DYN_LE00_IN: 82.5912195122, + x: 8.752, + y: 82.5912195122, + size: 127817277.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Philippines', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 94501233.0, + sum__SP_RUR_TOTL_ZS: 54.983, + sum__SP_DYN_LE00_IN: 68.3914878049, + x: 54.983, + y: 68.3914878049, + size: 94501233.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Vietnam', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 87840000.0, + sum__SP_RUR_TOTL_ZS: 68.971, + sum__SP_DYN_LE00_IN: 75.457902439, + x: 68.971, + y: 75.457902439, + size: 87840000.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Thailand', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 66902958.0, + sum__SP_RUR_TOTL_ZS: 54.606, + sum__SP_DYN_LE00_IN: 74.008902439, + x: 54.606, + y: 74.008902439, + size: 66902958.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + { + country_name: 'Myanmar', + region: 'East Asia & Pacific', + sum__SP_POP_TOTL: 52125411.0, + sum__SP_RUR_TOTL_ZS: 68.065, + sum__SP_DYN_LE00_IN: 64.7612439024, + x: 68.065, + y: 64.7612439024, + size: 52125411.0, + shape: 'circle', + group: 'East Asia & Pacific', + }, + ], + }, + { + key: 'South Asia', + values: [ + { + country_name: 'India', + region: 'South Asia', + sum__SP_POP_TOTL: 1247446011.0, + sum__SP_RUR_TOTL_ZS: 68.724, + sum__SP_DYN_LE00_IN: 65.9584878049, + x: 68.724, + y: 65.9584878049, + size: 1247446011.0, + shape: 'circle', + group: 'South Asia', + }, + { + country_name: 'Pakistan', + region: 'South Asia', + sum__SP_POP_TOTL: 173669648.0, + sum__SP_RUR_TOTL_ZS: 62.993, + sum__SP_DYN_LE00_IN: 66.2838780488, + x: 62.993, + y: 66.2838780488, + size: 173669648.0, + shape: 'circle', + group: 'South Asia', + }, + { + country_name: 'Bangladesh', + region: 'South Asia', + sum__SP_POP_TOTL: 153405612.0, + sum__SP_RUR_TOTL_ZS: 68.775, + sum__SP_DYN_LE00_IN: 69.891804878, + x: 68.775, + y: 69.891804878, + size: 153405612.0, + shape: 'circle', + group: 'South Asia', + }, + ], + }, + { + key: 'North America', + values: [ + { + country_name: 'United States', + region: 'North America', + sum__SP_POP_TOTL: 311721632.0, + sum__SP_RUR_TOTL_ZS: 19.06, + sum__SP_DYN_LE00_IN: 78.6414634146, + x: 19.06, + y: 78.6414634146, + size: 311721632.0, + shape: 'circle', + group: 'North America', + }, + ], + }, + { + key: 'Latin America & Caribbean', + values: [ + { + country_name: 'Brazil', + region: 'Latin America & Caribbean', + sum__SP_POP_TOTL: 200517584.0, + sum__SP_RUR_TOTL_ZS: 15.377, + sum__SP_DYN_LE00_IN: 73.3473658537, + x: 15.377, + y: 73.3473658537, + size: 200517584.0, + shape: 'circle', + group: 'Latin America & Caribbean', + }, + { + country_name: 'Mexico', + region: 'Latin America & Caribbean', + sum__SP_POP_TOTL: 120365271.0, + sum__SP_RUR_TOTL_ZS: 21.882, + sum__SP_DYN_LE00_IN: 76.9141707317, + x: 21.882, + y: 76.9141707317, + size: 120365271.0, + shape: 'circle', + group: 'Latin America & Caribbean', + }, + ], + }, + { + key: 'Sub-Saharan Africa', + values: [ + { + country_name: 'Nigeria', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 163770669.0, + sum__SP_RUR_TOTL_ZS: 55.638, + sum__SP_DYN_LE00_IN: 51.7102439024, + x: 55.638, + y: 51.7102439024, + size: 163770669.0, + shape: 'circle', + group: 'Sub-Saharan Africa', + }, + { + country_name: 'Ethiopia', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 89858696.0, + sum__SP_RUR_TOTL_ZS: 82.265, + sum__SP_DYN_LE00_IN: 62.2528536585, + x: 82.265, + y: 62.2528536585, + size: 89858696.0, + shape: 'circle', + group: 'Sub-Saharan Africa', + }, + { + country_name: 'Congo, Dem. Rep.', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 68087376.0, + sum__SP_RUR_TOTL_ZS: 59.558, + sum__SP_DYN_LE00_IN: 49.3007073171, + x: 59.558, + y: 49.3007073171, + size: 68087376.0, + shape: 'circle', + group: 'Sub-Saharan Africa', + }, + { + country_name: 'South Africa', + region: 'Sub-Saharan Africa', + sum__SP_POP_TOTL: 51553479.0, + sum__SP_RUR_TOTL_ZS: 37.254, + sum__SP_DYN_LE00_IN: 55.2956585366, + x: 37.254, + y: 55.2956585366, + size: 51553479.0, + shape: 'circle', + group: 'Sub-Saharan Africa', + }, + ], + }, + { + key: 'Europe & Central Asia', + values: [ + { + country_name: 'Russian Federation', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 142960868.0, + sum__SP_RUR_TOTL_ZS: 26.268, + sum__SP_DYN_LE00_IN: 69.6585365854, + x: 26.268, + y: 69.6585365854, + size: 142960868.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + { + country_name: 'Germany', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 81797673.0, + sum__SP_RUR_TOTL_ZS: 25.512, + sum__SP_DYN_LE00_IN: 80.7414634146, + x: 25.512, + y: 80.7414634146, + size: 81797673.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + { + country_name: 'Turkey', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 73199372.0, + sum__SP_RUR_TOTL_ZS: 28.718, + sum__SP_DYN_LE00_IN: 74.5404878049, + x: 28.718, + y: 74.5404878049, + size: 73199372.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + { + country_name: 'France', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 65342776.0, + sum__SP_RUR_TOTL_ZS: 21.416, + sum__SP_DYN_LE00_IN: 82.1146341463, + x: 21.416, + y: 82.1146341463, + size: 65342776.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + { + country_name: 'United Kingdom', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 63258918.0, + sum__SP_RUR_TOTL_ZS: 18.43, + sum__SP_DYN_LE00_IN: 80.9512195122, + x: 18.43, + y: 80.9512195122, + size: 63258918.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + { + country_name: 'Italy', + region: 'Europe & Central Asia', + sum__SP_POP_TOTL: 59379449.0, + sum__SP_RUR_TOTL_ZS: 31.556, + sum__SP_DYN_LE00_IN: 82.187804878, + x: 31.556, + y: 82.187804878, + size: 59379449.0, + shape: 'circle', + group: 'Europe & Central Asia', + }, + ], + }, + { + key: 'Middle East & North Africa', + values: [ + { + country_name: 'Egypt, Arab Rep.', + region: 'Middle East & North Africa', + sum__SP_POP_TOTL: 83787634.0, + sum__SP_RUR_TOTL_ZS: 57.0, + sum__SP_DYN_LE00_IN: 70.6785609756, + x: 57.0, + y: 70.6785609756, + size: 83787634.0, + shape: 'circle', + group: 'Middle East & North Africa', + }, + { + country_name: 'Iran, Islamic Rep.', + region: 'Middle East & North Africa', + sum__SP_POP_TOTL: 75184322.0, + sum__SP_RUR_TOTL_ZS: 28.8, + sum__SP_DYN_LE00_IN: 73.4493170732, + x: 28.8, + y: 73.4493170732, + size: 75184322.0, + shape: 'circle', + group: 'Middle East & North Africa', + }, + ], + }, +]; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/index.js b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/index.js new file mode 100644 index 000000000..ce1aec40b --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/index.js @@ -0,0 +1,13 @@ +import { ScatterPlotPlugin as LegacyScatterPlotPlugin } from '../../../../../superset-ui-preset-chart-xy/src/legacy'; +import { ScatterPlotPlugin } from '../../../../../superset-ui-preset-chart-xy/src'; +import BasicStories from './stories/basic'; +import BubbleStories from './stories/bubble'; +import LegacyStories from './stories/legacy'; +import { SCATTER_PLOT_PLUGIN_TYPE, SCATTER_PLOT_PLUGIN_LEGACY_TYPE } from './constants'; + +new LegacyScatterPlotPlugin().configure({ key: SCATTER_PLOT_PLUGIN_LEGACY_TYPE }).register(); +new ScatterPlotPlugin().configure({ key: SCATTER_PLOT_PLUGIN_TYPE }).register(); + +export default { + examples: [...BasicStories, ...BubbleStories, ...LegacyStories], +}; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/basic.jsx b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/basic.jsx new file mode 100644 index 000000000..ff9c12ec3 --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/basic.jsx @@ -0,0 +1,59 @@ +/* eslint-disable no-magic-numbers, sort-keys */ +import * as React from 'react'; +import { SuperChart, ChartProps } from '@superset-ui/chart'; +import { radios } from '@storybook/addon-knobs'; +import data from '../data/data'; +import { SCATTER_PLOT_PLUGIN_TYPE } from '../constants'; + +export default [ + { + renderStory: () => [ + , + ], + storyName: 'Basic', + storyPath: 'preset-chart-xy|ScatterPlotPlugin', + }, +]; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/bubble.jsx b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/bubble.jsx new file mode 100644 index 000000000..d4c98b0ff --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/bubble.jsx @@ -0,0 +1,67 @@ +/* eslint-disable no-magic-numbers, sort-keys */ +import * as React from 'react'; +import { SuperChart, ChartProps } from '@superset-ui/chart'; +import { radios } from '@storybook/addon-knobs'; +import data from '../data/data'; +import { SCATTER_PLOT_PLUGIN_TYPE } from '../constants'; + +export default [ + { + renderStory: () => [ + , + ], + storyName: 'Bubble', + storyPath: 'preset-chart-xy|ScatterPlotPlugin', + }, +]; diff --git a/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/legacy.tsx b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/legacy.tsx new file mode 100644 index 000000000..6e6d51a0f --- /dev/null +++ b/packages/superset-ui-plugins-demo/storybook/stories/preset-chart-xy/ScatterPlot/stories/legacy.tsx @@ -0,0 +1,49 @@ +/* eslint-disable no-magic-numbers */ +import * as React from 'react'; +import { SuperChart, ChartProps } from '@superset-ui/chart'; +import data from '../data/legacyData'; +import { SCATTER_PLOT_PLUGIN_LEGACY_TYPE } from '../constants'; + +export default [ + { + renderStory: () => [ + , + ], + storyName: 'Use Legacy API shim', + storyPath: 'preset-chart-xy|ScatterPlotPlugin', + }, +]; diff --git a/packages/superset-ui-preset-chart-xy/src/BoxPlot/BoxPlot.tsx b/packages/superset-ui-preset-chart-xy/src/BoxPlot/BoxPlot.tsx index 58676abfb..089f40d2e 100644 --- a/packages/superset-ui-preset-chart-xy/src/BoxPlot/BoxPlot.tsx +++ b/packages/superset-ui-preset-chart-xy/src/BoxPlot/BoxPlot.tsx @@ -1,21 +1,3 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ /* eslint-disable sort-keys, no-magic-numbers, complexity */ import React from 'react'; import { BoxPlotSeries, XYChart } from '@data-ui/xy-chart'; @@ -28,6 +10,7 @@ import WithLegend from '../components/WithLegend'; import ChartLegend from '../components/legend/ChartLegend'; import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder'; import { Dataset, PlainObject } from '../encodeable/types/Data'; +import { PartialSpec } from '../encodeable/types/Specification'; chartTheme.gridStyles.stroke = '#f1f3f5'; @@ -44,10 +27,10 @@ type Props = { width: string | number; height: string | number; margin?: Margin; - encoding: Encoding; data: Dataset; theme?: ChartTheme; -} & Readonly; +} & PartialSpec & + Readonly; export default class BoxPlot extends React.PureComponent { static defaultProps = defaultProps; @@ -59,24 +42,26 @@ export default class BoxPlot extends React.PureComponent { super(props); const createEncoder = createSelector( - (enc: Encoding) => enc, - (enc: Encoding) => new Encoder({ encoding: enc }), + (p: PartialSpec) => p.encoding, + p => p.commonEncoding, + p => p.options, + (encoding, commonEncoding, options) => new Encoder({ encoding, commonEncoding, options }), ); this.createEncoder = () => { - this.encoder = createEncoder(this.props.encoding); + this.encoder = createEncoder(this.props); }; - this.encoder = createEncoder(this.props.encoding); + this.encoder = createEncoder(this.props); this.renderChart = this.renderChart.bind(this); } renderChart(dim: Dimension) { const { width, height } = dim; - const { data, encoding, margin, theme } = this.props; + const { data, margin, theme } = this.props; const { channels } = this.encoder; - const isHorizontal = encoding.y.type === 'nominal'; + const isHorizontal = channels.y.definition.type === 'nominal'; const children = [ { stroke={(datum: PlainObject) => channels.color.encode(datum)} strokeWidth={1} widthRatio={0.6} - horizontal={encoding.y.type === 'nominal'} + horizontal={channels.y.definition.type === 'nominal'} />, ]; diff --git a/packages/superset-ui-preset-chart-xy/src/BoxPlot/createTooltip.tsx b/packages/superset-ui-preset-chart-xy/src/BoxPlot/createTooltip.tsx index b3efd5070..23c0633cd 100644 --- a/packages/superset-ui-preset-chart-xy/src/BoxPlot/createTooltip.tsx +++ b/packages/superset-ui-preset-chart-xy/src/BoxPlot/createTooltip.tsx @@ -11,21 +11,24 @@ export default function createBoxPlotTooltip(encoder: Encoder) { return function BoxPlotTooltip({ datum, color }: { datum: BoxPlotDataRow; color: string }) { const { label, min, max, median, firstQuartile, thirdQuartile, outliers } = datum; + const formatValue = + channels.y.definition.type === 'nominal' ? channels.x.formatValue : channels.y.formatValue; + const data = []; if (isDefined(min)) { - data.push({ key: 'Min', valueColumn: channels.y.formatValue(min) }); + data.push({ key: 'Min', valueColumn: formatValue(min) }); } if (isDefined(max)) { - data.push({ key: 'Max', valueColumn: channels.y.formatValue(max) }); + data.push({ key: 'Max', valueColumn: formatValue(max) }); } if (isDefined(median)) { - data.push({ key: 'Median', valueColumn: channels.y.formatValue(median) }); + data.push({ key: 'Median', valueColumn: formatValue(median) }); } if (isDefined(firstQuartile)) { - data.push({ key: '1st Quartile', valueColumn: channels.y.formatValue(firstQuartile) }); + data.push({ key: '1st Quartile', valueColumn: formatValue(firstQuartile) }); } if (isDefined(thirdQuartile)) { - data.push({ key: '3rd Quartile', valueColumn: channels.y.formatValue(thirdQuartile) }); + data.push({ key: '3rd Quartile', valueColumn: formatValue(thirdQuartile) }); } if (isDefined(outliers) && outliers.length > 0) { data.push({ key: '# Outliers', valueColumn: outliers.length }); diff --git a/packages/superset-ui-preset-chart-xy/src/Line/Line.tsx b/packages/superset-ui-preset-chart-xy/src/Line/Line.tsx index 69accd8dc..8bcd81b81 100644 --- a/packages/superset-ui-preset-chart-xy/src/Line/Line.tsx +++ b/packages/superset-ui-preset-chart-xy/src/Line/Line.tsx @@ -19,6 +19,7 @@ import WithLegend from '../components/WithLegend'; import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder'; import { Dataset, PlainObject } from '../encodeable/types/Data'; import ChartLegend from '../components/legend/ChartLegend'; +import { PartialSpec } from '../encodeable/types/Specification'; chartTheme.gridStyles.stroke = '#f1f3f5'; @@ -35,10 +36,10 @@ type Props = { width: string | number; height: string | number; margin?: Margin; - encoding: Encoding; data: Dataset; theme?: ChartTheme; -} & Readonly; +} & PartialSpec & + Readonly; export interface Series { key: string; @@ -67,15 +68,17 @@ class LineChart extends PureComponent { super(props); const createEncoder = createSelector( - (enc: Encoding) => enc, - (enc: Encoding) => new Encoder({ encoding: enc }), + (p: PartialSpec) => p.encoding, + p => p.commonEncoding, + p => p.options, + (encoding, commonEncoding, options) => new Encoder({ encoding, commonEncoding, options }), ); this.createEncoder = () => { - this.encoder = createEncoder(this.props.encoding); + this.encoder = createEncoder(this.props); }; - this.encoder = createEncoder(this.props.encoding); + this.encoder = createEncoder(this.props); this.renderChart = this.renderChart.bind(this); } diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/Encoder.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/Encoder.ts new file mode 100644 index 000000000..bbda94638 --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/Encoder.ts @@ -0,0 +1,48 @@ +import AbstractEncoder from '../encodeable/AbstractEncoder'; +import { PartialSpec } from '../encodeable/types/Specification'; +import { EncodingFromChannelsAndOutputs } from '../encodeable/types/Channel'; + +/** + * Define channel types + */ +const channelTypes = { + fill: 'Color', + size: 'Numeric', + stroke: 'Color', + x: 'X', + y: 'Y', +} as const; + +export type ChannelTypes = typeof channelTypes; + +/** + * Define output type for each channel + */ +export interface Outputs { + fill: string; + size: number; + stroke: string; + x: number | null; + y: number | null; +} + +/** + * Derive encoding config + */ +export type Encoding = EncodingFromChannelsAndOutputs; + +export default class Encoder extends AbstractEncoder { + static readonly DEFAULT_ENCODINGS: Encoding = { + fill: { value: '#222' }, + size: { value: 5 }, + stroke: { value: 'none' }, + x: { field: 'x', type: 'quantitative' }, + y: { field: 'y', type: 'quantitative' }, + }; + + static readonly CHANNEL_OPTIONS = {}; + + constructor(spec: PartialSpec) { + super(channelTypes, spec, Encoder.DEFAULT_ENCODINGS, Encoder.CHANNEL_OPTIONS); + } +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/ScatterPlot.tsx b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/ScatterPlot.tsx new file mode 100644 index 000000000..1ba6316f4 --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/ScatterPlot.tsx @@ -0,0 +1,150 @@ +/* eslint-disable sort-keys, no-magic-numbers, complexity */ +import React, { PureComponent } from 'react'; +import { XYChart, PointSeries } from '@data-ui/xy-chart'; +import { chartTheme, ChartTheme } from '@data-ui/theme'; +import { Margin, Dimension } from '@superset-ui/dimension'; +import { extent as d3Extent } from 'd3-array'; +import { createSelector } from 'reselect'; +import createTooltip from './createTooltip'; +import XYChartLayout from '../utils/XYChartLayout'; +import WithLegend from '../components/WithLegend'; +import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder'; +import { Dataset, PlainObject } from '../encodeable/types/Data'; +import ChartLegend from '../components/legend/ChartLegend'; +import { PartialSpec } from '../encodeable/types/Specification'; + +chartTheme.gridStyles.stroke = '#f1f3f5'; + +const DEFAULT_MARGIN = { top: 20, right: 20, left: 20, bottom: 20 }; + +const defaultProps = { + className: '', + margin: DEFAULT_MARGIN, + theme: chartTheme, +} as const; + +type Props = { + className?: string; + width: string | number; + height: string | number; + margin?: Margin; + data: Dataset; + theme?: ChartTheme; +} & PartialSpec & + Readonly; + +export interface EncodedPoint { + x: Outputs['x']; + y: Outputs['y']; + size: Outputs['size']; + fill: Outputs['fill']; + stroke: Outputs['stroke']; + data: PlainObject; +} + +export default class ScatterPlot extends PureComponent { + static defaultProps = defaultProps; + + encoder: Encoder; + private createEncoder: () => void; + + constructor(props: Props) { + super(props); + + const createEncoder = createSelector( + (p: PartialSpec) => p.encoding, + p => p.commonEncoding, + p => p.options, + (encoding, commonEncoding, options) => new Encoder({ encoding, commonEncoding, options }), + ); + + this.createEncoder = () => { + this.encoder = createEncoder(this.props); + }; + + this.encoder = createEncoder(this.props); + this.renderChart = this.renderChart.bind(this); + } + + renderChart(dim: Dimension) { + const { width, height } = dim; + const { data, margin, theme } = this.props; + const { channels } = this.encoder; + + if (typeof channels.size.scale !== 'undefined') { + const domain = d3Extent(data, d => channels.size.get(d)); + const [min, max] = domain; + const adjustedDomain = [Math.min(min || 0, 0), Math.max(max || 1, 1)]; + channels.size.scale.setDomain(adjustedDomain); + } + + const encodedData = data.map(d => ({ + x: channels.x.get(d), + y: channels.y.get(d), + size: channels.size.encode(d), + fill: channels.fill.encode(d), + stroke: channels.stroke.encode(d), + data: d, + })); + + const children = [ + d.fill} + fillOpacity={0.5} + stroke={(d: EncodedPoint) => d.stroke} + size={(d: EncodedPoint) => d.size} + />, + ]; + + const layout = new XYChartLayout({ + width, + height, + margin: { ...DEFAULT_MARGIN, ...margin }, + theme, + xEncoder: channels.x, + yEncoder: channels.y, + children, + }); + + return layout.renderChartWithFrame((chartDim: Dimension) => ( + + {children} + {layout.renderXAxis()} + {layout.renderYAxis()} + + )); + } + + render() { + const { className, data, width, height } = this.props; + + this.createEncoder(); + const renderLegend = this.encoder.hasLegend() + ? // eslint-disable-next-line react/jsx-props-no-multi-spaces + () => data={data} encoder={this.encoder} /> + : undefined; + + return ( + + ); + } +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createMetadata.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createMetadata.ts new file mode 100644 index 000000000..29a790802 --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createMetadata.ts @@ -0,0 +1,12 @@ +import { t } from '@superset-ui/translation'; +import { ChartMetadata } from '@superset-ui/chart'; +import thumbnail from './images/thumbnail.png'; + +export default function createMetadata(useLegacyApi = false) { + return new ChartMetadata({ + description: '', + name: t('Scatter Plot'), + thumbnail, + useLegacyApi, + }); +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createTooltip.tsx b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createTooltip.tsx new file mode 100644 index 000000000..aabb41f00 --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/createTooltip.tsx @@ -0,0 +1,57 @@ +/* eslint-disable no-magic-numbers */ + +import React from 'react'; +import TooltipFrame from '../components/tooltip/TooltipFrame'; +import TooltipTable from '../components/tooltip/TooltipTable'; +import Encoder from './Encoder'; +import { isFieldDef } from '../encodeable/types/ChannelDef'; +import { EncodedPoint } from './ScatterPlot'; + +export default function createTooltip(encoder: Encoder) { + function Tooltip({ datum }: { datum: EncodedPoint }) { + const { channels, commonChannels } = encoder; + const { x, y, size, fill, stroke } = channels; + + const tooltipRows = [ + { key: 'x', keyColumn: x.getTitle(), valueColumn: x.format(datum.data) }, + { key: 'y', keyColumn: y.getTitle(), valueColumn: y.format(datum.data) }, + ]; + + if (isFieldDef(fill.definition)) { + tooltipRows.push({ + key: 'fill', + keyColumn: fill.getTitle(), + valueColumn: fill.format(datum.data), + }); + } + if (isFieldDef(stroke.definition)) { + tooltipRows.push({ + key: 'stroke', + keyColumn: stroke.getTitle(), + valueColumn: stroke.format(datum.data), + }); + } + if (isFieldDef(size.definition)) { + tooltipRows.push({ + key: 'size', + keyColumn: size.getTitle(), + valueColumn: size.format(datum.data), + }); + } + commonChannels.group.forEach(g => { + tooltipRows.push({ + key: `${g.name}`, + keyColumn: g.getTitle(), + valueColumn: g.format(datum.data), + }); + }); + + return ( + + + + ); + } + + return Tooltip; +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnail.png b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnail.png new file mode 100644 index 000000000..ecb8dd566 Binary files /dev/null and b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnail.png differ diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnailLarge.png b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnailLarge.png new file mode 100644 index 000000000..ecb8dd566 Binary files /dev/null and b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/images/thumbnailLarge.png differ diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/index.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/index.ts new file mode 100644 index 000000000..08a9615de --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/index.ts @@ -0,0 +1,13 @@ +import { ChartPlugin } from '@superset-ui/chart'; +import createMetadata from './createMetadata'; +import transformProps from './transformProps'; + +export default class LineChartPlugin extends ChartPlugin { + constructor() { + super({ + loadChart: () => import('./ScatterPlot'), + metadata: createMetadata(), + transformProps, + }); + } +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/index.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/index.ts new file mode 100644 index 000000000..571dbef72 --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/index.ts @@ -0,0 +1,13 @@ +import { ChartPlugin } from '@superset-ui/chart'; +import createMetadata from '../createMetadata'; +import transformProps from './transformProps'; + +export default class LineChartPlugin extends ChartPlugin { + constructor() { + super({ + loadChart: () => import('../ScatterPlot'), + metadata: createMetadata(true), + transformProps, + }); + } +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/transformProps.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/transformProps.ts new file mode 100644 index 000000000..8c2cc418b --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/legacy/transformProps.ts @@ -0,0 +1,96 @@ +/* eslint-disable sort-keys */ +import { ChartProps } from '@superset-ui/chart'; +import { flatMap } from 'lodash'; + +interface DataRow { + key: string[]; + values: { + [key: string]: any; + }[]; +} + +export default function transformProps(chartProps: ChartProps) { + const { width, height, formData, payload } = chartProps; + const { + colorScheme, + entity, + maxBubbleSize, + series, + showLegend, + size, + x, + xAxisFormat, + xAxisLabel, + // TODO: These fields are not supported yet + // xAxisShowminmax, + // xLogScale, + y, + yAxisLabel, + yAxisFormat, + // TODO: These fields are not supported yet + // yAxisShowminmax, + // yLogScale, + } = formData; + const data = payload.data as DataRow[]; + + return { + data: flatMap( + data.map((row: DataRow) => + row.values.map(v => ({ + [x]: v[x], + [y]: v[y], + [series]: v[series], + [size]: v[size], + [entity]: v[entity], + })), + ), + ), + width, + height, + encoding: { + x: { + field: x, + type: 'quantitive', + format: xAxisFormat, + scale: { + type: 'linear', + }, + axis: { + orient: 'bottom', + title: xAxisLabel, + }, + }, + y: { + field: y, + type: 'quantitative', + format: yAxisFormat, + scale: { + type: 'linear', + }, + axis: { + orient: 'left', + title: yAxisLabel, + }, + }, + size: { + field: size, + type: 'quantitative', + scale: { + type: 'linear', + range: [0, maxBubbleSize], + }, + }, + fill: { + field: series, + type: 'nominal', + scale: { + scheme: colorScheme, + }, + legend: showLegend, + }, + }, + commonEncoding: { + group: [{ field: entity }], + }, + }; +} diff --git a/packages/superset-ui-preset-chart-xy/src/ScatterPlot/transformProps.ts b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/transformProps.ts new file mode 100644 index 000000000..94f2c864e --- /dev/null +++ b/packages/superset-ui-preset-chart-xy/src/ScatterPlot/transformProps.ts @@ -0,0 +1,18 @@ +import { ChartProps } from '@superset-ui/chart'; + +/* eslint-disable sort-keys */ + +export default function transformProps(chartProps: ChartProps) { + const { width, height, formData, payload } = chartProps; + const { encoding, commonEncoding, margin } = formData; + const { data } = payload; + + return { + data, + width, + height, + encoding, + commonEncoding, + margin, + }; +} diff --git a/packages/superset-ui-preset-chart-xy/src/encodeable/AbstractEncoder.ts b/packages/superset-ui-preset-chart-xy/src/encodeable/AbstractEncoder.ts index cd9f95394..4a290a362 100644 --- a/packages/superset-ui-preset-chart-xy/src/encodeable/AbstractEncoder.ts +++ b/packages/superset-ui-preset-chart-xy/src/encodeable/AbstractEncoder.ts @@ -7,7 +7,7 @@ import { ChannelInput, } from './types/Channel'; import { FullSpec, BaseOptions, PartialSpec } from './types/Specification'; -import { isFieldDef, isTypedFieldDef } from './types/ChannelDef'; +import { isFieldDef, isTypedFieldDef, FieldDef } from './types/ChannelDef'; import ChannelEncoder from './ChannelEncoder'; import { Dataset } from './types/Data'; @@ -28,6 +28,11 @@ export default abstract class AbstractEncoder< readonly [k in keyof ChannelTypes]: ChannelEncoder }; + readonly commonChannels: { + group: ChannelEncoder[]; + tooltip: ChannelEncoder[]; + }; + readonly legends: { [key: string]: (keyof ChannelTypes)[]; }; @@ -67,6 +72,25 @@ export default abstract class AbstractEncoder< return all; }, {}) as Channels; + this.commonChannels = { + group: this.spec.commonEncoding.group.map( + (def, i) => + new ChannelEncoder({ + definition: def, + name: `group${i}`, + type: 'Text', + }), + ), + tooltip: this.spec.commonEncoding.tooltip.map( + (def, i) => + new ChannelEncoder({ + definition: def, + name: `tooltip${i}`, + type: 'Text', + }), + ), + }; + // Group the channels that use the same field together // so they can share the same legend. this.legends = {}; @@ -94,9 +118,14 @@ export default abstract class AbstractEncoder< return spec as FullSpec; } - const { encoding, ...rest } = spec; + const { encoding, commonEncoding = {}, ...rest } = spec; + const { group = [], tooltip = [] } = commonEncoding; return { + commonEncoding: { + group, + tooltip, + }, ...rest, encoding: { ...defaultEncoding, diff --git a/packages/superset-ui-preset-chart-xy/src/encodeable/AxisAgent.ts b/packages/superset-ui-preset-chart-xy/src/encodeable/AxisAgent.ts index 94c1737c3..ffc15404f 100644 --- a/packages/superset-ui-preset-chart-xy/src/encodeable/AxisAgent.ts +++ b/packages/superset-ui-preset-chart-xy/src/encodeable/AxisAgent.ts @@ -142,7 +142,7 @@ export default class AxisAgent, Output extends Va if (this.channelEncoder.isX()) { if (strategyForLabelOverlap === 'flat') { - const labelHeight = labelDimensions[0].height; + const labelHeight = labelDimensions.length > 0 ? labelDimensions[0].height : 0; labelOffset = labelHeight + labelPadding; requiredMargin += labelHeight; } else if (strategyForLabelOverlap === 'rotate') { diff --git a/packages/superset-ui-preset-chart-xy/src/encodeable/types/Specification.ts b/packages/superset-ui-preset-chart-xy/src/encodeable/types/Specification.ts index 7631de62e..f8d7427d0 100644 --- a/packages/superset-ui-preset-chart-xy/src/encodeable/types/Specification.ts +++ b/packages/superset-ui-preset-chart-xy/src/encodeable/types/Specification.ts @@ -1,13 +1,23 @@ +import { FieldDef } from './ChannelDef'; + export interface BaseOptions { namespace?: string; } export interface PartialSpec { encoding: Partial; + commonEncoding?: Partial<{ + group: FieldDef[]; + tooltip: FieldDef[]; + }>; options?: Options; } export interface FullSpec { encoding: Encoding; + commonEncoding: { + group: FieldDef[]; + tooltip: FieldDef[]; + }; options?: Options; } diff --git a/packages/superset-ui-preset-chart-xy/src/index.ts b/packages/superset-ui-preset-chart-xy/src/index.ts index 498637196..3932d61ae 100644 --- a/packages/superset-ui-preset-chart-xy/src/index.ts +++ b/packages/superset-ui-preset-chart-xy/src/index.ts @@ -1,2 +1,3 @@ export { default as BoxPlotChartPlugin } from './BoxPlot'; +export { default as ScatterPlotPlugin } from './ScatterPlot'; export { default as LineChartPlugin } from './Line'; diff --git a/packages/superset-ui-preset-chart-xy/src/legacy.ts b/packages/superset-ui-preset-chart-xy/src/legacy.ts index c0403f479..24c83c6d3 100644 --- a/packages/superset-ui-preset-chart-xy/src/legacy.ts +++ b/packages/superset-ui-preset-chart-xy/src/legacy.ts @@ -1,2 +1,3 @@ export { default as BoxPlotChartPlugin } from './BoxPlot/legacy'; +export { default as ScatterPlotPlugin } from './ScatterPlot/legacy'; export { default as LineChartPlugin } from './Line/legacy'; diff --git a/packages/superset-ui-preset-chart-xy/types/@data-ui/xy-chart/index.d.ts b/packages/superset-ui-preset-chart-xy/types/@data-ui/xy-chart/index.d.ts index 504be9e51..308a07385 100644 --- a/packages/superset-ui-preset-chart-xy/types/@data-ui/xy-chart/index.d.ts +++ b/packages/superset-ui-preset-chart-xy/types/@data-ui/xy-chart/index.d.ts @@ -33,6 +33,7 @@ declare module '@data-ui/xy-chart' { export class CrossHair extends React.PureComponent {} export class LinearGradient extends React.PureComponent {} export class LineSeries extends React.PureComponent {} + export class PointSeries extends React.PureComponent {} export class WithTooltip extends React.PureComponent {} export class XYChart extends React.PureComponent {} export class XAxis extends React.PureComponent {}