diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_chart.vue b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_chart.vue
index b1cab2026751fb08c649f923670b56c366aaa051..61e0e7497d09099f600560bc0bcbb308c47baa99 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_chart.vue
+++ b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_chart.vue
@@ -1,5 +1,4 @@
 <script>
-import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
 import { joinPaths } from '~/lib/utils/url_utility';
 import { createAlert } from '~/alert';
 import { toYmd } from '~/analytics/shared/utils';
@@ -12,7 +11,7 @@ import ProjectFlowMetricsQuery from '../graphql/project_flow_metrics.query.graph
 import GroupDoraMetricsQuery from '../graphql/group_dora_metrics.query.graphql';
 import ProjectDoraMetricsQuery from '../graphql/project_dora_metrics.query.graphql';
 import { BUCKETING_INTERVAL_ALL, MERGE_REQUESTS_STATE_MERGED } from '../graphql/constants';
-import { DASHBOARD_LOADING_FAILURE, DASHBOARD_NO_DATA, CHART_LOADING_FAILURE } from '../constants';
+import { DASHBOARD_LOADING_FAILURE, CHART_LOADING_FAILURE } from '../constants';
 import {
   fetchMetricsForTimePeriods,
   extractGraphqlVulnerabilitiesData,
@@ -21,10 +20,10 @@ import {
   extractGraphqlMergeRequestsData,
 } from '../api';
 import {
-  hasDoraMetricValues,
-  generateDoraTimePeriodComparisonTable,
+  generateSkeletonTableData,
+  generateMetricComparisons,
   generateSparklineCharts,
-  mergeSparklineCharts,
+  mergeTableData,
   generateDateRanges,
   generateChartTimePeriods,
   generateValueStreamDashboardStartDate,
@@ -46,8 +45,6 @@ const extractQueryResponseFromNamespace = ({ result, resultKey }) => {
 export default {
   name: 'ComparisonChart',
   components: {
-    GlAlert,
-    GlSkeletonLoader,
     ComparisonTable,
   },
   props: {
@@ -72,25 +69,19 @@ export default {
   },
   data() {
     return {
-      tableData: [],
+      tableData: {},
       chartData: {},
-      loadingTable: false,
     };
   },
   computed: {
-    hasData() {
-      return Boolean(this.allData.length);
+    skeletonData() {
+      return generateSkeletonTableData(this.excludeMetrics);
     },
-    hasTableData() {
-      return Boolean(this.tableData.length);
-    },
-    hasChartData() {
-      return Boolean(Object.keys(this.chartData).length);
-    },
-    allData() {
-      return this.hasChartData
-        ? mergeSparklineCharts(this.tableData, this.chartData)
-        : this.tableData;
+    combinedData() {
+      let data = this.skeletonData;
+      data = mergeTableData(data, this.tableData);
+      data = mergeTableData(data, this.chartData);
+      return data;
     },
     namespaceRequestPath() {
       return this.isProject ? this.requestPath : joinPaths('groups', this.requestPath);
@@ -103,15 +94,8 @@ export default {
     },
   },
   async mounted() {
-    this.loadingTable = true;
-    try {
-      await this.fetchTableMetrics();
-      if (this.hasTableData) {
-        await this.fetchSparklineMetrics();
-      }
-    } finally {
-      this.loadingTable = false;
-    }
+    await this.fetchTableMetrics();
+    await this.fetchSparklineMetrics();
   },
   methods: {
     async fetchFlowMetricsQuery({ isProject, ...variables }) {
@@ -213,12 +197,7 @@ export default {
           this.fetchGraphqlData,
         );
 
-        this.tableData = hasDoraMetricValues(timePeriods)
-          ? generateDoraTimePeriodComparisonTable({
-              timePeriods,
-              excludeMetrics: this.excludeMetrics,
-            })
-          : [];
+        this.tableData = generateMetricComparisons(timePeriods);
       } catch (error) {
         createAlert({ message: DASHBOARD_LOADING_FAILURE, error, captureError: true });
       }
@@ -230,30 +209,20 @@ export default {
           this.fetchGraphqlData,
         );
 
-        this.chartData = hasDoraMetricValues(chartData) ? generateSparklineCharts(chartData) : {};
+        this.chartData = generateSparklineCharts(chartData);
       } catch (error) {
         createAlert({ message: CHART_LOADING_FAILURE, error, captureError: true });
       }
     },
   },
-  i18n: {
-    noData: DASHBOARD_NO_DATA,
-  },
   now,
 };
 </script>
 <template>
-  <div>
-    <gl-skeleton-loader v-if="loadingTable" />
-    <gl-alert v-else-if="!hasData" variant="info" :dismissible="false">{{
-      $options.i18n.noData
-    }}</gl-alert>
-    <comparison-table
-      v-else
-      :table-data="allData"
-      :request-path="namespaceRequestPath"
-      :is-project="isProject"
-      :now="$options.now"
-    />
-  </div>
+  <comparison-table
+    :table-data="combinedData"
+    :request-path="namespaceRequestPath"
+    :is-project="isProject"
+    :now="$options.now"
+  />
 </template>
diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
index 1741fe7a982dd46ca0d29af968423d1246e7c891..cc99f09f2cb2a6e2d7a53b8dd0f6c73b7728f476 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
+++ b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
@@ -66,26 +66,31 @@ export default {
     </template>
 
     <template #cell()="{ value: { value, change, valueLimitMessage }, item: { invertTrendColor } }">
-      {{ value }}
-      <gl-icon
-        v-if="valueLimitMessage"
-        v-gl-tooltip.hover
-        class="gl-text-blue-600"
-        name="information-o"
-        :title="valueLimitMessage"
-        data-testid="metric_max_value_info_icon"
-      />
-      <trend-indicator
-        v-else-if="change"
-        :change="change"
-        :invert-color="invertTrendColor"
-        data-testid="metric_trend_indicator"
-      />
+      <span v-if="value === undefined" data-testid="metric-comparison-skeleton">
+        <gl-skeleton-loader :lines="1" :width="100" />
+      </span>
+      <template v-else>
+        {{ value }}
+        <gl-icon
+          v-if="valueLimitMessage"
+          v-gl-tooltip.hover
+          class="gl-text-blue-600"
+          name="information-o"
+          :title="valueLimitMessage"
+          data-testid="metric-max-value-info-icon"
+        />
+        <trend-indicator
+          v-else-if="change"
+          :change="change"
+          :invert-color="invertTrendColor"
+          data-testid="metric-trend-indicator"
+        />
+      </template>
     </template>
 
     <template #cell(metric)="{ value: { identifier } }">
       <metric-table-cell
-        :data-testid="`${identifier}_metric_cell`"
+        :data-testid="`${identifier}-metric-cell`"
         :identifier="identifier"
         :request-path="requestPath"
         :is-project="isProject"
@@ -101,9 +106,9 @@ export default {
         :data="data"
         :smooth="0.2"
         :gradient="chartGradient(invertTrendColor)"
-        data-testid="metric_chart"
+        data-testid="metric-chart"
       />
-      <div v-else class="gl-py-4" data-testid="metric_chart_skeleton">
+      <div v-else class="gl-py-4" data-testid="metric-chart-skeleton">
         <gl-skeleton-loader :lines="1" :width="100" />
       </div>
     </template>
diff --git a/ee/app/assets/javascripts/analytics/dashboards/constants.js b/ee/app/assets/javascripts/analytics/dashboards/constants.js
index 0bb209f42a0cb1ced67bc25a649c9dccc4586a86..017a3fee307ea5e50dc95583ab1e7fd9ab1e7068 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/constants.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/constants.js
@@ -103,7 +103,6 @@ export const DASHBOARD_DESCRIPTION_GROUP = s__('DORA4Metrics|Metrics comparison
 export const DASHBOARD_DESCRIPTION_PROJECT = s__(
   'DORA4Metrics|Metrics comparison for %{name} project',
 );
-export const DASHBOARD_NO_DATA = __('No data available');
 export const DASHBOARD_LOADING_FAILURE = __('Failed to load');
 export const DASHBOARD_NAMESPACE_LOAD_ERROR = s__(
   'DORA4Metrics|Failed to load comparison chart for Namespace: %{fullPath}',
diff --git a/ee/app/assets/javascripts/analytics/dashboards/utils.js b/ee/app/assets/javascripts/analytics/dashboards/utils.js
index 07a53bfc742f4f92fd2fe4e949db3989c770a00c..6ec374fa2f1e35128b782a2c20bdaabee93d18dc 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/utils.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/utils.js
@@ -1,5 +1,4 @@
 import { s__, __ } from '~/locale';
-import { isNumeric } from '~/lib/utils/number_utils';
 import {
   formatDate,
   getStartOfDay,
@@ -88,51 +87,49 @@ export const percentChange = ({ current, previous }) =>
   previous > 0 && current > 0 ? (current - previous) / previous : 0;
 
 /**
- * Takes an array of timePeriod objects containing DORA metrics, and returns
- * true if any of the timePeriods contain metric values > 0.
+ * Creates the table rows filled with blank data for the comparison table. Once the data
+ * has loaded, it can be filled into the returned skeleton using `mergeTableData`.
  *
- * @param {Array} timePeriods - array of objects containing DORA metric values
- * @returns {Boolean} true if there is any metric data, otherwise false.
+ * @param {Array} excludeMetrics - Array of DORA metric identifiers to remove from the table
+ * @returns {Array} array of data-less table rows
  */
-export const hasDoraMetricValues = (timePeriods) =>
-  timePeriods.some((timePeriod) => {
-    // timePeriod may contain more attributes than just the DORA metrics,
-    // so filter out non-metrics before making a list of the raw values
-    const metricValues = Object.entries(timePeriod)
-      .filter(([k]) => Object.keys(TABLE_METRICS).includes(k))
-      .map(([, v]) => v.value);
+export const generateSkeletonTableData = (excludeMetrics = []) =>
+  Object.entries(TABLE_METRICS)
+    .filter(([identifier]) => !excludeMetrics.includes(identifier))
+    .map(([identifier, { label, invertTrendColor, valueLimit }]) => ({
+      invertTrendColor,
+      metric: { identifier, value: label },
+      valueLimit,
+    }));
 
-    return metricValues.some((value) => isNumeric(value) && Number(value) > 0);
+/**
+ * Fills the provided table rows with the matching metric data, returning a copy
+ * of the original table data.
+ *
+ * @param {Array} tableData - Table rows created by `generateSkeletonTableData`
+ * @param {Object} newData - New data to enter into the table rows. Object keys match the metric ID
+ * @returns {Array} A copy of `tableData` with the new data merged into each row
+ */
+export const mergeTableData = (tableData, newData) =>
+  tableData.map((row) => {
+    const data = newData[row.metric.identifier];
+    return data ? { ...row, ...data } : row;
   });
 
 /**
  * Takes N time periods for a metric and generates the row for the comparison table.
  *
  * @param {String} identifier - ID of the metric to create a table row for.
- * @param {String} label - User friendly name of the metric to show in the table row.
  * @param {String} units - The type of units used for this metric (ex. days, /day, count)
- * @param {Boolean} invertTrendColor - Inverts the color indicator used for metric trends.
  * @param {Array} timePeriods - Array of the metrics for different time periods
  * @param {Object} valueLimit - Object representing the maximum value of a metric, mask that replaces the value if the limit is reached and a description to be used in a tooltip.
  * @returns {Object} The metric data formatted for the comparison table.
  */
-const buildMetricComparisonTableRow = ({
-  identifier,
-  label,
-  units,
-  invertTrendColor,
-  timePeriods,
-  valueLimit,
-}) => {
-  const data = {
-    invertTrendColor,
-    metric: { identifier, value: label },
-    valueLimit,
-  };
-  timePeriods.forEach((timePeriod, index) => {
+const buildMetricComparisonTableRow = ({ identifier, units, timePeriods, valueLimit }) =>
+  timePeriods.reduce((acc, timePeriod, index) => {
     // The last timePeriod is not rendered, we just use it
     // to determine the % change for the 2nd last timePeriod
-    if (index === timePeriods.length - 1) return;
+    if (index === timePeriods.length - 1) return acc;
 
     const current = timePeriod[identifier];
     const previous = timePeriods[index + 1][identifier];
@@ -149,36 +146,35 @@ const buildMetricComparisonTableRow = ({
     const formattedMetric = hasCurrentValue ? formatMetric(current.value, units) : '-';
     const value = valueLimitMessage ? valueLimit?.mask : formattedMetric;
 
-    data[timePeriod.key] = {
-      value,
-      change,
-      valueLimitMessage,
-    };
-  });
-  return data;
-};
+    return Object.assign(acc, {
+      [timePeriod.key]: {
+        value,
+        change,
+        valueLimitMessage,
+      },
+    });
+  }, {});
 
 /**
- * Takes N time periods of DORA metrics and generates the data rows
- * for the comparison table.
+ * Takes N time periods of DORA metrics and sorts the data into an
+ * object of metric comparisons, per metric.
  *
  * @param {Array} timePeriods - Array of the DORA metrics for different time periods
- * @param {Array} excludeMetrics - Array of DORA metric identifiers to remove from the table
- * @returns {Array} array comparing each DORA metric between the different time periods
+ * @returns {Object} object containing a comparisons of values for each metric
  */
-export const generateDoraTimePeriodComparisonTable = ({ timePeriods, excludeMetrics = [] }) =>
-  Object.entries(TABLE_METRICS)
-    .filter(([identifier]) => !excludeMetrics.includes(identifier))
-    .map(([identifier, { label, units, invertTrendColor, valueLimit }]) =>
-      buildMetricComparisonTableRow({
-        identifier,
-        label,
-        units,
-        invertTrendColor,
-        timePeriods,
-        valueLimit,
+export const generateMetricComparisons = (timePeriods) =>
+  Object.entries(TABLE_METRICS).reduce(
+    (acc, [identifier, { units, valueLimit }]) =>
+      Object.assign(acc, {
+        [identifier]: buildMetricComparisonTableRow({
+          identifier,
+          units,
+          timePeriods,
+          valueLimit,
+        }),
       }),
-    );
+    {},
+  );
 
 /**
  * @param {Number|'-'|null|undefined} value
@@ -206,30 +202,18 @@ export const generateSparklineCharts = (timePeriods) =>
     (acc, [identifier, { units }]) =>
       Object.assign(acc, {
         [identifier]: {
-          tooltipLabel: CHART_TOOLTIP_UNITS[units],
-          data: timePeriods.map((timePeriod) => [
-            `${formatDate(timePeriod.start, 'mmm d')} - ${formatDate(timePeriod.end, 'mmm d')}`,
-            sanitizeSparklineData(timePeriod[identifier]?.value),
-          ]),
+          chart: {
+            tooltipLabel: CHART_TOOLTIP_UNITS[units],
+            data: timePeriods.map((timePeriod) => [
+              `${formatDate(timePeriod.start, 'mmm d')} - ${formatDate(timePeriod.end, 'mmm d')}`,
+              sanitizeSparklineData(timePeriod[identifier]?.value),
+            ]),
+          },
         },
       }),
     {},
   );
 
-/**
- * Merges the results of `generateDoraTimePeriodComparisonTable` and `generateSparklineCharts`
- * into a new array for the comparison table.
- *
- * @param {Array} tableData - Table rows created by `generateDoraTimePeriodComparisonTable`
- * @param {Object} chartData - Charts object created by `generateSparklineCharts`
- * @returns {Array} A copy of tableData with `chart` added in each row
- */
-export const mergeSparklineCharts = (tableData, chartData) =>
-  tableData.map((row) => {
-    const chart = chartData[row.metric.identifier];
-    return chart ? { ...row, chart } : row;
-  });
-
 /**
  * Generate the dashboard time periods
  * this month - last month - 2 month ago - 3 month ago
diff --git a/ee/spec/frontend/analytics/dashboards/components/comparison_chart_spec.js b/ee/spec/frontend/analytics/dashboards/components/comparison_chart_spec.js
index 2ea77af4e6b0de0875242d1551414c5c308210cb..75d4a9c89fd5cdb7dc67ff1cee04fdb733a40b20 100644
--- a/ee/spec/frontend/analytics/dashboards/components/comparison_chart_spec.js
+++ b/ee/spec/frontend/analytics/dashboards/components/comparison_chart_spec.js
@@ -6,6 +6,7 @@ import {
   DASHBOARD_LOADING_FAILURE,
   CHART_LOADING_FAILURE,
 } from 'ee/analytics/dashboards/constants';
+import { generateSkeletonTableData } from 'ee/analytics/dashboards/utils';
 import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue';
 import ComparisonTable from 'ee/analytics/dashboards/components/comparison_table.vue';
 import GroupVulnerabilitiesQuery from 'ee/analytics/dashboards/graphql/group_vulnerabilities.query.graphql';
@@ -39,10 +40,6 @@ import {
   mockFlowMetricsResponseData,
   mockMergeRequestsResponseData,
   mockExcludeMetrics,
-  mockEmptyVulnerabilityResponse,
-  mockEmptyDoraResponse,
-  mockEmptyFlowMetricsResponse,
-  mockEmptyMergeRequestsResponse,
 } from '../mock_data';
 
 const mockTypePolicy = {
@@ -165,6 +162,17 @@ describe('Comparison chart', () => {
     createAlert.mockClear();
   });
 
+  describe('loading table and chart data', () => {
+    beforeEach(() => {
+      setGraphqlQueryHandlerResponses();
+      createWrapper();
+    });
+
+    it('will pass skeleton data to the comparison table', () => {
+      expect(getTableData()).toEqual(generateSkeletonTableData());
+    });
+  });
+
   describe('with table and chart data available', () => {
     beforeEach(async () => {
       setGraphqlQueryHandlerResponses();
@@ -265,10 +273,6 @@ describe('Comparison chart', () => {
         error: expect.anything(),
       });
     });
-
-    it('renders no data message', () => {
-      expect(wrapper.text()).toContain('No data available');
-    });
   });
 
   describe('failed chart request', () => {
@@ -302,42 +306,6 @@ describe('Comparison chart', () => {
     });
   });
 
-  describe('no table data available', () => {
-    // When there is no table data available the chart data requests are skipped
-
-    beforeEach(async () => {
-      setGraphqlQueryHandlerResponses({
-        doraMetricsResponse: mockEmptyDoraResponse,
-        flowMetricsResponse: mockEmptyFlowMetricsResponse,
-        vulnerabilityResponse: mockEmptyVulnerabilityResponse,
-        mergeRequestsResponse: mockEmptyMergeRequestsResponse,
-      });
-      mockApolloProvider = createMockApolloProvider();
-
-      await createWrapper({ apolloProvider: mockApolloProvider });
-    });
-
-    it('will only request dora metrics for the table', () => {
-      expectDoraMetricsRequests(MOCK_TABLE_TIME_PERIODS);
-    });
-
-    it('will only request flow metrics for the table', () => {
-      expectFlowMetricsRequests(MOCK_TABLE_TIME_PERIODS);
-    });
-
-    it('will only request vulnerability metrics for the table', () => {
-      expectVulnerabilityRequests(MOCK_TABLE_TIME_PERIODS);
-    });
-
-    it('will only merge request metrics for the table', () => {
-      expectMergeRequestsRequests(MOCK_TABLE_TIME_PERIODS);
-    });
-
-    it('renders a message when theres no table data available', () => {
-      expect(wrapper.text()).toContain('No data available');
-    });
-  });
-
   describe('isProject=true', () => {
     const fakeProjectPath = 'fake/project/path';
 
diff --git a/ee/spec/frontend/analytics/dashboards/components/comparison_table_spec.js b/ee/spec/frontend/analytics/dashboards/components/comparison_table_spec.js
index 86688a0168b460960524b0e9dfb92fef835d8edb..11f66ac093bee4b46466f7793429878e991a55d0 100644
--- a/ee/spec/frontend/analytics/dashboards/components/comparison_table_spec.js
+++ b/ee/spec/frontend/analytics/dashboards/components/comparison_table_spec.js
@@ -29,11 +29,12 @@ describe('Comparison table', () => {
     });
   };
 
-  const findMetricTableCell = (identifier) => wrapper.findByTestId(`${identifier}_metric_cell`);
-  const findChart = () => wrapper.findByTestId('metric_chart');
-  const findChartSkeleton = () => wrapper.findByTestId('metric_chart_skeleton');
-  const findTrendIndicator = () => wrapper.findByTestId('metric_trend_indicator');
-  const findValueLimitInfoIcon = () => wrapper.findByTestId('metric_max_value_info_icon');
+  const findMetricTableCell = (identifier) => wrapper.findByTestId(`${identifier}-metric-cell`);
+  const findMetricComparisonSkeletons = () => wrapper.findAllByTestId('metric-comparison-skeleton');
+  const findChart = () => wrapper.findByTestId('metric-chart');
+  const findChartSkeleton = () => wrapper.findByTestId('metric-chart-skeleton');
+  const findTrendIndicator = () => wrapper.findByTestId('metric-trend-indicator');
+  const findValueLimitInfoIcon = () => wrapper.findByTestId('metric-max-value-info-icon');
 
   it.each(Object.keys(TABLE_METRICS))('renders table cell for %s metric', (identifier) => {
     createWrapper();
@@ -41,6 +42,11 @@ describe('Comparison table', () => {
     expect(findMetricTableCell(identifier).props('identifier')).toBe(identifier);
   });
 
+  it('shows loading skeletons for each metric comparison cell', () => {
+    createWrapper({ tableData: [{ metric: mockMetric }] });
+    expect(findMetricComparisonSkeletons().length).toBe(3);
+  });
+
   describe('date range table cell', () => {
     const valueLimit = {
       max: 10001,
diff --git a/ee/spec/frontend/analytics/dashboards/mock_data.js b/ee/spec/frontend/analytics/dashboards/mock_data.js
index 84d579c30fae86df18ba70ffb7d555ab5836fda3..3fc1add0a81608be150593f7054c0f70d2a8b635 100644
--- a/ee/spec/frontend/analytics/dashboards/mock_data.js
+++ b/ee/spec/frontend/analytics/dashboards/mock_data.js
@@ -416,6 +416,13 @@ export const mockComparativeTableData = [
   },
 ];
 
+export const mockGeneratedMetricComparisons = () =>
+  mockComparativeTableData.reduce(
+    (acc, { metric, lastMonth, thisMonth, twoMonthsAgo }) =>
+      Object.assign(acc, { [metric.identifier]: { lastMonth, thisMonth, twoMonthsAgo } }),
+    {},
+  );
+
 const mockChartDataValues = (values) => values.map((v) => [expect.anything(), v]);
 
 const mockChartDataWithSameValue = (count, value) =>
@@ -423,91 +430,139 @@ const mockChartDataWithSameValue = (count, value) =>
 
 export const mockSubsetChartData = {
   change_failure_rate: {
-    data: mockChartDataWithSameValue(2, 0),
-    tooltipLabel: '%',
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+      tooltipLabel: '%',
+    },
   },
   cycle_time: {
-    data: mockChartDataWithSameValue(2, 0),
-    tooltipLabel: 'days',
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+      tooltipLabel: 'days',
+    },
   },
   deployment_frequency: {
-    data: mockChartDataWithSameValue(2, 0),
-    tooltipLabel: '/day',
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+      tooltipLabel: '/day',
+    },
   },
   deploys: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
   issues: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
   issues_completed: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
   lead_time: {
-    data: mockChartDataWithSameValue(2, 0),
-    tooltipLabel: 'days',
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+      tooltipLabel: 'days',
+    },
   },
   lead_time_for_changes: {
-    data: mockChartDataValues([1, 2]),
-    tooltipLabel: 'days',
+    chart: {
+      data: mockChartDataValues([1, 2]),
+      tooltipLabel: 'days',
+    },
   },
   time_to_restore_service: {
-    data: mockChartDataValues([100, 99]),
-    tooltipLabel: 'days',
+    chart: {
+      data: mockChartDataValues([100, 99]),
+      tooltipLabel: 'days',
+    },
   },
   vulnerability_critical: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
   vulnerability_high: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
   merge_request_throughput: {
-    data: mockChartDataWithSameValue(2, 0),
+    chart: {
+      data: mockChartDataWithSameValue(2, 0),
+    },
   },
 };
 
 export const mockChartData = {
   lead_time_for_changes: {
-    tooltipLabel: 'days',
-    data: mockChartDataWithSameValue(6, null),
+    chart: {
+      tooltipLabel: 'days',
+      data: mockChartDataWithSameValue(6, null),
+    },
   },
   time_to_restore_service: {
-    tooltipLabel: 'days',
-    data: mockChartDataWithSameValue(6, 0),
+    chart: {
+      tooltipLabel: 'days',
+      data: mockChartDataWithSameValue(6, 0),
+    },
   },
   change_failure_rate: {
-    tooltipLabel: '%',
-    data: mockChartDataWithSameValue(6, 0),
+    chart: {
+      tooltipLabel: '%',
+      data: mockChartDataWithSameValue(6, 0),
+    },
   },
   deployment_frequency: {
-    tooltipLabel: '/day',
-    data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
+    chart: {
+      tooltipLabel: '/day',
+      data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
+    },
   },
   lead_time: {
-    tooltipLabel: 'days',
-    data: mockChartDataValues([1, 2, 3, 4, 5, 6]),
+    chart: {
+      tooltipLabel: 'days',
+      data: mockChartDataValues([1, 2, 3, 4, 5, 6]),
+    },
   },
   cycle_time: {
-    tooltipLabel: 'days',
-    data: mockChartDataValues([0, 2, 4, 6, 8, 10]),
+    chart: {
+      tooltipLabel: 'days',
+      data: mockChartDataValues([0, 2, 4, 6, 8, 10]),
+    },
   },
   issues: {
-    data: mockChartDataValues([100, 98, 96, 94, 92, 90]),
+    chart: {
+      data: mockChartDataValues([100, 98, 96, 94, 92, 90]),
+    },
   },
   issues_completed: {
-    data: mockChartDataValues([200, 198, 196, 194, 192, 190]),
+    chart: {
+      data: mockChartDataValues([200, 198, 196, 194, 192, 190]),
+    },
   },
   deploys: {
-    data: mockChartDataValues([0, 1, 4, 9, 16, 25]),
+    chart: {
+      data: mockChartDataValues([0, 1, 4, 9, 16, 25]),
+    },
   },
   vulnerability_critical: {
-    data: mockChartDataValues([0, 1, 2, 3, 0, 1]),
+    chart: {
+      data: mockChartDataValues([0, 1, 2, 3, 0, 1]),
+    },
   },
   vulnerability_high: {
-    data: mockChartDataValues([0, 1, 0, 1, 0, 1]),
+    chart: {
+      data: mockChartDataValues([0, 1, 0, 1, 0, 1]),
+    },
   },
   merge_request_throughput: {
-    data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
+    chart: {
+      data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
+    },
   },
 };
 
@@ -617,17 +672,6 @@ export const mockExcludeMetrics = [
   DORA_METRICS.LEAD_TIME_FOR_CHANGES,
 ];
 
-export const mockEmptyVulnerabilityResponse = [{ date: null, critical: null, high: null }];
-export const mockEmptyDoraResponse = { metrics: [] };
-export const mockEmptyMergeRequestsResponse = { mergeRequests: {} };
-export const mockEmptyFlowMetricsResponse = {
-  issues: null,
-  issues_completed: null,
-  deploys: null,
-  cycle_time: null,
-  lead_time: null,
-};
-
 export const MOCK_LABELS = [
   { id: 1, title: 'one', color: '#FFFFFF' },
   { id: 2, title: 'two', color: '#000000' },
diff --git a/ee/spec/frontend/analytics/dashboards/utils_spec.js b/ee/spec/frontend/analytics/dashboards/utils_spec.js
index e19f2c6830580c7227f4e67a6c3e4230dd541cb3..e5d5849edf1ad6e710bb07c0c7ab07faa6069cf0 100644
--- a/ee/spec/frontend/analytics/dashboards/utils_spec.js
+++ b/ee/spec/frontend/analytics/dashboards/utils_spec.js
@@ -5,10 +5,10 @@ import { useFakeDate } from 'helpers/fake_date';
 import {
   percentChange,
   formatMetric,
-  hasDoraMetricValues,
-  generateDoraTimePeriodComparisonTable,
+  generateSkeletonTableData,
+  generateMetricComparisons,
   generateSparklineCharts,
-  mergeSparklineCharts,
+  mergeTableData,
   hasTrailingDecimalZero,
   generateDateRanges,
   generateChartTimePeriods,
@@ -16,14 +16,13 @@ import {
   generateValueStreamDashboardStartDate,
   groupDoraPerformanceScoreCountsByCategory,
 } from 'ee/analytics/dashboards/utils';
-import { CHANGE_FAILURE_RATE, LEAD_TIME_FOR_CHANGES } from 'ee/api/dora_api';
 import { LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE } from '~/api/analytics_api';
 import {
   mockMonthToDateTimePeriod,
   mockPreviousMonthTimePeriod,
   mockTwoMonthsAgoTimePeriod,
   mockThreeMonthsAgoTimePeriod,
-  mockComparativeTableData,
+  mockGeneratedMetricComparisons,
   mockChartsTimePeriods,
   mockChartData,
   mockSubsetChartsTimePeriods,
@@ -93,7 +92,24 @@ describe('Analytics Dashboards utils', () => {
     });
   });
 
-  describe('generateDoraTimePeriodComparisonTable', () => {
+  describe('generateSkeletonTableData', () => {
+    it('returns blank row data for each metric', () => {
+      const tableData = generateSkeletonTableData();
+      tableData.forEach((data) =>
+        expect(Object.keys(data)).toEqual(['invertTrendColor', 'metric', 'valueLimit']),
+      );
+    });
+
+    it('does not include metrics that were in excludeMetrics', () => {
+      const excludeMetrics = [LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE];
+      const tableData = generateSkeletonTableData(excludeMetrics);
+
+      const metrics = tableData.map(({ metric }) => metric.identifier);
+      expect(metrics).not.toEqual(expect.arrayContaining(excludeMetrics));
+    });
+  });
+
+  describe('generateMetricComparisons', () => {
     const timePeriods = [
       mockMonthToDateTimePeriod,
       mockPreviousMonthTimePeriod,
@@ -102,30 +118,19 @@ describe('Analytics Dashboards utils', () => {
     ];
 
     it('calculates the changes between the 2 time periods', () => {
-      const tableData = generateDoraTimePeriodComparisonTable({ timePeriods });
-      expect(tableData).toEqual(mockComparativeTableData);
+      const tableData = generateMetricComparisons(timePeriods);
+      expect(tableData).toEqual(mockGeneratedMetricComparisons());
     });
 
     it('returns the comparison table fields + metadata for each row', () => {
-      generateDoraTimePeriodComparisonTable({ timePeriods }).forEach((row) => {
-        expect(Object.keys(row)).toEqual([
-          'invertTrendColor',
-          'metric',
-          'valueLimit',
-          'thisMonth',
-          'lastMonth',
-          'twoMonthsAgo',
-        ]);
+      Object.values(generateMetricComparisons(timePeriods)).forEach((row) => {
+        expect(row).toMatchObject({
+          thisMonth: expect.any(Object),
+          lastMonth: expect.any(Object),
+          twoMonthsAgo: expect.any(Object),
+        });
       });
     });
-
-    it('does not include metrics that were in excludeMetrics', () => {
-      const excludeMetrics = [LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE];
-      const tableData = generateDoraTimePeriodComparisonTable({ timePeriods, excludeMetrics });
-
-      const metrics = tableData.map(({ metric }) => metric.identifier);
-      expect(metrics).not.toEqual(expect.arrayContaining(excludeMetrics));
-    });
   });
 
   describe('generateSparklineCharts', () => {
@@ -150,46 +155,19 @@ describe('Analytics Dashboards utils', () => {
     });
   });
 
-  describe('mergeSparklineCharts', () => {
-    it('returns the table data with the additive chart data', () => {
-      const chart = { data: [1, 2, 3] };
-      const rowNoChart = { metric: { identifier: 'noChart' } };
-      const rowWithChart = { metric: { identifier: 'withChart' } };
+  describe('mergeTableData', () => {
+    it('correctly integrates existing and new data', () => {
+      const newData = { chart: { data: [1, 2, 3] }, lastMonth: { test: 'test' } };
+      const rowNoData = { metric: { identifier: 'noData' } };
+      const rowWithData = { metric: { identifier: 'withData' } };
 
-      expect(mergeSparklineCharts([rowNoChart, rowWithChart], { withChart: chart })).toEqual([
-        rowNoChart,
-        { ...rowWithChart, chart },
+      expect(mergeTableData([rowNoData, rowWithData], { withData: newData })).toEqual([
+        rowNoData,
+        { ...rowWithData, ...newData },
       ]);
     });
   });
 
-  describe('hasDoraMetricValues', () => {
-    it('returns false if only non-DORA metrics contain a value > 0', () => {
-      const timePeriods = [{ nonDoraMetric: { value: 100 } }];
-      expect(hasDoraMetricValues(timePeriods)).toBe(false);
-    });
-
-    it('returns false if all DORA metrics contain a non-numerical value', () => {
-      const timePeriods = [{ [LEAD_TIME_FOR_CHANGES]: { value: 'YEET' } }];
-      expect(hasDoraMetricValues(timePeriods)).toBe(false);
-    });
-
-    it('returns false if all DORA metrics contain a value == 0', () => {
-      const timePeriods = [{ [LEAD_TIME_FOR_CHANGES]: { value: 0 } }];
-      expect(hasDoraMetricValues(timePeriods)).toBe(false);
-    });
-
-    it('returns true if any DORA metrics contain a value > 0', () => {
-      const timePeriods = [
-        {
-          [LEAD_TIME_FOR_CHANGES]: { value: 0 },
-          [CHANGE_FAILURE_RATE]: { value: 100 },
-        },
-      ];
-      expect(hasDoraMetricValues(timePeriods)).toBe(true);
-    });
-  });
-
   describe('generateDateRanges', () => {
     it('return correct value', () => {
       const now = MOCK_TABLE_TIME_PERIODS[0].end;
@@ -235,7 +213,7 @@ describe('Analytics Dashboards utils', () => {
       useFakeDate(2020, 4, 4);
 
       it('will return the correct day', () => {
-        expect(generateValueStreamDashboardStartDate().toISOString()).toEqual(
+        expect(generateValueStreamDashboardStartDate().toISOString()).toBe(
           '2020-05-04T00:00:00.000Z',
         );
       });
@@ -245,7 +223,7 @@ describe('Analytics Dashboards utils', () => {
       useFakeDate(2023, 6, 1);
 
       it('will return the previous day', () => {
-        expect(generateValueStreamDashboardStartDate().toISOString()).toEqual(
+        expect(generateValueStreamDashboardStartDate().toISOString()).toBe(
           '2023-06-30T00:00:00.000Z',
         );
       });