You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/10/05 10:58:53 UTC

[superset] branch master updated: feat: Adds the ECharts Bubble chart (#22107)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new c81c60c91f feat: Adds the ECharts Bubble chart (#22107)
c81c60c91f is described below

commit c81c60c91fbcb09dd63c05f050e18ee09ceebfd6
Author: Mayur <ma...@gmail.com>
AuthorDate: Thu Oct 5 16:28:46 2023 +0530

    feat: Adds the ECharts Bubble chart (#22107)
    
    Co-authored-by: Michael S. Molina <mi...@gmail.com>
---
 .../plugin-chart-echarts/Bubble/Stories.tsx        | 128 +++++++++
 .../plugins/plugin-chart-echarts/Bubble/data.ts    |  80 ++++++
 .../legacy-preset-chart-nvd3/src/Bubble/index.js   |   9 +-
 .../src/Bubble/EchartsBubble.tsx                   |  33 +++
 .../plugin-chart-echarts/src/Bubble/buildQuery.ts  |  40 +++
 .../plugin-chart-echarts/src/Bubble/constants.ts   |  35 +++
 .../src/Bubble/controlPanel.tsx                    | 287 +++++++++++++++++++++
 .../src/Bubble/images/example1.png                 | Bin 0 -> 134620 bytes
 .../src/Bubble/images/example2.png                 | Bin 0 -> 107431 bytes
 .../src/Bubble/images/thumbnail.png                | Bin 0 -> 114350 bytes
 .../plugin-chart-echarts/src/Bubble/index.ts       |  60 +++++
 .../src/Bubble/transformProps.ts                   | 229 ++++++++++++++++
 .../plugin-chart-echarts/src/Bubble/types.ts       |  57 ++++
 .../plugins/plugin-chart-echarts/src/index.ts      |   2 +
 .../test/Bubble/buildQuery.test.ts                 |  93 +++++++
 .../test/Bubble/transformProps.test.ts             | 160 ++++++++++++
 .../controls/VizTypeControl/VizTypeGallery.tsx     |   1 +
 .../src/visualizations/presets/MainPreset.js       |   2 +
 18 files changed, 1214 insertions(+), 2 deletions(-)

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 0000000000..b4731ee226
--- /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 }) => (
+  <SuperChart
+    chartType="bubble_v2"
+    width={width}
+    height={height}
+    queriesData={[{ data: simpleBubbleData }]}
+    formData={{
+      entity: 'customer_name',
+      x: 'count',
+      y: {
+        aggregate: 'SUM',
+        column: {
+          advanced_data_type: null,
+          certification_details: null,
+          certified_by: null,
+          column_name: 'price_each',
+          description: null,
+          expression: null,
+          filterable: true,
+          groupby: true,
+          id: 570,
+          is_certified: false,
+          is_dttm: false,
+          python_date_format: null,
+          type: 'DOUBLE PRECISION',
+          type_generic: 0,
+          verbose_name: null,
+          warning_markdown: null,
+        },
+        expressionType: 'SIMPLE',
+        hasCustomLabel: false,
+        isNew: false,
+        label: 'SUM(price_each)',
+        optionName: 'metric_d9rpclvys0a_fs4bs0m2l1f',
+        sqlExpression: null,
+      },
+      adhocFilters: [],
+      size: {
+        aggregate: 'SUM',
+        column: {
+          advanced_data_type: null,
+          certification_details: null,
+          certified_by: null,
+          column_name: 'sales',
+          description: null,
+          expression: null,
+          filterable: true,
+          groupby: true,
+          id: 571,
+          is_certified: false,
+          is_dttm: false,
+          python_date_format: null,
+          type: 'DOUBLE PRECISION',
+          type_generic: 0,
+          verbose_name: null,
+          warning_markdown: null,
+        },
+        expressionType: 'SIMPLE',
+        hasCustomLabel: false,
+        isNew: false,
+        label: 'SUM(sales)',
+        optionName: 'metric_itj9wncjxk_dp3yibib0q',
+        sqlExpression: null,
+      },
+      limit: 10,
+      colorScheme: 'supersetColors',
+      maxBubbleSize: select('Max bubble size', [5, 10, 25, 50, 100, 125], 10),
+      xAxisTitle: text('X axis title', ''),
+      xAxisTitleMargin: number('X axis title margin', 30),
+      yAxisTitle: text('Y axis title', ''),
+      yAxisTitleMargin: number('Y axis title margin', 30),
+      yAxisTitlePosition: 'Left',
+      xAxisFormat: null,
+      logYAxis: boolean('Log Y axis', false),
+      yAxisFormat: null,
+      logXAxis: boolean('Log X axis', false),
+      truncateYAxis: false,
+      yAxisBounds: [],
+      extraFormData: {},
+    }}
+  />
+);
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 0000000000..c434e33e94
--- /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 4b5a032ee6..c2916c7a42 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 0000000000..1d64b25161
--- /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 (
+    <Echart
+      height={height}
+      width={width}
+      echartOptions={echartOptions}
+      refs={refs}
+    />
+  );
+}
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 0000000000..31cdc0d9f6
--- /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 0000000000..0f9bc0f305
--- /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<EchartsBubbleFormData> = {
+  ...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 0000000000..53fba5de2b
--- /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 0000000000..5fdbdbd1c8
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 0000000000..3559d7d3ee
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 0000000000..11fd818011
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 0000000000..c07776c4ac
--- /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 0000000000..7962bc2c36
--- /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]} </br> ${params.data[4]}`
+    : params.data[3];
+
+  return `<p>${title}</p>
+        ${xAxisLabel}: ${xAxisFormatter(params.data[0])} <br/>
+        ${yAxisLabel}: ${yAxisFormatter(params.data[1])} <br/>
+        ${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 0000000000..ebb23174a7
--- /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<EchartsBubbleFormData> {
+  formData: EchartsBubbleFormData;
+  queriesData: ChartDataResponseResult[];
+}
+
+export type BubbleChartTransformedProps =
+  BaseTransformedProps<EchartsBubbleFormData> & CrossFilterTransformedProps;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts
index 0301f265b0..f8c7cf6103 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 0000000000..cbe6003eb4
--- /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 0000000000..2bb4ae0fc6
--- /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(
+      `<p>bubble title </br> bubble dimension</p>
+        x-axis-label: $10,000.00 <br/>
+        y-axis-label: $20,000.00 <br/>
+        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(
+      `<p>bubble title</p>
+        x-axis-label: $10,000.00 <br/>
+        y-axis-label: $25,000.00 <br/>
+        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 90b7856b05..c194d2fae1 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 735027fdd0..451196c35d 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,
       ],
     });