Skip to content

Commit e8e208d

Browse files
feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
1 parent 8bee6ed commit e8e208d

File tree

7 files changed

+369
-225
lines changed

7 files changed

+369
-225
lines changed

superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx

+88-16
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import React, { createRef } from 'react';
19+
import React, { createRef, useMemo } from 'react';
2020
import { css, styled, useTheme } from '@superset-ui/core';
21-
import { PopKPIComparisonValueStyleProps, PopKPIProps } from './types';
21+
import {
22+
PopKPIComparisonSymbolStyleProps,
23+
PopKPIComparisonValueStyleProps,
24+
PopKPIProps,
25+
} from './types';
2226

2327
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
2428
${({ theme, subheaderFontSize }) => `
@@ -30,16 +34,29 @@ const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
3034
`}
3135
`;
3236

37+
const SymbolWrapper = styled.div<PopKPIComparisonSymbolStyleProps>`
38+
${({ theme, backgroundColor, textColor }) => `
39+
background-color: ${backgroundColor};
40+
color: ${textColor};
41+
padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
42+
border-radius: ${theme.gridUnit * 2}px;
43+
display: inline-block;
44+
margin-right: ${theme.gridUnit}px;
45+
`}
46+
`;
47+
3348
export default function PopKPI(props: PopKPIProps) {
3449
const {
3550
height,
3651
width,
3752
bigNumber,
3853
prevNumber,
3954
valueDifference,
40-
percentDifference,
55+
percentDifferenceFormattedString,
4156
headerFontSize,
4257
subheaderFontSize,
58+
comparisonColorEnabled,
59+
percentDifferenceNumber,
4360
} = props;
4461

4562
const rootElem = createRef<HTMLDivElement>();
@@ -63,9 +80,60 @@ export default function PopKPI(props: PopKPIProps) {
6380
text-align: center;
6481
`;
6582

83+
const getArrowIndicatorColor = () => {
84+
if (!comparisonColorEnabled) return theme.colors.grayscale.base;
85+
return percentDifferenceNumber > 0
86+
? theme.colors.success.base
87+
: theme.colors.error.base;
88+
};
89+
90+
const arrowIndicatorStyle = css`
91+
color: ${getArrowIndicatorColor()};
92+
margin-left: ${theme.gridUnit}px;
93+
`;
94+
95+
const defaultBackgroundColor = theme.colors.grayscale.light4;
96+
const defaultTextColor = theme.colors.grayscale.base;
97+
const { backgroundColor, textColor } = useMemo(() => {
98+
let bgColor = defaultBackgroundColor;
99+
let txtColor = defaultTextColor;
100+
if (percentDifferenceNumber > 0) {
101+
if (comparisonColorEnabled) {
102+
bgColor = theme.colors.success.light2;
103+
txtColor = theme.colors.success.base;
104+
}
105+
} else if (percentDifferenceNumber < 0) {
106+
if (comparisonColorEnabled) {
107+
bgColor = theme.colors.error.light2;
108+
txtColor = theme.colors.error.base;
109+
}
110+
}
111+
112+
return {
113+
backgroundColor: bgColor,
114+
textColor: txtColor,
115+
};
116+
}, [theme, comparisonColorEnabled, percentDifferenceNumber]);
117+
118+
const SYMBOLS_WITH_VALUES = useMemo(
119+
() => [
120+
['#', prevNumber],
121+
['△', valueDifference],
122+
['%', percentDifferenceFormattedString],
123+
],
124+
[prevNumber, valueDifference, percentDifferenceFormattedString],
125+
);
126+
66127
return (
67128
<div ref={rootElem} css={wrapperDivStyles}>
68-
<div css={bigValueContainerStyles}>{bigNumber}</div>
129+
<div css={bigValueContainerStyles}>
130+
{bigNumber}
131+
{percentDifferenceNumber !== 0 && (
132+
<span css={arrowIndicatorStyle}>
133+
{percentDifferenceNumber > 0 ? '↑' : '↓'}
134+
</span>
135+
)}
136+
</div>
69137
<div
70138
css={css`
71139
width: 100%;
@@ -77,18 +145,22 @@ export default function PopKPI(props: PopKPIProps) {
77145
display: table-row;
78146
`}
79147
>
80-
<ComparisonValue subheaderFontSize={subheaderFontSize}>
81-
{' '}
82-
#: {prevNumber}
83-
</ComparisonValue>
84-
<ComparisonValue subheaderFontSize={subheaderFontSize}>
85-
{' '}
86-
Δ: {valueDifference}
87-
</ComparisonValue>
88-
<ComparisonValue subheaderFontSize={subheaderFontSize}>
89-
{' '}
90-
%: {percentDifference}
91-
</ComparisonValue>
148+
{SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
149+
<ComparisonValue
150+
key={`comparison-symbol-${symbol_with_value[0]}`}
151+
subheaderFontSize={subheaderFontSize}
152+
>
153+
<SymbolWrapper
154+
backgroundColor={
155+
index > 0 ? backgroundColor : defaultBackgroundColor
156+
}
157+
textColor={index > 0 ? textColor : defaultTextColor}
158+
>
159+
{symbol_with_value[0]}
160+
</SymbolWrapper>
161+
{symbol_with_value[1]}
162+
</ComparisonValue>
163+
))}
92164
</div>
93165
</div>
94166
</div>
Loading

superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts

+6-202
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
buildQueryContext,
2222
QueryFormData,
2323
} from '@superset-ui/core';
24-
import moment, { Moment } from 'moment';
24+
import { computeQueryBComparator } from '../utils';
2525

2626
/**
2727
* The buildQuery function is used to create an instance of QueryContext that's
@@ -38,184 +38,6 @@ import moment, { Moment } from 'moment';
3838
* if a viz needs multiple different result sets.
3939
*/
4040

41-
type MomentTuple = [moment.Moment | null, moment.Moment | null];
42-
43-
function getSinceUntil(
44-
timeRange: string | null = null,
45-
relativeStart: string | null = null,
46-
relativeEnd: string | null = null,
47-
): MomentTuple {
48-
const separator = ' : ';
49-
const effectiveRelativeStart = relativeStart || 'today';
50-
const effectiveRelativeEnd = relativeEnd || 'today';
51-
52-
if (!timeRange) {
53-
return [null, null];
54-
}
55-
56-
let modTimeRange: string | null = timeRange;
57-
58-
if (timeRange === 'NO_TIME_RANGE' || timeRange === '_(NO_TIME_RANGE)') {
59-
return [null, null];
60-
}
61-
62-
if (timeRange?.startsWith('last') && !timeRange.includes(separator)) {
63-
modTimeRange = timeRange + separator + effectiveRelativeEnd;
64-
}
65-
66-
if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
67-
modTimeRange = effectiveRelativeStart + separator + timeRange;
68-
}
69-
70-
if (
71-
timeRange?.startsWith('previous calendar week') &&
72-
!timeRange.includes(separator)
73-
) {
74-
return [
75-
moment().subtract(1, 'week').startOf('week'),
76-
moment().startOf('week'),
77-
];
78-
}
79-
80-
if (
81-
timeRange?.startsWith('previous calendar month') &&
82-
!timeRange.includes(separator)
83-
) {
84-
return [
85-
moment().subtract(1, 'month').startOf('month'),
86-
moment().startOf('month'),
87-
];
88-
}
89-
90-
if (
91-
timeRange?.startsWith('previous calendar year') &&
92-
!timeRange.includes(separator)
93-
) {
94-
return [
95-
moment().subtract(1, 'year').startOf('year'),
96-
moment().startOf('year'),
97-
];
98-
}
99-
100-
const timeRangeLookup: Array<[RegExp, (...args: string[]) => Moment]> = [
101-
[
102-
/^last\s+(day|week|month|quarter|year)$/i,
103-
(unit: string) =>
104-
moment().subtract(1, unit as moment.unitOfTime.DurationConstructor),
105-
],
106-
[
107-
/^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
108-
(delta: string, unit: string) =>
109-
moment().subtract(delta, unit as moment.unitOfTime.DurationConstructor),
110-
],
111-
[
112-
/^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
113-
(delta: string, unit: string) =>
114-
moment().add(delta, unit as moment.unitOfTime.DurationConstructor),
115-
],
116-
[
117-
// eslint-disable-next-line no-useless-escape
118-
/DATEADD\(DATETIME\("([^"]+)"\),\s*(-?\d+),\s*([^\)]+)\)/i,
119-
(timePart: string, delta: string, unit: string) => {
120-
if (timePart === 'now') {
121-
return moment().add(
122-
delta,
123-
unit as moment.unitOfTime.DurationConstructor,
124-
);
125-
}
126-
if (moment(timePart.toUpperCase(), true).isValid()) {
127-
return moment(timePart).add(
128-
delta,
129-
unit as moment.unitOfTime.DurationConstructor,
130-
);
131-
}
132-
return moment();
133-
},
134-
],
135-
];
136-
137-
const sinceAndUntilPartition = modTimeRange
138-
.split(separator, 2)
139-
.map(part => part.trim());
140-
141-
const sinceAndUntil: (Moment | null)[] = sinceAndUntilPartition.map(part => {
142-
if (!part) {
143-
return null;
144-
}
145-
146-
let transformedValue: Moment | null = null;
147-
// Matching time_range_lookup
148-
const matched = timeRangeLookup.some(([pattern, fn]) => {
149-
const result = part.match(pattern);
150-
if (result) {
151-
transformedValue = fn(...result.slice(1));
152-
return true;
153-
}
154-
155-
if (part === 'today') {
156-
transformedValue = moment().startOf('day');
157-
return true;
158-
}
159-
160-
if (part === 'now') {
161-
transformedValue = moment();
162-
return true;
163-
}
164-
return false;
165-
});
166-
167-
if (matched && transformedValue !== null) {
168-
// Handle the transformed value
169-
} else {
170-
// Handle the case when there was no match
171-
transformedValue = moment(`${part}`);
172-
}
173-
174-
return transformedValue;
175-
});
176-
177-
const [_since, _until] = sinceAndUntil;
178-
179-
if (_since && _until && _since.isAfter(_until)) {
180-
throw new Error('From date cannot be larger than to date');
181-
}
182-
183-
return [_since, _until];
184-
}
185-
186-
function calculatePrev(
187-
startDate: Moment | null,
188-
endDate: Moment | null,
189-
calcType: String,
190-
) {
191-
if (!startDate || !endDate) {
192-
return [null, null];
193-
}
194-
195-
const daysBetween = endDate.diff(startDate, 'days');
196-
197-
let startDatePrev = moment();
198-
let endDatePrev = moment();
199-
if (calcType === 'y') {
200-
startDatePrev = startDate.subtract(1, 'year');
201-
endDatePrev = endDate.subtract(1, 'year');
202-
} else if (calcType === 'w') {
203-
startDatePrev = startDate.subtract(1, 'week');
204-
endDatePrev = endDate.subtract(1, 'week');
205-
} else if (calcType === 'm') {
206-
startDatePrev = startDate.subtract(1, 'month');
207-
endDatePrev = endDate.subtract(1, 'month');
208-
} else if (calcType === 'r') {
209-
startDatePrev = startDate.clone().subtract(daysBetween.valueOf(), 'day');
210-
endDatePrev = startDate;
211-
} else {
212-
startDatePrev = startDate.subtract(1, 'year');
213-
endDatePrev = endDate.subtract(1, 'year');
214-
}
215-
216-
return [startDatePrev, endDatePrev];
217-
}
218-
21941
export default function buildQuery(formData: QueryFormData) {
22042
const {
22143
cols: groupby,
@@ -240,37 +62,19 @@ export default function buildQuery(formData: QueryFormData) {
24062
? formData.adhoc_filters[timeFilterIndex]
24163
: null;
24264

243-
let testSince = null;
244-
let testUntil = null;
245-
246-
if (
247-
timeFilter &&
248-
'comparator' in timeFilter &&
249-
typeof timeFilter.comparator === 'string'
250-
) {
251-
let timeRange = timeFilter.comparator.toLocaleLowerCase();
252-
if (extraFormData?.time_range) {
253-
timeRange = extraFormData.time_range;
254-
}
255-
[testSince, testUntil] = getSinceUntil(timeRange);
256-
}
257-
25865
let formDataB: QueryFormData;
66+
let queryBComparator = null;
25967

26068
if (timeComparison !== 'c') {
261-
const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
262-
testSince,
263-
testUntil,
69+
queryBComparator = computeQueryBComparator(
70+
formData.adhoc_filters || [],
26471
timeComparison,
72+
extraFormData,
26573
);
26674

267-
const queryBComparator = `${prevStartDateMoment?.format(
268-
'YYYY-MM-DDTHH:mm:ss',
269-
)} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
270-
27175
const queryBFilter: any = {
27276
...timeFilter,
273-
comparator: queryBComparator.replace(/Z/g, ''),
77+
comparator: queryBComparator,
27478
};
27579

27680
const otherFilters = formData.adhoc_filters?.filter(

superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts

+12
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ const config: ControlPanelConfig = {
181181
},
182182
},
183183
],
184+
[
185+
{
186+
name: 'comparison_color_enabled',
187+
config: {
188+
type: 'CheckboxControl',
189+
label: t('Add color for positive/negative change'),
190+
renderTrigger: true,
191+
default: false,
192+
description: t('Add color for positive/negative change'),
193+
},
194+
},
195+
],
184196
],
185197
},
186198
],

0 commit comments

Comments
 (0)