diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/Stories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/Stories.tsx new file mode 100644 index 000000000..b4731ee22 --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/Stories.tsx @@ -0,0 +1,128 @@ +/** + * 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. + */ +import React from 'react'; +import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; +import { + boolean, + number, + select, + text, + withKnobs, +} from '@storybook/addon-knobs'; +import { + EchartsBubbleChartPlugin, + BubbleTransformProps, +} from '@superset-ui/plugin-chart-echarts'; +import { simpleBubbleData } from './data'; +import { withResizableChartDemo } from '../../../../shared/components/ResizableChartDemo'; + +new EchartsBubbleChartPlugin().configure({ key: 'bubble_v2' }).register(); + +getChartTransformPropsRegistry().registerValue( + 'bubble_v2', + BubbleTransformProps, +); + +export default { + title: 'Chart Plugins/plugin-chart-echarts/Bubble', + decorators: [withKnobs, withResizableChartDemo], +}; + +export const SimpleBubble = ({ width, height }) => ( + +); diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/data.ts b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/data.ts new file mode 100644 index 000000000..c434e33e9 --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Bubble/data.ts @@ -0,0 +1,80 @@ +/** + * 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. + */ +export const simpleBubbleData = [ + { + customer_name: 'AV Stores, Co.', + count: 51, + 'SUM(price_each)': 3975.33, + 'SUM(sales)': 157807.80999999997, + }, + { + customer_name: 'Alpha Cognac', + count: 20, + 'SUM(price_each)': 1701.95, + 'SUM(sales)': 70488.44, + }, + { + customer_name: 'Amica Models & Co.', + count: 26, + 'SUM(price_each)': 2218.41, + 'SUM(sales)': 94117.26000000002, + }, + { + customer_name: "Anna's Decorations, Ltd", + count: 46, + 'SUM(price_each)': 3843.67, + 'SUM(sales)': 153996.13000000003, + }, + { + customer_name: 'Atelier graphique', + count: 7, + 'SUM(price_each)': 558.4300000000001, + 'SUM(sales)': 24179.96, + }, + { + customer_name: 'Australian Collectables, Ltd', + count: 23, + 'SUM(price_each)': 1809.7099999999998, + 'SUM(sales)': 64591.46000000001, + }, + { + customer_name: 'Australian Collectors, Co.', + count: 55, + 'SUM(price_each)': 4714.479999999999, + 'SUM(sales)': 200995.40999999997, + }, + { + customer_name: 'Australian Gift Network, Co', + count: 15, + 'SUM(price_each)': 1271.05, + 'SUM(sales)': 59469.11999999999, + }, + { + customer_name: 'Auto Assoc. & Cie.', + count: 18, + 'SUM(price_each)': 1484.8600000000001, + 'SUM(sales)': 64834.32000000001, + }, + { + customer_name: 'Auto Canal Petit', + count: 27, + 'SUM(price_each)': 2188.82, + 'SUM(sales)': 93170.65999999999, + }, +]; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js index 4b5a032ee..c2916c7a4 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core'; +import { t, ChartMetadata, ChartPlugin, ChartLabel } from '@superset-ui/core'; import transformProps from '../transformProps'; import example from './images/example.jpg'; import thumbnail from './images/thumbnail.png'; @@ -29,7 +29,8 @@ const metadata = new ChartMetadata({ 'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.', ), exampleGallery: [{ url: example }], - name: t('Bubble Chart'), + label: ChartLabel.DEPRECATED, + name: t('Bubble Chart (legacy)'), tags: [ t('Multi-Dimensions'), t('Aesthetic'), @@ -39,11 +40,15 @@ const metadata = new ChartMetadata({ t('Time'), t('Trend'), t('nvd3'), + t('Deprecated'), ], thumbnail, useLegacyApi: true, }); +/** + * @deprecated in version 4.0. + */ export default class BubbleChartPlugin extends ChartPlugin { constructor() { super({ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx new file mode 100644 index 000000000..1d64b2516 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx @@ -0,0 +1,33 @@ +/** + * 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. + */ +import React from 'react'; +import { BubbleChartTransformedProps } from './types'; +import Echart from '../components/Echart'; + +export default function EchartsBubble(props: BubbleChartTransformedProps) { + const { height, width, echartOptions, refs } = props; + return ( + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts new file mode 100644 index 000000000..31cdc0d9f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts @@ -0,0 +1,40 @@ +/** + * 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. + */ +import { + buildQueryContext, + ensureIsArray, + QueryFormData, +} from '@superset-ui/core'; + +export default function buildQuery(formData: QueryFormData) { + const columns = [ + ...ensureIsArray(formData.entity), + ...ensureIsArray(formData.series), + ]; + + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns, + orderby: baseQueryObject.orderby + ? [[baseQueryObject.orderby[0], !baseQueryObject.order_desc]] + : undefined, + }, + ]); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts new file mode 100644 index 000000000..0f9bc0f30 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts @@ -0,0 +1,35 @@ +/** + * 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. + */ +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; +import { EchartsBubbleFormData } from './types'; + +export const DEFAULT_FORM_DATA: Partial = { + ...DEFAULT_LEGEND_FORM_DATA, + emitFilter: false, + logXAis: false, + logYAxis: false, + xAxisTitleMargin: 30, + yAxisTitleMargin: 30, + truncateYAxis: false, + yAxisBounds: [null, null], + xAxisLabelRotation: 0, + opacity: 0.6, +}; + +export const MINIMUM_BUBBLE_SIZE = 5; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx new file mode 100644 index 000000000..53fba5de2 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx @@ -0,0 +1,287 @@ +/** + * 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. + */ +import { t } from '@superset-ui/core'; +import { + ControlPanelConfig, + formatSelectOptions, + sections, + ControlPanelsContainerProps, + sharedControls, +} from '@superset-ui/chart-controls'; + +import { DEFAULT_FORM_DATA } from './constants'; +import { legendSection } from '../controls'; + +const { logAxis, truncateYAxis, yAxisBounds, xAxisLabelRotation, opacity } = + DEFAULT_FORM_DATA; + +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['series'], + ['entity'], + ['x'], + ['y'], + ['adhoc_filters'], + ['size'], + ['orderby'], + [ + { + name: 'order_desc', + config: { + ...sharedControls.order_desc, + visibility: ({ controls }) => Boolean(controls.orderby.value), + }, + }, + ], + ['row_limit'], + ], + }, + { + label: t('Chart Options'), + expanded: true, + tabOverride: 'customize', + controlSetRows: [ + ['color_scheme'], + ...legendSection, + [ + { + name: 'max_bubble_size', + config: { + type: 'SelectControl', + renderTrigger: true, + freeForm: true, + label: t('Max Bubble Size'), + default: '25', + choices: formatSelectOptions([ + '5', + '10', + '15', + '25', + '50', + '75', + '100', + ]), + }, + }, + ], + [ + { + name: 'tooltipSizeFormat', + config: { + ...sharedControls.y_axis_format, + label: t('Bubble size number format'), + }, + }, + ], + [ + { + name: 'opacity', + config: { + type: 'SliderControl', + label: t('Bubble Opacity'), + renderTrigger: true, + min: 0, + max: 1, + step: 0.1, + default: opacity, + description: t( + 'Opacity of bubbles, 0 means completely transparent, 1 means opaque', + ), + }, + }, + ], + ], + }, + { + label: t('X Axis'), + expanded: true, + controlSetRows: [ + [ + { + name: 'x_axis_label', + config: { + type: 'TextControl', + label: t('X Axis Title'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'xAxisLabelRotation', + config: { + type: 'SelectControl', + freeForm: true, + clearable: false, + label: t('Rotate x axis label'), + choices: [ + [0, '0°'], + [45, '45°'], + ], + default: xAxisLabelRotation, + renderTrigger: true, + description: t( + 'Input field supports custom rotation. e.g. 30 for 30°', + ), + }, + }, + ], + [ + { + name: 'x_axis_title_margin', + config: { + type: 'SelectControl', + freeForm: true, + clearable: true, + label: t('X AXIS TITLE MARGIN'), + renderTrigger: true, + default: sections.TITLE_MARGIN_OPTIONS[1], + choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), + }, + }, + ], + [ + { + name: 'xAxisFormat', + config: { + ...sharedControls.y_axis_format, + label: t('X Axis Format'), + }, + }, + ], + [ + { + name: 'logXAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic x-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic x-axis'), + }, + }, + ], + ], + }, + { + label: t('Y Axis'), + expanded: true, + controlSetRows: [ + [ + { + name: 'y_axis_label', + config: { + type: 'TextControl', + label: t('Y Axis Title'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'yAxisLabelRotation', + config: { + type: 'SelectControl', + freeForm: true, + clearable: false, + label: t('Rotate y axis label'), + choices: [ + [0, '0°'], + [45, '45°'], + ], + default: xAxisLabelRotation, + renderTrigger: true, + description: t( + 'Input field supports custom rotation. e.g. 30 for 30°', + ), + }, + }, + ], + [ + { + name: 'y_axis_title_margin', + config: { + type: 'SelectControl', + freeForm: true, + clearable: true, + label: t('Y AXIS TITLE MARGIN'), + renderTrigger: true, + default: sections.TITLE_MARGIN_OPTIONS[1], + choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), + }, + }, + ], + ['y_axis_format'], + [ + { + name: 'logYAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic y-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic y-axis'), + }, + }, + ], + [ + { + name: 'truncateYAxis', + config: { + type: 'CheckboxControl', + label: t('Truncate Y Axis'), + default: truncateYAxis, + renderTrigger: true, + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Y Axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + 'Bounds for the Y-axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.truncateYAxis?.value), + }, + }, + ], + ], + }, + ], +}; + +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example1.png new file mode 100644 index 000000000..5fdbdbd1c Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example2.png b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example2.png new file mode 100644 index 000000000..3559d7d3e Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/example2.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/thumbnail.png new file mode 100644 index 000000000..11fd81801 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts new file mode 100644 index 000000000..c07776c4a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts @@ -0,0 +1,60 @@ +/** + * 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. + */ +import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core'; +import thumbnail from './images/thumbnail.png'; +import transformProps from './transformProps'; +import buildQuery from './buildQuery'; +import controlPanel from './controlPanel'; +import example1 from './images/example1.png'; +import example2 from './images/example2.png'; +import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types'; + +export default class EchartsBubbleChartPlugin extends ChartPlugin< + EchartsBubbleFormData, + EchartsBubbleChartProps +> { + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsBubble'), + metadata: new ChartMetadata({ + behaviors: [Behavior.INTERACTIVE_CHART], + category: t('Correlation'), + credits: ['https://echarts.apache.org'], + description: t( + 'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.', + ), + exampleGallery: [{ url: example1 }, { url: example2 }], + name: t('Bubble Chart'), + tags: [ + t('Multi-Dimensions'), + t('Aesthetic'), + t('Comparison'), + t('Scatter'), + t('Time'), + t('Trend'), + t('ECharts'), + ], + thumbnail, + }), + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts new file mode 100644 index 000000000..7962bc2c3 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts @@ -0,0 +1,229 @@ +/** + * 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. + */ +import { EChartsCoreOption, ScatterSeriesOption } from 'echarts'; +import { extent } from 'd3-array'; +import { + CategoricalColorNamespace, + getNumberFormatter, + AxisType, + getMetricLabel, + NumberFormatter, +} from '@superset-ui/core'; +import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types'; +import { DEFAULT_FORM_DATA, MINIMUM_BUBBLE_SIZE } from './constants'; +import { defaultGrid } from '../defaults'; +import { getLegendProps } from '../utils/series'; +import { Refs } from '../types'; +import { parseYAxisBound } from '../utils/controls'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { getPadding } from '../Timeseries/transformers'; +import { convertInteger } from '../utils/convertInteger'; +import { NULL_STRING } from '../constants'; + +function normalizeSymbolSize( + nodes: ScatterSeriesOption[], + maxBubbleValue: number, +) { + const [bubbleMinValue, bubbleMaxValue] = extent(nodes, x => x.data![0][2]); + const nodeSpread = bubbleMaxValue - bubbleMinValue; + nodes.forEach(node => { + // eslint-disable-next-line no-param-reassign + node.symbolSize = + (((node.data![0][2] - bubbleMinValue) / nodeSpread) * + (maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE; + }); +} + +export function formatTooltip( + params: any, + xAxisLabel: string, + yAxisLabel: string, + sizeLabel: string, + xAxisFormatter: NumberFormatter, + yAxisFormatter: NumberFormatter, + tooltipSizeFormatter: NumberFormatter, +) { + const title = params.data[4] + ? `${params.data[3]}
${params.data[4]}` + : params.data[3]; + + return `

${title}

+ ${xAxisLabel}: ${xAxisFormatter(params.data[0])}
+ ${yAxisLabel}: ${yAxisFormatter(params.data[1])}
+ ${sizeLabel}: ${tooltipSizeFormatter(params.data[2])}`; +} + +export default function transformProps(chartProps: EchartsBubbleChartProps) { + const { height, width, hooks, queriesData, formData, inContextMenu, theme } = + chartProps; + + const { data = [] } = queriesData[0]; + const { + x, + y, + size, + entity, + maxBubbleSize, + colorScheme, + series: bubbleSeries, + xAxisLabel: bubbleXAxisTitle, + yAxisLabel: bubbleYAxisTitle, + xAxisFormat, + yAxisFormat, + yAxisBounds, + logXAxis, + logYAxis, + xAxisTitleMargin, + yAxisTitleMargin, + truncateYAxis, + xAxisLabelRotation, + yAxisLabelRotation, + tooltipSizeFormat, + opacity, + showLegend, + legendOrientation, + legendMargin, + legendType, + }: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData }; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + + const legends: string[] = []; + const series: ScatterSeriesOption[] = []; + + const xAxisLabel: string = getMetricLabel(x); + const yAxisLabel: string = getMetricLabel(y); + const sizeLabel: string = getMetricLabel(size); + + const refs: Refs = {}; + + data.forEach(datum => { + const name = + ((bubbleSeries ? datum[bubbleSeries] : datum[entity]) as string) || + NULL_STRING; + const bubbleSeriesValue = bubbleSeries ? datum[bubbleSeries] : null; + + series.push({ + name, + data: [ + [ + datum[xAxisLabel], + datum[yAxisLabel], + datum[sizeLabel], + datum[entity], + bubbleSeriesValue as any, + ], + ], + type: 'scatter', + itemStyle: { color: colorFn(name), opacity }, + }); + legends.push(name); + }); + + normalizeSymbolSize(series, maxBubbleSize); + + const xAxisFormatter = getNumberFormatter(xAxisFormat); + const yAxisFormatter = getNumberFormatter(yAxisFormat); + const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat); + + const [min, max] = yAxisBounds.map(parseYAxisBound); + + const padding = getPadding( + showLegend, + legendOrientation, + true, + false, + legendMargin, + true, + 'Left', + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + ); + + const echartOptions: EChartsCoreOption = { + series, + xAxis: { + axisLabel: { formatter: xAxisFormatter }, + splitLine: { + lineStyle: { + type: 'dashed', + }, + }, + nameRotate: xAxisLabelRotation, + scale: true, + name: bubbleXAxisTitle, + nameLocation: 'middle', + nameTextStyle: { + fontWight: 'bolder', + }, + nameGap: convertInteger(xAxisTitleMargin), + type: logXAxis ? AxisType.log : AxisType.value, + }, + yAxis: { + axisLabel: { formatter: yAxisFormatter }, + splitLine: { + lineStyle: { + type: 'dashed', + }, + }, + nameRotate: yAxisLabelRotation, + scale: truncateYAxis, + name: bubbleYAxisTitle, + nameLocation: 'middle', + nameTextStyle: { + fontWight: 'bolder', + }, + nameGap: convertInteger(yAxisTitleMargin), + min, + max, + type: logYAxis ? AxisType.log : AxisType.value, + }, + legend: { + ...getLegendProps(legendType, legendOrientation, showLegend, theme), + data: legends, + }, + tooltip: { + show: !inContextMenu, + ...getDefaultTooltip(refs), + formatter: (params: any): string => + formatTooltip( + params, + xAxisLabel, + yAxisLabel, + sizeLabel, + xAxisFormatter, + yAxisFormatter, + tooltipSizeFormatter, + ), + }, + grid: { ...defaultGrid, ...padding }, + }; + + const { onContextMenu, setDataMask = () => {} } = hooks; + + return { + refs, + height, + width, + echartOptions, + onContextMenu, + setDataMask, + formData, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts new file mode 100644 index 000000000..ebb23174a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts @@ -0,0 +1,57 @@ +/** + * 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. + */ +import { + ChartProps, + ChartDataResponseResult, + QueryFormData, +} from '@superset-ui/core'; +import { + LegendFormData, + BaseTransformedProps, + CrossFilterTransformedProps, +} from '../types'; + +export type EchartsBubbleFormData = QueryFormData & + LegendFormData & { + series?: string; + entity: string; + xAxisFormat: string; + yAXisFormat: string; + logXAxis: boolean; + logYAxis: boolean; + xAxisBounds: [number | undefined | null, number | undefined | null]; + yAxisBounds: [number | undefined | null, number | undefined | null]; + xAxisLabel?: string; + colorScheme?: string; + defaultValue?: string[] | null; + dateFormat: string; + emitFilter: boolean; + tooltipFormat: string; + x: string; + y: string; + }; + +export interface EchartsBubbleChartProps + extends ChartProps { + formData: EchartsBubbleFormData; + queriesData: ChartDataResponseResult[]; +} + +export type BubbleChartTransformedProps = + BaseTransformedProps & CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index 0301f265b..f8c7cf610 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -34,6 +34,7 @@ export { default as EchartsTreeChartPlugin } from './Tree'; export { default as EchartsTreemapChartPlugin } from './Treemap'; export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber'; export { default as EchartsSunburstChartPlugin } from './Sunburst'; +export { default as EchartsBubbleChartPlugin } from './Bubble'; export { default as BoxPlotTransformProps } from './BoxPlot/transformProps'; export { default as FunnelTransformProps } from './Funnel/transformProps'; @@ -46,6 +47,7 @@ export { default as TimeseriesTransformProps } from './Timeseries/transformProps export { default as TreeTransformProps } from './Tree/transformProps'; export { default as TreemapTransformProps } from './Treemap/transformProps'; export { default as SunburstTransformProps } from './Sunburst/transformProps'; +export { default as BubbleTransformProps } from './Bubble/transformProps'; export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts new file mode 100644 index 000000000..cbe6003eb --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts @@ -0,0 +1,93 @@ +/** + * 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. + */ +import buildQuery from '../../src/Bubble/buildQuery'; + +describe('Bubble buildQuery', () => { + const formData = { + datasource: '1__table', + viz_type: 'echarts_bubble', + entity: 'customer_name', + x: 'count', + y: { + aggregate: 'sum', + column: { + column_name: 'price_each', + }, + expressionType: 'simple', + label: 'SUM(price_each)', + }, + size: { + aggregate: 'sum', + column: { + column_name: 'sales', + }, + expressionType: 'simple', + label: 'SUM(sales)', + }, + }; + + it('Should build query without dimension', () => { + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.columns).toEqual(['customer_name']); + expect(query.metrics).toEqual([ + 'count', + { + aggregate: 'sum', + column: { + column_name: 'price_each', + }, + expressionType: 'simple', + label: 'SUM(price_each)', + }, + { + aggregate: 'sum', + column: { + column_name: 'sales', + }, + expressionType: 'simple', + label: 'SUM(sales)', + }, + ]); + }); + it('Should build query with dimension', () => { + const queryContext = buildQuery({ ...formData, series: 'state' }); + const [query] = queryContext.queries; + expect(query.columns).toEqual(['customer_name', 'state']); + expect(query.metrics).toEqual([ + 'count', + { + aggregate: 'sum', + column: { + column_name: 'price_each', + }, + expressionType: 'simple', + label: 'SUM(price_each)', + }, + { + aggregate: 'sum', + column: { + column_name: 'sales', + }, + expressionType: 'simple', + label: 'SUM(sales)', + }, + ]); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts new file mode 100644 index 000000000..2bb4ae0fc --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts @@ -0,0 +1,160 @@ +/** + * 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. + */ +import { + ChartProps, + getNumberFormatter, + SqlaFormData, + supersetTheme, +} from '@superset-ui/core'; +import { EchartsBubbleChartProps } from 'plugins/plugin-chart-echarts/src/Bubble/types'; + +import transformProps, { formatTooltip } from '../../src/Bubble/transformProps'; + +describe('Bubble transformProps', () => { + const formData: SqlaFormData = { + datasource: '1__table', + viz_type: 'echarts_bubble', + entity: 'customer_name', + x: 'count', + y: { + aggregate: 'sum', + column: { + column_name: 'price_each', + }, + expressionType: 'simple', + label: 'SUM(price_each)', + }, + size: { + aggregate: 'sum', + column: { + column_name: 'sales', + }, + expressionType: 'simple', + label: 'SUM(sales)', + }, + yAxisBounds: [null, null], + }; + const chartProps = new ChartProps({ + formData, + height: 800, + width: 800, + queriesData: [ + { + data: [ + { + customer_name: 'AV Stores, Co.', + count: 10, + 'SUM(price_each)': 20, + 'SUM(sales)': 30, + }, + { + customer_name: 'Alpha Cognac', + count: 40, + 'SUM(price_each)': 50, + 'SUM(sales)': 60, + }, + { + customer_name: 'Amica Models & Co.', + count: 70, + 'SUM(price_each)': 80, + 'SUM(sales)': 90, + }, + ], + }, + ], + theme: supersetTheme, + }); + + it('Should transform props for viz', () => { + expect(transformProps(chartProps as EchartsBubbleChartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 800, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: expect.arrayContaining([ + [10, 20, 30, 'AV Stores, Co.', null], + ]), + }), + expect.objectContaining({ + data: expect.arrayContaining([ + [40, 50, 60, 'Alpha Cognac', null], + ]), + }), + expect.objectContaining({ + data: expect.arrayContaining([ + [70, 80, 90, 'Amica Models & Co.', null], + ]), + }), + ]), + }), + }), + ); + }); +}); + +describe('Bubble formatTooltip', () => { + const dollerFormatter = getNumberFormatter('$,.2f'); + const percentFormatter = getNumberFormatter(',.1%'); + + it('Should generate correct bubble label content with dimension', () => { + const params = { + data: [10000, 20000, 3, 'bubble title', 'bubble dimension'], + }; + + expect( + formatTooltip( + params, + 'x-axis-label', + 'y-axis-label', + 'size-label', + dollerFormatter, + dollerFormatter, + percentFormatter, + ), + ).toEqual( + `

bubble title
bubble dimension

+ x-axis-label: $10,000.00
+ y-axis-label: $20,000.00
+ size-label: 300.0%`, + ); + }); + it('Should generate correct bubble label content without dimension', () => { + const params = { + data: [10000, 25000, 3, 'bubble title', null], + }; + expect( + formatTooltip( + params, + 'x-axis-label', + 'y-axis-label', + 'size-label', + dollerFormatter, + dollerFormatter, + percentFormatter, + ), + ).toEqual( + `

bubble title

+ x-axis-label: $10,000.00
+ y-axis-label: $25,000.00
+ size-label: 300.0%`, + ); + }); +}); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index 90b7856b0..c194d2fae 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -101,6 +101,7 @@ const DEFAULT_ORDER = [ 'cal_heatmap', 'rose', 'bubble', + 'bubble_v2', 'deck_geojson', 'horizon', 'deck_multi', diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index 735027fdd..451196c35 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -65,6 +65,7 @@ import { EchartsMixedTimeseriesChartPlugin, EchartsTreeChartPlugin, EchartsSunburstChartPlugin, + EchartsBubbleChartPlugin, } from '@superset-ui/plugin-chart-echarts'; import { SelectFilterPlugin, @@ -160,6 +161,7 @@ export default class MainPreset extends Preset { new EchartsTreeChartPlugin().configure({ key: 'tree_chart' }), new EchartsSunburstChartPlugin().configure({ key: 'sunburst_v2' }), new HandlebarsChartPlugin().configure({ key: 'handlebars' }), + new EchartsBubbleChartPlugin().configure({ key: 'bubble_v2' }), ...experimentalplugins, ], });