Skip to content

Commit

Permalink
Adm 944 [frontend] add trending icon in chart (#1472)
Browse files Browse the repository at this point in the history
* ADM-944 feat: add trend number and icon

* ADM-944 refactor: extract trend color

* ADM-944 feat: add dara trend

* ADM-944 refactor: refactor

* ADM-944 feat: add tooltip

* ADM-944 fix: fix eslint

* ADM-944 test: add unit test

* ADM-944 test: fix test

* ADM-944 refactor: rename

* ADM-944: [frontend] feat: add test

* ADM-944: [frontend] feat: fix lint

* ADM-944: [frontend] feat: change import to @src

* ADM-944: [frontend] feat: fix sonar isuue

* ADM-944: [frontend] feat: fix prettier

---------

Co-authored-by: Leiqiuhong <[email protected]>
  • Loading branch information
Mandy-Tang and Leiqiuhong authored May 29, 2024
1 parent ef20d99 commit 22f65b1
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper';
import { CHART_TYPE, TREND_ICON } from '@src/constants/resources';
import { render, screen } from '@testing-library/react';

describe('ChartAndTitleWrapper', () => {
it('should render green up icon when icon is set to up and green', () => {
const testedTrendInfo = {
color: 'green',
icon: TREND_ICON.UP,
trendNumber: 83.72,
type: CHART_TYPE.VELOCITY,
};
render(<ChartAndTitleWrapper trendInfo={testedTrendInfo} />);
const icon = screen.getByTestId('TrendingUpSharpIcon');

expect(icon).toBeInTheDocument();
expect(icon.parentElement?.parentElement).toHaveStyle({ color: 'green' });
});

it('should render down icon when icon is set to down', () => {
const testedTrendInfo = {
color: 'red',
icon: TREND_ICON.DOWN,
trendNumber: -83.72,
type: CHART_TYPE.VELOCITY,
};
render(<ChartAndTitleWrapper trendInfo={testedTrendInfo} />);
const icon = screen.getByTestId('TrendingDownSharpIcon');

expect(screen.getByTestId('TrendingDownSharpIcon')).toBeInTheDocument();
expect(icon.parentElement?.parentElement).toHaveStyle({ color: 'red' });
});
});
88 changes: 87 additions & 1 deletion frontend/__tests__/utils/Util.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
calculateTrendInfo,
combineBoardInfo,
convertCycleTimeSettings,
exportToJsonFile,
Expand All @@ -18,8 +19,15 @@ import {
updateResponseCrews,
xAxisLabelDateFormatter,
} from '@src/utils/util';
import {
CHART_TYPE,
CYCLE_TIME_SETTINGS_TYPES,
DOWN_TREND_IS_BETTER,
METRICS_CONSTANTS,
TREND_ICON,
UP_TREND_IS_BETTER,
} from '@src/constants/resources';
import { CleanedBuildKiteEmoji, OriginBuildKiteEmoji } from '@src/constants/emojis/emoji';
import { CYCLE_TIME_SETTINGS_TYPES, METRICS_CONSTANTS } from '@src/constants/resources';
import { ICycleTimeSetting, IPipelineConfig } from '@src/context/Metrics/metricsSlice';
import { IPipeline } from '@src/context/config/pipelineTool/verifyResponseSlice';
import { BoardInfoResponse } from '@src/hooks/useGetBoardInfo';
Expand Down Expand Up @@ -618,6 +626,84 @@ describe('xAxisLabelDateFormatter function', () => {
});
});

describe('calculateTrendInfo function', () => {
const dateRangeList = [
'2024/01/15-2024/01/19',
'2024/01/20-2024/01/21',
'2024/01/22-2024/01/23',
'2024/01/24-2024/01/25',
];
it('should only return type given the valid data length is less 2', () => {
const dataList = [0, 0, 3, 0];
const result = calculateTrendInfo(dataList, dateRangeList, CHART_TYPE.VELOCITY);

expect(result.dateRangeList).toEqual(undefined);
expect(result.trendNumber).toEqual(undefined);
expect(result.color).toEqual(undefined);
expect(result.icon).toEqual(undefined);
expect(result.type).toEqual(CHART_TYPE.VELOCITY);
});
it.each(UP_TREND_IS_BETTER)(
'should get better result given the type is the up trend is better and the data is up',
(type) => {
const dataList = [1, 0, 3, 0];

const result = calculateTrendInfo(dataList, dateRangeList, type);

expect(result.dateRangeList).toEqual(['2024/01/22-2024/01/23', '2024/01/15-2024/01/19']);
expect(result.trendNumber).toEqual(2);
expect(result.color).toEqual('green');
expect(result.icon).toEqual(TREND_ICON.UP);
expect(result.type).toEqual(type);
},
);

it.each(UP_TREND_IS_BETTER)(
'should get worse result given the type is the up trend is better and the data is down',
(type) => {
const dataList = [3, 0, 1, 0];

const result = calculateTrendInfo(dataList, dateRangeList, type);

expect(result.dateRangeList).toEqual(['2024/01/22-2024/01/23', '2024/01/15-2024/01/19']);
expect(Number(result.trendNumber?.toFixed(2))).toEqual(-0.67);
expect(result.color).toEqual('red');
expect(result.icon).toEqual(TREND_ICON.DOWN);
expect(result.type).toEqual(type);
},
);

it.each(DOWN_TREND_IS_BETTER)(
'should get better result given the type is the down trend is better and the data is down',
(type) => {
const dataList = [3, 0, 1, 0];

const result = calculateTrendInfo(dataList, dateRangeList, type);

expect(result.dateRangeList).toEqual(['2024/01/22-2024/01/23', '2024/01/15-2024/01/19']);
expect(Number(result.trendNumber?.toFixed(2))).toEqual(-0.67);
expect(result.color).toEqual('green');
expect(result.icon).toEqual(TREND_ICON.DOWN);
expect(result.type).toEqual(type);
},
);

it.each(DOWN_TREND_IS_BETTER)(
'should get worse result given the type is the down trend is better and the data is up',
(type) => {
const dataList = [1, 0, 3, 0];

const result = calculateTrendInfo(dataList, dateRangeList, type);

expect(result.dateRangeList).toEqual(['2024/01/22-2024/01/23', '2024/01/15-2024/01/19']);
expect(result.trendNumber).toEqual(2);
expect(result.color).toEqual('red');
expect(result.icon).toEqual(TREND_ICON.UP);
expect(result.type).toEqual(type);
},
);
});

