mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
fix(chart): Restore subheader used in bignumber with trendline (#33196)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -188,19 +188,21 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
);
|
||||
}
|
||||
|
||||
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<BigNumberVizProps> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="subtitle-line"
|
||||
className="subheader-line"
|
||||
style={{
|
||||
fontSize,
|
||||
height: maxHeight,
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
className="subtitle-line subheader-line"
|
||||
style={{
|
||||
fontSize: `${fontSize}px`,
|
||||
height: maxHeight,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderTrendline(maxHeight: number) {
|
||||
const { width, trendLineData, echartOptions, refs } = this.props;
|
||||
|
||||
@@ -275,6 +323,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
kickerFontSize,
|
||||
headerFontSize,
|
||||
subtitleFontSize,
|
||||
subheaderFontSize,
|
||||
} = this.props;
|
||||
const className = this.getClassName();
|
||||
|
||||
@@ -294,6 +343,11 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
{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<BigNumberVizProps> {
|
||||
{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))}
|
||||
</div>
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => '<div>tooltip</div>'),
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user