Commit 79f4bf53 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'add-utils-methods-for-ca-duration-chart' into 'master'

Add util methods for CA duration chart

See merge request gitlab-org/gitlab!20566
parents f13bf12e 3784191e
......@@ -602,3 +602,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => {
* @return {Number} number of milliseconds
*/
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 { convertToSnakeCase } from '~/lib/utils/text_utility';
import { newDate, dayAfter, secondsToDays } from '~/lib/utils/datetime_utility';
import { isString } from 'underscore';
import dateFormat from 'dateformat';
import { dateFormats } from '../shared/constants';
const EVENT_TYPE_LABEL = 'label';
......@@ -45,3 +48,108 @@ export const nestQueryStringKeys = (obj = null, targetKey = '') => {
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;
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 { events } = getJSONFixture(endpoints.stageEvents(stage));
return {
......@@ -105,3 +106,39 @@ export const customStageEvents = [
];
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 {
eventsByIdentifier,
getLabelEventsIdentifiers,
nestQueryStringKeys,
flattenDurationChartData,
getDurationChartData,
} from 'ee/analytics/cycle_analytics/utils';
import {
customStageEvents as events,
labelStartEvent,
labelStopEvent,
customStageStartEvents as startEvents,
transformedDurationData,
flattenedDurationData,
durationChartPlottableData,
startDate,
endDate,
} from './mock_data';
const labelEvents = [labelStartEvent, labelStopEvent].map(i => i.identifier);
......@@ -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', () => {
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