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

Adm 944 [frontend] add trending icon in chart #1472

Merged
merged 15 commits into from
May 29, 2024
Merged
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
Loading