feat: Adds the ECharts Bubble chart (#22107)

Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
Mayur 2023-10-05 16:28:46 +05:30 committed by GitHub
parent 50b0816e37
commit c81c60c91f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1214 additions and 2 deletions

View File

@ -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: {},
}}
/>
);

View File

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

View File

@ -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({

View File

@ -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}
/>
);
}

View File

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

View File

@ -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;

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

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

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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';

View File

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

View File

@ -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%`,
);
});
});

View File

@ -101,6 +101,7 @@ const DEFAULT_ORDER = [
'cal_heatmap',
'rose',
'bubble',
'bubble_v2',
'deck_geojson',
'horizon',
'deck_multi',

View File

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