You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ar...@apache.org on 2024/02/12 18:10:10 UTC
(superset) branch master updated: feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
This is an automated email from the ASF dual-hosted git repository.
arivero 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 e8e208dd14 feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
e8e208dd14 is described below
commit e8e208dd14b132339b5187b7368e86326a44e3f4
Author: Antonio Rivero <38...@users.noreply.github.com>
AuthorDate: Mon Feb 12 19:10:04 2024 +0100
feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
---
.../src/PopKPI.tsx | 104 +++++++++--
.../src/images/thumbnail.png | Bin 23099 -> 10434 bytes
.../src/plugin/buildQuery.ts | 208 +--------------------
.../src/plugin/controlPanel.ts | 12 ++
.../src/plugin/transformProps.ts | 5 +-
.../src/types.ts | 19 +-
.../src/{plugin/buildQuery.ts => utils.ts} | 111 +++--------
7 files changed, 148 insertions(+), 311 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
index e780e93ca4..85156ae951 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
@@ -16,9 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { createRef } from 'react';
+import React, { createRef, useMemo } from 'react';
import { css, styled, useTheme } from '@superset-ui/core';
-import { PopKPIComparisonValueStyleProps, PopKPIProps } from './types';
+import {
+ PopKPIComparisonSymbolStyleProps,
+ PopKPIComparisonValueStyleProps,
+ PopKPIProps,
+} from './types';
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
${({ theme, subheaderFontSize }) => `
@@ -30,6 +34,17 @@ const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
`}
`;
+const SymbolWrapper = styled.div<PopKPIComparisonSymbolStyleProps>`
+ ${({ theme, backgroundColor, textColor }) => `
+ background-color: ${backgroundColor};
+ color: ${textColor};
+ padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
+ border-radius: ${theme.gridUnit * 2}px;
+ display: inline-block;
+ margin-right: ${theme.gridUnit}px;
+ `}
+`;
+
export default function PopKPI(props: PopKPIProps) {
const {
height,
@@ -37,9 +52,11 @@ export default function PopKPI(props: PopKPIProps) {
bigNumber,
prevNumber,
valueDifference,
- percentDifference,
+ percentDifferenceFormattedString,
headerFontSize,
subheaderFontSize,
+ comparisonColorEnabled,
+ percentDifferenceNumber,
} = props;
const rootElem = createRef<HTMLDivElement>();
@@ -63,9 +80,60 @@ export default function PopKPI(props: PopKPIProps) {
text-align: center;
`;
+ const getArrowIndicatorColor = () => {
+ if (!comparisonColorEnabled) return theme.colors.grayscale.base;
+ return percentDifferenceNumber > 0
+ ? theme.colors.success.base
+ : theme.colors.error.base;
+ };
+
+ const arrowIndicatorStyle = css`
+ color: ${getArrowIndicatorColor()};
+ margin-left: ${theme.gridUnit}px;
+ `;
+
+ const defaultBackgroundColor = theme.colors.grayscale.light4;
+ const defaultTextColor = theme.colors.grayscale.base;
+ const { backgroundColor, textColor } = useMemo(() => {
+ let bgColor = defaultBackgroundColor;
+ let txtColor = defaultTextColor;
+ if (percentDifferenceNumber > 0) {
+ if (comparisonColorEnabled) {
+ bgColor = theme.colors.success.light2;
+ txtColor = theme.colors.success.base;
+ }
+ } else if (percentDifferenceNumber < 0) {
+ if (comparisonColorEnabled) {
+ bgColor = theme.colors.error.light2;
+ txtColor = theme.colors.error.base;
+ }
+ }
+
+ return {
+ backgroundColor: bgColor,
+ textColor: txtColor,
+ };
+ }, [theme, comparisonColorEnabled, percentDifferenceNumber]);
+
+ const SYMBOLS_WITH_VALUES = useMemo(
+ () => [
+ ['#', prevNumber],
+ ['△', valueDifference],
+ ['%', percentDifferenceFormattedString],
+ ],
+ [prevNumber, valueDifference, percentDifferenceFormattedString],
+ );
+
return (
<div ref={rootElem} css={wrapperDivStyles}>
- <div css={bigValueContainerStyles}>{bigNumber}</div>
+ <div css={bigValueContainerStyles}>
+ {bigNumber}
+ {percentDifferenceNumber !== 0 && (
+ <span css={arrowIndicatorStyle}>
+ {percentDifferenceNumber > 0 ? '↑' : '↓'}
+ </span>
+ )}
+ </div>
<div
css={css`
width: 100%;
@@ -77,18 +145,22 @@ export default function PopKPI(props: PopKPIProps) {
display: table-row;
`}
>
- <ComparisonValue subheaderFontSize={subheaderFontSize}>
- {' '}
- #: {prevNumber}
- </ComparisonValue>
- <ComparisonValue subheaderFontSize={subheaderFontSize}>
- {' '}
- Δ: {valueDifference}
- </ComparisonValue>
- <ComparisonValue subheaderFontSize={subheaderFontSize}>
- {' '}
- %: {percentDifference}
- </ComparisonValue>
+ {SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
+ <ComparisonValue
+ key={`comparison-symbol-${symbol_with_value[0]}`}
+ subheaderFontSize={subheaderFontSize}
+ >
+ <SymbolWrapper
+ backgroundColor={
+ index > 0 ? backgroundColor : defaultBackgroundColor
+ }
+ textColor={index > 0 ? textColor : defaultTextColor}
+ >
+ {symbol_with_value[0]}
+ </SymbolWrapper>
+ {symbol_with_value[1]}
+ </ComparisonValue>
+ ))}
</div>
</div>
</div>
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png
index 30c9e07b0c..3be299145b 100644
Binary files a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png and b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png differ
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
index aa0477e48f..38346007b4 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
@@ -21,7 +21,7 @@ import {
buildQueryContext,
QueryFormData,
} from '@superset-ui/core';
-import moment, { Moment } from 'moment';
+import { computeQueryBComparator } from '../utils';
/**
* The buildQuery function is used to create an instance of QueryContext that's
@@ -38,184 +38,6 @@ import moment, { Moment } from 'moment';
* if a viz needs multiple different result sets.
*/
-type MomentTuple = [moment.Moment | null, moment.Moment | null];
-
-function getSinceUntil(
- timeRange: string | null = null,
- relativeStart: string | null = null,
- relativeEnd: string | null = null,
-): MomentTuple {
- const separator = ' : ';
- const effectiveRelativeStart = relativeStart || 'today';
- const effectiveRelativeEnd = relativeEnd || 'today';
-
- if (!timeRange) {
- return [null, null];
- }
-
- let modTimeRange: string | null = timeRange;
-
- if (timeRange === 'NO_TIME_RANGE' || timeRange === '_(NO_TIME_RANGE)') {
- return [null, null];
- }
-
- if (timeRange?.startsWith('last') && !timeRange.includes(separator)) {
- modTimeRange = timeRange + separator + effectiveRelativeEnd;
- }
-
- if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
- modTimeRange = effectiveRelativeStart + separator + timeRange;
- }
-
- if (
- timeRange?.startsWith('previous calendar week') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'week').startOf('week'),
- moment().startOf('week'),
- ];
- }
-
- if (
- timeRange?.startsWith('previous calendar month') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'month').startOf('month'),
- moment().startOf('month'),
- ];
- }
-
- if (
- timeRange?.startsWith('previous calendar year') &&
- !timeRange.includes(separator)
- ) {
- return [
- moment().subtract(1, 'year').startOf('year'),
- moment().startOf('year'),
- ];
- }
-
- const timeRangeLookup: Array<[RegExp, (...args: string[]) => Moment]> = [
- [
- /^last\s+(day|week|month|quarter|year)$/i,
- (unit: string) =>
- moment().subtract(1, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- /^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
- (delta: string, unit: string) =>
- moment().subtract(delta, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- /^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
- (delta: string, unit: string) =>
- moment().add(delta, unit as moment.unitOfTime.DurationConstructor),
- ],
- [
- // eslint-disable-next-line no-useless-escape
- /DATEADD\(DATETIME\("([^"]+)"\),\s*(-?\d+),\s*([^\)]+)\)/i,
- (timePart: string, delta: string, unit: string) => {
- if (timePart === 'now') {
- return moment().add(
- delta,
- unit as moment.unitOfTime.DurationConstructor,
- );
- }
- if (moment(timePart.toUpperCase(), true).isValid()) {
- return moment(timePart).add(
- delta,
- unit as moment.unitOfTime.DurationConstructor,
- );
- }
- return moment();
- },
- ],
- ];
-
- const sinceAndUntilPartition = modTimeRange
- .split(separator, 2)
- .map(part => part.trim());
-
- const sinceAndUntil: (Moment | null)[] = sinceAndUntilPartition.map(part => {
- if (!part) {
- return null;
- }
-
- let transformedValue: Moment | null = null;
- // Matching time_range_lookup
- const matched = timeRangeLookup.some(([pattern, fn]) => {
- const result = part.match(pattern);
- if (result) {
- transformedValue = fn(...result.slice(1));
- return true;
- }
-
- if (part === 'today') {
- transformedValue = moment().startOf('day');
- return true;
- }
-
- if (part === 'now') {
- transformedValue = moment();
- return true;
- }
- return false;
- });
-
- if (matched && transformedValue !== null) {
- // Handle the transformed value
- } else {
- // Handle the case when there was no match
- transformedValue = moment(`${part}`);
- }
-
- return transformedValue;
- });
-
- const [_since, _until] = sinceAndUntil;
-
- if (_since && _until && _since.isAfter(_until)) {
- throw new Error('From date cannot be larger than to date');
- }
-
- return [_since, _until];
-}
-
-function calculatePrev(
- startDate: Moment | null,
- endDate: Moment | null,
- calcType: String,
-) {
- if (!startDate || !endDate) {
- return [null, null];
- }
-
- const daysBetween = endDate.diff(startDate, 'days');
-
- let startDatePrev = moment();
- let endDatePrev = moment();
- if (calcType === 'y') {
- startDatePrev = startDate.subtract(1, 'year');
- endDatePrev = endDate.subtract(1, 'year');
- } else if (calcType === 'w') {
- startDatePrev = startDate.subtract(1, 'week');
- endDatePrev = endDate.subtract(1, 'week');
- } else if (calcType === 'm') {
- startDatePrev = startDate.subtract(1, 'month');
- endDatePrev = endDate.subtract(1, 'month');
- } else if (calcType === 'r') {
- startDatePrev = startDate.clone().subtract(daysBetween.valueOf(), 'day');
- endDatePrev = startDate;
- } else {
- startDatePrev = startDate.subtract(1, 'year');
- endDatePrev = endDate.subtract(1, 'year');
- }
-
- return [startDatePrev, endDatePrev];
-}
-
export default function buildQuery(formData: QueryFormData) {
const {
cols: groupby,
@@ -240,37 +62,19 @@ export default function buildQuery(formData: QueryFormData) {
? formData.adhoc_filters[timeFilterIndex]
: null;
- let testSince = null;
- let testUntil = null;
-
- if (
- timeFilter &&
- 'comparator' in timeFilter &&
- typeof timeFilter.comparator === 'string'
- ) {
- let timeRange = timeFilter.comparator.toLocaleLowerCase();
- if (extraFormData?.time_range) {
- timeRange = extraFormData.time_range;
- }
- [testSince, testUntil] = getSinceUntil(timeRange);
- }
-
let formDataB: QueryFormData;
+ let queryBComparator = null;
if (timeComparison !== 'c') {
- const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
- testSince,
- testUntil,
+ queryBComparator = computeQueryBComparator(
+ formData.adhoc_filters || [],
timeComparison,
+ extraFormData,
);
- const queryBComparator = `${prevStartDateMoment?.format(
- 'YYYY-MM-DDTHH:mm:ss',
- )} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
-
const queryBFilter: any = {
...timeFilter,
- comparator: queryBComparator.replace(/Z/g, ''),
+ comparator: queryBComparator,
};
const otherFilters = formData.adhoc_filters?.filter(
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
index 89afdb4835..3d2504f639 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
@@ -181,6 +181,18 @@ const config: ControlPanelConfig = {
},
},
],
+ [
+ {
+ name: 'comparison_color_enabled',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Add color for positive/negative change'),
+ renderTrigger: true,
+ default: false,
+ description: t('Add color for positive/negative change'),
+ },
+ },
+ ],
],
},
],
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
index 80737f6032..e5de882f6d 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
@@ -81,6 +81,7 @@ export default function transformProps(chartProps: ChartProps) {
yAxisFormat,
currencyFormat,
subheaderFontSize,
+ comparisonColorEnabled,
} = formData;
const { data: dataA = [] } = queriesData[0];
const { data: dataB = [] } = queriesData[1];
@@ -138,11 +139,13 @@ export default function transformProps(chartProps: ChartProps) {
bigNumber,
prevNumber,
valueDifference,
- percentDifference,
+ percentDifferenceFormattedString: percentDifference,
boldText,
headerFontSize,
subheaderFontSize,
headerText,
compType,
+ comparisonColorEnabled,
+ percentDifferenceNumber: percentDifferenceNum,
};
}
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
index b13f2115ef..a239a29593 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
@@ -29,6 +29,7 @@ export interface PopKPIStylesProps {
headerFontSize: keyof typeof supersetTheme.typography.sizes;
subheaderFontSize: keyof typeof supersetTheme.typography.sizes;
boldText: boolean;
+ comparisonColorEnabled: boolean;
}
interface PopKPICustomizeProps {
@@ -39,6 +40,11 @@ export interface PopKPIComparisonValueStyleProps {
subheaderFontSize?: keyof typeof supersetTheme.typography.sizes;
}
+export interface PopKPIComparisonSymbolStyleProps {
+ backgroundColor: string;
+ textColor: string;
+}
+
export type PopKPIQueryFormData = QueryFormData &
PopKPIStylesProps &
PopKPICustomizeProps;
@@ -47,10 +53,11 @@ export type PopKPIProps = PopKPIStylesProps &
PopKPICustomizeProps & {
data: TimeseriesDataRecord[];
metrics: Metric[];
- metricName: String;
- bigNumber: Number;
- prevNumber: Number;
- valueDifference: Number;
- percentDifference: Number;
- compType: String;
+ metricName: string;
+ bigNumber: string;
+ prevNumber: string;
+ valueDifference: string;
+ percentDifferenceFormattedString: string;
+ compType: string;
+ percentDifferenceNumber: number;
};
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
similarity index 70%
copy from superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
copy to superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
index aa0477e48f..4ce2ff1e4c 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
@@ -16,35 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
- AdhocFilter,
- buildQueryContext,
- QueryFormData,
-} from '@superset-ui/core';
-import moment, { Moment } from 'moment';
-/**
- * The buildQuery function is used to create an instance of QueryContext that's
- * sent to the chart data endpoint. In addition to containing information of which
- * datasource to use, it specifies the type (e.g. full payload, samples, query) and
- * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
- * the datasource as opposed to using a cached copy of the data, if available.
- *
- * More importantly though, QueryContext contains a property `queries`, which is an array of
- * QueryObjects specifying individual data requests to be made. A QueryObject specifies which
- * columns, metrics and filters, among others, to use during the query. Usually it will be enough
- * to specify just one query based on the baseQueryObject, but for some more advanced use cases
- * it is possible to define post processing operations in the QueryObject, or multiple queries
- * if a viz needs multiple different result sets.
- */
+import { AdhocFilter } from '@superset-ui/core';
+import moment, { Moment } from 'moment';
type MomentTuple = [moment.Moment | null, moment.Moment | null];
-function getSinceUntil(
+const getSinceUntil = (
timeRange: string | null = null,
relativeStart: string | null = null,
relativeEnd: string | null = null,
-): MomentTuple {
+): MomentTuple => {
const separator = ' : ';
const effectiveRelativeStart = relativeStart || 'today';
const effectiveRelativeEnd = relativeEnd || 'today';
@@ -181,13 +163,13 @@ function getSinceUntil(
}
return [_since, _until];
-}
+};
-function calculatePrev(
+const calculatePrev = (
startDate: Moment | null,
endDate: Moment | null,
calcType: String,
-) {
+) => {
if (!startDate || !endDate) {
return [null, null];
}
@@ -214,31 +196,21 @@ function calculatePrev(
}
return [startDatePrev, endDatePrev];
-}
-
-export default function buildQuery(formData: QueryFormData) {
- const {
- cols: groupby,
- time_comparison: timeComparison,
- extra_form_data: extraFormData,
- } = formData;
-
- const queryContextA = buildQueryContext(formData, baseQueryObject => [
- {
- ...baseQueryObject,
- groupby,
- },
- ]);
-
- const timeFilterIndex: number =
- formData.adhoc_filters?.findIndex(
+};
+
+export const computeQueryBComparator = (
+ adhocFilters: AdhocFilter[],
+ timeComparison: string,
+ extraFormData: any,
+ join = ':',
+) => {
+ const timeFilterIndex =
+ adhocFilters?.findIndex(
filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
) ?? -1;
- const timeFilter: AdhocFilter | null =
- timeFilterIndex !== -1 && formData.adhoc_filters
- ? formData.adhoc_filters[timeFilterIndex]
- : null;
+ const timeFilter =
+ timeFilterIndex !== -1 ? adhocFilters[timeFilterIndex] : null;
let testSince = null;
let testUntil = null;
@@ -255,8 +227,6 @@ export default function buildQuery(formData: QueryFormData) {
[testSince, testUntil] = getSinceUntil(timeRange);
}
- let formDataB: QueryFormData;
-
if (timeComparison !== 'c') {
const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
testSince,
@@ -264,44 +234,13 @@ export default function buildQuery(formData: QueryFormData) {
timeComparison,
);
- const queryBComparator = `${prevStartDateMoment?.format(
+ return `${prevStartDateMoment?.format(
'YYYY-MM-DDTHH:mm:ss',
- )} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
-
- const queryBFilter: any = {
- ...timeFilter,
- comparator: queryBComparator.replace(/Z/g, ''),
- };
-
- const otherFilters = formData.adhoc_filters?.filter(
- (_value: any, index: number) => timeFilterIndex !== index,
+ )} ${join} ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`.replace(
+ /Z/g,
+ '',
);
- const queryBFilters = otherFilters
- ? [queryBFilter, ...otherFilters]
- : [queryBFilter];
-
- formDataB = {
- ...formData,
- adhoc_filters: queryBFilters,
- extra_form_data: {},
- };
- } else {
- formDataB = {
- ...formData,
- adhoc_filters: formData.adhoc_custom,
- extra_form_data: {},
- };
}
- const queryContextB = buildQueryContext(formDataB, baseQueryObject => [
- {
- ...baseQueryObject,
- groupby,
- },
- ]);
-
- return {
- ...queryContextA,
- queries: [...queryContextA.queries, ...queryContextB.queries],
- };
-}
+ return null;
+};