diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts index f41008eb60d..e34e352683d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts @@ -103,6 +103,38 @@ describe('BigNumberTotal transformProps', () => { expect(result.subtitle).toBe('test'); }); + const baseChartProps = { + width: 400, + height: 300, + queriesData: [{ data: [], coltypes: [] }], + rawFormData: { dummy: 'raw' }, + hooks: { onContextMenu: jest.fn() }, + datasource: { + currencyFormats: { value: '$0,0.00' }, + columnFormats: { value: '$0,0.00' }, + metrics: [{ metric_name: 'value', d3format: '.2f' }], + }, + }; + + it('uses subtitle font size when subtitle is provided', () => { + const result = transformProps({ + ...baseChartProps, + formData: { + subtitle: 'Subtitle wins', + subheader: 'Fallback subheader', + subtitleFontSize: 0.4, + subheaderFontSize: 0.99, + metric: 'value', + headerFontSize: 0.3, + yAxisFormat: 'SMART_NUMBER', + timeFormat: 'smart_date', + }, + } as unknown as BigNumberTotalChartProps); + + expect(result.subtitle).toBe('Subtitle wins'); + expect(result.subtitleFontSize).toBe(0.4); + }); + it('should compute bigNumber using parseMetricValue when data exists', () => { const chartProps = { width: 500, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts index e892ca33f7d..c0f9b2baeb3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts @@ -61,8 +61,10 @@ export default function transformProps( const { data = [], coltypes = [] } = queriesData[0]; const granularity = extractTimegrain(rawFormData as QueryFormData); const metricName = getMetricLabel(metric); - const formattedSubtitle = subtitle || subheader || ''; - const formattedSubtitleFontSize = subheaderFontSize || subtitleFontSize; + const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || ''; + const formattedSubtitleFontSize = subtitle?.trim() + ? (subtitleFontSize ?? 1) + : (subheaderFontSize ?? 1); const bigNumber = data.length === 0 ? null : parseMetricValue(data[0][metricName]); @@ -108,6 +110,7 @@ export default function transformProps( bigNumber, headerFormatter, headerFontSize, + subheaderFontSize, subtitleFontSize: formattedSubtitleFontSize, subtitle: formattedSubtitle, onContextMenu, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index 540efa49087..79876f2d6c1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -188,19 +188,21 @@ class BigNumberVis extends PureComponent { ); } - renderSubtitle(maxHeight: number) { - const { subtitle, width } = this.props; + rendermetricComparisonSummary(maxHeight: number) { + const { subheader, width } = this.props; let fontSize = 0; - if (subtitle) { + const text = subheader; + + if (text) { const container = this.createTemporaryContainer(); document.body.append(container); try { fontSize = computeMaxFontSize({ - text: subtitle, + text, maxWidth: width * 0.9, maxHeight, - className: 'subtitle-line', + className: 'subheader-line', container, }); } finally { @@ -209,19 +211,65 @@ class BigNumberVis extends PureComponent { return (
- {subtitle} + {text}
); } return null; } + renderSubtitle(maxHeight: number) { + const { subtitle, width, bigNumber, bigNumberFallback } = this.props; + let fontSize = 0; + + const NO_DATA_OR_HASNT_LANDED = t( + 'No data after filtering or data is NULL for the latest time record', + ); + const NO_DATA = t( + 'Try applying different filters or ensuring your datasource has data', + ); + + let text = subtitle; + if (bigNumber === null) { + text = + subtitle || (bigNumberFallback ? NO_DATA : NO_DATA_OR_HASNT_LANDED); + } + + if (text) { + const container = this.createTemporaryContainer(); + document.body.append(container); + fontSize = computeMaxFontSize({ + text, + maxWidth: width * 0.9, + maxHeight, + className: 'subtitle-line', + container, + }); + container.remove(); + + return ( + <> +
+ {text} +
+ + ); + } + return null; + } + renderTrendline(maxHeight: number) { const { width, trendLineData, echartOptions, refs } = this.props; @@ -275,6 +323,7 @@ class BigNumberVis extends PureComponent { kickerFontSize, headerFontSize, subtitleFontSize, + subheaderFontSize, } = this.props; const className = this.getClassName(); @@ -294,6 +343,11 @@ class BigNumberVis extends PureComponent { {this.renderHeader( Math.ceil(headerFontSize * (1 - PROPORTION.TRENDLINE) * height), )} + {this.rendermetricComparisonSummary( + Math.ceil( + subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height, + ), + )} {this.renderSubtitle( Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * height), )} @@ -308,6 +362,9 @@ class BigNumberVis extends PureComponent { {this.renderFallbackWarning()} {this.renderKicker((kickerFontSize || 0) * height)} {this.renderHeader(Math.ceil(headerFontSize * height))} + {this.rendermetricComparisonSummary( + Math.ceil(subheaderFontSize * height), + )} {this.renderSubtitle(Math.ceil(subtitleFontSize * height))} ); @@ -359,12 +416,12 @@ export default styled(BigNumberVis)` .subheader-line { line-height: 1em; - padding-bottom: 0.3em; + padding-bottom: 0; } .subtitle-line { line-height: 1em; - padding-top: 0.3em; + padding-bottom: 0; } &.is-fallback-value { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts new file mode 100644 index 00000000000..069b25722dc --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts @@ -0,0 +1,196 @@ +/** + * 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 { GenericDataType } from '@superset-ui/core'; +import transformProps from './transformProps'; +import { BigNumberWithTrendlineChartProps, BigNumberDatum } from '../types'; + +jest.mock('@superset-ui/core', () => ({ + GenericDataType: { Temporal: 2, String: 1 }, + extractTimegrain: jest.fn(() => 'P1D'), + getMetricLabel: jest.fn(metric => metric), + getXAxisLabel: jest.fn(() => '__timestamp'), + getValueFormatter: jest.fn(() => ({ + format: (v: number) => `$${v}`, + })), + getNumberFormatter: jest.fn(() => (v: number) => `${(v * 100).toFixed(1)}%`), + t: jest.fn(v => v), + tooltipHtml: jest.fn(() => '
tooltip
'), + NumberFormats: { + PERCENT_SIGNED_1_POINT: '.1%', + }, +})); + +jest.mock('../utils', () => ({ + getDateFormatter: jest.fn(() => (v: any) => `${v}pm`), + parseMetricValue: jest.fn(val => Number(val)), +})); + +jest.mock('../../utils/tooltip', () => ({ + getDefaultTooltip: jest.fn(() => ({})), +})); + +describe('BigNumberWithTrendline transformProps', () => { + const onContextMenu = jest.fn(); + const baseFormData = { + headerFontSize: 20, + metric: 'value', + subtitle: 'subtitle message', + subtitleFontSize: 14, + forceTimestampFormatting: false, + timeFormat: 'YYYY-MM-DD', + yAxisFormat: 'SMART_NUMBER', + compareLag: 1, + compareSuffix: 'WoW', + colorPicker: { r: 0, g: 0, b: 0 }, + currencyFormat: { symbol: '$', symbolPosition: 'prefix' }, + }; + + const baseDatasource = { + currencyFormats: { value: '$0,0.00' }, + columnFormats: { value: '$0,0.00' }, + metrics: [{ metric_name: 'value', d3format: '.2f' }], + }; + + const baseHooks = { onContextMenu }; + const baseRawFormData = { dummy: 'raw' }; + + it('should return null bigNumber when no data is provided', () => { + const chartProps = { + width: 400, + height: 300, + queriesData: [{ data: [] as unknown as BigNumberDatum[], coltypes: [] }], + formData: baseFormData, + rawFormData: baseRawFormData, + hooks: baseHooks, + datasource: baseDatasource, + theme: { colors: { grayscale: { light5: '#eee' } } }, + }; + + const result = transformProps( + chartProps as unknown as BigNumberWithTrendlineChartProps, + ); + expect(result.bigNumber).toBeNull(); + expect(result.subtitle).toBe('subtitle message'); + }); + + it('should calculate subheader as percent change with suffix', () => { + const chartProps = { + width: 500, + height: 400, + queriesData: [ + { + data: [ + { __timestamp: 2, value: 110 }, + { __timestamp: 1, value: 100 }, + ] as unknown as BigNumberDatum[], + colnames: ['__timestamp', 'value'], + coltypes: ['TEMPORAL', 'NUMERIC'], + }, + ], + formData: baseFormData, + rawFormData: baseRawFormData, + hooks: baseHooks, + datasource: baseDatasource, + theme: { colors: { grayscale: { light5: '#eee' } } }, + }; + + const result = transformProps( + chartProps as unknown as BigNumberWithTrendlineChartProps, + ); + expect(result.subheader).toBe('10.0% WoW'); + }); + + it('should compute bigNumber from parseMetricValue', () => { + const chartProps = { + width: 600, + height: 450, + queriesData: [ + { + data: [ + { __timestamp: 2, value: '456' }, + ] as unknown as BigNumberDatum[], + colnames: ['__timestamp', 'value'], + coltypes: [GenericDataType.Temporal, GenericDataType.String], + }, + ], + formData: baseFormData, + rawFormData: baseRawFormData, + hooks: baseHooks, + datasource: baseDatasource, + theme: { colors: { grayscale: { light5: '#eee' } } }, + }; + + const result = transformProps( + chartProps as unknown as BigNumberWithTrendlineChartProps, + ); + expect(result.bigNumber).toEqual(456); + }); + + it('should use formatTime as headerFormatter for Temporal/String or forced', () => { + const formData = { ...baseFormData, forceTimestampFormatting: true }; + const chartProps = { + width: 600, + height: 450, + queriesData: [ + { + data: [ + { __timestamp: 2, value: '123' }, + ] as unknown as BigNumberDatum[], + colnames: ['__timestamp', 'value'], + coltypes: [0, GenericDataType.String], + }, + ], + formData, + rawFormData: baseRawFormData, + hooks: baseHooks, + datasource: baseDatasource, + theme: { colors: { grayscale: { light5: '#eee' } } }, + }; + + const result = transformProps( + chartProps as unknown as BigNumberWithTrendlineChartProps, + ); + expect(result.headerFormatter(5)).toBe('5pm'); + }); + + it('should use numberFormatter when not Temporal/String and not forced', () => { + const formData = { ...baseFormData, forceTimestampFormatting: false }; + const chartProps = { + width: 600, + height: 450, + queriesData: [ + { + data: [{ __timestamp: 2, value: 500 }] as unknown as BigNumberDatum[], + colnames: ['__timestamp', 'value'], + coltypes: [0, 0], + }, + ], + formData, + rawFormData: baseRawFormData, + hooks: baseHooks, + datasource: baseDatasource, + theme: { colors: { grayscale: { light5: '#eee' } } }, + }; + + const result = transformProps( + chartProps as unknown as BigNumberWithTrendlineChartProps, + ); + expect(result.headerFormatter.format(500)).toBe('$500'); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts index 9de4065b67b..09766ed4bf1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts @@ -129,8 +129,5 @@ export const subtitleControl: CustomControlItem = { label: t('Subtitle'), renderTrigger: true, description: t('Description text that shows up below your Big Number'), - mapStateToProps: state => ({ - value: state.form_data.subheader || state.form_data.subtitle, - }), }, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts index 8b5a5abddb2..44abef0c789 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts @@ -77,7 +77,9 @@ export type BigNumberVizProps = { formatTime?: TimeFormatter; headerFontSize: number; kickerFontSize?: number; + subheader?: string; subtitle: string; + subheaderFontSize: number; subtitleFontSize: number; showTimestamp?: boolean; showTrendLine?: boolean; diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index 96c239b9cf4..822a54ecafe 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -734,6 +734,17 @@ const retainQueryModeRequirements = hiddenFormData => key => !QUERY_MODE_REQUISITES.has(key), ); +function patchBigNumberTotalFormData(form_data, slice) { + if ( + form_data.viz_type === 'big_number_total' && + !form_data.subtitle && + slice?.form_data?.subheader + ) { + return { ...form_data, subtitle: slice.form_data.subheader }; + } + return form_data; +} + function mapStateToProps(state) { const { explore, @@ -768,6 +779,25 @@ function mapStateToProps(state) { dashboardId = undefined; } + if ( + form_data.viz_type === 'big_number_total' && + slice?.form_data?.subheader && + (!controls.subtitle?.value || controls.subtitle.value === '') + ) { + controls.subtitle = { + ...controls.subtitle, + value: slice.form_data.subheader, + }; + if (slice?.form_data?.subheader_font_size) { + controls.subtitle_font_size = { + ...controls.subtitle_font_size, + value: slice.form_data.subheader_font_size, + }; + } + } + + const patchedFormData = patchBigNumberTotalFormData(form_data, slice); + return { isDatasourceMetaLoading: explore.isDatasourceMetaLoading, datasource, @@ -789,7 +819,7 @@ function mapStateToProps(state) { slice, sliceName: explore.sliceName ?? slice?.slice_name ?? null, triggerRender: explore.triggerRender, - form_data, + form_data: patchedFormData, table_name: datasource.table_name, vizType: form_data.viz_type, standalone: !!explore.standalone,