Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add units to number visualization #21339

Merged
merged 11 commits into from
Jan 22, 2025
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 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,

Check warning on line 38 in graylog2-web-interface/src/views/components/visualizations/number/Trend.tsx

View workflow job for this annotation

GitHub Actions / Reviewbot

propType "unit" is not required, but has no corresponding defaultProps declaration.

No further rule information available.
};

const background = (theme: DefaultTheme, trend: TrendDirection = 'neutral') => ({
Expand Down Expand Up @@ -98,19 +106,53 @@
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
Loading