diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 10a9703a4c7cf8a99843656d43347c7047e49a5a..8487da3d62186d9efe345a225979dcff8836ee00 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -1,8 +1,9 @@
 <script>
 import { GlAlert, GlButton, GlEmptyState, GlSprintf } from '@gitlab/ui';
 import { isEmpty } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
 import { __ } from '~/locale';
+import { fetchPolicies } from '~/lib/graphql';
+import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
 import DagGraph from './dag_graph.vue';
 import DagAnnotations from './dag_annotations.vue';
 import {
@@ -27,23 +28,58 @@ export default {
     GlEmptyState,
     GlButton,
   },
-  props: {
-    graphUrl: {
-      type: String,
-      required: false,
-      default: '',
+  inject: {
+    dagDocPath: {
+      default: null,
     },
     emptySvgPath: {
-      type: String,
-      required: true,
       default: '',
     },
-    dagDocPath: {
-      type: String,
-      required: true,
+    pipelineIid: {
+      default: '',
+    },
+    pipelineProjectPath: {
       default: '',
     },
   },
+  apollo: {
+    graphData: {
+      fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+      query: getDagVisData,
+      variables() {
+        return {
+          projectPath: this.pipelineProjectPath,
+          iid: this.pipelineIid,
+        };
+      },
+      update(data) {
+        const {
+          stages: { nodes: stages },
+        } = data.project.pipeline;
+
+        const unwrappedGroups = stages
+          .map(({ name, groups: { nodes: groups } }) => {
+            return groups.map(group => {
+              return { category: name, ...group };
+            });
+          })
+          .flat(2);
+
+        const nodes = unwrappedGroups.map(group => {
+          const jobs = group.jobs.nodes.map(({ name, needs }) => {
+            return { name, needs: needs.nodes.map(need => need.name) };
+          });
+
+          return { ...group, jobs };
+        });
+
+        return nodes;
+      },
+      error() {
+        this.reportFailure(LOAD_FAILURE);
+      },
+    },
+  },
   data() {
     return {
       annotationsMap: {},
@@ -90,32 +126,20 @@ export default {
         default:
           return {
             text: this.$options.errorTexts[DEFAULT],
-            vatiant: 'danger',
+            variant: 'danger',
           };
       }
     },
+    processedData() {
+      return this.processGraphData(this.graphData);
+    },
     shouldDisplayAnnotations() {
       return !isEmpty(this.annotationsMap);
     },
     shouldDisplayGraph() {
-      return Boolean(!this.showFailureAlert && this.graphData);
+      return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
     },
   },
-  mounted() {
-    const { processGraphData, reportFailure } = this;
-
-    if (!this.graphUrl) {
-      reportFailure();
-      return;
-    }
-
-    axios
-      .get(this.graphUrl)
-      .then(response => {
-        processGraphData(response.data);
-      })
-      .catch(() => reportFailure(LOAD_FAILURE));
-  },
   methods: {
     addAnnotationToMap({ uid, source, target }) {
       this.$set(this.annotationsMap, uid, { source, target });
@@ -124,25 +148,25 @@ export default {
       let parsed;
 
       try {
-        parsed = parseData(data.stages);
+        parsed = parseData(data);
       } catch {
         this.reportFailure(PARSE_FAILURE);
-        return;
+        return {};
       }
 
       if (parsed.links.length === 1) {
         this.reportFailure(UNSUPPORTED_DATA);
-        return;
+        return {};
       }
 
       // If there are no links, we don't report failure
       // as it simply means the user does not use job dependencies
       if (parsed.links.length === 0) {
         this.hasNoDependentJobs = true;
-        return;
+        return {};
       }
 
-      this.graphData = parsed;
+      return parsed;
     },
     hideAlert() {
       this.showFailureAlert = false;
@@ -182,7 +206,7 @@ export default {
       <dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
       <dag-graph
         v-if="shouldDisplayGraph"
-        :graph-data="graphData"
+        :graph-data="processedData"
         @onFailure="reportFailure"
         @update-annotation="updateAnnotation"
       />
@@ -209,7 +233,7 @@ export default {
             </p>
           </div>
         </template>
-        <template #actions>
+        <template v-if="dagDocPath" #actions>
           <gl-button :href="dagDocPath" target="__blank" variant="success">
             {{ $options.emptyStateTexts.button }}
           </gl-button>
diff --git a/app/assets/javascripts/pipelines/components/dag/parsing_utils.js b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
index 3234f80ee912e3a761aa7459ca4caf8e65ddc29d..1ed415688f29f4472b2dc3464952cfda4b9f2e62 100644
--- a/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
     received from the endpoint into the format the d3 graph expects.
 
     Input is of the form:
-    [stages]
-      stages: {name, groups}
-        groups: [{ name, size, jobs }]
-          name is a group name; in the case that the group has one job, it is
-            also the job name
-          size is the number of parallel jobs
-          jobs: [{ name, needs}]
-            job name is either the same as the group name or group x/y
+    [nodes]
+      nodes: [{category, name, jobs, size}]
+        category is the stage name
+        name is a group name; in the case that the group has one job, it is
+          also the job name
+        size is the number of parallel jobs
+        jobs: [{ name, needs}]
+          job name is either the same as the group name or group x/y
+          needs: [job-names]
+          needs is an array of job-name strings
 
     Output is of the form:
     { nodes: [node], links: [link] }
@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
       link: { source, target, value }, with source & target being node names
         and value being a constant
 
-    We create nodes, create links, and then dedupe the links, so that in the case where
+    We create nodes in the GraphQL update function, and then here we create the node dictionary,
+    then create links, and then dedupe the links, so that in the case where
     job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
     from job 1 to job 2 then another from job 2 to job 4.
 
-    CREATE NODES
-    stage.name -> node.category
-    stage.group.name -> node.name (this is the group name if there are parallel jobs)
-    stage.group.jobs -> node.jobs
-    stage.group.size -> node.size
-
     CREATE LINKS
-    stages.groups.name -> target
-    stages.groups.needs.each -> source (source is the name of the group, not the parallel job)
+    nodes.name -> target
+    nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
     10 -> value (constant)
   */
 
-export const createNodes = data => {
-  return data.flatMap(({ groups, name }) => {
-    return groups.map(group => {
-      return { ...group, category: name };
-    });
-  });
-};
-
 export const createNodeDict = nodes => {
   return nodes.reduce((acc, node) => {
     const newNode = {
@@ -62,13 +51,6 @@ export const createNodeDict = nodes => {
   }, {});
 };
 
-export const createNodesStructure = data => {
-  const nodes = createNodes(data);
-  const nodeDict = createNodeDict(nodes);
-
-  return { nodes, nodeDict };
-};
-
 export const makeLinksFromNodes = (nodes, nodeDict) => {
   const constantLinkValue = 10; // all links are the same weight
   return nodes
@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
     return !allAncestors.includes(source);
   });
 
-export const parseData = data => {
-  const { nodes, nodeDict } = createNodesStructure(data);
+export const parseData = nodes => {
+  const nodeDict = createNodeDict(nodes);
   const allLinks = makeLinksFromNodes(nodes, nodeDict);
   const filteredLinks = filterByAncestors(allLinks, nodeDict);
   const links = uniqWith(filteredLinks, isEqual);
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..c73b186739edb888f66752884cff1f0d9c24178e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
@@ -0,0 +1,27 @@
+query getDagVisData($projectPath: ID!, $iid: ID!) {
+  project(fullPath: $projectPath) {
+    pipeline(iid: $iid) {
+      stages {
+        nodes {
+          name
+          groups {
+            nodes {
+              name
+              size
+              jobs {
+                nodes {
+                  name
+                  needs {
+                    nodes {
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 127d24c5473c219bdc9c79519e1b426939840102..e154c3f2422bb9050927a13435eefd7a3724a7a4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate';
 import { __ } from '~/locale';
 import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
 import pipelineGraph from './components/graph/graph_component.vue';
-import Dag from './components/dag/dag.vue';
+import createDagApp from './pipeline_details_dag';
 import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
 import PipelinesMediator from './pipeline_details_mediator';
 import pipelineHeader from './components/header_component.vue';
@@ -114,32 +114,6 @@ const createTestDetails = () => {
   });
 };
 
-const createDagApp = () => {
-  if (!window.gon?.features?.dagPipelineTab) {
-    return;
-  }
-
-  const el = document.querySelector('#js-pipeline-dag-vue');
-  const { pipelineDataPath, emptySvgPath, dagDocPath } = el?.dataset;
-
-  // eslint-disable-next-line no-new
-  new Vue({
-    el,
-    components: {
-      Dag,
-    },
-    render(createElement) {
-      return createElement('dag', {
-        props: {
-          graphUrl: pipelineDataPath,
-          emptySvgPath,
-          dagDocPath,
-        },
-      });
-    },
-  });
-};
-
 export default () => {
   const { dataset } = document.querySelector('.js-pipeline-details-vue');
   const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc03b4572655fac7377575f7a7922d496d86f2f6
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import Dag from './components/dag/dag.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+  defaultClient: createDefaultClient(),
+});
+
+const createDagApp = () => {
+  if (!window.gon?.features?.dagPipelineTab) {
+    return;
+  }
+
+  const el = document.querySelector('#js-pipeline-dag-vue');
+  const { pipelineProjectPath, pipelineIid, emptySvgPath, dagDocPath } = el?.dataset;
+
+  // eslint-disable-next-line no-new
+  new Vue({
+    el,
+    components: {
+      Dag,
+    },
+    apolloProvider,
+    provide: {
+      pipelineProjectPath,
+      pipelineIid,
+      emptySvgPath,
+      dagDocPath,
+    },
+    render(createElement) {
+      return createElement('dag', {});
+    },
+  });
+};
+
+export default createDagApp;
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 9199fdb99d63c8a6a432a40ee60e3855651d27e9..4ae06e1e16f6a3bcaceec8389daa3a624009f506 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -81,7 +81,7 @@
 
   - if dag_pipeline_tab_enabled
     #js-tab-dag.tab-pane
-      #js-pipeline-dag-vue{ data: { pipeline_data_path: dag_project_pipeline_path(@project, @pipeline), empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
+      #js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
 
   #js-tab-tests.tab-pane
     #js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index 0416e6197244a5b7e2a31e59e51337407fa5a858..989f6c17197198e489440c5f4464894d7104c9b9 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -1,8 +1,5 @@
 import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
 import { GlAlert, GlEmptyState } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
 import Dag from '~/pipelines/components/dag/dag.vue';
 import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
 import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
@@ -11,13 +8,11 @@ import {
   ADD_NOTE,
   REMOVE_NOTE,
   REPLACE_NOTES,
-  DEFAULT,
   PARSE_FAILURE,
-  LOAD_FAILURE,
   UNSUPPORTED_DATA,
 } from '~/pipelines/components/dag//constants';
 import {
-  mockBaseData,
+  mockParsedGraphQLNodes,
   tooSmallGraph,
   unparseableGraph,
   graphWithoutDependencies,
@@ -27,7 +22,6 @@ import {
 
 describe('Pipeline DAG graph wrapper', () => {
   let wrapper;
-  let mock;
   const getAlert = () => wrapper.find(GlAlert);
   const getAllAlerts = () => wrapper.findAll(GlAlert);
   const getGraph = () => wrapper.find(DagGraph);
@@ -35,45 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
   const getErrorText = type => wrapper.vm.$options.errorTexts[type];
   const getEmptyState = () => wrapper.find(GlEmptyState);
 
-  const dataPath = '/root/test/pipelines/90/dag.json';
-
-  const createComponent = (propsData = {}, method = shallowMount) => {
+  const createComponent = ({
+    graphData = mockParsedGraphQLNodes,
+    provideOverride = {},
+    method = shallowMount,
+  } = {}) => {
     if (wrapper?.destroy) {
       wrapper.destroy();
     }
 
     wrapper = method(Dag, {
-      propsData: {
+      provide: {
+        pipelineProjectPath: 'root/abc-dag',
+        pipelineIid: '1',
         emptySvgPath: '/my-svg',
         dagDocPath: '/my-doc',
-        ...propsData,
+        ...provideOverride,
       },
       data() {
         return {
+          graphData,
           showFailureAlert: false,
         };
       },
     });
   };
 
-  beforeEach(() => {
-    mock = new MockAdapter(axios);
-  });
-
   afterEach(() => {
-    mock.restore();
     wrapper.destroy();
     wrapper = null;
   });
 
-  describe('when there is no dataUrl', () => {
+  describe('when a query argument is undefined', () => {
     beforeEach(() => {
-      createComponent({ graphUrl: undefined });
+      createComponent({
+        provideOverride: { pipelineProjectPath: undefined },
+        graphData: null,
+      });
     });
 
-    it('shows the DEFAULT alert and not the graph', () => {
-      expect(getAlert().exists()).toBe(true);
-      expect(getAlert().text()).toBe(getErrorText(DEFAULT));
+    it('does not render the graph', async () => {
       expect(getGraph().exists()).toBe(false);
     });
 
@@ -82,36 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
     });
   });
 
-  describe('when there is a dataUrl', () => {
-    describe('but the data fetch fails', () => {
+  describe('when all query variables are defined', () => {
+    describe('but the parse fails', () => {
       beforeEach(async () => {
-        mock.onGet(dataPath).replyOnce(500);
-        createComponent({ graphUrl: dataPath });
-
-        await wrapper.vm.$nextTick();
-
-        return waitForPromises();
-      });
-
-      it('shows the LOAD_FAILURE alert and not the graph', () => {
-        expect(getAlert().exists()).toBe(true);
-        expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE));
-        expect(getGraph().exists()).toBe(false);
-      });
-
-      it('does not render the empty state', () => {
-        expect(getEmptyState().exists()).toBe(false);
-      });
-    });
-
-    describe('the data fetch succeeds but the parse fails', () => {
-      beforeEach(async () => {
-        mock.onGet(dataPath).replyOnce(200, unparseableGraph);
-        createComponent({ graphUrl: dataPath });
-
-        await wrapper.vm.$nextTick();
-
-        return waitForPromises();
+        createComponent({
+          graphData: unparseableGraph,
+        });
       });
 
       it('shows the PARSE_FAILURE alert and not the graph', () => {
@@ -125,14 +96,9 @@ describe('Pipeline DAG graph wrapper', () => {
       });
     });
 
-    describe('and the data fetch and parse succeeds', () => {
+    describe('parse succeeds', () => {
       beforeEach(async () => {
-        mock.onGet(dataPath).replyOnce(200, mockBaseData);
-        createComponent({ graphUrl: dataPath }, mount);
-
-        await wrapper.vm.$nextTick();
-
-        return waitForPromises();
+        createComponent({ method: mount });
       });
 
       it('shows the graph', () => {
@@ -144,14 +110,11 @@ describe('Pipeline DAG graph wrapper', () => {
       });
     });
 
-    describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
+    describe('parse succeeds, but the resulting graph is too small', () => {
       beforeEach(async () => {
-        mock.onGet(dataPath).replyOnce(200, tooSmallGraph);
-        createComponent({ graphUrl: dataPath });
-
-        await wrapper.vm.$nextTick();
-
-        return waitForPromises();
+        createComponent({
+          graphData: tooSmallGraph,
+        });
       });
 
       it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
@@ -165,14 +128,12 @@ describe('Pipeline DAG graph wrapper', () => {
       });
     });
 
-    describe('the data fetch succeeds but the returned data is empty', () => {
+    describe('the returned data is empty', () => {
       beforeEach(async () => {
-        mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies);
-        createComponent({ graphUrl: dataPath }, mount);
-
-        await wrapper.vm.$nextTick();
-
-        return waitForPromises();
+        createComponent({
+          method: mount,
+          graphData: graphWithoutDependencies,
+        });
       });
 
       it('does not render an error alert or the graph', () => {
@@ -188,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
 
   describe('annotations', () => {
     beforeEach(async () => {
-      mock.onGet(dataPath).replyOnce(200, mockBaseData);
-      createComponent({ graphUrl: dataPath }, mount);
-
-      await wrapper.vm.$nextTick();
-
-      return waitForPromises();
+      createComponent();
     });
 
     it('toggles on link mouseover and mouseout', async () => {
diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
index a50163411ed4f858f994832568bf7378a406a7ba..37a7d07485b2b4e328bdaf217d115201ce2355a2 100644
--- a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
@@ -1,9 +1,9 @@
 import { createSankey } from '~/pipelines/components/dag/drawing_utils';
 import { parseData } from '~/pipelines/components/dag/parsing_utils';
-import { mockBaseData } from './mock_data';
+import { mockParsedGraphQLNodes } from './mock_data';
 
 describe('DAG visualization drawing utilities', () => {
-  const parsed = parseData(mockBaseData.stages);
+  const parsed = parseData(mockParsedGraphQLNodes);
 
   const layoutSettings = {
     width: 200,
diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/pipelines/components/dag/mock_data.js
index 3b39b9cd21cfc43554d22133560fc58da1912b40..e7e938041954252479df5d848bcb3a7bf91dc18a 100644
--- a/spec/frontend/pipelines/components/dag/mock_data.js
+++ b/spec/frontend/pipelines/components/dag/mock_data.js
@@ -1,127 +1,56 @@
-/*
-  It is important that the simple base include parallel jobs
-  as well as non-parallel jobs with spaces in the name to prevent
-  us relying on spaces as an indicator.
-*/
-export const mockBaseData = {
-  stages: [
-    {
-      name: 'test',
-      groups: [
-        {
-          name: 'jest',
-          size: 2,
-          jobs: [{ name: 'jest 1/2', needs: ['frontend fixtures'] }, { name: 'jest 2/2' }],
-        },
-        {
-          name: 'rspec',
-          size: 1,
-          jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
-        },
-      ],
-    },
-    {
-      name: 'fixtures',
-      groups: [
-        {
-          name: 'frontend fixtures',
-          size: 1,
-          jobs: [{ name: 'frontend fixtures' }],
-        },
-      ],
-    },
-    {
-      name: 'un-needed',
-      groups: [
-        {
-          name: 'un-needed',
-          size: 1,
-          jobs: [{ name: 'un-needed' }],
-        },
-      ],
-    },
-  ],
-};
-
-export const tooSmallGraph = {
-  stages: [
-    {
-      name: 'test',
-      groups: [
-        {
-          name: 'jest',
-          size: 2,
-          jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
-        },
-        {
-          name: 'rspec',
-          size: 1,
-          jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
-        },
-      ],
-    },
-    {
-      name: 'fixtures',
-      groups: [
-        {
-          name: 'frontend fixtures',
-          size: 1,
-          jobs: [{ name: 'frontend fixtures' }],
-        },
-      ],
-    },
-    {
-      name: 'un-needed',
-      groups: [
-        {
-          name: 'un-needed',
-          size: 1,
-          jobs: [{ name: 'un-needed' }],
-        },
-      ],
-    },
-  ],
-};
+export const tooSmallGraph = [
+  {
+    category: 'test',
+    name: 'jest',
+    size: 2,
+    jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
+  },
+  {
+    category: 'test',
+    name: 'rspec',
+    size: 1,
+    jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
+  },
+  {
+    category: 'fixtures',
+    name: 'frontend fixtures',
+    size: 1,
+    jobs: [{ name: 'frontend fixtures' }],
+  },
+  {
+    category: 'un-needed',
+    name: 'un-needed',
+    size: 1,
+    jobs: [{ name: 'un-needed' }],
+  },
+];
 
-export const graphWithoutDependencies = {
-  stages: [
-    {
-      name: 'test',
-      groups: [
-        {
-          name: 'jest',
-          size: 2,
-          jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
-        },
-        {
-          name: 'rspec',
-          size: 1,
-          jobs: [{ name: 'rspec' }],
-        },
-      ],
-    },
-    {
-      name: 'fixtures',
-      groups: [
-        {
-          name: 'frontend fixtures',
-          size: 1,
-          jobs: [{ name: 'frontend fixtures' }],
-        },
-      ],
-    },
-    {
-      name: 'un-needed',
-      groups: [
-        {
-          name: 'un-needed',
-          size: 1,
-          jobs: [{ name: 'un-needed' }],
-        },
-      ],
-    },
-  ],
-};
+export const graphWithoutDependencies = [
+  {
+    category: 'test',
+    name: 'jest',
+    size: 2,
+    jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
+  },
+  {
+    category: 'test',
+    name: 'rspec',
+    size: 1,
+    jobs: [{ name: 'rspec' }],
+  },
+  {
+    category: 'fixtures',
+    name: 'frontend fixtures',
+    size: 1,
+    jobs: [{ name: 'frontend fixtures' }],
+  },
+  {
+    category: 'un-needed',
+    name: 'un-needed',
+    size: 1,
+    jobs: [{ name: 'un-needed' }],
+  },
+];
 
 export const unparseableGraph = [
   {
@@ -468,3 +397,264 @@ export const multiNote = {
     },
   },
 };
+
+/*
+  It is important that the base include parallel jobs
+  as well as non-parallel jobs with spaces in the name to prevent
+  us relying on spaces as an indicator.
+*/
+
+export const mockParsedGraphQLNodes = [
+  {
+    category: 'build',
+    name: 'build_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'build_a',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'build',
+    name: 'build_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'build_b',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'test',
+    name: 'test_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'test_a',
+        needs: ['build_a'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'test',
+    name: 'test_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'test_b',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'test',
+    name: 'test_c',
+    size: 1,
+    jobs: [
+      {
+        name: 'test_c',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'test',
+    name: 'test_d',
+    size: 1,
+    jobs: [
+      {
+        name: 'test_d',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'post-test',
+    name: 'post_test_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'post_test_a',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'post-test',
+    name: 'post_test_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'post_test_b',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'post-test',
+    name: 'post_test_c',
+    size: 1,
+    jobs: [
+      {
+        name: 'post_test_c',
+        needs: ['test_b', 'test_a'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'staging',
+    name: 'staging_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'staging_a',
+        needs: ['post_test_a'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'staging',
+    name: 'staging_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'staging_b',
+        needs: ['post_test_b'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'staging',
+    name: 'staging_c',
+    size: 1,
+    jobs: [
+      {
+        name: 'staging_c',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'staging',
+    name: 'staging_d',
+    size: 1,
+    jobs: [
+      {
+        name: 'staging_d',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'staging',
+    name: 'staging_e',
+    size: 1,
+    jobs: [
+      {
+        name: 'staging_e',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'canary',
+    name: 'canary_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'canary_a',
+        needs: ['staging_b', 'staging_a'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'canary',
+    name: 'canary_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'canary_b',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'canary',
+    name: 'canary_c',
+    size: 1,
+    jobs: [
+      {
+        name: 'canary_c',
+        needs: ['staging_b'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'production',
+    name: 'production_a',
+    size: 1,
+    jobs: [
+      {
+        name: 'production_a',
+        needs: ['canary_a'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'production',
+    name: 'production_b',
+    size: 1,
+    jobs: [
+      {
+        name: 'production_b',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'production',
+    name: 'production_c',
+    size: 1,
+    jobs: [
+      {
+        name: 'production_c',
+        needs: [],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+  {
+    category: 'production',
+    name: 'production_d',
+    size: 1,
+    jobs: [
+      {
+        name: 'production_d',
+        needs: ['canary_c'],
+      },
+    ],
+    __typename: 'CiGroup',
+  },
+];
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
index d9a1296e5720b1818cef697d656aefdc27a115c7..e93fa8e67600cb877336080e8966290afb67c69c 100644
--- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -1,5 +1,5 @@
 import {
-  createNodesStructure,
+  createNodeDict,
   makeLinksFromNodes,
   filterByAncestors,
   parseData,
@@ -8,56 +8,17 @@ import {
 } from '~/pipelines/components/dag/parsing_utils';
 
 import { createSankey } from '~/pipelines/components/dag/drawing_utils';
-import { mockBaseData } from './mock_data';
+import { mockParsedGraphQLNodes } from './mock_data';
 
 describe('DAG visualization parsing utilities', () => {
-  const { nodes, nodeDict } = createNodesStructure(mockBaseData.stages);
-  const unfilteredLinks = makeLinksFromNodes(nodes, nodeDict);
-  const parsed = parseData(mockBaseData.stages);
-
-  const layoutSettings = {
-    width: 200,
-    height: 200,
-    nodeWidth: 10,
-    nodePadding: 20,
-    paddingForLabels: 100,
-  };
-
-  const sankeyLayout = createSankey(layoutSettings)(parsed);
-
-  describe('createNodesStructure', () => {
-    const parallelGroupName = 'jest';
-    const parallelJobName = 'jest 1/2';
-    const singleJobName = 'frontend fixtures';
-
-    const { name, jobs, size } = mockBaseData.stages[0].groups[0];
-
-    it('returns the expected node structure', () => {
-      expect(nodes[0]).toHaveProperty('category', mockBaseData.stages[0].name);
-      expect(nodes[0]).toHaveProperty('name', name);
-      expect(nodes[0]).toHaveProperty('jobs', jobs);
-      expect(nodes[0]).toHaveProperty('size', size);
-    });
-
-    it('adds needs to top level of nodeDict entries', () => {
-      expect(nodeDict[parallelGroupName]).toHaveProperty('needs');
-      expect(nodeDict[parallelJobName]).toHaveProperty('needs');
-      expect(nodeDict[singleJobName]).toHaveProperty('needs');
-    });
-
-    it('makes entries in nodeDict for jobs and parallel jobs', () => {
-      const nodeNames = Object.keys(nodeDict);
-
-      expect(nodeNames.includes(parallelGroupName)).toBe(true);
-      expect(nodeNames.includes(parallelJobName)).toBe(true);
-      expect(nodeNames.includes(singleJobName)).toBe(true);
-    });
-  });
+  const nodeDict = createNodeDict(mockParsedGraphQLNodes);
+  const unfilteredLinks = makeLinksFromNodes(mockParsedGraphQLNodes, nodeDict);
+  const parsed = parseData(mockParsedGraphQLNodes);
 
   describe('makeLinksFromNodes', () => {
     it('returns the expected link structure', () => {
-      expect(unfilteredLinks[0]).toHaveProperty('source', 'frontend fixtures');
-      expect(unfilteredLinks[0]).toHaveProperty('target', 'jest');
+      expect(unfilteredLinks[0]).toHaveProperty('source', 'build_a');
+      expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a');
       expect(unfilteredLinks[0]).toHaveProperty('value', 10);
     });
   });
@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
 
   describe('removeOrphanNodes', () => {
     it('removes sankey nodes that have no needs and are not needed', () => {
+      const layoutSettings = {
+        width: 200,
+        height: 200,
+        nodeWidth: 10,
+        nodePadding: 20,
+        paddingForLabels: 100,
+      };
+
+      const sankeyLayout = createSankey(layoutSettings)(parsed);
       const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
-      expect(cleanedNodes).toHaveLength(sankeyLayout.nodes.length - 1);
+      /*
+        These lengths are determined by the mock data.
+        If the data changes, the numbers may also change.
+      */
+      expect(parsed.nodes).toHaveLength(21);
+      expect(cleanedNodes).toHaveLength(12);
     });
   });