Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/analytics/instance_statistics')
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue30
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue64
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue215
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue143
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/constants.js5
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql4
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql4
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql34
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql76
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql13
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/index.js24
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/utils.js69
12 files changed, 681 insertions, 0 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
new file mode 100644
index 00000000000..7aa5c98aa0b
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -0,0 +1,30 @@
+<script>
+import InstanceCounts from './instance_counts.vue';
+import PipelinesChart from './pipelines_chart.vue';
+import UsersChart from './users_chart.vue';
+import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
+
+export default {
+ name: 'InstanceStatisticsApp',
+ components: {
+ InstanceCounts,
+ PipelinesChart,
+ UsersChart,
+ },
+ TOTAL_DAYS_TO_SHOW,
+ START_DATE,
+ TODAY,
+};
+</script>
+
+<template>
+ <div>
+ <instance-counts />
+ <users-chart
+ :start-date="$options.START_DATE"
+ :end-date="$options.TODAY"
+ :total-data-points="$options.TOTAL_DAYS_TO_SHOW"
+ />
+ <pipelines-chart />
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue
new file mode 100644
index 00000000000..4fbfb4daf22
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue
@@ -0,0 +1,64 @@
+<script>
+import * as Sentry from '~/sentry/wrapper';
+import { s__ } from '~/locale';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
+import MetricCard from '~/analytics/shared/components/metric_card.vue';
+import instanceStatisticsCountQuery from '../graphql/queries/instance_statistics_count.query.graphql';
+
+const defaultPrecision = 0;
+
+export default {
+ name: 'InstanceCounts',
+ components: {
+ MetricCard,
+ },
+ data() {
+ return {
+ counts: [],
+ };
+ },
+ apollo: {
+ counts: {
+ query: instanceStatisticsCountQuery,
+ update(data) {
+ return Object.entries(data).map(([key, obj]) => {
+ const label = this.$options.i18n.labels[key];
+ const formatter = getFormatter(SUPPORTED_FORMATS.number);
+ const value = obj.nodes?.length ? formatter(obj.nodes[0].count, defaultPrecision) : null;
+
+ return {
+ key,
+ value,
+ label,
+ };
+ });
+ },
+ error(error) {
+ createFlash(this.$options.i18n.loadCountsError);
+ Sentry.captureException(error);
+ },
+ },
+ },
+ i18n: {
+ labels: {
+ users: s__('InstanceStatistics|Users'),
+ projects: s__('InstanceStatistics|Projects'),
+ groups: s__('InstanceStatistics|Groups'),
+ issues: s__('InstanceStatistics|Issues'),
+ mergeRequests: s__('InstanceStatistics|Merge Requests'),
+ pipelines: s__('InstanceStatistics|Pipelines'),
+ },
+ loadCountsError: s__('Could not load instance counts. Please refresh the page to try again.'),
+ },
+};
+</script>
+
+<template>
+ <metric-card
+ :title="__('Instance Statistics')"
+ :metrics="counts"
+ :is-loading="$apollo.queries.counts.loading"
+ class="gl-mt-4"
+ />
+</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
new file mode 100644
index 00000000000..b16d960402b
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue
@@ -0,0 +1,215 @@
+<script>
+import { GlLineChart } from '@gitlab/ui/dist/charts';
+import { GlAlert } from '@gitlab/ui';
+import { mapKeys, mapValues, pick, some, sum } from 'lodash';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
+import { s__ } from '~/locale';
+import {
+ differenceInMonths,
+ formatDateAsMonth,
+ getDayDifference,
+} from '~/lib/utils/datetime_utility';
+import { getAverageByMonth, sortByDate, extractValues } from '../utils';
+import pipelineStatsQuery from '../graphql/queries/pipeline_stats.query.graphql';
+import { TODAY, START_DATE } from '../constants';
+
+const DATA_KEYS = [
+ 'pipelinesTotal',
+ 'pipelinesSucceeded',
+ 'pipelinesFailed',
+ 'pipelinesCanceled',
+ 'pipelinesSkipped',
+];
+const PREFIX = 'pipelines';
+
+export default {
+ name: 'PipelinesChart',
+ components: {
+ GlLineChart,
+ GlAlert,
+ ChartSkeletonLoader,
+ },
+ startDate: START_DATE,
+ endDate: TODAY,
+ i18n: {
+ loadPipelineChartError: s__(
+ 'InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again.',
+ ),
+ noDataMessage: s__('InstanceAnalytics|There is no data available.'),
+ total: s__('InstanceAnalytics|Total'),
+ succeeded: s__('InstanceAnalytics|Succeeded'),
+ failed: s__('InstanceAnalytics|Failed'),
+ canceled: s__('InstanceAnalytics|Canceled'),
+ skipped: s__('InstanceAnalytics|Skipped'),
+ chartTitle: s__('InstanceAnalytics|Pipelines'),
+ yAxisTitle: s__('InstanceAnalytics|Items'),
+ xAxisTitle: s__('InstanceAnalytics|Month'),
+ },
+ data() {
+ return {
+ loading: true,
+ loadingError: null,
+ };
+ },
+ apollo: {
+ pipelineStats: {
+ query: pipelineStatsQuery,
+ variables() {
+ return {
+ firstTotal: this.totalDaysToShow,
+ firstSucceeded: this.totalDaysToShow,
+ firstFailed: this.totalDaysToShow,
+ firstCanceled: this.totalDaysToShow,
+ firstSkipped: this.totalDaysToShow,
+ };
+ },
+ update(data) {
+ const allData = extractValues(data, DATA_KEYS, PREFIX, 'nodes');
+ const allPageInfo = extractValues(data, DATA_KEYS, PREFIX, 'pageInfo');
+
+ return {
+ ...mapValues(allData, sortByDate),
+ ...allPageInfo,
+ };
+ },
+ result() {
+ if (this.hasNextPage) {
+ this.fetchNextPage();
+ }
+ },
+ error() {
+ this.handleError();
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.pipelineStats.loading;
+ },
+ totalDaysToShow() {
+ return getDayDifference(this.$options.startDate, this.$options.endDate);
+ },
+ firstVariables() {
+ const allData = pick(this.pipelineStats, [
+ 'nodesTotal',
+ 'nodesSucceeded',
+ 'nodesFailed',
+ 'nodesCanceled',
+ 'nodesSkipped',
+ ]);
+ const allDayDiffs = mapValues(allData, data => {
+ const firstdataPoint = data[0];
+ if (!firstdataPoint) {
+ return 0;
+ }
+
+ return Math.max(
+ 0,
+ getDayDifference(this.$options.startDate, new Date(firstdataPoint.recordedAt)),
+ );
+ });
+
+ return mapKeys(allDayDiffs, (value, key) => key.replace('nodes', 'first'));
+ },
+ cursorVariables() {
+ const pageInfoKeys = [
+ 'pageInfoTotal',
+ 'pageInfoSucceeded',
+ 'pageInfoFailed',
+ 'pageInfoCanceled',
+ 'pageInfoSkipped',
+ ];
+
+ return extractValues(this.pipelineStats, pageInfoKeys, 'pageInfo', 'endCursor');
+ },
+ hasNextPage() {
+ return (
+ sum(Object.values(this.firstVariables)) > 0 &&
+ some(this.pipelineStats, ({ hasNextPage }) => hasNextPage)
+ );
+ },
+ hasEmptyDataSet() {
+ return this.chartData.every(({ data }) => data.length === 0);
+ },
+ chartData() {
+ const allData = pick(this.pipelineStats, [
+ 'nodesTotal',
+ 'nodesSucceeded',
+ 'nodesFailed',
+ 'nodesCanceled',
+ 'nodesSkipped',
+ ]);
+ const options = { shouldRound: true };
+ return Object.keys(allData).map(key => {
+ const i18nName = key.slice('nodes'.length).toLowerCase();
+ return {
+ name: this.$options.i18n[i18nName],
+ data: getAverageByMonth(allData[key], options),
+ };
+ });
+ },
+ range() {
+ return {
+ min: this.$options.startDate,
+ max: this.$options.endDate,
+ };
+ },
+ chartOptions() {
+ const { endDate, startDate, i18n } = this.$options;
+ return {
+ xAxis: {
+ ...this.range,
+ name: i18n.xAxisTitle,
+ type: 'time',
+ splitNumber: differenceInMonths(startDate, endDate) + 1,
+ axisLabel: {
+ interval: 0,
+ showMinLabel: false,
+ showMaxLabel: false,
+ align: 'right',
+ formatter: formatDateAsMonth,
+ },
+ },
+ yAxis: {
+ name: i18n.yAxisTitle,
+ },
+ };
+ },
+ },
+ methods: {
+ handleError() {
+ this.loadingError = true;
+ },
+ fetchNextPage() {
+ this.$apollo.queries.pipelineStats
+ .fetchMore({
+ variables: {
+ ...this.firstVariables,
+ ...this.cursorVariables,
+ },
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return Object.keys(fetchMoreResult).reduce((memo, key) => {
+ const { nodes, ...rest } = fetchMoreResult[key];
+ const previousNodes = previousResult[key].nodes;
+ return { ...memo, [key]: { ...rest, nodes: [...previousNodes, ...nodes] } };
+ }, {});
+ },
+ })
+ .catch(this.handleError);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h3>{{ $options.i18n.chartTitle }}</h3>
+ <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
+ {{ this.$options.i18n.loadPipelineChartError }}
+ </gl-alert>
+ <chart-skeleton-loader v-else-if="isLoading" />
+ <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3">
+ {{ $options.i18n.noDataMessage }}
+ </gl-alert>
+ <gl-line-chart v-else :option="chartOptions" :include-legend-avg-max="true" :data="chartData" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue
new file mode 100644
index 00000000000..a4a1d40b70b
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue
@@ -0,0 +1,143 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import produce from 'immer';
+import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/wrapper';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
+import { __ } from '~/locale';
+import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
+import usersQuery from '../graphql/queries/users.query.graphql';
+import { getAverageByMonth } from '../utils';
+
+const sortByDate = data => sortBy(data, item => new Date(item[0]).getTime());
+
+export default {
+ name: 'UsersChart',
+ components: { GlAlert, GlAreaChart, ChartSkeletonLoader },
+ props: {
+ startDate: {
+ type: Date,
+ required: true,
+ },
+ endDate: {
+ type: Date,
+ required: true,
+ },
+ totalDataPoints: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loadingError: null,
+ users: [],
+ pageInfo: null,
+ };
+ },
+ apollo: {
+ users: {
+ query: usersQuery,
+ variables() {
+ return {
+ first: this.totalDataPoints,
+ after: null,
+ };
+ },
+ update(data) {
+ return data.users?.nodes || [];
+ },
+ result({ data }) {
+ const {
+ users: { pageInfo },
+ } = data;
+ this.pageInfo = pageInfo;
+ this.fetchNextPage();
+ },
+ error(error) {
+ this.handleError(error);
+ },
+ },
+ },
+ i18n: {
+ yAxisTitle: __('Total users'),
+ xAxisTitle: __('Month'),
+ loadUserChartError: __('Could not load the user chart. Please refresh the page to try again.'),
+ noDataMessage: __('There is no data available.'),
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.users.loading || this.pageInfo?.hasNextPage;
+ },
+ chartUserData() {
+ const averaged = getAverageByMonth(
+ this.users.length > this.totalDataPoints
+ ? this.users.slice(0, this.totalDataPoints)
+ : this.users,
+ { shouldRound: true },
+ );
+ return sortByDate(averaged);
+ },
+ options() {
+ return {
+ xAxis: {
+ name: this.$options.i18n.xAxisTitle,
+ type: 'category',
+ axisLabel: {
+ formatter: formatDateAsMonth,
+ },
+ },
+ yAxis: {
+ name: this.$options.i18n.yAxisTitle,
+ },
+ };
+ },
+ },
+ methods: {
+ handleError(error) {
+ this.loadingError = true;
+ this.users = [];
+ Sentry.captureException(error);
+ },
+ fetchNextPage() {
+ if (this.pageInfo?.hasNextPage) {
+ this.$apollo.queries.users
+ .fetchMore({
+ variables: { first: this.totalDataPoints, after: this.pageInfo.endCursor },
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return produce(fetchMoreResult, newUsers => {
+ // eslint-disable-next-line no-param-reassign
+ newUsers.users.nodes = [...previousResult.users.nodes, ...newUsers.users.nodes];
+ });
+ },
+ })
+ .catch(this.handleError);
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h3>{{ $options.i18n.yAxisTitle }}</h3>
+ <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
+ {{ this.$options.i18n.loadUserChartError }}
+ </gl-alert>
+ <chart-skeleton-loader v-else-if="isLoading" />
+ <gl-alert v-else-if="!chartUserData.length" variant="info" :dismissible="false" class="gl-mt-3">
+ {{ $options.i18n.noDataMessage }}
+ </gl-alert>
+ <gl-area-chart
+ v-else
+ :option="options"
+ :include-legend-avg-max="true"
+ :data="[
+ {
+ name: $options.i18n.yAxisTitle,
+ data: chartUserData,
+ },
+ ]"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/instance_statistics/constants.js b/app/assets/javascripts/analytics/instance_statistics/constants.js
new file mode 100644
index 00000000000..846c0ef408b
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/constants.js
@@ -0,0 +1,5 @@
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+
+export const TOTAL_DAYS_TO_SHOW = 365;
+export const TODAY = new Date();
+export const START_DATE = getDateInPast(TODAY, TOTAL_DAYS_TO_SHOW);
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql
new file mode 100644
index 00000000000..40cef95c2e7
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Count on InstanceStatisticsMeasurement {
+ count
+ recordedAt
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql
new file mode 100644
index 00000000000..40cef95c2e7
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Count on InstanceStatisticsMeasurement {
+ count
+ recordedAt
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql
new file mode 100644
index 00000000000..f14c2658674
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql
@@ -0,0 +1,34 @@
+#import "../fragments/count.fragment.graphql"
+
+query getInstanceCounts {
+ projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+ groups: instanceStatisticsMeasurements(identifier: GROUPS, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+ users: instanceStatisticsMeasurements(identifier: USERS, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+ issues: instanceStatisticsMeasurements(identifier: ISSUES, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+ mergeRequests: instanceStatisticsMeasurements(identifier: MERGE_REQUESTS, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+ pipelines: instanceStatisticsMeasurements(identifier: PIPELINES, first: 1) {
+ nodes {
+ ...Count
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql
new file mode 100644
index 00000000000..3bf40403f91
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql
@@ -0,0 +1,76 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "./count.fragment.graphql"
+
+query pipelineStats(
+ $firstTotal: Int
+ $firstSucceeded: Int
+ $firstFailed: Int
+ $firstCanceled: Int
+ $firstSkipped: Int
+ $endCursorTotal: String
+ $endCursorSucceeded: String
+ $endCursorFailed: String
+ $endCursorCanceled: String
+ $endCursorSkipped: String
+) {
+ pipelinesTotal: instanceStatisticsMeasurements(
+ identifier: PIPELINES
+ first: $firstTotal
+ after: $endCursorTotal
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesSucceeded: instanceStatisticsMeasurements(
+ identifier: PIPELINES_SUCCEEDED
+ first: $firstSucceeded
+ after: $endCursorSucceeded
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesFailed: instanceStatisticsMeasurements(
+ identifier: PIPELINES_FAILED
+ first: $firstFailed
+ after: $endCursorFailed
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesCanceled: instanceStatisticsMeasurements(
+ identifier: PIPELINES_CANCELED
+ first: $firstCanceled
+ after: $endCursorCanceled
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ pipelinesSkipped: instanceStatisticsMeasurements(
+ identifier: PIPELINES_SKIPPED
+ first: $firstSkipped
+ after: $endCursorSkipped
+ ) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql
new file mode 100644
index 00000000000..6235e36eb89
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql
@@ -0,0 +1,13 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/count.fragment.graphql"
+
+query getUsersCount($first: Int, $after: String) {
+ users: instanceStatisticsMeasurements(identifier: USERS, first: $first, after: $after) {
+ nodes {
+ ...Count
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/instance_statistics/index.js b/app/assets/javascripts/analytics/instance_statistics/index.js
new file mode 100644
index 00000000000..0d7dcf6ace8
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import InstanceStatisticsApp from './components/app.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.getElementById('js-instance-statistics-app');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(h) {
+ return h(InstanceStatisticsApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js
new file mode 100644
index 00000000000..907482c0c72
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/utils.js
@@ -0,0 +1,69 @@
+import { masks } from 'dateformat';
+import { mapKeys, mapValues, pick, sortBy } from 'lodash';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+const { isoDate } = masks;
+
+/**
+ * Takes an array of items and returns one item per month with the average of the `count`s from that month
+ * @param {Array} items
+ * @param {Number} items[index].count value to be averaged
+ * @param {String} items[index].recordedAt item dateTime time stamp to be collected into a month
+ * @param {Object} options
+ * @param {Object} options.shouldRound an option to specify whether the retuned averages should be rounded
+ * @return {Array} items collected into [month, average],
+ * where month is a dateTime string representing the first of the given month
+ * and average is the average of the count
+ */
+export function getAverageByMonth(items = [], options = {}) {
+ const { shouldRound = false } = options;
+ const itemsMap = items.reduce((memo, item) => {
+ const { count, recordedAt } = item;
+ const date = new Date(recordedAt);
+ const month = formatDate(new Date(date.getFullYear(), date.getMonth(), 1), isoDate);
+ if (memo[month]) {
+ const { sum, recordCount } = memo[month];
+ return { ...memo, [month]: { sum: sum + count, recordCount: recordCount + 1 } };
+ }
+
+ return { ...memo, [month]: { sum: count, recordCount: 1 } };
+ }, {});
+
+ return Object.keys(itemsMap).map(month => {
+ const { sum, recordCount } = itemsMap[month];
+ const avg = sum / recordCount;
+ if (shouldRound) {
+ return [month, Math.round(avg)];
+ }
+
+ return [month, avg];
+ });
+}
+
+/**
+ * Extracts values given a data set and a set of keys
+ * @example
+ * const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' };
+ * extractValues(data, ['fooBar'], 'foo', 'baz') => { bazBar: 'quis' }
+ * @param {Object} data set to extract values from
+ * @param {Array} dataKeys keys describing where to look for values in the data set
+ * @param {String} replaceKey name key to be replaced in the data set
+ * @param {String} nestedKey key nested in the data set to be extracted,
+ * this is also used to rename the newly created data set
+ * @return {Object} the newly created data set with the extracted values
+ */
+export function extractValues(data, dataKeys = [], replaceKey, nestedKey) {
+ return mapKeys(pick(mapValues(data, nestedKey), dataKeys), (value, key) =>
+ key.replace(replaceKey, nestedKey),
+ );
+}
+
+/**
+ * Creates a new array of items sorted by the date string of each item
+ * @param {Array} items [description]
+ * @param {String} items[0] date string
+ * @return {Array} the new sorted array.
+ */
+export function sortByDate(items = []) {
+ return sortBy(items, ({ recordedAt }) => new Date(recordedAt).getTime());
+}