Skip to content

Commit

Permalink
feat(plugins): Adding colors to BigNumber with Time Comparison chart (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Antonio-RiveroMartnez authored and pull[bot] committed Apr 24, 2024
1 parent e05ae62 commit e3949f6
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => `
Expand All @@ -30,16 +34,29 @@ 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,
width,
bigNumber,
prevNumber,
valueDifference,
percentDifference,
percentDifferenceFormattedString,
headerFontSize,
subheaderFontSize,
comparisonColorEnabled,
percentDifferenceNumber,
} = props;

const rootElem = createRef<HTMLDivElement>();
Expand All @@ -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%;
Expand All @@ -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>
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
},
],
],
},
],
Expand Down

0 comments on commit e3949f6

Please sign in to comment.