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 d619078d25 feat: Improves the Waterfall chart (#25557)
d619078d25 is described below
commit d619078d25dde63c55e9afd87e98f05d4fb82b86
Author: Michael S. Molina <[email protected]>
AuthorDate: Fri Nov 3 13:24:15 2023 -0300
feat: Improves the Waterfall chart (#25557)
---
.../packages/superset-ui-core/src/color/index.ts | 1 +
.../packages/superset-ui-core/src/color/types.ts | 7 +
.../packages/superset-ui-core/src/color/utils.ts | 33 ++
.../superset-ui-core/test/color/utils.test.ts | 26 +-
.../src/MixedTimeseries/transformProps.ts | 8 +-
.../src/Timeseries/transformProps.ts | 8 +-
.../src/Timeseries/transformers.ts | 28 --
.../src/Waterfall/EchartsWaterfall.tsx | 58 +---
.../src/Waterfall/buildQuery.ts | 16 +-
.../src/Waterfall/controlPanel.tsx | 80 +++--
.../src/Waterfall/images/example1.png | Bin 0 -> 69717 bytes
.../src/Waterfall/images/example2.png | Bin 0 -> 51985 bytes
.../src/Waterfall/images/example3.png | Bin 0 -> 57935 bytes
.../src/Waterfall/images/thumbnail.png | Bin 77020 -> 54196 bytes
.../plugin-chart-echarts/src/Waterfall/index.ts | 19 +-
.../src/Waterfall/transformProps.ts | 349 +++++++++++++--------
.../plugin-chart-echarts/src/Waterfall/types.ts | 36 ++-
.../utils/{getYAxisFormatter.ts => formatters.ts} | 28 ++
.../test/Waterfall/buildQuery.test.ts | 11 +-
.../test/Waterfall/transformProps.test.ts | 112 +++----
.../src/components/Collapse/Collapse.test.tsx | 3 +-
.../components/MetadataBar/MetadataBar.test.tsx | 3 +-
.../controls/VizTypeControl/VizTypeGallery.tsx | 1 +
superset-frontend/src/utils/colorUtils.ts | 50 ---
24 files changed, 486 insertions(+), 391 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/color/index.ts
b/superset-frontend/packages/superset-ui-core/src/color/index.ts
index 3bbdb5d0dc..cb7e569b47 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/index.ts
@@ -32,6 +32,7 @@ export * from './SequentialScheme';
export { default as ColorSchemeRegistry } from './ColorSchemeRegistry';
export * from './colorSchemes';
export * from './utils';
+export * from './types';
export {
default as getSharedLabelColor,
SharedLabelColor,
diff --git a/superset-frontend/packages/superset-ui-core/src/color/types.ts
b/superset-frontend/packages/superset-ui-core/src/color/types.ts
index 2e4b528f9b..bc8b50d7a9 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/types.ts
@@ -24,3 +24,10 @@ export interface ColorsInitLookup {
export interface ColorsLookup {
[key: string]: string;
}
+
+export interface RgbaColor {
+ r: number;
+ g: number;
+ b: number;
+ a: number;
+}
diff --git a/superset-frontend/packages/superset-ui-core/src/color/utils.ts
b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
index 1b362efe3e..55284f16b4 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/utils.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
@@ -86,3 +86,36 @@ export function addAlpha(color: string, opacity: number):
string {
return `${color}${alpha}`;
}
+
+export function hexToRgb(h: string) {
+ let r = '0';
+ let g = '0';
+ let b = '0';
+
+ // 3 digits
+ if (h.length === 4) {
+ r = `0x${h[1]}${h[1]}`;
+ g = `0x${h[2]}${h[2]}`;
+ b = `0x${h[3]}${h[3]}`;
+
+ // 6 digits
+ } else if (h.length === 7) {
+ r = `0x${h[1]}${h[2]}`;
+ g = `0x${h[3]}${h[4]}`;
+ b = `0x${h[5]}${h[6]}`;
+ }
+
+ return `rgb(${+r}, ${+g}, ${+b})`;
+}
+
+export function rgbToHex(red: number, green: number, blue: number) {
+ let r = red.toString(16);
+ let g = green.toString(16);
+ let b = blue.toString(16);
+
+ if (r.length === 1) r = `0${r}`;
+ if (g.length === 1) g = `0${g}`;
+ if (b.length === 1) b = `0${b}`;
+
+ return `#${r}${g}${b}`;
+}
diff --git
a/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
b/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
index 308eec726b..131ea04a78 100644
--- a/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
@@ -17,7 +17,12 @@
* under the License.
*/
-import { getContrastingColor, addAlpha } from '@superset-ui/core';
+import {
+ getContrastingColor,
+ addAlpha,
+ hexToRgb,
+ rgbToHex,
+} from '@superset-ui/core';
describe('color utils', () => {
describe('getContrastingColor', () => {
@@ -82,4 +87,23 @@ describe('color utils', () => {
}).toThrow();
});
});
+ describe('hexToRgb', () => {
+ it('convert 3 digits hex', () => {
+ expect(hexToRgb('#fff')).toBe('rgb(255, 255, 255)');
+ });
+ it('convert 6 digits hex', () => {
+ expect(hexToRgb('#ffffff')).toBe('rgb(255, 255, 255)');
+ });
+ it('convert invalid hex', () => {
+ expect(hexToRgb('#ffffffffffffff')).toBe('rgb(0, 0, 0)');
+ });
+ });
+ describe('rgbToHex', () => {
+ it('convert rgb to hex - white', () => {
+ expect(rgbToHex(255, 255, 255)).toBe('#ffffff');
+ });
+ it('convert rgb to hex - black', () => {
+ expect(rgbToHex(0, 0, 0)).toBe('#000000');
+ });
+ });
});
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index 25b7e5364a..47411e2477 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -78,8 +78,6 @@ import { convertInteger } from '../utils/convertInteger';
import { defaultGrid, defaultYAxis } from '../defaults';
import {
getPadding,
- getTooltipTimeFormatter,
- getXAxisFormatter,
transformEventAnnotation,
transformFormulaAnnotation,
transformIntervalAnnotation,
@@ -88,7 +86,11 @@ import {
} from '../Timeseries/transformers';
import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
-import { getYAxisFormatter } from '../utils/getYAxisFormatter';
+import {
+ getTooltipTimeFormatter,
+ getXAxisFormatter,
+ getYAxisFormatter,
+} from '../utils/formatters';
const getFormatter = (
customFormatters: Record<string, ValueFormatter>,
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index f76c457e1c..d44ae93580 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -80,8 +80,6 @@ import { defaultGrid, defaultYAxis } from '../defaults';
import {
getBaselineSeriesForStream,
getPadding,
- getTooltipTimeFormatter,
- getXAxisFormatter,
transformEventAnnotation,
transformFormulaAnnotation,
transformIntervalAnnotation,
@@ -94,7 +92,11 @@ import {
TIMEGRAIN_TO_TIMESTAMP,
} from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
-import { getYAxisFormatter } from '../utils/getYAxisFormatter';
+import {
+ getTooltipTimeFormatter,
+ getXAxisFormatter,
+ getYAxisFormatter,
+} from '../utils/formatters';
export default function transformProps(
chartProps: EchartsTimeseriesChartProps,
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index 0bcc5baf8d..3b62417f16 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -24,14 +24,10 @@ import {
EventAnnotationLayer,
FilterState,
FormulaAnnotationLayer,
- getTimeFormatter,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
LegendState,
- smartDateDetailedFormatter,
- smartDateFormatter,
SupersetTheme,
- TimeFormatter,
TimeseriesAnnotationLayer,
TimeseriesDataRecord,
ValueFormatter,
@@ -582,27 +578,3 @@ export function getPadding(
: TIMESERIES_CONSTANTS.gridOffsetRight,
});
}
-
-export function getTooltipTimeFormatter(
- format?: string,
-): TimeFormatter | StringConstructor {
- if (format === smartDateFormatter.id) {
- return smartDateDetailedFormatter;
- }
- if (format) {
- return getTimeFormatter(format);
- }
- return String;
-}
-
-export function getXAxisFormatter(
- format?: string,
-): TimeFormatter | StringConstructor | undefined {
- if (format === smartDateFormatter.id || !format) {
- return undefined;
- }
- if (format) {
- return getTimeFormatter(format);
- }
- return String;
-}
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx
index a448c9f93e..b69d2d72b7 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx
@@ -16,59 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useCallback } from 'react';
+import React from 'react';
import Echart from '../components/Echart';
-import { allEventHandlers } from '../utils/eventHandlers';
import { WaterfallChartTransformedProps } from './types';
+import { EventHandlers } from '../types';
export default function EchartsWaterfall(
props: WaterfallChartTransformedProps,
) {
- const {
- height,
- width,
- echartOptions,
- setDataMask,
- labelMap,
- groupby,
- refs,
- selectedValues,
- } = props;
- const handleChange = useCallback(
- (values: string[]) => {
- const groupbyValues = values.map(value => labelMap[value]);
+ const { height, width, echartOptions, refs, onLegendStateChanged } = props;
- setDataMask({
- extraFormData: {
- filters:
- values.length === 0
- ? []
- : groupby.map((col, idx) => {
- const val = groupbyValues.map(v => v[idx]);
- if (val === null || val === undefined)
- return {
- col,
- op: 'IS NULL',
- };
- return {
- col,
- op: 'IN',
- val: val as (string | number | boolean)[],
- };
- }),
- },
- filterState: {
- value: groupbyValues.length ? groupbyValues : null,
- selectedValues: values.length ? values : null,
- },
- });
+ const eventHandlers: EventHandlers = {
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
+ },
+ legendinverseselect: payload => {
+ onLegendStateChanged?.(payload.selected);
},
- [setDataMask, groupby, labelMap],
- );
-
- const eventHandlers = {
- ...allEventHandlers(props),
- handleChange,
};
return (
@@ -78,7 +45,6 @@ export default function EchartsWaterfall(
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
- selectedValues={selectedValues}
/>
);
}
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts
index 353ee8fa20..e47effb3c2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts
@@ -16,14 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { buildQueryContext, QueryFormData } from '@superset-ui/core';
+import {
+ buildQueryContext,
+ ensureIsArray,
+ getXAxisColumn,
+ isXAxisSet,
+ QueryFormData,
+} from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
- const { series, columns } = formData;
+ const columns = [
+ ...(isXAxisSet(formData) ? ensureIsArray(getXAxisColumn(formData)) : []),
+ ...ensureIsArray(formData.groupby),
+ ];
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
- columns: columns?.length ? [series, columns] : [series],
+ columns,
+ orderby: columns?.map(column => [column, true]),
},
]);
}
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx
index 852f2680b3..7a71dd4fcb 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx
@@ -17,24 +17,26 @@
* under the License.
*/
import React from 'react';
-import { ensureIsArray, t } from '@superset-ui/core';
+import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
+ ControlSubSectionHeader,
+ D3_TIME_FORMAT_DOCS,
+ DEFAULT_TIME_FORMAT,
formatSelectOptions,
- getStandardizedControls,
- sections,
+ sharedControls,
} from '@superset-ui/chart-controls';
import { showValueControl } from '../controls';
const config: ControlPanelConfig = {
controlPanelSections: [
- sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
- ['series'],
- ['columns'],
+ ['x_axis'],
+ ['time_grain_sqla'],
+ ['groupby'],
['metric'],
['adhoc_filters'],
['row_limit'],
@@ -44,7 +46,6 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
- ['color_scheme'],
[showValueControl],
[
{
@@ -58,21 +59,41 @@ const config: ControlPanelConfig = {
},
},
],
+ [
+ <ControlSubSectionHeader>
+ {t('Series colors')}
+ </ControlSubSectionHeader>,
+ ],
[
{
- name: 'rich_tooltip',
+ name: 'increase_color',
config: {
- type: 'CheckboxControl',
- label: t('Rich tooltip'),
+ label: t('Increase'),
+ type: 'ColorPickerControl',
+ default: { r: 90, g: 193, b: 137, a: 1 },
+ renderTrigger: true,
+ },
+ },
+ {
+ name: 'decrease_color',
+ config: {
+ label: t('Decrease'),
+ type: 'ColorPickerControl',
+ default: { r: 224, g: 67, b: 85, a: 1 },
+ renderTrigger: true,
+ },
+ },
+ {
+ name: 'total_color',
+ config: {
+ label: t('Total'),
+ type: 'ColorPickerControl',
+ default: { r: 102, g: 102, b: 102, a: 1 },
renderTrigger: true,
- default: true,
- description: t(
- 'Shows a list of all series available at that point in time',
- ),
},
},
],
- [<div className="section-header">{t('X Axis')}</div>],
+ [<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
[
{
name: 'x_axis_label',
@@ -84,6 +105,16 @@ const config: ControlPanelConfig = {
},
},
],
+ [
+ {
+ name: 'x_axis_time_format',
+ config: {
+ ...sharedControls.x_axis_time_format,
+ default: DEFAULT_TIME_FORMAT,
+ description: `${D3_TIME_FORMAT_DOCS}.`,
+ },
+ },
+ ],
[
{
name: 'x_ticks_layout',
@@ -104,7 +135,7 @@ const config: ControlPanelConfig = {
},
},
],
- [<div className="section-header">{t('Y Axis')}</div>],
+ [<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
[
{
name: 'y_axis_label',
@@ -117,26 +148,19 @@ const config: ControlPanelConfig = {
},
],
['y_axis_format'],
+ ['currency_format'],
],
},
],
controlOverrides: {
- columns: {
+ groupby: {
label: t('Breakdowns'),
- description: t('Defines how each series is broken down'),
+ description:
+ t(`Breaks down the series by the category specified in this control.
+ This can help viewers understand how each category affects the overall
value.`),
multi: false,
},
},
- formDataOverrides: formData => {
- const series = getStandardizedControls()
- .popAllColumns()
- .filter(col => !ensureIsArray(formData.columns).includes(col));
- return {
- ...formData,
- series,
- metric: getStandardizedControls().shiftMetric(),
- };
- },
};
export default config;
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example1.png
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example1.png
new file mode 100644
index 0000000000..4785cace4a
Binary files /dev/null and
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example1.png
differ
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example2.png
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example2.png
new file mode 100644
index 0000000000..aee32be991
Binary files /dev/null and
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example2.png
differ
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example3.png
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example3.png
new file mode 100644
index 0000000000..6e3248b03e
Binary files /dev/null and
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/example3.png
differ
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png
index 91ef20f515..95a79df590 100644
Binary files
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png
and
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png
differ
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts
index 5242434f94..c0d7a11067 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts
@@ -22,6 +22,9 @@ import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
+import example1 from './images/example1.png';
+import example2 from './images/example2.png';
+import example3 from './images/example3.png';
import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from './types';
export default class EchartsWaterfallChartPlugin extends ChartPlugin<
@@ -44,14 +47,22 @@ export default class EchartsWaterfallChartPlugin extends
ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsWaterfall'),
metadata: new ChartMetadata({
- behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+ behaviors: [Behavior.INTERACTIVE_CHART],
credits: ['https://echarts.apache.org'],
category: t('Evolution'),
- description: '',
- exampleGallery: [],
+ description: t(
+ `A waterfall chart is a form of data visualization that helps in
understanding
+ the cumulative effect of sequentially introduced positive or
negative values.
+ These intermediate values can either be time based or category
based.`,
+ ),
+ exampleGallery: [
+ { url: example1 },
+ { url: example2 },
+ { url: example3 },
+ ],
name: t('Waterfall Chart'),
+ tags: [t('Categorical'), t('Comparison'), t('ECharts')],
thumbnail,
- tags: [],
}),
transformProps,
});
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts
index 8ea8f68826..7b5faed1b2 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts
@@ -17,51 +17,68 @@
* under the License.
*/
import {
- CategoricalColorNamespace,
+ CurrencyFormatter,
DataRecord,
- getColumnLabel,
+ ensureIsArray,
+ GenericDataType,
getMetricLabel,
getNumberFormatter,
+ getTimeFormatter,
+ isAdhocColumn,
NumberFormatter,
+ rgbToHex,
SupersetTheme,
} from '@superset-ui/core';
import { EChartsOption, BarSeriesOption } from 'echarts';
-import { CallbackDataParams } from 'echarts/types/src/util/types';
import {
- EchartsWaterfallFormData,
EchartsWaterfallChartProps,
ISeriesData,
WaterfallChartTransformedProps,
+ ICallbackDataParams,
} from './types';
import { getDefaultTooltip } from '../utils/tooltip';
import { defaultGrid, defaultYAxis } from '../defaults';
import { ASSIST_MARK, LEGEND, TOKEN, TOTAL_MARK } from './constants';
-import { extractGroupbyLabel, getColtypesMapping } from '../utils/series';
+import { getColtypesMapping } from '../utils/series';
import { Refs } from '../types';
+import { NULL_STRING } from '../constants';
function formatTooltip({
theme,
params,
- numberFormatter,
- richTooltip,
+ breakdownName,
+ defaultFormatter,
+ xAxisFormatter,
}: {
theme: SupersetTheme;
- params: any;
- numberFormatter: NumberFormatter;
- richTooltip: boolean;
+ params: ICallbackDataParams[];
+ breakdownName?: string;
+ defaultFormatter: NumberFormatter | CurrencyFormatter;
+ xAxisFormatter: (value: number | string, index: number) => string;
}) {
- const htmlMaker = (params: any) =>
- `
- <div>${params.name}</div>
+ const series = params.find(
+ param => param.seriesName !== ASSIST_MARK && param.data.value !== TOKEN,
+ );
+
+ // We may have no matching series depending on the legend state
+ if (!series) {
+ return '';
+ }
+
+ const isTotal = series?.seriesName === LEGEND.TOTAL;
+ if (!series) {
+ return NULL_STRING;
+ }
+
+ const createRow = (name: string, value: string) => `
<div>
- ${params.marker}
<span style="
font-size:${theme.typography.sizes.m}px;
color:${theme.colors.grayscale.base};
font-weight:${theme.typography.weights.normal};
margin-left:${theme.gridUnit * 0.5}px;"
>
- ${params.seriesName}:
+ ${name}:
</span>
<span style="
float:right;
@@ -70,42 +87,39 @@ function formatTooltip({
color:${theme.colors.grayscale.base};
font-weight:${theme.typography.weights.bold}"
>
- ${numberFormatter(params.data)}
+ ${value}
</span>
</div>
`;
- if (richTooltip) {
- const [, increaseParams, decreaseParams, totalParams] = params;
- if (increaseParams.data !== TOKEN || increaseParams.data === null) {
- return htmlMaker(increaseParams);
- }
- if (decreaseParams.data !== TOKEN) {
- return htmlMaker(decreaseParams);
- }
- if (totalParams.data !== TOKEN) {
- return htmlMaker(totalParams);
- }
- } else if (params.seriesName !== ASSIST_MARK) {
- return htmlMaker(params);
+ let result = '';
+ if (!isTotal || breakdownName) {
+ result = xAxisFormatter(series.name, series.dataIndex);
}
- return '';
+ if (!isTotal) {
+ result += createRow(
+ series.seriesName!,
+ defaultFormatter(series.data.originalValue),
+ );
+ }
+ result += createRow(TOTAL_MARK, defaultFormatter(series.data.totalSum));
+ return result;
}
function transformer({
data,
- breakdown,
- series,
+ xAxis,
metric,
+ breakdown,
}: {
data: DataRecord[];
- breakdown: string;
- series: string;
+ xAxis: string;
metric: string;
+ breakdown?: string;
}) {
// Group by series (temporary map)
const groupedData = data.reduce((acc, cur) => {
- const categoryLabel = cur[series] as string;
+ const categoryLabel = cur[xAxis] as string;
const categoryData = acc.get(categoryLabel) || [];
categoryData.push(cur);
acc.set(categoryLabel, categoryData);
@@ -114,7 +128,7 @@ function transformer({
const transformedData: DataRecord[] = [];
- if (breakdown?.length) {
+ if (breakdown) {
groupedData.forEach((value, key) => {
const tempValue = value;
// Calc total per period
@@ -124,7 +138,7 @@ function transformer({
);
// Push total per period to the end of period values array
tempValue.push({
- [series]: key,
+ [xAxis]: key,
[breakdown]: TOTAL_MARK,
[metric]: sum,
});
@@ -138,13 +152,13 @@ function transformer({
0,
);
transformedData.push({
- [series]: key,
+ [xAxis]: key,
[metric]: sum,
});
total += sum;
});
transformedData.push({
- [series]: TOTAL_MARK,
+ [xAxis]: TOTAL_MARK,
[metric]: total,
});
}
@@ -159,50 +173,53 @@ export default function transformProps(
width,
height,
formData,
+ legendState,
queriesData,
hooks,
- filterState,
theme,
inContextMenu,
} = chartProps;
const refs: Refs = {};
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
- const { setDataMask = () => {}, onContextMenu } = hooks;
+ const { setDataMask = () => {}, onContextMenu, onLegendStateChanged } =
hooks;
const {
- colorScheme,
+ currencyFormat,
+ groupby,
+ increaseColor,
+ decreaseColor,
+ totalColor,
metric = '',
- columns,
- series,
+ xAxis,
xTicksLayout,
+ xAxisTimeFormat,
showLegend,
yAxisLabel,
xAxisLabel,
yAxisFormat,
- richTooltip,
showValue,
- sliceId,
- } = formData as EchartsWaterfallFormData;
- const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
- const numberFormatter = getNumberFormatter(yAxisFormat);
- const formatter = (params: CallbackDataParams) => {
- const { value, seriesName } = params;
- let formattedValue = numberFormatter(value as number);
- if (seriesName === LEGEND.DECREASE) {
- formattedValue = `-${formattedValue}`;
- }
- return formattedValue;
+ } = formData;
+ const defaultFormatter = currencyFormat?.symbol
+ ? new CurrencyFormatter({ d3Format: yAxisFormat, currency: currencyFormat
})
+ : getNumberFormatter(yAxisFormat);
+
+ const seriesformatter = (params: ICallbackDataParams) => {
+ const { data } = params;
+ const { originalValue } = data;
+ return defaultFormatter(originalValue as number);
};
- const breakdown = columns?.length ? columns : '';
- const groupby = breakdown ? [series, breakdown] : [series];
+ const groupbyArray = ensureIsArray(groupby);
+ const breakdownColumn = groupbyArray.length ? groupbyArray[0] : undefined;
+ const breakdownName = isAdhocColumn(breakdownColumn)
+ ? breakdownColumn.label!
+ : breakdownColumn;
+ const xAxisName = isAdhocColumn(xAxis) ? xAxis.label! : xAxis;
const metricLabel = getMetricLabel(metric);
- const columnLabels = groupby.map(getColumnLabel);
- const columnsLabelMap = new Map<string, string[]>();
const transformedData = transformer({
data,
- breakdown,
- series,
+ breakdown: breakdownName,
+ xAxis: xAxisName,
metric: metricLabel,
});
@@ -211,48 +228,128 @@ export default function transformProps(
const decreaseData: ISeriesData[] = [];
const totalData: ISeriesData[] = [];
+ let previousTotal = 0;
+
transformedData.forEach((datum, index, self) => {
const totalSum = self.slice(0, index + 1).reduce((prev, cur, i) => {
- if (breakdown?.length) {
- if (cur[breakdown] !== TOTAL_MARK || i === 0) {
+ if (breakdownName) {
+ if (cur[breakdownName] !== TOTAL_MARK || i === 0) {
return prev + ((cur[metricLabel] as number) ?? 0);
}
- } else if (cur[series] !== TOTAL_MARK) {
+ } else if (cur[xAxisName] !== TOTAL_MARK) {
return prev + ((cur[metricLabel] as number) ?? 0);
}
return prev;
}, 0);
- const joinedName = extractGroupbyLabel({
- datum,
- groupby: columnLabels,
- coltypeMapping,
- });
- columnsLabelMap.set(
- joinedName,
- columnLabels.map(col => datum[col] as string),
- );
- const value = datum[metricLabel] as number;
- const isNegative = value < 0;
- if (datum[breakdown] === TOTAL_MARK || datum[series] === TOTAL_MARK) {
- increaseData.push(TOKEN);
- decreaseData.push(TOKEN);
- assistData.push(TOKEN);
- totalData.push(totalSum);
- } else if (isNegative) {
- increaseData.push(TOKEN);
- decreaseData.push(Math.abs(value));
- assistData.push(totalSum);
- totalData.push(TOKEN);
+ const isTotal =
+ (breakdownName && datum[breakdownName] === TOTAL_MARK) ||
+ datum[xAxisName] === TOTAL_MARK;
+
+ const originalValue = datum[metricLabel] as number;
+ let value = originalValue;
+ const oppositeSigns = Math.sign(previousTotal) !== Math.sign(totalSum);
+ if (oppositeSigns) {
+ value = Math.sign(value) * (Math.abs(value) - Math.abs(previousTotal));
+ }
+
+ if (isTotal) {
+ increaseData.push({ value: TOKEN });
+ decreaseData.push({ value: TOKEN });
+ totalData.push({
+ value: totalSum,
+ originalValue: totalSum,
+ totalSum,
+ });
+ } else if (value < 0) {
+ increaseData.push({ value: TOKEN });
+ decreaseData.push({
+ value: totalSum < 0 ? value : -value,
+ originalValue,
+ totalSum,
+ });
+ totalData.push({ value: TOKEN });
} else {
- increaseData.push(value);
- decreaseData.push(TOKEN);
- assistData.push(totalSum - value);
- totalData.push(TOKEN);
+ increaseData.push({
+ value: totalSum > 0 ? value : -value,
+ originalValue,
+ totalSum,
+ });
+ decreaseData.push({ value: TOKEN });
+ totalData.push({ value: TOKEN });
}
+
+ const color = oppositeSigns
+ ? value > 0
+ ? rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b)
+ : rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b)
+ : 'transparent';
+
+ let opacity = 1;
+ if (legendState?.[LEGEND.INCREASE] === false && value > 0) {
+ opacity = 0;
+ } else if (legendState?.[LEGEND.DECREASE] === false && value < 0) {
+ opacity = 0;
+ }
+
+ if (isTotal) {
+ assistData.push({ value: TOKEN });
+ } else if (index === 0) {
+ assistData.push({
+ value: 0,
+ });
+ } else if (oppositeSigns || Math.abs(totalSum) > Math.abs(previousTotal)) {
+ assistData.push({
+ value: previousTotal,
+ itemStyle: { color, opacity },
+ });
+ } else {
+ assistData.push({
+ value: totalSum,
+ itemStyle: { color, opacity },
+ });
+ }
+
+ previousTotal = totalSum;
+ });
+
+ const xAxisColumns: string[] = [];
+ const xAxisData = transformedData.map(row => {
+ let column = xAxisName;
+ let value = row[xAxisName];
+ if (breakdownName && row[breakdownName] !== TOTAL_MARK) {
+ column = breakdownName;
+ value = row[breakdownName];
+ }
+ if (!value) {
+ value = NULL_STRING;
+ }
+ if (typeof value !== 'string' && typeof value !== 'number') {
+ value = String(value);
+ }
+ xAxisColumns.push(column);
+ return value;
});
- let axisLabel;
+ const xAxisFormatter = (value: number | string, index: number) => {
+ if (value === TOTAL_MARK) {
+ return TOTAL_MARK;
+ }
+ if (coltypeMapping[xAxisColumns[index]] === GenericDataType.TEMPORAL) {
+ if (typeof value === 'string') {
+ return getTimeFormatter(xAxisTimeFormat)(Number.parseInt(value, 10));
+ }
+ return getTimeFormatter(xAxisTimeFormat)(value);
+ }
+ return String(value);
+ };
+
+ let axisLabel: {
+ rotate?: number;
+ hideOverlap?: boolean;
+ show?: boolean;
+ formatter?: typeof xAxisFormatter;
+ };
if (xTicksLayout === '45°') {
axisLabel = { rotate: -45 };
} else if (xTicksLayout === '90°') {
@@ -264,75 +361,59 @@ export default function transformProps(
} else {
axisLabel = { show: true };
}
+ axisLabel.formatter = xAxisFormatter;
+ axisLabel.hideOverlap = false;
- let xAxisData: string[] = [];
- if (breakdown?.length) {
- xAxisData = transformedData.map(row => {
- if (row[breakdown] === TOTAL_MARK) {
- return row[series] as string;
- }
- return row[breakdown] as string;
- });
- } else {
- xAxisData = transformedData.map(row => row[series] as string);
- }
+ const seriesProps: Pick<BarSeriesOption, 'type' | 'stack' | 'emphasis'> = {
+ type: 'bar',
+ stack: 'stack',
+ emphasis: {
+ disabled: true,
+ },
+ };
const barSeries: BarSeriesOption[] = [
{
+ ...seriesProps,
name: ASSIST_MARK,
- type: 'bar',
- stack: 'stack',
- itemStyle: {
- borderColor: 'transparent',
- color: 'transparent',
- },
- emphasis: {
- itemStyle: {
- borderColor: 'transparent',
- color: 'transparent',
- },
- },
data: assistData,
},
{
+ ...seriesProps,
name: LEGEND.INCREASE,
- type: 'bar',
- stack: 'stack',
label: {
show: showValue,
position: 'top',
- formatter,
+ formatter: seriesformatter,
},
itemStyle: {
- color: colorFn(LEGEND.INCREASE, sliceId),
+ color: rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b),
},
data: increaseData,
},
{
+ ...seriesProps,
name: LEGEND.DECREASE,
- type: 'bar',
- stack: 'stack',
label: {
show: showValue,
position: 'bottom',
- formatter,
+ formatter: seriesformatter,
},
itemStyle: {
- color: colorFn(LEGEND.DECREASE, sliceId),
+ color: rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b),
},
data: decreaseData,
},
{
+ ...seriesProps,
name: LEGEND.TOTAL,
- type: 'bar',
- stack: 'stack',
label: {
show: showValue,
position: 'top',
- formatter,
+ formatter: seriesformatter,
},
itemStyle: {
- color: colorFn(LEGEND.TOTAL, sliceId),
+ color: rgbToHex(totalColor.r, totalColor.g, totalColor.b),
},
data: totalData,
},
@@ -348,11 +429,12 @@ export default function transformProps(
},
legend: {
show: showLegend,
+ selected: legendState,
data: [LEGEND.INCREASE, LEGEND.DECREASE, LEGEND.TOTAL],
},
xAxis: {
- type: 'category',
data: xAxisData,
+ type: 'category',
name: xAxisLabel,
nameTextStyle: {
padding: [theme.gridUnit * 4, 0, 0, 0],
@@ -368,19 +450,20 @@ export default function transformProps(
},
nameLocation: 'middle',
name: yAxisLabel,
- axisLabel: { formatter: numberFormatter },
+ axisLabel: { formatter: defaultFormatter },
},
tooltip: {
...getDefaultTooltip(refs),
appendToBody: true,
- trigger: richTooltip ? 'axis' : 'item',
+ trigger: 'axis',
show: !inContextMenu,
formatter: (params: any) =>
formatTooltip({
theme,
params,
- numberFormatter,
- richTooltip,
+ breakdownName,
+ defaultFormatter,
+ xAxisFormatter,
}),
},
series: barSeries,
@@ -393,9 +476,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
- labelMap: Object.fromEntries(columnsLabelMap),
- groupby,
- selectedValues: filterState.selectedValues || [],
onContextMenu,
+ onLegendStateChanged,
};
}
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts
index 9821cf3146..4386501199 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts
@@ -19,16 +19,14 @@
import {
ChartDataResponseResult,
ChartProps,
+ QueryFormColumn,
QueryFormData,
QueryFormMetric,
+ RgbaColor,
} from '@superset-ui/core';
import { BarDataItemOption } from 'echarts/types/src/chart/bar/BarSeries';
-import { OptionDataValue } from 'echarts/types/src/util/types';
-import {
- BaseTransformedProps,
- CrossFilterTransformedProps,
- LegendFormData,
-} from '../types';
+import { CallbackDataParams } from 'echarts/types/src/util/types';
+import { BaseTransformedProps, LegendFormData } from '../types';
export type WaterfallFormXTicksLayout =
| '45°'
@@ -37,20 +35,28 @@ export type WaterfallFormXTicksLayout =
| 'flat'
| 'staggered';
-export type ISeriesData =
- | BarDataItemOption
- | OptionDataValue
- | OptionDataValue[];
+export type ISeriesData = {
+ originalValue?: number;
+ totalSum?: number;
+} & BarDataItemOption;
+
+export type ICallbackDataParams = CallbackDataParams & {
+ axisValueLabel: string;
+ data: ISeriesData;
+};
export type EchartsWaterfallFormData = QueryFormData &
LegendFormData & {
+ increaseColor: RgbaColor;
+ decreaseColor: RgbaColor;
+ totalColor: RgbaColor;
metric: QueryFormMetric;
- yAxisLabel: string;
+ xAxis: QueryFormColumn;
xAxisLabel: string;
- yAxisFormat: string;
+ xAxisTimeFormat?: string;
xTicksLayout?: WaterfallFormXTicksLayout;
- series: string;
- columns?: string;
+ yAxisLabel: string;
+ yAxisFormat: string;
};
export const DEFAULT_FORM_DATA: Partial<EchartsWaterfallFormData> = {
@@ -63,4 +69,4 @@ export interface EchartsWaterfallChartProps extends
ChartProps {
}
export type WaterfallChartTransformedProps =
- BaseTransformedProps<EchartsWaterfallFormData> & CrossFilterTransformedProps;
+ BaseTransformedProps<EchartsWaterfallFormData>;
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/utils/getYAxisFormatter.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts
similarity index 74%
rename from
superset-frontend/plugins/plugin-chart-echarts/src/utils/getYAxisFormatter.ts
rename to superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts
index 00843c1612..5416fa1577 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/utils/getYAxisFormatter.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/formatters.ts
@@ -21,8 +21,12 @@ import {
CurrencyFormatter,
ensureIsArray,
getNumberFormatter,
+ getTimeFormatter,
isSavedMetric,
QueryFormMetric,
+ smartDateDetailedFormatter,
+ smartDateFormatter,
+ TimeFormatter,
ValueFormatter,
} from '@superset-ui/core';
@@ -51,3 +55,27 @@ export const getYAxisFormatter = (
}
return defaultFormatter ?? getNumberFormatter();
};
+
+export function getTooltipTimeFormatter(
+ format?: string,
+): TimeFormatter | StringConstructor {
+ if (format === smartDateFormatter.id) {
+ return smartDateDetailedFormatter;
+ }
+ if (format) {
+ return getTimeFormatter(format);
+ }
+ return String;
+}
+
+export function getXAxisFormatter(
+ format?: string,
+): TimeFormatter | StringConstructor | undefined {
+ if (format === smartDateFormatter.id || !format) {
+ return undefined;
+ }
+ if (format) {
+ return getTimeFormatter(format);
+ }
+ return String;
+}
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts
index 9c5d28376b..0eb72be3ef 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts
@@ -24,15 +24,18 @@ describe('Waterfall buildQuery', () => {
datasource: '5__table',
granularity_sqla: 'ds',
metric: 'foo',
- series: 'bar',
- columns: 'baz',
- viz_type: 'my_chart',
+ x_axis: 'bar',
+ groupby: ['baz'],
+ viz_type: 'waterfall',
};
it('should build query fields from form data', () => {
const queryContext = buildQuery(formData as unknown as SqlaFormData);
const [query] = queryContext.queries;
expect(query.metrics).toEqual(['foo']);
- expect(query.columns).toEqual(['bar', 'baz']);
+ expect(query.columns?.[0]).toEqual(
+ expect.objectContaining({ sqlExpression: 'bar' }),
+ );
+ expect(query.columns?.[1]).toEqual('baz');
});
});
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts
index c221b93033..a4abec6d49 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts
@@ -17,27 +17,41 @@
* under the License.
*/
import { ChartProps, supersetTheme } from '@superset-ui/core';
-import { EchartsWaterfallChartProps } from '../../src/Waterfall/types';
+import {
+ EchartsWaterfallChartProps,
+ WaterfallChartTransformedProps,
+} from '../../src/Waterfall/types';
import transformProps from '../../src/Waterfall/transformProps';
+const extractSeries = (props: WaterfallChartTransformedProps) => {
+ const { echartOptions } = props;
+ const { series } = echartOptions as unknown as {
+ series: [{ data: [{ value: number }] }];
+ };
+ return series.map(item => item.data).map(item => item.map(i => i.value));
+};
+
describe('Waterfall tranformProps', () => {
const data = [
- { foo: 'Sylvester', bar: '2019', sum: 10 },
- { foo: 'Arnold', bar: '2019', sum: 3 },
- { foo: 'Sylvester', bar: '2020', sum: -10 },
- { foo: 'Arnold', bar: '2020', sum: 5 },
+ { year: '2019', name: 'Sylvester', sum: 10 },
+ { year: '2019', name: 'Arnold', sum: 3 },
+ { year: '2020', name: 'Sylvester', sum: -10 },
+ { year: '2020', name: 'Arnold', sum: 5 },
];
+ const formData = {
+ colorScheme: 'bnbColors',
+ datasource: '3__table',
+ x_axis: 'year',
+ metric: 'sum',
+ increaseColor: { r: 0, b: 0, g: 0 },
+ decreaseColor: { r: 0, b: 0, g: 0 },
+ totalColor: { r: 0, b: 0, g: 0 },
+ };
+
it('should tranform chart props for viz when breakdown not exist', () => {
- const formData1 = {
- colorScheme: 'bnbColors',
- datasource: '3__table',
- granularity_sqla: 'ds',
- metric: 'sum',
- series: 'bar',
- };
const chartProps = new ChartProps({
- formData: formData1,
+ formData: { ...formData, series: 'bar' },
width: 800,
height: 600,
queriesData: [
@@ -47,43 +61,20 @@ describe('Waterfall tranformProps', () => {
],
theme: supersetTheme,
});
- expect(
- transformProps(chartProps as unknown as EchartsWaterfallChartProps),
- ).toEqual(
- expect.objectContaining({
- width: 800,
- height: 600,
- echartOptions: expect.objectContaining({
- series: [
- expect.objectContaining({
- data: [0, 8, '-'],
- }),
- expect.objectContaining({
- data: [13, '-', '-'],
- }),
- expect.objectContaining({
- data: ['-', 5, '-'],
- }),
- expect.objectContaining({
- data: ['-', '-', 8],
- }),
- ],
- }),
- }),
+ const transformedProps = transformProps(
+ chartProps as unknown as EchartsWaterfallChartProps,
);
+ expect(extractSeries(transformedProps)).toEqual([
+ [0, 8, '-'],
+ [13, '-', '-'],
+ ['-', 5, '-'],
+ ['-', '-', 8],
+ ]);
});
it('should tranform chart props for viz when breakdown exist', () => {
- const formData1 = {
- colorScheme: 'bnbColors',
- datasource: '3__table',
- granularity_sqla: 'ds',
- metric: 'sum',
- series: 'bar',
- columns: 'foo',
- };
const chartProps = new ChartProps({
- formData: formData1,
+ formData: { ...formData, groupby: 'name' },
width: 800,
height: 600,
queriesData: [
@@ -93,29 +84,14 @@ describe('Waterfall tranformProps', () => {
],
theme: supersetTheme,
});
- expect(
- transformProps(chartProps as unknown as EchartsWaterfallChartProps),
- ).toEqual(
- expect.objectContaining({
- width: 800,
- height: 600,
- echartOptions: expect.objectContaining({
- series: [
- expect.objectContaining({
- data: [0, 10, '-', 3, 3, '-'],
- }),
- expect.objectContaining({
- data: [10, 3, '-', '-', 5, '-'],
- }),
- expect.objectContaining({
- data: ['-', '-', '-', 10, '-', '-'],
- }),
- expect.objectContaining({
- data: ['-', '-', 13, '-', '-', 8],
- }),
- ],
- }),
- }),
+ const transformedProps = transformProps(
+ chartProps as unknown as EchartsWaterfallChartProps,
);
+ expect(extractSeries(transformedProps)).toEqual([
+ [0, 10, '-', 3, 3, '-'],
+ [10, 3, '-', '-', 5, '-'],
+ ['-', '-', '-', 10, '-', '-'],
+ ['-', '-', 13, '-', '-', 8],
+ ]);
});
});
diff --git a/superset-frontend/src/components/Collapse/Collapse.test.tsx
b/superset-frontend/src/components/Collapse/Collapse.test.tsx
index 99cc623027..75e004604a 100644
--- a/superset-frontend/src/components/Collapse/Collapse.test.tsx
+++ b/superset-frontend/src/components/Collapse/Collapse.test.tsx
@@ -19,8 +19,7 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
-import { supersetTheme } from '@superset-ui/core';
-import { hexToRgb } from 'src/utils/colorUtils';
+import { supersetTheme, hexToRgb } from '@superset-ui/core';
import Collapse, { CollapseProps } from '.';
function renderCollapse(props?: CollapseProps) {
diff --git a/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
b/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
index 549b917ec1..7a7f7430f2 100644
--- a/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
+++ b/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
@@ -20,8 +20,7 @@ import React from 'react';
import { render, screen, within } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import * as resizeDetector from 'react-resize-detector';
-import { supersetTheme } from '@superset-ui/core';
-import { hexToRgb } from 'src/utils/colorUtils';
+import { supersetTheme, hexToRgb } from '@superset-ui/core';
import MetadataBar, {
MIN_NUMBER_ITEMS,
MAX_NUMBER_ITEMS,
diff --git
a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
index c194d2fae1..2563dba01c 100644
---
a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
+++
b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
@@ -854,6 +854,7 @@ export default function VizTypeGallery(props:
VizTypeGalleryProps) {
<Examples>
{(selectedVizMetadata?.exampleGallery || []).map(example => (
<img
+ key={example.url}
src={example.url}
alt={example.caption}
title={example.caption}
diff --git a/superset-frontend/src/utils/colorUtils.ts
b/superset-frontend/src/utils/colorUtils.ts
deleted file mode 100644
index f30828df97..0000000000
--- a/superset-frontend/src/utils/colorUtils.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * 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 function hexToRgb(h: string) {
- let r = '0';
- let g = '0';
- let b = '0';
-
- // 3 digits
- if (h.length === 4) {
- r = `0x${h[1]}${h[1]}`;
- g = `0x${h[2]}${h[2]}`;
- b = `0x${h[3]}${h[3]}`;
-
- // 6 digits
- } else if (h.length === 7) {
- r = `0x${h[1]}${h[2]}`;
- g = `0x${h[3]}${h[4]}`;
- b = `0x${h[5]}${h[6]}`;
- }
-
- return `rgb(${+r}, ${+g}, ${+b})`;
-}
-
-export function rgbToHex(red: number, green: number, blue: number) {
- let r = red.toString(16);
- let g = green.toString(16);
- let b = blue.toString(16);
-
- if (r.length === 1) r = `0${r}`;
- if (g.length === 1) g = `0${g}`;
- if (b.length === 1) b = `0${b}`;
-
- return `#${r}${g}${b}`;
-}