Commit 3784191e authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Martin Wortschack

Add util methods for CA duration chart

The methods added in this commit are used for computing the
data which is used for the cycle analytics duration chart.
parent f13bf12e
...@@ -602,3 +602,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => { ...@@ -602,3 +602,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => {
* @return {Number} number of milliseconds * @return {Number} number of milliseconds
*/ */
export const secondsToMilliseconds = seconds => seconds * 1000; export const secondsToMilliseconds = seconds => seconds * 1000;
/**
* Converts the supplied number of seconds to days.
*
* @param {Number} seconds
* @return {Number} number of days
*/
export const secondsToDays = seconds => Math.round(seconds / 86400);
/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
* @return {Date} the date following the date provided
*/
export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1));
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { newDate, dayAfter, secondsToDays } from '~/lib/utils/datetime_utility';
import { isString } from 'underscore'; import { isString } from 'underscore';
import dateFormat from 'dateformat';
import { dateFormats } from '../shared/constants';
const EVENT_TYPE_LABEL = 'label'; const EVENT_TYPE_LABEL = 'label';
...@@ -45,3 +48,108 @@ export const nestQueryStringKeys = (obj = null, targetKey = '') => { ...@@ -45,3 +48,108 @@ export const nestQueryStringKeys = (obj = null, targetKey = '') => {
return { ...prev, [customKey]: value }; return { ...prev, [customKey]: value };
}, {}); }, {});
}; };
/**
* Takes the duration data for selected stages, transforms the date values and returns
* the data in a flattened array
*
* The received data is expected to be the following format; One top level object in the array per stage,
* each potentially having multiple data entries.
* [
* {
* slug: 'issue',
* selected: true,
* data: [
* {
* 'duration_in_seconds': 1234,
* 'finished_at': '2019-09-02T18:25:43.511Z'
* },
* ...
* ]
* },
* ...
* ]
*
* The data is then transformed and flattened into the following format;
* [
* {
* 'duration_in_seconds': 1234,
* 'finished_at': '2019-09-02'
* },
* ...
* ]
*
* @param {Array} data - The duration data for selected stages
* @returns {Array} An array with each item being an object containing the duration_in_seconds and finished_at values for an event
*/
export const flattenDurationChartData = data =>
data
.map(stage =>
stage.data.map(event => {
const date = new Date(event.finished_at);
return {
...event,
finished_at: dateFormat(date, dateFormats.isoDate),
};
}),
)
.flat();
/**
* Takes the duration data for selected stages, groups the data by day and calculates the total duration
* per day.
*
* The received data is expected to be the following format; One top level object in the array per stage,
* each potentially having multiple data entries.
* [
* {
* slug: 'issue',
* selected: true,
* data: [
* {
* 'duration_in_seconds': 1234,
* 'finished_at': '2019-09-02T18:25:43.511Z'
* },
* ...
* ]
* },
* ...
* ]
*
* The data is then computed and transformed into a format that can be passed to the chart:
* [
* ['2019-09-02', 7, '2019-09-02'],
* ['2019-09-03', 10, '2019-09-03'],
* ['2019-09-04', 8, '2019-09-04'],
* ...
* ]
*
* In the data above, each array i represents a point in the scatterplot with the following data:
* i[0] = date, displayed on x axis
* i[1] = metric, displayed on y axis
* i[2] = date, used in the tooltip
*
* @param {Array} data - The duration data for selected stages
* @param {Date} startDate - The globally selected cycle analytics start date
* @param {Date} endDate - The globally selected cycle analytics stendart date
* @returns {Array} An array with each item being another arry of three items (plottable date, computed total, tooltip display date)
*/
export const getDurationChartData = (data, startDate, endDate) => {
const flattenedData = flattenDurationChartData(data);
const eventData = [];
for (
let currentDate = newDate(startDate);
currentDate <= endDate;
currentDate = dayAfter(currentDate)
) {
const currentISODate = dateFormat(newDate(currentDate), dateFormats.isoDate);
const valuesForDay = flattenedData.filter(object => object.finished_at === currentISODate);
const summedData = valuesForDay.reduce((total, value) => total + value.duration_in_seconds, 0);
const summedDataInDays = secondsToDays(summedData);
if (summedDataInDays) eventData.push([currentISODate, summedDataInDays, currentISODate]);
}
return eventData;
};
...@@ -55,7 +55,8 @@ export const rawEvents = rawIssueEvents.events; ...@@ -55,7 +55,8 @@ export const rawEvents = rawIssueEvents.events;
const deepCamelCase = obj => convertObjectPropsToCamelCase(obj, { deep: true }); const deepCamelCase = obj => convertObjectPropsToCamelCase(obj, { deep: true });
const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging', 'production']; export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging', 'production'];
const stageFixtures = defaultStages.reduce((acc, stage) => { const stageFixtures = defaultStages.reduce((acc, stage) => {
const { events } = getJSONFixture(endpoints.stageEvents(stage)); const { events } = getJSONFixture(endpoints.stageEvents(stage));
return { return {
...@@ -105,3 +106,39 @@ export const customStageEvents = [ ...@@ -105,3 +106,39 @@ export const customStageEvents = [
]; ];
export const tasksByTypeData = getJSONFixture('analytics/type_of_work/tasks_by_type.json'); export const tasksByTypeData = getJSONFixture('analytics/type_of_work/tasks_by_type.json');
export const rawDurationData = [
{
duration_in_seconds: 1234000,
finished_at: '2019-01-01T00:00:00.000Z',
},
{
duration_in_seconds: 4321000,
finished_at: '2019-01-02T00:00:00.000Z',
},
];
export const transformedDurationData = [
{
slug: 'issue',
selected: true,
data: rawDurationData,
},
{
slug: 'plan',
selected: true,
data: rawDurationData,
},
];
export const flattenedDurationData = [
{ duration_in_seconds: 1234000, finished_at: '2019-01-01' },
{ duration_in_seconds: 4321000, finished_at: '2019-01-02' },
{ duration_in_seconds: 1234000, finished_at: '2019-01-01' },
{ duration_in_seconds: 4321000, finished_at: '2019-01-02' },
];
export const durationChartPlottableData = [
['2019-01-01', 29, '2019-01-01'],
['2019-01-02', 100, '2019-01-02'],
];
...@@ -6,12 +6,19 @@ import { ...@@ -6,12 +6,19 @@ import {
eventsByIdentifier, eventsByIdentifier,
getLabelEventsIdentifiers, getLabelEventsIdentifiers,
nestQueryStringKeys, nestQueryStringKeys,
flattenDurationChartData,
getDurationChartData,
} from 'ee/analytics/cycle_analytics/utils'; } from 'ee/analytics/cycle_analytics/utils';
import { import {
customStageEvents as events, customStageEvents as events,
labelStartEvent, labelStartEvent,
labelStopEvent, labelStopEvent,
customStageStartEvents as startEvents, customStageStartEvents as startEvents,
transformedDurationData,
flattenedDurationData,
durationChartPlottableData,
startDate,
endDate,
} from './mock_data'; } from './mock_data';
const labelEvents = [labelStartEvent, labelStopEvent].map(i => i.identifier); const labelEvents = [labelStartEvent, labelStopEvent].map(i => i.identifier);
...@@ -130,4 +137,20 @@ describe('Cycle analytics utils', () => { ...@@ -130,4 +137,20 @@ describe('Cycle analytics utils', () => {
}); });
}); });
}); });
describe('flattenDurationChartData', () => {
it('flattens the data as expected', () => {
const flattenedData = flattenDurationChartData(transformedDurationData);
expect(flattenedData).toStrictEqual(flattenedDurationData);
});
});
describe('cycleAnalyticsDurationChart', () => {
it('computes the plottable data as expected', () => {
const plottableData = getDurationChartData(transformedDurationData, startDate, endDate);
expect(plottableData).toStrictEqual(durationChartPlottableData);
});
});
}); });
...@@ -482,3 +482,27 @@ describe('secondsToMilliseconds', () => { ...@@ -482,3 +482,27 @@ describe('secondsToMilliseconds', () => {
expect(datetimeUtility.secondsToMilliseconds(123)).toBe(123000); expect(datetimeUtility.secondsToMilliseconds(123)).toBe(123000);
}); });
}); });
describe('dayAfter', () => {
const date = new Date('2019-07-16T00:00:00.000Z');
it('returns the following date', () => {
const nextDay = datetimeUtility.dayAfter(date);
const expectedNextDate = new Date('2019-07-17T00:00:00.000Z');
expect(nextDay).toStrictEqual(expectedNextDate);
});
it('does not modifiy the original date', () => {
datetimeUtility.dayAfter(date);
expect(date).toStrictEqual(new Date('2019-07-16T00:00:00.000Z'));
});
});
describe('secondsToDays', () => {
it('converts seconds to days correctly', () => {
expect(datetimeUtility.secondsToDays(0)).toBe(0);
expect(datetimeUtility.secondsToDays(90000)).toBe(1);
expect(datetimeUtility.secondsToDays(270000)).toBe(3);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment