& 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,
],
});