Skip to content

Commit

Permalink
Add units to number visualization (#21339)
Browse files Browse the repository at this point in the history
* Add a feature flag which hides the Executive Dashboard from the user.

* Revert "Add a feature flag which hides the Executive Dashboard from the user."

This reverts commit 2bee266.

* Add units to number visualization

* Add units to trend component. Add to _getPrettifiedValue support of negative values

* Show current value unit

* fix tests. remove unnecessary variable

* add changelog

* fix linter error

---------

Co-authored-by: Dennis Oelkers <[email protected]>
  • Loading branch information
maxiadlovskii and dennisoelkers authored Jan 22, 2025
1 parent 5f57208 commit a2613aa
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 15 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-20965.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "a"
message = "Add unit handling to a single number widget"

issues = ["20965"]
pulls = ["21339"]
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React, { useContext, useEffect, useRef } from 'react';
import React, { useContext, useEffect, useRef, useMemo } from 'react';
import styled, { css } from 'styled-components';

import type { Rows } from 'views/logic/searchtypes/pivot/PivotHandler';
Expand All @@ -27,9 +27,13 @@ import NumberVisualizationConfig from 'views/logic/aggregationbuilder/visualizat
import type { VisualizationComponentProps } from 'views/components/aggregationbuilder/AggregationBuilder';
import { makeVisualization, retrieveChartData } from 'views/components/aggregationbuilder/AggregationBuilder';
import ElementDimensions from 'components/common/ElementDimensions';
import useWidgetUnits from 'views/components/visualizations/hooks/useWidgetUnits';
import useFeature from 'hooks/useFeature';
import { UNIT_FEATURE_FLAG } from 'views/components/visualizations/Constants';
import { parseSeries } from 'views/logic/aggregationbuilder/Series';

import Trend from './Trend';
import AutoFontSizer from './AutoFontSizer';
import Trend from './Trend';

const Container = styled.div<{ $height: number }>(({ $height }) => css`
height: ${$height}px;
Expand Down Expand Up @@ -89,6 +93,8 @@ const _extractFirstSeriesName = (config) => {

const NumberVisualization = ({ config, fields, data, height: heightProp }: VisualizationComponentProps) => {
const targetRef = useRef();
const unitFeatureEnabled = useFeature(UNIT_FEATURE_FLAG);
const widgetUnits = useWidgetUnits(config);
const onRenderComplete = useContext(RenderCompletionCallback);
const visualizationConfig = (config.visualizationConfig as NumberVisualizationConfig) ?? NumberVisualizationConfig.create();

Expand All @@ -99,6 +105,13 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua
const trendRows = data.trend;
const { value } = _extractValueAndField(chartRows);
const { value: previousValue } = _extractValueAndField(trendRows || []);
const unit = useMemo(() => {
if (!unitFeatureEnabled) return undefined;

const fieldNameKey = parseSeries(field).field;

return widgetUnits.getFieldUnit(fieldNameKey);
}, [field, unitFeatureEnabled, widgetUnits]);

if (!field || (value !== 0 && !value)) {
return <>N/A</>;
Expand All @@ -115,7 +128,8 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua
<Value field={field}
type={fieldTypeFor(field, fields)}
value={value}
render={DecoratedValue} />
render={DecoratedValue}
unit={unit} />
</CustomHighlighting>
</AutoFontSizer>
)}
Expand All @@ -127,7 +141,8 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua
<Trend ref={targetRef}
current={value}
previous={previousValue}
trendPreference={visualizationConfig.trendPreference} />
trendPreference={visualizationConfig.trendPreference}
unit={unit} />
</AutoFontSizer>
)}
</TrendBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ import numeral from 'numeral';

import Icon from 'components/common/Icon';
import type { TrendPreference } from 'views/logic/aggregationbuilder/visualizations/NumberVisualizationConfig';
import type FieldUnit from 'views/logic/aggregationbuilder/FieldUnit';
import {
getPrettifiedValue,
convertValueToUnit,
} from 'views/components/visualizations/utils/unitConverters';
import formatValueWithUnitLabel from 'views/components/visualizations/utils/formatValueWithUnitLabel';
import getUnitTextLabel from 'views/components/visualizations/utils/getUnitTextLabel';

type TrendDirection = 'good' | 'bad' | 'neutral';

type Props = {
current: number,
previous: number | undefined | null,
trendPreference: TrendPreference,
unit?: FieldUnit,
};

const background = (theme: DefaultTheme, trend: TrendDirection = 'neutral') => ({
Expand Down Expand Up @@ -98,19 +106,53 @@ const diff = (current: number | undefined, previous: number | undefined): [numbe
return [NaN, NaN];
};

const Trend = React.forwardRef<HTMLSpanElement, Props>(({ current, previous, trendPreference }: Props, ref) => {
const getTrendConvertedValues = (current: number, previous: number, fieldUNit: FieldUnit): {
previousConverted: number | string,
differenceConverted: number,
differencePercent: number,
unitAbbrevString: string,
} => {
const [difference, differencePercent] = diff(current, previous);

const backgroundTrend = _trendDirection(difference, trendPreference);
const trendIcon = _trendIcon(difference);
if (!fieldUNit?.isDefined) {
return ({
previousConverted: previous,
differenceConverted: difference,
differencePercent,
unitAbbrevString: '',
});
}

const originalParams = { unitType: fieldUNit?.unitType, abbrev: fieldUNit?.abbrev };
const { unit: currentPrettyUnit } = getPrettifiedValue(current, originalParams);
const currentPrettyParams = { unitType: currentPrettyUnit?.unitType, abbrev: currentPrettyUnit?.abbrev };
const { value: prettyDiff } = convertValueToUnit(difference, originalParams, currentPrettyParams);
const { value: previousPretty } = convertValueToUnit(previous, originalParams, currentPrettyParams);

return ({
previousConverted: `${formatValueWithUnitLabel(previousPretty, currentPrettyUnit.abbrev)} (${previous})`,
unitAbbrevString: ` ${getUnitTextLabel(currentPrettyUnit.abbrev)}`,
differenceConverted: prettyDiff,
differencePercent,
});
};

const Trend = React.forwardRef<HTMLSpanElement, Props>(({ current, previous, trendPreference, unit = undefined }: Props, ref) => {
const { differenceConverted, differencePercent, unitAbbrevString, previousConverted } = getTrendConvertedValues(current, previous, unit);

const backgroundTrend = _trendDirection(differenceConverted, trendPreference);
const trendIcon = _trendIcon(differenceConverted);

const absoluteDifference = Number.isFinite(difference) ? numeral(difference).format('+0,0[.]0[000]') : '--';
const absoluteDifference = Number.isFinite(differenceConverted) ? `${numeral(differenceConverted).format('+0,0[.]0[000]')}${unitAbbrevString}` : '--';
const relativeDifference = Number.isFinite(differencePercent) ? numeral(differencePercent).format('+0[.]0[0]%') : '--';

return (
<Background trend={backgroundTrend} data-testid="trend-background">
<TextContainer trend={backgroundTrend} ref={ref}>
<StyledIcon name={trendIcon} trend={backgroundTrend} data-testid="trend-icon" /> <span data-testid="trend-value" title={`Previous value: ${previous}`}>{absoluteDifference} / {relativeDifference}</span>
<StyledIcon name={trendIcon} trend={backgroundTrend} data-testid="trend-icon" />{' '}
<span data-testid="trend-value" title={`Previous value: ${previousConverted}`}>
{absoluteDifference} / {relativeDifference}
</span>
</TextContainer>
</Background>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,28 @@ const _convertValueToUnit = (units: FieldUnitTypes, value: number, fromParams: C
export const _getPrettifiedValue = (units: FieldUnitTypes, initValue: number | string, params: ConversionParams): ConvertedResult => {
const currentUnit = units?.[params?.unitType] ?? null;

const value = initValue === null ? null : toNumber(initValue);
if (!(value && currentUnit)) return ({ value, unit: currentUnit ? currentUnit.find(({ abbrev }) => abbrev === params.abbrev) : null });
const _value = initValue === null ? null : toNumber(initValue);
if (!(_value && currentUnit)) return ({ value: _value, unit: currentUnit ? currentUnit.find(({ abbrev }) => abbrev === params.abbrev) : null });

const allConvertedValues = Object.values(currentUnit).map((unit: Unit) => _convertValueToUnit(units, value, params, { abbrev: unit.abbrev, unitType: unit.unitType }));
const sign = Math.sign(_value);
const absolutValue = Math.abs(_value);

const allConvertedValues = Object.values(currentUnit).map((unit: Unit) => _convertValueToUnit(units, absolutValue, params, { abbrev: unit.abbrev, unitType: unit.unitType }));

const filtratedValues = allConvertedValues.filter(({ value: val, unit }) => val >= 1 && unit.useInPrettier);

let result: ConvertedResult;

if (filtratedValues.length > 0) {
return minBy(filtratedValues, ({ value: val }) => val);
result = minBy(filtratedValues, ({ value: val }) => val);
} else {
const filtratedValuesLower = allConvertedValues.filter(({ value: val, unit }) => val < 1 && unit.useInPrettier);
result = maxBy(filtratedValuesLower, ({ value: val }) => val);
}

const filtratedValuesLower = allConvertedValues.filter(({ value: val, unit }) => val < 1 && unit.useInPrettier);
result.value *= sign;

return maxBy(filtratedValuesLower, ({ value: val }) => val);
return result;
};

export type ConvertValueToUnit = (value: number, fromParams: ConversionParams, toParams: ConversionParams) => ConvertedResult
Expand Down

0 comments on commit a2613aa

Please sign in to comment.