chore(explore): Migrate BigNumber to v1 api [ID-28][ID-55] (#17587)

* chore(explore): Migrate BigNumber to v1 api

* Move to echarts

* Use Echarts trendline

* Fix imports

* Fix parsing dates as strings

* Add from_dttm and to_dttm to v1 chart response

* Fix post processing

* Fix timeRangeFixed

* Fix tests

* Remove from and to dttm from cache

* Cleanup date formatting

* Fix storybook

* Fix missing types

* Fix timestamp with timezone

* Add types to demo's tsconfig

* bug fix

* fix import

* Fix cypress tests

* add sort

* add resample to handle missing values properly

* Sync ChartDataResponseResult schema with ts interface

* Lint fix

* Add migration

* Fix migration

* Remove pass

* Re-raise the exception in migration

* Typo fix

* Update revision

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
This commit is contained in:
Kamil Gabryjelski
2021-12-15 10:15:14 +01:00
committed by GitHub
parent 142b5bc506
commit 124af4c566
45 changed files with 765 additions and 583 deletions

View File

@@ -0,0 +1,23 @@
/**
* 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, QueryFormData } from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => [baseQueryObject]);
}

View File

@@ -0,0 +1,99 @@
/**
* 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 { smartDateFormatter, t } from '@superset-ui/core';
import {
ControlPanelConfig,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
sections,
} from '@superset-ui/chart-controls';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
export default {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [['metric'], ['adhoc_filters']],
},
{
label: t('Options'),
expanded: true,
tabOverride: 'data',
controlSetRows: [
[
{
name: 'subheader',
config: {
type: 'TextControl',
label: t('Subheader'),
renderTrigger: true,
description: t(
'Description text that shows up below your Big Number',
),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[headerFontSize],
[subheaderFontSize],
['y_axis_format'],
[
{
name: 'time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
default: smartDateFormatter.id,
},
},
],
[
{
name: 'force_timestamp_formatting',
config: {
type: 'CheckboxControl',
label: t('Force date format'),
renderTrigger: true,
default: false,
description: t(
'Use date formatting even when metric value is not a timestamp',
),
},
},
],
],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number format'),
},
},
} as ControlPanelConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,64 @@
/**
* 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, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example1 from './images/BigNumber.jpg';
import example2 from './images/BigNumber2.jpg';
import thumbnail from './images/thumbnail.png';
import { BigNumberTotalChartProps, BigNumberTotalFormData } from '../types';
const metadata = new ChartMetadata({
category: t('KPI'),
description: t(
'Showcases a single metric front-and-center. Big number is best used to call attention to a KPI or the one thing you want your audience to focus on.',
),
exampleGallery: [
{ url: example1, caption: t('A Big Number') },
{ url: example2, caption: t('With a subheader') },
],
name: t('Big Number'),
tags: [
t('Additive'),
t('Business'),
t('Formattable'),
t('Legacy'),
t('Percentages'),
t('Popular'),
t('Report'),
t('Description'),
],
thumbnail,
});
export default class BigNumberTotalChartPlugin extends ChartPlugin<
BigNumberTotalFormData,
BigNumberTotalChartProps
> {
constructor() {
super({
loadChart: () => import('../BigNumberViz'),
metadata,
buildQuery,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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 {
getNumberFormatter,
GenericDataType,
getMetricLabel,
extractTimegrain,
QueryFormData,
} from '@superset-ui/core';
import { BigNumberTotalChartProps } from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
export default function transformProps(chartProps: BigNumberTotalChartProps) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const {
headerFontSize,
metric = 'value',
subheader = '',
subheaderFontSize,
forceTimestampFormatting,
timeFormat,
yAxisFormat,
} = formData;
const { data = [], coltypes = [] } = queriesData[0];
const granularity = extractTimegrain(rawFormData as QueryFormData);
const metricName = getMetricLabel(metric);
const formattedSubheader = subheader;
const bigNumber =
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
let metricEntry;
if (chartProps.datasource && chartProps.datasource.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricItem => metricItem.metric_name === metric,
);
}
const formatTime = getDateFormatter(
timeFormat,
granularity,
metricEntry?.d3format,
);
const headerFormatter =
coltypes[0] === GenericDataType.TEMPORAL ||
coltypes[0] === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
return {
width,
height,
bigNumber,
headerFormatter,
headerFontSize,
subheaderFontSize,
subheader: formattedSubheader,
};
}

View File

@@ -0,0 +1,338 @@
/**
* 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 {
t,
getNumberFormatter,
NumberFormatter,
smartDateVerboseFormatter,
TimeFormatter,
computeMaxFontSize,
BRAND_COLOR,
styled,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import Echart from '../components/Echart';
import { TimeSeriesDatum } from './types';
const defaultNumberFormatter = getNumberFormatter();
const PROPORTION = {
// text size: proportion of the chart container sans trendline
KICKER: 0.1,
HEADER: 0.3,
SUBHEADER: 0.125,
// trendline size: proportion of the whole chart container
TRENDLINE: 0.3,
};
type BigNumberVisProps = {
className?: string;
width: number;
height: number;
bigNumber?: number | null;
bigNumberFallback?: TimeSeriesDatum;
headerFormatter: NumberFormatter | TimeFormatter;
formatTime: TimeFormatter;
headerFontSize: number;
kickerFontSize: number;
subheader: string;
subheaderFontSize: number;
showTimestamp?: boolean;
showTrendLine?: boolean;
startYAxisAtZero?: boolean;
timeRangeFixed?: boolean;
timestamp?: number;
trendLineData?: TimeSeriesDatum[];
mainColor: string;
echartOptions: EChartsCoreOption;
};
class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
static defaultProps = {
className: '',
headerFormatter: defaultNumberFormatter,
formatTime: smartDateVerboseFormatter,
headerFontSize: PROPORTION.HEADER,
kickerFontSize: PROPORTION.KICKER,
mainColor: BRAND_COLOR,
showTimestamp: false,
showTrendLine: false,
startYAxisAtZero: true,
subheader: '',
subheaderFontSize: PROPORTION.SUBHEADER,
timeRangeFixed: false,
};
getClassName() {
const { className, showTrendLine, bigNumberFallback } = this.props;
const names = `superset-legacy-chart-big-number ${className} ${
bigNumberFallback ? 'is-fallback-value' : ''
}`;
if (showTrendLine) return names;
return `${names} no-trendline`;
}
createTemporaryContainer() {
const container = document.createElement('div');
container.className = this.getClassName();
container.style.position = 'absolute'; // so it won't disrupt page layout
container.style.opacity = '0'; // and not visible
return container;
}
renderFallbackWarning() {
const { bigNumberFallback, formatTime, showTimestamp } = this.props;
if (!bigNumberFallback || showTimestamp) return null;
return (
<span
className="alert alert-warning"
role="alert"
title={t(
`Last available value seen on %s`,
formatTime(bigNumberFallback[0]),
)}
>
{t('Not up to date')}
</span>
);
}
renderKicker(maxHeight: number) {
const { timestamp, showTimestamp, formatTime, width } = this.props;
if (!showTimestamp) return null;
const text = timestamp === null ? '' : formatTime(timestamp);
const container = this.createTemporaryContainer();
document.body.append(container);
const fontSize = computeMaxFontSize({
text,
maxWidth: width,
maxHeight,
className: 'kicker',
container,
});
container.remove();
return (
<div
className="kicker"
style={{
fontSize,
height: maxHeight,
}}
>
{text}
</div>
);
}
renderHeader(maxHeight: number) {
const { bigNumber, headerFormatter, width } = this.props;
const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber);
const container = this.createTemporaryContainer();
document.body.append(container);
const fontSize = computeMaxFontSize({
text,
maxWidth: width,
maxHeight,
className: 'header-line',
container,
});
container.remove();
return (
<div
className="header-line"
style={{
fontSize,
height: maxHeight,
}}
>
{text}
</div>
);
}
renderSubheader(maxHeight: number) {
const { bigNumber, subheader, width, 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 = subheader;
if (bigNumber === null) {
text = bigNumberFallback ? NO_DATA : NO_DATA_OR_HASNT_LANDED;
}
if (text) {
const container = this.createTemporaryContainer();
document.body.append(container);
fontSize = computeMaxFontSize({
text,
maxWidth: width,
maxHeight,
className: 'subheader-line',
container,
});
container.remove();
return (
<div
className="subheader-line"
style={{
fontSize,
height: maxHeight,
}}
>
{text}
</div>
);
}
return null;
}
renderTrendline(maxHeight: number) {
const { width, trendLineData, echartOptions } = this.props;
// if can't find any non-null values, no point rendering the trendline
if (!trendLineData?.some(d => d[1] !== null)) {
return null;
}
return (
<Echart
width={Math.floor(width)}
height={maxHeight}
echartOptions={echartOptions}
/>
);
}
render() {
const {
showTrendLine,
height,
kickerFontSize,
headerFontSize,
subheaderFontSize,
} = this.props;
const className = this.getClassName();
if (showTrendLine) {
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
const allTextHeight = height - chartHeight;
return (
<div className={className}>
<div className="text-container" style={{ height: allTextHeight }}>
{this.renderFallbackWarning()}
{this.renderKicker(
Math.ceil(kickerFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{this.renderHeader(
Math.ceil(headerFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{this.renderSubheader(
Math.ceil(
subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height,
),
)}
</div>
{this.renderTrendline(chartHeight)}
</div>
);
}
return (
<div className={className} style={{ height }}>
{this.renderFallbackWarning()}
{this.renderKicker(kickerFontSize * height)}
{this.renderHeader(Math.ceil(headerFontSize * height))}
{this.renderSubheader(Math.ceil(subheaderFontSize * height))}
</div>
);
}
}
export default styled(BigNumberVis)`
font-family: ${({ theme }) => theme.typography.families.sansSerif};
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
&.no-trendline .subheader-line {
padding-bottom: 0.3em;
}
.text-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
.alert {
font-size: ${({ theme }) => theme.typography.sizes.s};
margin: -0.5em 0 0.4em;
line-height: 1;
padding: 2px 4px 3px;
border-radius: 3px;
}
}
.kicker {
font-weight: ${({ theme }) => theme.typography.weights.light};
line-height: 1em;
padding-bottom: 2em;
}
.header-line {
font-weight: ${({ theme }) => theme.typography.weights.normal};
position: relative;
line-height: 1em;
span {
position: absolute;
bottom: 0;
}
}
.subheader-line {
font-weight: ${({ theme }) => theme.typography.weights.light};
line-height: 1em;
padding-bottom: 0;
}
&.is-fallback-value {
.kicker,
.header-line,
.subheader-line {
opacity: 0.5;
}
}
.superset-data-ui-tooltip {
z-index: 1000;
background: #000;
}
`;

View File

@@ -0,0 +1,92 @@
/**
* 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,
DTTM_ALIAS,
PostProcessingResample,
QueryFormData,
} from '@superset-ui/core';
import {
rollingWindowOperator,
TIME_COLUMN,
} from '@superset-ui/chart-controls';
const TIME_GRAIN_MAP: Record<string, string> = {
PT1S: 'S',
PT1M: 'min',
PT5M: '5min',
PT10M: '10min',
PT15M: '15min',
PT30M: '30min',
PT1H: 'H',
P1D: 'D',
P1M: 'M',
P3M: 'Q',
P1Y: 'A',
// TODO: these need to be mapped carefully, as the first day of week
// can vary from engine to engine
// P1W: 'W',
// '1969-12-28T00:00:00Z/P1W': 'W',
// '1969-12-29T00:00:00Z/P1W': 'W',
// 'P1W/1970-01-03T00:00:00Z': 'W',
// 'P1W/1970-01-04T00:00:00Z': 'W',
};
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => {
const rollingProc = rollingWindowOperator(formData, baseQueryObject);
if (rollingProc) {
rollingProc.options = { ...rollingProc.options, is_pivot_df: false };
}
const { time_grain_sqla } = formData;
let resampleProc: PostProcessingResample | undefined;
if (rollingProc && time_grain_sqla) {
const rule = TIME_GRAIN_MAP[time_grain_sqla];
if (rule) {
resampleProc = {
operation: 'resample',
options: {
method: 'asfreq',
rule,
fill_value: null,
time_column: TIME_COLUMN,
},
};
}
}
return [
{
...baseQueryObject,
is_timeseries: true,
post_processing: [
{
operation: 'sort',
options: {
columns: {
[DTTM_ALIAS]: true,
},
},
},
resampleProc,
rollingProc,
],
},
];
});
}

View File

@@ -0,0 +1,230 @@
/**
* 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 { smartDateFormatter, t } from '@superset-ui/core';
import {
ControlPanelConfig,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
formatSelectOptions,
sections,
} from '@superset-ui/chart-controls';
import React from 'react';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [['metric'], ['adhoc_filters']],
},
{
label: t('Options'),
tabOverride: 'data',
expanded: true,
controlSetRows: [
[
{
name: 'compare_lag',
config: {
type: 'TextControl',
label: t('Comparison Period Lag'),
isInt: true,
description: t(
'Based on granularity, number of time periods to compare against',
),
},
},
],
[
{
name: 'compare_suffix',
config: {
type: 'TextControl',
label: t('Comparison suffix'),
description: t('Suffix to apply after the percentage display'),
},
},
],
[
{
name: 'show_timestamp',
config: {
type: 'CheckboxControl',
label: t('Show Timestamp'),
renderTrigger: true,
default: false,
description: t('Whether to display the timestamp'),
},
},
],
[
{
name: 'show_trend_line',
config: {
type: 'CheckboxControl',
label: t('Show Trend Line'),
renderTrigger: true,
default: true,
description: t('Whether to display the trend line'),
},
},
],
[
{
name: 'start_y_axis_at_zero',
config: {
type: 'CheckboxControl',
label: t('Start y-axis at 0'),
renderTrigger: true,
default: true,
description: t(
'Start y-axis at zero. Uncheck to start y-axis at minimum value in the data.',
),
},
},
],
[
{
name: 'time_range_fixed',
config: {
type: 'CheckboxControl',
label: t('Fix to selected Time Range'),
description: t(
'Fix the trend line to the full time range specified in case filtered results do not include the start or end dates',
),
renderTrigger: true,
visibility(props) {
const { time_range: timeRange } = props.form_data;
// only display this option when a time range is selected
return !!timeRange && timeRange !== 'No filter';
},
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_picker', null],
[headerFontSize],
[subheaderFontSize],
['y_axis_format'],
[
{
name: 'time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
default: smartDateFormatter.id,
},
},
],
[
{
name: 'force_timestamp_formatting',
config: {
type: 'CheckboxControl',
label: t('Force date format'),
renderTrigger: true,
default: false,
description: t(
'Use date formatting even when metric value is not a timestamp',
),
},
},
],
],
},
{
label: t('Advanced Analytics'),
expanded: false,
controlSetRows: [
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Rolling Window')}</h1>],
[
{
name: 'rolling_type',
config: {
type: 'SelectControl',
label: t('Rolling Function'),
default: 'None',
choices: formatSelectOptions([
'None',
'mean',
'sum',
'std',
'cumsum',
]),
description: t(
'Defines a rolling window function to apply, works along ' +
'with the [Periods] text box',
),
},
},
],
[
{
name: 'rolling_periods',
config: {
type: 'TextControl',
label: t('Periods'),
isInt: true,
description: t(
'Defines the size of the rolling window function, ' +
'relative to the time granularity selected',
),
},
},
],
[
{
name: 'min_periods',
config: {
type: 'TextControl',
label: t('Min Periods'),
isInt: true,
description: t(
'The minimum number of rolling periods required to show ' +
'a value. For instance if you do a cumulative sum on 7 days ' +
'you may want your "Min Period" to be 7, so that all data points ' +
'shown are the total of 7 periods. This will hide the "ramp up" ' +
'taking place over the first 7 periods',
),
},
},
],
],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number format'),
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,63 @@
/**
* 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, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example from './images/Big_Number_Trendline.jpg';
import thumbnail from './images/thumbnail.png';
import {
BigNumberWithTrendlineChartProps,
BigNumberWithTrendlineFormData,
} from '../types';
const metadata = new ChartMetadata({
category: t('KPI'),
description: t(
'Showcases a single number accompanied by a simple line chart, to call attention to an important metric along with its change over time or other dimension.',
),
exampleGallery: [{ url: example }],
name: t('Big Number with Trendline'),
tags: [
t('Advanced-Analytics'),
t('Formattable'),
t('Line'),
t('Percentages'),
t('Popular'),
t('Report'),
t('Description'),
t('Trend'),
],
thumbnail,
});
export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
BigNumberWithTrendlineFormData,
BigNumberWithTrendlineChartProps
> {
constructor() {
super({
loadChart: () => import('../BigNumberViz'),
metadata,
buildQuery,
transformProps,
controlPanel,
});
}
}

View File

@@ -0,0 +1,252 @@
/**
* 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 {
extractTimegrain,
getNumberFormatter,
NumberFormats,
QueryFormData,
GenericDataType,
getMetricLabel,
t,
smartDateVerboseFormatter,
NumberFormatter,
TimeFormatter,
} from '@superset-ui/core';
import { EChartsCoreOption, graphic } from 'echarts';
import {
BigNumberDatum,
BigNumberWithTrendlineChartProps,
TimeSeriesDatum,
} from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
const defaultNumberFormatter = getNumberFormatter();
export function renderTooltipFactory(
formatDate: TimeFormatter = smartDateVerboseFormatter,
formatValue: NumberFormatter | TimeFormatter = defaultNumberFormatter,
) {
return function renderTooltip(params: { data: TimeSeriesDatum }[]) {
return `
${formatDate(params[0].data[0])}
<br />
<strong>
${
params[0].data[1] === null ? t('N/A') : formatValue(params[0].data[1])
}
</strong>
`;
};
}
const TIME_COLUMN = '__timestamp';
const formatPercentChange = getNumberFormatter(
NumberFormats.PERCENT_SIGNED_1_POINT,
);
export default function transformProps(
chartProps: BigNumberWithTrendlineChartProps,
) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const {
colorPicker,
compareLag: compareLag_,
compareSuffix = '',
timeFormat,
headerFontSize,
metric = 'value',
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader = '',
subheaderFontSize,
forceTimestampFormatting,
yAxisFormat,
timeRangeFixed,
} = formData;
const granularity = extractTimegrain(rawFormData as QueryFormData);
const {
data = [],
colnames = [],
coltypes = [],
from_dttm: fromDatetime,
to_dttm: toDatetime,
} = queriesData[0];
const metricName = getMetricLabel(metric);
const compareLag = Number(compareLag_) || 0;
let formattedSubheader = subheader;
const { r, g, b } = colorPicker;
const mainColor = `rgb(${r}, ${g}, ${b})`;
let trendLineData;
let percentChange = 0;
let bigNumber = data.length === 0 ? null : data[0][metricName];
let timestamp = data.length === 0 ? null : data[0][TIME_COLUMN];
let bigNumberFallback;
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
const metricColtype =
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
if (data.length > 0) {
const sortedData = (data as BigNumberDatum[])
.map(d => [d[TIME_COLUMN], parseMetricValue(d[metricName])])
// sort in time descending order
.sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0));
bigNumber = sortedData[0][1];
timestamp = sortedData[0][0];
if (bigNumber === null) {
bigNumberFallback = sortedData.find(d => d[1] !== null);
bigNumber = bigNumberFallback ? bigNumberFallback[1] : null;
timestamp = bigNumberFallback ? bigNumberFallback[0] : null;
}
if (compareLag > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex][1];
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null && compareValue !== 0) {
percentChange = (bigNumber - compareValue) / Math.abs(compareValue);
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
}
}
sortedData.reverse();
trendLineData = showTrendLine ? sortedData : undefined;
}
let className = '';
if (percentChange > 0) {
className = 'positive';
} else if (percentChange < 0) {
className = 'negative';
}
let metricEntry;
if (chartProps.datasource && chartProps.datasource.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricEntry => metricEntry.metric_name === metric,
);
}
const formatTime = getDateFormatter(
timeFormat,
granularity,
metricEntry?.d3format,
);
const headerFormatter =
metricColtype === GenericDataType.TEMPORAL ||
metricColtype === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
if (trendLineData && timeRangeFixed && fromDatetime) {
const toDatetimeOrToday = toDatetime ?? Date.now();
if (!trendLineData[0][0] || trendLineData[0][0] > fromDatetime) {
trendLineData.unshift([fromDatetime, null]);
}
if (
!trendLineData[trendLineData.length - 1][0] ||
trendLineData[trendLineData.length - 1][0]! < toDatetimeOrToday
) {
trendLineData.push([toDatetimeOrToday, null]);
}
}
const echartOptions: EChartsCoreOption = trendLineData
? {
series: [
{
data: trendLineData,
type: 'line',
smooth: true,
symbol: 'circle',
showSymbol: false,
color: mainColor,
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: mainColor,
},
{
offset: 1,
color: 'white',
},
]),
},
},
],
xAxis: {
min: trendLineData[0][0],
max: trendLineData[trendLineData.length - 1][0],
show: false,
type: 'value',
},
yAxis: {
scale: !startYAxisAtZero,
show: false,
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
tooltip: {
show: true,
trigger: 'axis',
confine: true,
formatter: renderTooltipFactory(formatTime, headerFormatter),
},
aria: {
enabled: true,
label: {
description: `Big number visualization ${subheader}`,
},
},
}
: {};
return {
width,
height,
bigNumber,
bigNumberFallback,
className,
headerFormatter,
formatTime,
headerFontSize,
subheaderFontSize,
mainColor,
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader: formattedSubheader,
timestamp,
trendLineData,
echartOptions,
};
}

View File

@@ -0,0 +1,46 @@
<!--
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.
-->
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-big-number
## [0.17.62](https://github.com/apache-superset/superset-ui/compare/v0.17.61...v0.17.62) (2021-07-02)
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-big-number
## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02)
### Bug Fixes
* **legacy-preset-chart-big-number:** example images got mixed up ([#1196](https://github.com/apache-superset/superset-ui/issues/1196)) ([60215b5](https://github.com/apache-superset/superset-ui/commit/60215b5a4c5ae416fc27418c276276ed38ab7f19))

View File

@@ -0,0 +1,21 @@
/*
* 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 { default as BigNumberChartPlugin } from './BigNumberWithTrendline';
export { default as BigNumberTotalChartPlugin } from './BigNumberTotal';

View File

@@ -0,0 +1,90 @@
/**
* 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.
*/
// These are control configurations that are shared ONLY within the BigNumberWithTrendline viz plugin repo.
import { t } from '@superset-ui/core';
import { CustomControlItem } from '@superset-ui/chart-controls';
export const headerFontSize: CustomControlItem = {
name: 'header_font_size',
config: {
type: 'SelectControl',
label: t('Big Number Font Size'),
renderTrigger: true,
clearable: false,
default: 0.4,
// Values represent the percentage of space a header should take
options: [
{
label: t('Tiny'),
value: 0.2,
},
{
label: t('Small'),
value: 0.3,
},
{
label: t('Normal'),
value: 0.4,
},
{
label: t('Large'),
value: 0.5,
},
{
label: t('Huge'),
value: 0.6,
},
],
},
};
export const subheaderFontSize: CustomControlItem = {
name: 'subheader_font_size',
config: {
type: 'SelectControl',
label: t('Subheader Font Size'),
renderTrigger: true,
clearable: false,
default: 0.15,
// Values represent the percentage of space a subheader should take
options: [
{
label: t('Tiny'),
value: 0.125,
},
{
label: t('Small'),
value: 0.15,
},
{
label: t('Normal'),
value: 0.2,
},
{
label: t('Large'),
value: 0.3,
},
{
label: t('Huge'),
value: 0.4,
},
],
},
};

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 {
ChartDataResponseResult,
ChartProps,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
export interface BigNumberDatum {
[key: string]: number | null;
}
export type BigNumberTotalFormData = QueryFormData & {
metric?: QueryFormMetric;
yAxisFormat?: string;
forceTimestampFormatting?: boolean;
};
export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & {
colorPicker: {
r: number;
g: number;
b: number;
};
compareLag?: string | number;
};
export type BigNumberTotalChartProps = ChartProps & {
formData: BigNumberTotalFormData;
queriesData: (ChartDataResponseResult & {
data?: BigNumberDatum[];
})[];
};
export type BigNumberWithTrendlineChartProps = BigNumberTotalChartProps & {
formData: BigNumberWithTrendlineFormData;
};
export type TimeSeriesDatum = [number, number | null];

View File

@@ -0,0 +1,46 @@
/**
* 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 moment from 'moment';
import {
getTimeFormatter,
getTimeFormatterForGranularity,
smartDateFormatter,
TimeGranularity,
} from '@superset-ui/core';
export const parseMetricValue = (metricValue: number | string | null) => {
if (typeof metricValue === 'string') {
const dateObject = moment.utc(metricValue, moment.ISO_8601, true);
if (dateObject.isValid()) {
return dateObject.valueOf();
}
return null;
}
return metricValue;
};
export const getDateFormatter = (
timeFormat: string,
granularity?: TimeGranularity,
fallbackFormat?: string | null,
) =>
timeFormat === smartDateFormatter.id
? getTimeFormatterForGranularity(granularity)
: getTimeFormatter(timeFormat ?? fallbackFormat);

View File

@@ -32,6 +32,7 @@ export { default as EchartsRadarChartPlugin } from './Radar';
export { default as EchartsFunnelChartPlugin } from './Funnel';
export { default as EchartsTreeChartPlugin } from './Tree';
export { default as EchartsTreemapChartPlugin } from './Treemap';
export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber';
export { default as BoxPlotTransformProps } from './BoxPlot/transformProps';
export { default as FunnelTransformProps } from './Funnel/transformProps';