diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3ced4eb691a785c232942ee5365d2dc9ab7255dd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -0,0 +1,391 @@
+<script>
+import * as d3 from 'd3';
+import tooltip from '../directives/tooltip';
+import Icon from './icon.vue';
+import SvgGradient from './svg_gradient.vue';
+import {
+  GRADIENT_COLORS,
+  GRADIENT_OPACITY,
+  INVERSE_GRADIENT_COLORS,
+  INVERSE_GRADIENT_OPACITY,
+} from './bar_chart_constants';
+
+/**
+ * Renders a bar chart that can be dragged(scrolled) when the number
+ * of elements to renders surpasses that of the available viewport space
+ * while keeping even padding and a width of 24px (customizable)
+ *
+ * It can render data with the following format:
+ * graphData: [{
+ *   name: 'element' // x domain data
+ *   value: 1 // y domain data
+ * }]
+ *
+ * Used in:
+ * - Contribution analytics - all of the rows describing pushes, merge requests and issues
+ */
+
+export default {
+  directives: {
+    tooltip,
+  },
+  components: {
+    Icon,
+    SvgGradient,
+  },
+  props: {
+    graphData: {
+      type: Array,
+      required: true,
+    },
+    barWidth: {
+      type: Number,
+      required: false,
+      default: 24,
+    },
+    yAxisLabel: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      minX: -40,
+      minY: 0,
+      vbWidth: 0,
+      vbHeight: 0,
+      vpWidth: 0,
+      vpHeight: 350,
+      preserveAspectRatioType: 'xMidYMid meet',
+      containerMargin: {
+        leftRight: 30,
+      },
+      viewBoxMargin: {
+        topBottom: 150,
+      },
+      panX: 0,
+      xScale: {},
+      yScale: {},
+      zoom: {},
+      bars: {},
+      xGraphRange: 0,
+      isLoading: true,
+      paddingThreshold: 50,
+      showScrollIndicator: false,
+      showLeftScrollIndicator: false,
+      isGrabbed: false,
+      isPanAvailable: false,
+      gradientColors: GRADIENT_COLORS,
+      gradientOpacity: GRADIENT_OPACITY,
+      inverseGradientColors: INVERSE_GRADIENT_COLORS,
+      inverseGradientOpacity: INVERSE_GRADIENT_OPACITY,
+      maxTextWidth: 72,
+      rectYAxisLabelDims: {},
+      xAxisTextElements: {},
+      yAxisRectTransformPadding: 20,
+      yAxisTextTransformPadding: 10,
+      yAxisTextRotation: 90,
+    };
+  },
+  computed: {
+    svgViewBox() {
+      return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`;
+    },
+    xAxisLocation() {
+      return `translate(${this.panX}, ${this.vbHeight})`;
+    },
+    barTranslationTransform() {
+      return `translate(${this.panX}, 0)`;
+    },
+    scrollIndicatorTransform() {
+      return `translate(${this.vbWidth - 80}, 0)`;
+    },
+    activateGrabCursor() {
+      return {
+        'svg-graph-container-with-grab': this.isPanAvailable,
+        'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed,
+      };
+    },
+    yAxisLabelRectTransform() {
+      const rectWidth =
+        this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
+      const yCoord = this.vbHeight / 2 - rectWidth;
+
+      return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`;
+    },
+    yAxisLabelTextTransform() {
+      const rectWidth =
+        this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
+      const yCoord = this.vbHeight / 2 + rectWidth - 5;
+
+      return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`;
+    },
+  },
+  mounted() {
+    if (!this.allValuesEmpty) {
+      this.draw();
+    }
+  },
+  methods: {
+    draw() {
+      // update viewport
+      this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight;
+      // update viewbox
+      this.vbWidth = this.vpWidth;
+      this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom;
+      let padding = 0;
+      if (this.graphData.length * this.barWidth > this.vbWidth) {
+        this.xGraphRange = this.graphData.length * this.barWidth;
+        padding = this.calculatePadding(this.barWidth);
+        this.showScrollIndicator = true;
+        this.isPanAvailable = true;
+      } else {
+        this.xGraphRange = this.vbWidth - Math.abs(this.minX);
+      }
+
+      this.xScale = d3
+        .scaleBand()
+        .range([0, this.xGraphRange])
+        .round(true)
+        .paddingInner(padding);
+      this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]);
+
+      this.xScale.domain(this.graphData.map(d => d.name));
+      this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]);
+
+      // Zoom/Panning Function
+      this.zoom = d3
+        .zoom()
+        .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]])
+        .on('zoom', this.panGraph)
+        .on('end', this.removeGrabStyling);
+
+      const xAxis = d3.axisBottom().scale(this.xScale);
+
+      const yAxis = d3
+        .axisLeft()
+        .scale(this.yScale)
+        .ticks(4);
+
+      const renderedXAxis = d3
+        .select(this.$refs.baseSvg)
+        .select('.x-axis')
+        .call(xAxis);
+
+      this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text');
+
+      renderedXAxis.select('.domain').remove();
+
+      renderedXAxis
+        .selectAll('text')
+        .style('text-anchor', 'end')
+        .attr('dx', '-.3em')
+        .attr('dy', '-.95em')
+        .attr('class', 'tick-text')
+        .attr('transform', 'rotate(-90)');
+
+      renderedXAxis.selectAll('line').remove();
+
+      const { maxTextWidth } = this;
+      renderedXAxis.selectAll('text').each(function formatText() {
+        const axisText = d3.select(this);
+        let textLength = axisText.node().getComputedTextLength();
+        let textContent = axisText.text();
+        while (textLength > maxTextWidth && textContent.length > 0) {
+          textContent = textContent.slice(0, -1);
+          axisText.text(`${textContent}...`);
+          textLength = axisText.node().getComputedTextLength();
+        }
+      });
+
+      const width = this.vbWidth;
+
+      const renderedYAxis = d3
+        .select(this.$refs.baseSvg)
+        .select('.y-axis')
+        .call(yAxis);
+
+      renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) {
+        if (i > 0) {
+          d3
+            .select(this)
+            .select('line')
+            .attr('x2', width)
+            .attr('class', 'axis-tick');
+        }
+      });
+
+      // Add the panning capabilities
+      if (this.isPanAvailable) {
+        d3
+          .select(this.$refs.baseSvg)
+          .call(this.zoom)
+          .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel
+      }
+
+      this.isLoading = false;
+      // Update the yAxisLabel coordinates
+      const labelDims = this.$refs.yAxisLabel.getBBox();
+      this.rectYAxisLabelDims = {
+        height: labelDims.width + 10,
+      };
+    },
+    panGraph() {
+      const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold;
+      const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll;
+      this.isGrabbed = true;
+      this.panX = d3.event.transform.x;
+
+      if (d3.event.transform.x === 0) {
+        this.showLeftScrollIndicator = false;
+      } else {
+        this.showLeftScrollIndicator = true;
+        this.showScrollIndicator = true;
+      }
+
+      if (!graphMaxPan) {
+        this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold);
+        this.showScrollIndicator = false;
+      }
+    },
+    setTooltipTitle(data) {
+      return data !== null ? `${data.name}: ${data.value}` : '';
+    },
+    calculatePadding(desiredBarWidth) {
+      const widthWithMargin = this.vbWidth - Math.abs(this.minX);
+      const dividend = widthWithMargin - this.graphData.length * desiredBarWidth;
+      const divisor = widthWithMargin - desiredBarWidth;
+
+      return dividend / divisor;
+    },
+    removeGrabStyling() {
+      this.isGrabbed = false;
+    },
+    barHoveredIn(index) {
+      this.xAxisTextElements[index].classList.add('x-axis-text');
+    },
+    barHoveredOut(index) {
+      this.xAxisTextElements[index].classList.remove('x-axis-text');
+    },
+  },
+};
+</script>
+<template>
+  <div
+    ref="svgContainer"
+    :class="activateGrabCursor"
+    class="svg-graph-container"
+  >
+    <svg
+      ref="baseSvg"
+      :width="vpWidth"
+      :height="vpHeight"
+      :viewBox="svgViewBox"
+      :preserveAspectRatio="preserveAspectRatioType">
+      <g
+        ref="xAxis"
+        :transform="xAxisLocation"
+        class="x-axis"
+      />
+      <g v-if="!isLoading">
+        <template
+          v-for="(data, index) in graphData">
+          <rect
+            v-tooltip
+            :key="index"
+            :width="xScale.bandwidth()"
+            :x="xScale(data.name)"
+            :y="yScale(data.value)"
+            :height="vbHeight - yScale(data.value)"
+            :transform="barTranslationTransform"
+            :title="setTooltipTitle(data)"
+            class="bar-rect"
+            data-placement="top"
+            @mouseover="barHoveredIn(index)"
+            @mouseout="barHoveredOut(index)"
+          />
+        </template>
+      </g>
+      <rect
+        :height="vbHeight + 100"
+        transform="translate(-100, -5)"
+        width="100"
+        fill="#fff"
+      />
+      <g class="y-axis-label">
+        <line
+          :x1="0"
+          :x2="0"
+          :y1="0"
+          :y2="vbHeight"
+          transform="translate(-35, 0)"
+          stroke="black"
+        />
+        <!--Get text length and change the height of this rect accordingly-->
+        <rect
+          :height="rectYAxisLabelDims.height"
+          :transform="yAxisLabelRectTransform"
+          :width="30"
+          fill="#fff"
+        />
+        <text
+          ref="yAxisLabel"
+          :transform="yAxisLabelTextTransform"
+        >
+          {{ yAxisLabel }}
+        </text>
+      </g>
+      <g
+        class="y-axis"
+      />
+      <g v-if="showScrollIndicator">
+        <rect
+          :height="vbHeight + 100"
+          :transform="`translate(${vpWidth - 60}, -5)`"
+          width="40"
+          fill="#fff"
+        />
+        <icon
+          :x="vpWidth - 50"
+          :y="vbHeight / 2"
+          :width="14"
+          :height="14"
+          name="chevron-right"
+          class="animate-flicker"
+        />
+      </g>
+      <!--The line that shows up when the data elements surpass the available width -->
+      <g
+        v-if="showScrollIndicator"
+        :transform="scrollIndicatorTransform">
+        <rect
+          :height="vbHeight"
+          x="0"
+          y="0"
+          width="20"
+          fill="url(#shadow-gradient)"
+        />
+      </g>
+      <!--Left scroll indicator-->
+      <g
+        v-if="showLeftScrollIndicator"
+        transform="translate(0, 0)">
+        <rect
+          :height="vbHeight"
+          x="0"
+          y="0"
+          width="20"
+          fill="url(#left-shadow-gradient)"
+        />
+      </g>
+      <svg-gradient
+        :colors="gradientColors"
+        :opacity="gradientOpacity"
+        identifier-name="shadow-gradient"/>
+      <svg-gradient
+        :colors="inverseGradientColors"
+        :opacity="inverseGradientOpacity"
+        identifier-name="left-shadow-gradient"/>
+    </svg>
+  </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..6957b112da61773f75bc27efaf37b1370bea907a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
@@ -0,0 +1,4 @@
+export const GRADIENT_COLORS = ['#000', '#a7a7a7'];
+export const GRADIENT_OPACITY = ['0', '0.4'];
+export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000'];
+export const INVERSE_GRADIENT_OPACITY = ['0.4', '0'];
diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b64942de972eabfed8de630ad545c783dd4993c2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/svg_gradient.vue
@@ -0,0 +1,43 @@
+<script>
+export default {
+  props: {
+    colors: {
+      type: Array,
+      required: true,
+      validator(value) {
+        return value.length === 2;
+      },
+    },
+    opacity: {
+      type: Array,
+      required: true,
+      validator(value) {
+        return value.length === 2;
+      },
+    },
+    identifierName: {
+      type: String,
+      required: true,
+    },
+  },
+};
+</script>
+<template>
+  <svg
+    height="0"
+    width="0">
+    <defs>
+      <linearGradient
+        :id="identifierName">
+        <stop
+          :stop-color="colors[0]"
+          :stop-opacity="opacity[0]"
+          offset="0%" />
+        <stop
+          :stop-color="colors[1]"
+          :stop-opacity="opacity[1]"
+          offset="100%" />
+      </linearGradient>
+    </defs>
+  </svg>
+</template>
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 84da9180f936d529e8a6557b61a4ee5bb059ff29..49d8a5d959b106136d0fd59c0bb4d48a30977a46 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -31,3 +31,61 @@
     color: $gl-text-red;
   }
 }
+
+.svg-graph-container {
+  width: 100%;
+
+  .axis-tick {
+    opacity: 0.4;
+  }
+
+  .tick-text {
+    fill: $gl-text-color-secondary;
+  }
+
+  .x-axis-text {
+    fill: $theme-gray-900;
+  }
+
+  .bar-rect {
+    fill: rgba($blue-500, 0.1);
+    stroke: $blue-500;
+  }
+
+  .bar-rect:hover {
+    fill: rgba($blue-700, 0.3);
+  }
+
+  .y-axis-label {
+    line {
+      stroke: $stat-graph-axis-fill;
+    }
+
+    text {
+      font-weight: bold;
+      font-size: 12px;
+      fill: $theme-gray-800;
+    }
+  }
+}
+
+.svg-graph-container-with-grab {
+  cursor: grab;
+  cursor: -webkit-grab;
+}
+
+.svg-graph-container-grabbed {
+  cursor: grabbing;
+  cursor: -webkit-grabbing;
+}
+
+@keyframes flickerAnimation {
+  0% { opacity: 1; }
+  50% { opacity: 0; }
+  100% { opacity: 1; }
+}
+
+.animate-flicker {
+  animation: flickerAnimation 1.5s infinite;
+  fill: $theme-gray-500;
+}
diff --git a/ee/app/assets/javascripts/pages/groups/analytics/show/index.js b/ee/app/assets/javascripts/pages/groups/analytics/show/index.js
index 9f9ca8647eaf255af585914943ed4a6fef365721..73c24e40b2db582c92ae31201b26c4be4410b9e5 100644
--- a/ee/app/assets/javascripts/pages/groups/analytics/show/index.js
+++ b/ee/app/assets/javascripts/pages/groups/analytics/show/index.js
@@ -1,37 +1,96 @@
-import Chart from 'chart.js';
+import Vue from 'vue';
+import _ from 'underscore';
 import initGroupMemberContributions from 'ee/group_member_contributions';
+import BarChart from '~/vue_shared/components/bar_chart.vue';
+import { __ } from '~/locale';
+
+function sortByValue(data) {
+  return _.sortBy(data, 'value').reverse();
+}
+
+function allValuesEmpty(graphData) {
+  const emptyCount = graphData.reduce((acc, data) => acc + Math.min(0, data.value), 0);
+
+  return emptyCount === 0;
+}
 
 document.addEventListener('DOMContentLoaded', () => {
   const dataEl = document.getElementById('js-analytics-data');
   if (dataEl) {
     const data = JSON.parse(dataEl.innerHTML);
-    const { labels } = data;
     const outputElIds = ['push', 'issues_closed', 'merge_requests_created'];
+    const formattedData = {
+      push: [],
+      issues_closed: [],
+      merge_requests_created: [],
+    };
 
     outputElIds.forEach((id) => {
-      const el = document.getElementById(id);
-      const ctx = el.getContext('2d');
-      const chart = new Chart(ctx);
-
-      chart.Bar({
-        labels,
-        datasets: [{
-          fillColor: 'rgba(220,220,220,0.5)',
-          strokeColor: 'rgba(220,220,220,1)',
-          barStrokeWidth: 1,
-          barValueSpacing: 1,
-          barDatasetSpacing: 1,
-          data: data[id].data,
-        }],
-      },
-        {
-          scaleOverlay: true,
-          responsive: true,
-          maintainAspectRatio: false,
-        },
-      );
+      data[id].data.forEach((d, index) => {
+        formattedData[id].push({
+          name: data.labels[index],
+          value: d,
+        });
+      });
     });
 
     initGroupMemberContributions();
+
+    const pushesEl = document.getElementById('js_pushes_chart_vue');
+    if (allValuesEmpty(formattedData.push)) {
+      // eslint-disable-next-line no-new
+      new Vue({
+        el: pushesEl,
+        components: {
+          BarChart,
+        },
+        render(createElement) {
+          return createElement('bar-chart', {
+            props: {
+              graphData: sortByValue(formattedData.push),
+              yAxisLabel: __('Pushes'),
+            },
+          });
+        },
+      });
+    }
+
+    const mergeRequestEl = document.getElementById('js_merge_requests_chart_vue');
+    if (allValuesEmpty(formattedData.merge_requests_created)) {
+      // eslint-disable-next-line no-new
+      new Vue({
+        el: mergeRequestEl,
+        components: {
+          BarChart,
+        },
+        render(createElement) {
+          return createElement('bar-chart', {
+            props: {
+              graphData: sortByValue(formattedData.merge_requests_created),
+              yAxisLabel: __('Merge Requests created'),
+            },
+          });
+        },
+      });
+    }
+
+    const issueEl = document.getElementById('js_issues_chart_vue');
+    if (allValuesEmpty(formattedData.issues_closed)) {
+      // eslint-disable-next-line no-new
+      new Vue({
+        el: issueEl,
+        components: {
+          BarChart,
+        },
+        render(createElement) {
+          return createElement('bar-chart', {
+            props: {
+              graphData: sortByValue(formattedData.issues_closed),
+              yAxisLabel: __('Issues closed'),
+            },
+          });
+        },
+      });
+    }
   }
 });
diff --git a/ee/app/views/groups/analytics/show.html.haml b/ee/app/views/groups/analytics/show.html.haml
index a84d1ba2aae7b684a4b45073edc54fbd6f86b08d..ad2b57b3ecaeb0704213ee3a0998d99f7cf4e3f1 100644
--- a/ee/app/views/groups/analytics/show.html.haml
+++ b/ee/app/views/groups/analytics/show.html.haml
@@ -21,59 +21,43 @@
       Contribution analytics for issues, merge requests and push events since #{@start_date}
 
   %h3 Push
+  - code_push_count = @events.code_push.count
+  - commits_count = @events.code_push.map(&:commits_count).sum
+  - person_count = @events.code_push.pluck(:author_id).uniq.count
+  - person_count_string = pluralize person_count, 'person'
+  - pushes_string = _('<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors.').html_safe % { pushes: code_push_count, commits: commits_count , people: person_count_string }
+  - if code_push_count > 0 || commits_count > 0 || person_count > 0
+    = pushes_string
+  - else
+    = _('No pushes for the selected time period.')
 
   .row
-    .col-md-4
-      %ul
-        %li
-          = @events.code_push.count
-          times
-        %li
-          more than
-          = @events.code_push.map(&:commits_count).sum
-          commits
-        %li
-          by
-          = pluralize @events.code_push.pluck(:author_id).uniq.count, 'person'
-
-    .col-md-8
-      %div
-        %p.light Push events per group member
-        %canvas#push{ height: 250 }
+    .col-md-12
+      #js_pushes_chart_vue
 
   %h3 Merge Requests
+  - mr_created_count = @events.merge_requests.created.count
+  - mr_merged_count = @events.merge_requests.merged.count
+  - if mr_created_count > 0 || mr_merged_count > 0
+    = _('<strong>%{created_count}</strong> created, <strong>%{accepted_count}</strong> accepted.').html_safe % { created_count: mr_created_count, accepted_count: mr_merged_count  }
+  - else
+    = _('No merge requests for the selected time period.')
 
   .row
-    .col-md-4
-      %ul
-        %li
-          = @events.merge_requests.created.count
-          created
-        %li
-          = @events.merge_requests.merged.count
-          accepted
-
-    .col-md-8
-      %div
-        %p.light Merge requests created per group member
-        %canvas#merge_requests_created{ height: 250 }
+    .col-md-12
+      #js_merge_requests_chart_vue
 
   %h3 Issues
+  - issues_created_count = @events.issues.created.count
+  - issues_closed_count = @events.issues.closed.pluck(:target_id).uniq.count
+  - if issues_created_count > 0 && issues_closed_count > 0
+    = _('<strong>%{created_count}</strong> created, <strong>%{closed_count}</strong> closed.').html_safe % { created_count: issues_created_count, closed_count: issues_closed_count }
+  - else
+    = _('No issues for the selected time period.')
 
   .row
-    .col-md-4
-      %ul
-        %li
-          = @events.issues.created.count
-          created
-        %li
-          = @events.issues.closed.pluck(:target_id).uniq.count
-          closed
-
-    .col-md-8
-      %div
-        %p.light Issues closed per group member
-        %canvas#issues_closed{ height: 250 }
+    .col-md-12
+      #js_issues_chart_vue
 
   #js-group-member-contributions{ data: { member_contributions_path: group_analytics_path(@group, { start_date: @start_date, format: :json }) } }
   -# haml-lint:disable InlineJavaScript
diff --git a/ee/changelogs/unreleased/jivl-redesign-contributors-graph.yml b/ee/changelogs/unreleased/jivl-redesign-contributors-graph.yml
new file mode 100644
index 0000000000000000000000000000000000000000..657f18b3681a033f4b413bbfaf544f6ab62eb91d
--- /dev/null
+++ b/ee/changelogs/unreleased/jivl-redesign-contributors-graph.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign contribution analytics graphs
+merge_request: 6194
+author:
+type: changed
diff --git a/package.json b/package.json
index e50f041f6daaa3bcdef6108f04308865531b1486..443b465896b4f98e36c63da0c108193148902649 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
     "core-js": "^2.4.1",
     "cropper": "^2.3.0",
     "css-loader": "^0.28.11",
+    "d3": "4.12.2",
     "d3-array": "^1.2.1",
     "d3-axis": "^1.0.8",
     "d3-brush": "^1.0.4",
diff --git a/spec/javascripts/vue_shared/components/bar_chart_spec.js b/spec/javascripts/vue_shared/components/bar_chart_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7e91cd6f63fb85e656d3905a2f33894c03b3b95a
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/bar_chart_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import BarChart from '~/vue_shared/components/bar_chart.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+function getRandomArbitrary(min, max) {
+  return Math.random() * (max - min) + min;
+}
+
+function generateRandomData(dataNumber) {
+  const randomGraphData = [];
+
+  for (let i = 1; i <= dataNumber; i += 1) {
+    randomGraphData.push({
+      name: `random ${i}`,
+      value: parseInt(getRandomArbitrary(1, 8), 10),
+    });
+  }
+
+  return randomGraphData;
+}
+
+describe('Bar chart component', () => {
+  let barChart;
+  const graphData = generateRandomData(10);
+
+  beforeEach(() => {
+    const BarChartComponent = Vue.extend(BarChart);
+
+    barChart = mountComponent(BarChartComponent, {
+      graphData,
+      yAxisLabel: 'data',
+    });
+  });
+
+  afterEach(() => {
+    barChart.$destroy();
+  });
+
+  it('calculates the padding for even distribution across bars', () => {
+    barChart.vbWidth = 1000;
+    const result = barChart.calculatePadding(30);
+
+    // since padding can't be higher than 1 and lower than 0
+    // for more info: https://github.com/d3/d3-scale#band-scales
+    expect(result).not.toBeLessThan(0);
+    expect(result).not.toBeGreaterThan(1);
+  });
+
+  it('formats the tooltip title', () => {
+    const tooltipTitle = barChart.setTooltipTitle(barChart.graphData[0]);
+
+    expect(tooltipTitle).toContain('random 1:');
+  });
+
+  it('has a translates the bar graphs on across the X axis', () => {
+    barChart.panX = 100;
+
+    expect(barChart.barTranslationTransform).toEqual('translate(100, 0)');
+  });
+
+  it('translates the scroll indicator to the far right side', () => {
+    barChart.vbWidth = 500;
+
+    expect(barChart.scrollIndicatorTransform).toEqual('translate(420, 0)');
+  });
+
+  it('translates the x-axis to the bottom of the viewbox and pan coordinates', () => {
+    barChart.panX = 100;
+    barChart.vbHeight = 250;
+
+    expect(barChart.xAxisLocation).toEqual('translate(100, 250)');
+  });
+
+  it('Contains a total of 4 ticks across the y axis', () => {
+    const ticks = barChart.$el.querySelector('.y-axis').querySelectorAll('.tick').length;
+
+    expect(ticks).toEqual(4);
+  });
+
+  it('rotates the x axis labels a total of 90 degress (CCW)', () => {
+    const xAxisLabel = barChart.$el.querySelector('.x-axis').querySelectorAll('text')[0];
+
+    expect(xAxisLabel.getAttribute('transform')).toEqual('rotate(-90)');
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index 6f8fb09cde5d84c923dd3bbaf87fbd1daf9f42b8..a2aa85467b2d741c7915eb9d45f33ae7c97a5b3a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1862,7 +1862,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.13.0, commander@^2.15.1, commander@^2.9.0:
+commander@2, commander@^2.13.0, commander@^2.15.1, commander@^2.9.0:
   version "2.15.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
 
@@ -2208,15 +2208,15 @@ cyclist@~0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
 
-d3-array@^1.2.0, d3-array@^1.2.1:
+d3-array@1, d3-array@1.2.1, d3-array@^1.2.0, d3-array@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
 
-d3-axis@^1.0.8:
+d3-axis@1.0.8, d3-axis@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
 
-d3-brush@^1.0.4:
+d3-brush@1.0.4, d3-brush@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
   dependencies:
@@ -2226,44 +2226,103 @@ d3-brush@^1.0.4:
     d3-selection "1"
     d3-transition "1"
 
-d3-collection@1:
+d3-chord@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
+  dependencies:
+    d3-array "1"
+    d3-path "1"
+
+d3-collection@1, d3-collection@1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
 
-d3-color@1:
+d3-color@1, d3-color@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
 
-d3-dispatch@1:
+d3-dispatch@1, d3-dispatch@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
 
-d3-drag@1:
+d3-drag@1, d3-drag@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
   dependencies:
     d3-dispatch "1"
     d3-selection "1"
 
-d3-ease@1, d3-ease@^1.0.3:
+d3-dsv@1, d3-dsv@1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae"
+  dependencies:
+    commander "2"
+    iconv-lite "0.4"
+    rw "1"
+
+d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
 
-d3-format@1:
+d3-force@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
+  dependencies:
+    d3-collection "1"
+    d3-dispatch "1"
+    d3-quadtree "1"
+    d3-timer "1"
+
+d3-format@1, d3-format@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f"
 
-d3-interpolate@1:
+d3-geo@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
+  dependencies:
+    d3-array "1"
+
+d3-hierarchy@1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
+
+d3-interpolate@1, d3-interpolate@1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
   dependencies:
     d3-color "1"
 
-d3-path@1:
+d3-path@1, d3-path@1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
 
-d3-scale@^1.0.7:
+d3-polygon@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
+
+d3-quadtree@1, d3-quadtree@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
+
+d3-queue@3.0.7:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
+
+d3-random@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
+
+d3-request@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/d3-request/-/d3-request-1.0.6.tgz#a1044a9ef4ec28c824171c9379fae6d79474b19f"
+  dependencies:
+    d3-collection "1"
+    d3-dispatch "1"
+    d3-dsv "1"
+    xmlhttprequest "1"
+
+d3-scale@1.0.7, d3-scale@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
   dependencies:
@@ -2275,31 +2334,31 @@ d3-scale@^1.0.7:
     d3-time "1"
     d3-time-format "2"
 
-d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.2.0:
+d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0, d3-selection@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88"
 
-d3-shape@^1.2.0:
+d3-shape@1.2.0, d3-shape@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
   dependencies:
     d3-path "1"
 
-d3-time-format@2, d3-time-format@^2.1.1:
+d3-time-format@2, d3-time-format@2.1.1, d3-time-format@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
   dependencies:
     d3-time "1"
 
-d3-time@1, d3-time@^1.0.8:
+d3-time@1, d3-time@1.0.8, d3-time@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
 
-d3-timer@1:
+d3-timer@1, d3-timer@1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
 
-d3-transition@1, d3-transition@^1.1.1:
+d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
   dependencies:
@@ -2310,10 +2369,59 @@ d3-transition@1, d3-transition@^1.1.1:
     d3-selection "^1.1.0"
     d3-timer "1"
 
+d3-voronoi@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
+
+d3-zoom@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
 d3@3.5.17:
   version "3.5.17"
   resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
 
+d3@4.12.2:
+  version "4.12.2"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.2.tgz#12f775564c6a9de229f63db03446e2cb7bb56c8f"
+  dependencies:
+    d3-array "1.2.1"
+    d3-axis "1.0.8"
+    d3-brush "1.0.4"
+    d3-chord "1.0.4"
+    d3-collection "1.0.4"
+    d3-color "1.0.3"
+    d3-dispatch "1.0.3"
+    d3-drag "1.2.1"
+    d3-dsv "1.0.8"
+    d3-ease "1.0.3"
+    d3-force "1.1.0"
+    d3-format "1.2.1"
+    d3-geo "1.9.1"
+    d3-hierarchy "1.1.5"
+    d3-interpolate "1.1.6"
+    d3-path "1.0.5"
+    d3-polygon "1.0.3"
+    d3-quadtree "1.0.3"
+    d3-queue "3.0.7"
+    d3-random "1.1.0"
+    d3-request "1.0.6"
+    d3-scale "1.0.7"
+    d3-selection "1.2.0"
+    d3-shape "1.2.0"
+    d3-time "1.0.8"
+    d3-time-format "2.1.1"
+    d3-timer "1.0.7"
+    d3-transition "1.1.1"
+    d3-voronoi "1.1.2"
+    d3-zoom "1.7.1"
+
 dagre-d3-renderer@^0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45"
@@ -3949,6 +4057,12 @@ https-proxy-agent@1:
     debug "2"
     extend "3"
 
+iconv-lite@0.4:
+  version "0.4.23"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
 iconv-lite@0.4.15:
   version "0.4.15"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
@@ -6822,6 +6936,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+
 rx-lite-aggregates@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
@@ -8332,6 +8450,10 @@ xmlhttprequest-ssl@~1.5.4:
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
 
+xmlhttprequest@1:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
+
 xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"