describe('percentageFormatter function', () => {
it('should return the correct data format with percentage symbol', () => {
const inputData = 66.66;
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/constants/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,46 @@ export enum METRICS_TITLE {
DEV_MEAN_TIME_TO_RECOVERY = 'Dev Mean Time To Recovery',
}

export enum CHART_TYPE {
VELOCITY = 'Velocity',
AVERAGE_CYCLE_TIME = 'Average Cycle Time',
CYCLE_TIME_ALLOCATION = 'Cycle Time Allocation',
REWORK = 'Rework',
LEAD_TIME_FOR_CHANGES = 'Lead Time For Changes',
DEPLOYMENT_FREQUENCY = 'Deployment Frequency',
DEV_CHANGE_FAILURE_RATE = 'Dev Change Failure Rate',
DEV_MEAN_TIME_TO_RECOVERY = 'Dev Mean Time To Recovery',
}

export enum TREND_ICON {
UP = 'UP',
DOWN = 'DOWN',
}

export const CHART_TREND_TIP = {
[CHART_TYPE.VELOCITY]: 'Velocity(Story point)',
[CHART_TYPE.AVERAGE_CYCLE_TIME]: 'Days/Story point',
[CHART_TYPE.CYCLE_TIME_ALLOCATION]: 'Total development time/Total cycle time',
[CHART_TYPE.REWORK]: 'Total rework times',
[CHART_TYPE.LEAD_TIME_FOR_CHANGES]: 'PR Lead Time',
[CHART_TYPE.DEPLOYMENT_FREQUENCY]: 'Mean Time To Recovery',
[CHART_TYPE.DEV_CHANGE_FAILURE_RATE]: 'Dev Change Failure Rate',
[CHART_TYPE.DEV_MEAN_TIME_TO_RECOVERY]: 'Dev Mean Time To Recovery',
};

export const UP_TREND_IS_BETTER: CHART_TYPE[] = [
CHART_TYPE.VELOCITY,
CHART_TYPE.CYCLE_TIME_ALLOCATION,
CHART_TYPE.DEPLOYMENT_FREQUENCY,
];
export const DOWN_TREND_IS_BETTER: CHART_TYPE[] = [
CHART_TYPE.REWORK,
CHART_TYPE.AVERAGE_CYCLE_TIME,
CHART_TYPE.LEAD_TIME_FOR_CHANGES,
CHART_TYPE.DEV_MEAN_TIME_TO_RECOVERY,
CHART_TYPE.DEV_CHANGE_FAILURE_RATE,
];

export enum METRICS_SUBTITLE {
PR_LEAD_TIME = 'PR Lead Time(Hours)',
PIPELINE_LEAD_TIME = 'Pipeline Lead Time(Hours)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import { percentageFormatter, xAxisLabelDateFormatter } from '@src/utils/util';
import { theme } from '@src/theme';

export interface BarOptionProps {
title: string;
xAxis: string[];
yAxis: YAxis;
series: Series[] | undefined;
color: string[];
}

export interface AreaOptionProps {
title: string;
xAxis: XAxis;
yAxis: YAxis[];
series: Series[] | undefined;
Expand Down Expand Up @@ -90,10 +88,6 @@ const commonConfig = {

export const stackedAreaOptionMapper = (props: AreaOptionProps) => {
return {
title: {
text: props.title,
left: '22',
},
legend: {
data: props.series?.map((item) => item.name),
...commonConfig.legend,
Expand Down Expand Up @@ -139,10 +133,6 @@ export const stackedAreaOptionMapper = (props: AreaOptionProps) => {

export const stackedBarOptionMapper = (props: BarOptionProps) => {
return {
title: {
text: props.title,
left: '22',
},
legend: {
data: props.series?.map((item) => item.name),
...commonConfig.legend,
Expand Down
28 changes: 17 additions & 11 deletions frontend/src/containers/ReportStep/BoardMetricsChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {
stackedAreaOptionMapper,
stackedBarOptionMapper,
} from '@src/containers/ReportStep/BoardMetricsChart/ChartOption';
import { CYCLE_TIME_CHARTS_MAPPING, REQUIRED_DATA } from '@src/constants/resources';
import { ChartContainer, ChartWrapper } from '@src/containers/MetricsStep/style';
import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper';
import { CHART_TYPE, CYCLE_TIME_CHARTS_MAPPING } from '@src/constants/resources';
import { calculateTrendInfo, xAxisLabelDateFormatter } from '@src/utils/util';
import { ReportResponse, Swimlane } from '@src/clients/report/dto/response';
import { ChartContainer } from '@src/containers/MetricsStep/style';
import { IReportInfo } from '@src/hooks/useGenerateReportEffect';
import { reportMapper } from '@src/hooks/reportMapper/report';
import { xAxisLabelDateFormatter } from '@src/utils/util';
import React, { useEffect, useRef } from 'react';
import { theme } from '@src/theme';
import * as echarts from 'echarts';
Expand Down Expand Up @@ -62,8 +63,8 @@ function extractVelocityData(dateRanges: string[], mappedData?: ReportResponse[]
const data = mappedData?.map((item) => item.velocityList);
const velocity = data?.map((item) => item?.[0]?.valueList?.[0]?.value as number);
const throughput = data?.map((item) => item?.[1]?.valueList?.[0]?.value as number);
const trendInfo = calculateTrendInfo(velocity, dateRanges, CHART_TYPE.VELOCITY);
return {
title: REQUIRED_DATA.VELOCITY,
xAxis: {
data: dateRanges,
boundaryGap: false,
Expand Down Expand Up @@ -100,15 +101,16 @@ function extractVelocityData(dateRanges: string[], mappedData?: ReportResponse[]
},
],
color: [theme.main.boardChart.lineColorA, theme.main.boardChart.lineColorB],
trendInfo,
};
}

function extractAverageCycleTimeData(dateRanges: string[], mappedData?: ReportResponse[]) {
const data = mappedData?.map((item) => item.cycleTimeList);
const storyPoints = data?.map((item) => item?.[0]?.valueList?.[0]?.value as number);
const cardCount = data?.map((item) => item?.[0]?.valueList?.[1]?.value as number);
const trendInfo = calculateTrendInfo(storyPoints, dateRanges, CHART_TYPE.AVERAGE_CYCLE_TIME);
return {
title: 'Average Cycle Time',
xAxis: {
data: dateRanges,
boundaryGap: false,
Expand Down Expand Up @@ -145,6 +147,7 @@ function extractAverageCycleTimeData(dateRanges: string[], mappedData?: ReportRe
},
],
color: [theme.main.boardChart.lineColorA, theme.main.boardChart.lineColorB],
trendInfo,
};
}

Expand All @@ -156,8 +159,8 @@ function extractCycleTimeData(dateRanges: string[], mappedData?: ReportResponse[
for (const [name, data] of Object.entries(cycleTimeByStatus)) {
indicators.push({ data, name: CYCLE_TIME_CHARTS_MAPPING[name], type: 'bar' });
}
const trendInfo = calculateTrendInfo(totalCycleTime, dateRanges, CHART_TYPE.CYCLE_TIME_ALLOCATION);
return {
title: 'Cycle Time Allocation',
xAxis: dateRanges,
yAxis: {
name: 'Value/Total cycle time',
Expand All @@ -173,6 +176,7 @@ function extractCycleTimeData(dateRanges: string[], mappedData?: ReportResponse[
theme.main.boardChart.barColorE,
theme.main.boardChart.barColorF,
],
trendInfo,
};
}

Expand All @@ -181,8 +185,9 @@ function extractReworkData(dateRanges: string[], mappedData?: ReportResponse[])
const totalReworkTimes = data?.map((item) => item?.totalReworkTimes as number);
const totalReworkCards = data?.map((item) => item?.totalReworkCards as number);
const reworkCardsRatio = data?.map((item) => (item?.reworkCardsRatio as number) * 100);

const trendInfo = calculateTrendInfo(totalReworkTimes, dateRanges, CHART_TYPE.REWORK);
return {
title: 'Rework',
xAxis: {
data: dateRanges,
boundaryGap: true,
Expand Down Expand Up @@ -227,6 +232,7 @@ function extractReworkData(dateRanges: string[], mappedData?: ReportResponse[])
},
],
color: [theme.main.boardChart.lineColorB, theme.main.boardChart.barColorA, theme.main.boardChart.barColorB],
trendInfo,
};
}

Expand Down Expand Up @@ -291,10 +297,10 @@ export const BoardMetricsChart = ({ data, dateRanges }: BoardMetricsChartProps)

return (
<ChartContainer>
<ChartWrapper ref={velocity}></ChartWrapper>
<ChartWrapper ref={averageCycleTime}></ChartWrapper>
<ChartWrapper ref={cycleTime}></ChartWrapper>
<ChartWrapper ref={rework}></ChartWrapper>
<ChartAndTitleWrapper trendInfo={velocityData.trendInfo} ref={velocity} />
<ChartAndTitleWrapper trendInfo={averageCycleTimeData.trendInfo} ref={averageCycleTime} />
<ChartAndTitleWrapper trendInfo={cycleTimeData.trendInfo} ref={cycleTime} />
<ChartAndTitleWrapper trendInfo={reworkData.trendInfo} ref={rework} />
</ChartContainer>
);
};
Loading

0 comments on commit 22f65b1

Please sign in to comment.