diff --git a/ee/app/assets/javascripts/roadmap/components/app.vue b/ee/app/assets/javascripts/roadmap/components/app.vue
index 4dd6338338d7fb65e08ce5447e14f60dcd599619..2da718a0b1aba9b4f56bf18da87ae057ad53d325 100644
--- a/ee/app/assets/javascripts/roadmap/components/app.vue
+++ b/ee/app/assets/javascripts/roadmap/components/app.vue
@@ -6,6 +6,9 @@ import { s__ } from '~/locale';
 import { GlLoadingIcon } from '@gitlab/ui';
 import epicsListEmpty from './epics_list_empty.vue';
 import roadmapShell from './roadmap_shell.vue';
+import eventHub from '../event_hub';
+
+import { EXTEND_AS } from '../constants';
 
 export default {
   components: {
@@ -96,6 +99,28 @@ export default {
           Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
         });
     },
+    fetchEpicsForTimeframe({ timeframe, roadmapTimelineEl, extendType }) {
+      this.hasError = false;
+      this.service
+        .getEpicsForTimeframe(this.presetType, timeframe)
+        .then(res => res.data)
+        .then(epics => {
+          if (epics.length) {
+            this.store.addEpics(epics);
+            this.$nextTick(() => {
+              // Re-render timeline bars with updated timeline
+              eventHub.$emit('refreshTimeline', {
+                height: window.innerHeight - roadmapTimelineEl.offsetTop,
+                todayBarReady: extendType === EXTEND_AS.PREPEND,
+              });
+            });
+          }
+        })
+        .catch(() => {
+          this.hasError = true;
+          Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
+        });
+    },
     /**
      * Roadmap view works with absolute sizing and positioning
      * of following child components of RoadmapShell;
@@ -117,6 +142,49 @@ export default {
         this.isLoading = false;
       }, 200)();
     },
+    /**
+     * Once timeline is expanded (either with prepend or append)
+     * We need performing following actions;
+     *
+     * 1. Reset start and end edges of the timeline for
+     *    infinite scrolling to continue further.
+     * 2. Re-render timeline bars to account for
+     *    updated timeframe.
+     * 3. In case of prepending timeframe,
+     *    reset scroll-position (due to DOM prepend).
+     */
+    processExtendedTimeline({ extendType = EXTEND_AS.PREPEND, roadmapTimelineEl, itemsCount = 0 }) {
+      // Re-render timeline bars with updated timeline
+      eventHub.$emit('refreshTimeline', {
+        height: window.innerHeight - roadmapTimelineEl.offsetTop,
+        todayBarReady: extendType === EXTEND_AS.PREPEND,
+      });
+
+      if (extendType === EXTEND_AS.PREPEND) {
+        // When DOM is prepended with elements
+        // we compensate the scrolling for added elements' width
+        roadmapTimelineEl.parentElement.scrollBy(
+          roadmapTimelineEl.querySelector('.timeline-header-item').clientWidth * itemsCount,
+          0,
+        );
+      }
+    },
+    handleScrollToExtend(roadmapTimelineEl, extendType = EXTEND_AS.PREPEND) {
+      const timeframe = this.store.extendTimeframe(extendType);
+      this.$nextTick(() => {
+        this.processExtendedTimeline({
+          itemsCount: timeframe ? timeframe.length : 0,
+          extendType,
+          roadmapTimelineEl,
+        });
+
+        this.fetchEpicsForTimeframe({
+          timeframe,
+          roadmapTimelineEl,
+          extendType,
+        });
+      });
+    },
   },
 };
 </script>
@@ -135,6 +203,8 @@ export default {
       :epics="epics"
       :timeframe="timeframe"
       :current-group-id="currentGroupId"
+      @onScrollToStart="handleScrollToExtend"
+      @onScrollToEnd="handleScrollToExtend"
     />
     <epics-list-empty
       v-if="isEpicsListEmpty"
diff --git a/ee/app/assets/javascripts/roadmap/components/epic_item.vue b/ee/app/assets/javascripts/roadmap/components/epic_item.vue
index 59fcc1c59bc4a63ddfe08f05dea74d99e2a586ad..c46dd568b8e38461eba147ad0d43a8044464f768 100644
--- a/ee/app/assets/javascripts/roadmap/components/epic_item.vue
+++ b/ee/app/assets/javascripts/roadmap/components/epic_item.vue
@@ -1,7 +1,11 @@
 <script>
+import _ from 'underscore';
+
 import epicItemDetails from './epic_item_details.vue';
 import epicItemTimeline from './epic_item_timeline.vue';
 
+import { EPIC_HIGHLIGHT_REMOVE_AFTER } from '../constants';
+
 export default {
   components: {
     epicItemDetails,
@@ -33,11 +37,36 @@ export default {
       required: true,
     },
   },
+  updated() {
+    this.removeHighlight();
+  },
+  methods: {
+    /**
+     * When new epics are added to the list on
+     * timeline scroll, we set `newEpic` flag
+     * as true and then use it in template
+     * to set `newly-added-epic` class for
+     * highlighting epic using CSS animations
+     *
+     * Once animation is complete, we need to
+     * remove the flag so that animation is not
+     * replayed when list is re-rendered.
+     */
+    removeHighlight() {
+      if (this.epic.newEpic) {
+        this.$nextTick(() => {
+          _.delay(() => {
+            this.epic.newEpic = false;
+          }, EPIC_HIGHLIGHT_REMOVE_AFTER);
+        });
+      }
+    },
+  },
 };
 </script>
 
 <template>
-  <div class="epics-list-item clearfix">
+  <div :class="{ 'newly-added-epic': epic.newEpic }" class="epics-list-item clearfix">
     <epic-item-details :epic="epic" :current-group-id="currentGroupId" />
     <epic-item-timeline
       v-for="(timeframeItem, index) in timeframe"
diff --git a/ee/app/assets/javascripts/roadmap/components/epic_item_timeline.vue b/ee/app/assets/javascripts/roadmap/components/epic_item_timeline.vue
index 03d79bd6c2bbe7e858cfe958dc6204af63c9dfa2..70a12e8a929c084d3971922594b211e91f9af1bd 100644
--- a/ee/app/assets/javascripts/roadmap/components/epic_item_timeline.vue
+++ b/ee/app/assets/javascripts/roadmap/components/epic_item_timeline.vue
@@ -5,13 +5,9 @@ import QuartersPresetMixin from '../mixins/quarters_preset_mixin';
 import MonthsPresetMixin from '../mixins/months_preset_mixin';
 import WeeksPresetMixin from '../mixins/weeks_preset_mixin';
 
-import {
-  EPIC_DETAILS_CELL_WIDTH,
-  TIMELINE_CELL_MIN_WIDTH,
-  TIMELINE_END_OFFSET_FULL,
-  TIMELINE_END_OFFSET_HALF,
-  PRESET_TYPES,
-} from '../constants';
+import eventHub from '../event_hub';
+
+import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from '../constants';
 
 export default {
   directives: {
@@ -66,6 +62,12 @@ export default {
       this.renderTimelineBar();
     },
   },
+  mounted() {
+    eventHub.$on('refreshTimeline', this.renderTimelineBar);
+  },
+  beforeDestroy() {
+    eventHub.$off('refreshTimeline', this.renderTimelineBar);
+  },
   methods: {
     /**
      * Gets cell width based on total number months for
@@ -89,49 +91,6 @@ export default {
       }
       return false;
     },
-    getTimelineBarEndOffsetHalf() {
-      if (this.presetType === PRESET_TYPES.QUARTERS) {
-        return TIMELINE_END_OFFSET_HALF;
-      } else if (this.presetType === PRESET_TYPES.MONTHS) {
-        return TIMELINE_END_OFFSET_HALF;
-      } else if (this.presetType === PRESET_TYPES.WEEKS) {
-        return this.getTimelineBarEndOffsetHalfForWeek();
-      }
-      return 0;
-    },
-    /**
-     * In case startDate or endDate for any epic is undefined or is out of range
-     * for current timeframe, we have to provide specific offset while
-     * setting width to ensure that;
-     *
-     * 1. Timeline bar ends at correct position based on end date.
-     * 2. A "triangle" shape is shown at the end of timeline bar
-     *    when endDate is out of range.
-     */
-    getTimelineBarEndOffset() {
-      let offset = 0;
-
-      if (
-        (this.epic.startDateOutOfRange && this.epic.endDateOutOfRange) ||
-        (this.epic.startDateUndefined && this.epic.endDateOutOfRange)
-      ) {
-        // If Epic startDate is undefined or out of range
-        // AND
-        // endDate is out of range
-        // Reduce offset size from the width to compensate for fadeout of timelinebar
-        // and/or showing triangle at the end and beginning
-        offset = TIMELINE_END_OFFSET_FULL;
-      } else if (this.epic.endDateOutOfRange) {
-        // If Epic end date is out of range
-        // Reduce offset size from the width to compensate for triangle (which is sized at 8px)
-        offset = this.getTimelineBarEndOffsetHalf();
-      } else {
-        // No offset needed if all dates are defined.
-        offset = 0;
-      }
-
-      return offset;
-    },
     /**
      * Renders timeline bar only if current
      * timeframe item has startDate for the epic.
@@ -160,9 +119,7 @@ export default {
         :href="epic.webUrl"
         :class="{
           'start-date-undefined': epic.startDateUndefined,
-          'start-date-outside': epic.startDateOutOfRange,
           'end-date-undefined': epic.endDateUndefined,
-          'end-date-outside': epic.endDateOutOfRange,
         }"
         :style="timelineBarStyles"
         class="timeline-bar"
diff --git a/ee/app/assets/javascripts/roadmap/components/epics_list_empty.vue b/ee/app/assets/javascripts/roadmap/components/epics_list_empty.vue
index f4aec7cc9813553641c62593d477d3f2e34e4a24..d13fb7a103498a0fc661162876f79ae98b07ef24 100644
--- a/ee/app/assets/javascripts/roadmap/components/epics_list_empty.vue
+++ b/ee/app/assets/javascripts/roadmap/components/epics_list_empty.vue
@@ -2,7 +2,7 @@
 import { s__, sprintf } from '~/locale';
 import { dateInWords } from '~/lib/utils/datetime_utility';
 
-import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
+import { PRESET_TYPES, emptyStateDefault, emptyStateWithFilters } from '../constants';
 
 import NewEpic from '../../epics/new_epic/components/new_epic.vue';
 
@@ -82,12 +82,12 @@ export default {
     },
     subMessage() {
       if (this.hasFiltersApplied) {
-        return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateWithFilters, {
+        return sprintf(emptyStateWithFilters, {
           startDate: this.timeframeRange.startDate,
           endDate: this.timeframeRange.endDate,
         });
       }
-      return sprintf(PRESET_DEFAULTS[this.presetType].emptyStateDefault, {
+      return sprintf(emptyStateDefault, {
         startDate: this.timeframeRange.startDate,
         endDate: this.timeframeRange.endDate,
       });
diff --git a/ee/app/assets/javascripts/roadmap/components/epics_list_section.vue b/ee/app/assets/javascripts/roadmap/components/epics_list_section.vue
index 0dd982908444898565f914c70973bf145c54b2b4..2597e3ad9b778edc9b0810d2c922487dce4cc732 100644
--- a/ee/app/assets/javascripts/roadmap/components/epics_list_section.vue
+++ b/ee/app/assets/javascripts/roadmap/components/epics_list_section.vue
@@ -3,6 +3,8 @@ import eventHub from '../event_hub';
 
 import SectionMixin from '../mixins/section_mixin';
 
+import { TIMELINE_CELL_MIN_WIDTH } from '../constants';
+
 import epicItem from './epic_item.vue';
 
 export default {
@@ -72,12 +74,18 @@ export default {
   },
   mounted() {
     eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
+    eventHub.$on('refreshTimeline', () => {
+      this.initEmptyRow(false);
+    });
     this.$nextTick(() => {
       this.initMounted();
     });
   },
   beforeDestroy() {
     eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
+    eventHub.$off('refreshTimeline', () => {
+      this.initEmptyRow(false);
+    });
   },
   methods: {
     initMounted() {
@@ -104,7 +112,7 @@ export default {
      * based on height of available list items and sets it to component
      * props.
      */
-    initEmptyRow() {
+    initEmptyRow(showEmptyRow = false) {
       const children = this.$children;
       let approxChildrenHeight = children[0].$el.clientHeight * this.epics.length;
 
@@ -122,18 +130,16 @@ export default {
         this.emptyRowHeight = this.shellHeight - approxChildrenHeight;
         this.showEmptyRow = true;
       } else {
+        this.showEmptyRow = showEmptyRow;
         this.showBottomShadow = true;
       }
     },
     /**
-     * `clientWidth` is full width of list section, and we need to
-     * scroll up to 60% of the view where today indicator is present.
-     *
-     * Reason for 60% is that "today" always falls in the middle of timeframe range.
+     * Scroll timeframe to the right of the timeline
+     * by half the column size
      */
     scrollToTodayIndicator() {
-      const uptoTodayIndicator = Math.ceil((this.$el.clientWidth * 60) / 100);
-      this.$el.scrollTo(uptoTodayIndicator, 0);
+      this.$el.parentElement.scrollBy(TIMELINE_CELL_MIN_WIDTH / 2, 0);
     },
     handleEpicsListScroll({ scrollTop, clientHeight, scrollHeight }) {
       this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
diff --git a/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_item.vue b/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_item.vue
index 542e13ac3223f27d9291897416771b508e37241f..b9614c551add4c4f889b98ba0ad135ede6034608 100644
--- a/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_item.vue
+++ b/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_item.vue
@@ -29,8 +29,6 @@ export default {
 
     return {
       currentDate,
-      quarterBeginDate: this.timeframeItem.range[0],
-      quarterEndDate: this.timeframeItem.range[2],
     };
   },
   computed: {
@@ -39,6 +37,12 @@ export default {
         width: `${this.itemWidth}px`,
       };
     },
+    quarterBeginDate() {
+      return this.timeframeItem.range[0];
+    },
+    quarterEndDate() {
+      return this.timeframeItem.range[2];
+    },
     timelineHeaderLabel() {
       const { quarterSequence } = this.timeframeItem;
       if (quarterSequence === 1 || (this.timeframeIndex === 0 && quarterSequence !== 1)) {
diff --git a/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item.vue b/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item.vue
index c80f73f36907a8d85c2eafe97a6cfa15ea5f334f..4120c1c1986e62f4d3b231e7db7b475db689a46d 100644
--- a/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item.vue
+++ b/ee/app/assets/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item.vue
@@ -20,13 +20,13 @@ export default {
       required: true,
     },
   },
-  data() {
-    return {
-      quarterBeginDate: this.timeframeItem.range[0],
-      quarterEndDate: this.timeframeItem.range[2],
-    };
-  },
   computed: {
+    quarterBeginDate() {
+      return this.timeframeItem.range[0];
+    },
+    quarterEndDate() {
+      return this.timeframeItem.range[2];
+    },
     headerSubItems() {
       return this.timeframeItem.range;
     },
diff --git a/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_item.vue b/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_item.vue
index fcf983a6a15a60870ebbb179f57b43ca1881a968..ab6f62b16dfa10308b583f47ee0ab2cba664c94b 100644
--- a/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_item.vue
+++ b/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_item.vue
@@ -29,12 +29,8 @@ export default {
     const currentDate = new Date();
     currentDate.setHours(0, 0, 0, 0);
 
-    const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
-    lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
-
     return {
       currentDate,
-      lastDayOfCurrentWeek,
     };
   },
   computed: {
@@ -43,19 +39,35 @@ export default {
         width: `${this.itemWidth}px`,
       };
     },
+    lastDayOfCurrentWeek() {
+      const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
+      lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
+
+      return lastDayOfCurrentWeek;
+    },
     timelineHeaderLabel() {
-      if (this.timeframeIndex === 0) {
+      const timeframeItemMonth = this.timeframeItem.getMonth();
+      const timeframeItemDate = this.timeframeItem.getDate();
+
+      if (this.timeframeIndex === 0 || (timeframeItemMonth === 0 && timeframeItemDate <= 7)) {
         return `${this.timeframeItem.getFullYear()} ${monthInWords(
           this.timeframeItem,
           true,
-        )} ${this.timeframeItem.getDate()}`;
+        )} ${timeframeItemDate}`;
       }
-      return `${monthInWords(this.timeframeItem, true)} ${this.timeframeItem.getDate()}`;
+
+      return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
     },
     timelineHeaderClass() {
-      if (this.currentDate >= this.timeframeItem && this.currentDate <= this.lastDayOfCurrentWeek) {
+      const currentDateTime = this.currentDate.getTime();
+      const lastDayOfCurrentWeekTime = this.lastDayOfCurrentWeek.getTime();
+
+      if (
+        currentDateTime >= this.timeframeItem.getTime() &&
+        currentDateTime <= lastDayOfCurrentWeekTime
+      ) {
         return 'label-dark label-bold';
-      } else if (this.currentDate < this.lastDayOfCurrentWeek) {
+      } else if (currentDateTime < lastDayOfCurrentWeekTime) {
         return 'label-dark';
       }
       return '';
diff --git a/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item.vue b/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item.vue
index cd9528fa85271bc47a45d845d5e25e9b9b08b4dc..ed7e83fd3c04344106bcb39d2225b19264563894 100644
--- a/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item.vue
+++ b/ee/app/assets/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item.vue
@@ -18,28 +18,26 @@ export default {
       required: true,
     },
   },
-  data() {
-    const timeframeItem = new Date(this.timeframeItem.getTime());
-    const headerSubItems = new Array(7)
-      .fill()
-      .map(
-        (val, i) =>
-          new Date(
-            timeframeItem.getFullYear(),
-            timeframeItem.getMonth(),
-            timeframeItem.getDate() + i,
-          ),
-      );
-
-    return {
-      headerSubItems,
-    };
-  },
   computed: {
+    headerSubItems() {
+      const timeframeItem = new Date(this.timeframeItem.getTime());
+      const headerSubItems = new Array(7)
+        .fill()
+        .map(
+          (val, i) =>
+            new Date(
+              timeframeItem.getFullYear(),
+              timeframeItem.getMonth(),
+              timeframeItem.getDate() + i,
+            ),
+        );
+
+      return headerSubItems;
+    },
     hasToday() {
       return (
-        this.currentDate >= this.headerSubItems[0] &&
-        this.currentDate <= this.headerSubItems[this.headerSubItems.length - 1]
+        this.currentDate.getTime() >= this.headerSubItems[0].getTime() &&
+        this.currentDate.getTime() <= this.headerSubItems[this.headerSubItems.length - 1].getTime()
       );
     },
   },
diff --git a/ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue b/ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue
index 780d9164587d25b685520a7a38fece601cc21aca..9b6b0ac3f5b1c89d3caee989331ccb439312472b 100644
--- a/ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue
+++ b/ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue
@@ -1,6 +1,7 @@
 <script>
 import bp from '~/breakpoints';
-import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, SHELL_MIN_WIDTH } from '../constants';
+import { isInViewport } from '~/lib/utils/common_utils';
+import { SCROLL_BAR_SIZE, EPIC_ITEM_HEIGHT, SHELL_MIN_WIDTH, EXTEND_AS } from '../constants';
 import eventHub from '../event_hub';
 
 import epicsListSection from './epics_list_section.vue';
@@ -34,6 +35,7 @@ export default {
       shellWidth: 0,
       shellHeight: 0,
       noScroll: false,
+      timeframeStartOffset: 0,
     };
   },
   computed: {
@@ -61,6 +63,11 @@ export default {
         this.shellHeight = window.innerHeight - this.$el.offsetTop;
         this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
         this.shellWidth = this.$el.parentElement.clientWidth + this.getWidthOffset();
+
+        this.timeframeStartOffset = this.$refs.roadmapTimeline.$el
+          .querySelector('.timeline-header-item')
+          .querySelector('.item-sublabel .sublabel-value:first-child')
+          .getBoundingClientRect().left;
       }
     });
   },
@@ -70,6 +77,22 @@ export default {
     },
     handleScroll() {
       const { scrollTop, scrollLeft, clientHeight, scrollHeight } = this.$el;
+      const timelineEdgeStartEl = this.$refs.roadmapTimeline.$el
+        .querySelector('.timeline-header-item')
+        .querySelector('.item-sublabel .sublabel-value:first-child');
+      const timelineEdgeEndEl = this.$refs.roadmapTimeline.$el
+        .querySelector('.timeline-header-item:last-child')
+        .querySelector('.item-sublabel .sublabel-value:last-child');
+
+      // If timeline was scrolled to start
+      if (isInViewport(timelineEdgeStartEl, { left: this.timeframeStartOffset })) {
+        this.$emit('onScrollToStart', this.$refs.roadmapTimeline.$el, EXTEND_AS.PREPEND);
+      } else if (isInViewport(timelineEdgeEndEl)) {
+        // If timeline was scrolled to end
+        this.$emit('onScrollToEnd', this.$refs.roadmapTimeline.$el, EXTEND_AS.APPEND);
+      }
+
+      this.noScroll = this.shellHeight > EPIC_ITEM_HEIGHT * (this.epics.length + 1);
       eventHub.$emit('epicsListScrolled', { scrollTop, scrollLeft, clientHeight, scrollHeight });
     },
   },
@@ -84,6 +107,7 @@ export default {
     @scroll="handleScroll"
   >
     <roadmap-timeline-section
+      ref="roadmapTimeline"
       :preset-type="presetType"
       :epics="epics"
       :timeframe="timeframe"
diff --git a/ee/app/assets/javascripts/roadmap/components/timeline_today_indicator.vue b/ee/app/assets/javascripts/roadmap/components/timeline_today_indicator.vue
index 5244ae65cf50602bfbe1b473fecd2558da81cb72..3d142f5aaa8132b083e1da669e15735a5c8d2fba 100644
--- a/ee/app/assets/javascripts/roadmap/components/timeline_today_indicator.vue
+++ b/ee/app/assets/javascripts/roadmap/components/timeline_today_indicator.vue
@@ -29,10 +29,12 @@ export default {
   mounted() {
     eventHub.$on('epicsListRendered', this.handleEpicsListRender);
     eventHub.$on('epicsListScrolled', this.handleEpicsListScroll);
+    eventHub.$on('refreshTimeline', this.handleEpicsListRender);
   },
   beforeDestroy() {
     eventHub.$off('epicsListRendered', this.handleEpicsListRender);
     eventHub.$off('epicsListScrolled', this.handleEpicsListScroll);
+    eventHub.$off('refreshTimeline', this.handleEpicsListRender);
   },
   methods: {
     /**
@@ -40,7 +42,7 @@ export default {
      * and renders vertical line over the area where
      * today falls in current timeline
      */
-    handleEpicsListRender({ height }) {
+    handleEpicsListRender({ height, todayBarReady }) {
       let left = 0;
 
       // Get total days of current timeframe Item and then
@@ -68,7 +70,7 @@ export default {
         height: `${height + 20}px`,
         left: `${left}%`,
       };
-      this.todayBarReady = true;
+      this.todayBarReady = todayBarReady === undefined ? true : todayBarReady;
     },
     handleEpicsListScroll() {
       const indicatorX = this.$el.getBoundingClientRect().x;
diff --git a/ee/app/assets/javascripts/roadmap/constants.js b/ee/app/assets/javascripts/roadmap/constants.js
index 80cf0fa44bfbe33d5beb113571a25227fb4e3998..a76b299b6c0b9db1cc65c6dde2a941ebf8bf50bb 100644
--- a/ee/app/assets/javascripts/roadmap/constants.js
+++ b/ee/app/assets/javascripts/roadmap/constants.js
@@ -1,7 +1,5 @@
 import { s__ } from '~/locale';
 
-export const TIMEFRAME_LENGTH = 6;
-
 export const EPIC_DETAILS_CELL_WIDTH = 320;
 
 export const EPIC_ITEM_HEIGHT = 50;
@@ -12,9 +10,7 @@ export const SHELL_MIN_WIDTH = 1620;
 
 export const SCROLL_BAR_SIZE = 15;
 
-export const TIMELINE_END_OFFSET_HALF = 8;
-
-export const TIMELINE_END_OFFSET_FULL = 16;
+export const EPIC_HIGHLIGHT_REMOVE_AFTER = 3000;
 
 export const PRESET_TYPES = {
   QUARTERS: 'QUARTERS',
@@ -22,32 +18,27 @@ export const PRESET_TYPES = {
   WEEKS: 'WEEKS',
 };
 
+export const EXTEND_AS = {
+  PREPEND: 'prepend',
+  APPEND: 'append',
+};
+
+export const emptyStateDefault = s__(
+  'GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from %{startDate} to %{endDate}.',
+);
+
+export const emptyStateWithFilters = s__(
+  'GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}.',
+);
+
 export const PRESET_DEFAULTS = {
   QUARTERS: {
-    TIMEFRAME_LENGTH: 18,
-    emptyStateDefault: s__(
-      'GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
-    emptyStateWithFilters: s__(
-      'GroupRoadmap|To widen your search, change or remove filters. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
+    TIMEFRAME_LENGTH: 21,
   },
   MONTHS: {
-    TIMEFRAME_LENGTH: 7,
-    emptyStateDefault: s__(
-      'GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
-    emptyStateWithFilters: s__(
-      'GroupRoadmap|To widen your search, change or remove filters. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
+    TIMEFRAME_LENGTH: 8,
   },
   WEEKS: {
-    TIMEFRAME_LENGTH: 42,
-    emptyStateDefault: s__(
-      'GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
-    emptyStateWithFilters: s__(
-      'GroupRoadmap|To widen your search, change or remove filters. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from %{startDate} to %{endDate}.',
-    ),
+    TIMEFRAME_LENGTH: 7,
   },
 };
diff --git a/ee/app/assets/javascripts/roadmap/index.js b/ee/app/assets/javascripts/roadmap/index.js
index 252d3ead4d3b928202493111c8ced3622bf48f4c..9c8234716188cdda5ad4b07397a6875ac7fbbbb3 100644
--- a/ee/app/assets/javascripts/roadmap/index.js
+++ b/ee/app/assets/javascripts/roadmap/index.js
@@ -5,7 +5,7 @@ import Translate from '~/vue_shared/translate';
 import { parseBoolean } from '~/lib/utils/common_utils';
 import { visitUrl, mergeUrlParams } from '~/lib/utils/url_utility';
 
-import { PRESET_TYPES } from './constants';
+import { PRESET_TYPES, EPIC_DETAILS_CELL_WIDTH } from './constants';
 
 import { getTimeframeForPreset, getEpicsPathForPreset } from './utils/roadmap_utils';
 
@@ -48,23 +48,38 @@ export default () => {
           ? dataset.presetType
           : PRESET_TYPES.MONTHS;
       const filterQueryString = window.location.search.substring(1);
-      const timeframe = getTimeframeForPreset(presetType);
-      const epicsPath = getEpicsPathForPreset({
+      const timeframe = getTimeframeForPreset(
+        presetType,
+        window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
+      );
+      const initialEpicsPath = getEpicsPathForPreset({
         basePath: dataset.epicsPath,
+        epicsState: dataset.epicsState,
         filterQueryString,
         presetType,
         timeframe,
-        state: dataset.epicsState,
       });
 
-      const store = new RoadmapStore(parseInt(dataset.groupId, 0), timeframe, presetType);
-      const service = new RoadmapService(epicsPath);
+      const store = new RoadmapStore({
+        groupId: parseInt(dataset.groupId, 0),
+        sortedBy: dataset.sortedBy,
+        timeframe,
+        presetType,
+      });
+
+      const service = new RoadmapService({
+        initialEpicsPath,
+        filterQueryString,
+        basePath: dataset.epicsPath,
+        epicsState: dataset.epicsState,
+      });
 
       return {
         store,
         service,
         presetType,
         hasFiltersApplied,
+        epicsState: dataset.epicsState,
         newEpicEndpoint: dataset.newEpicEndpoint,
         emptyStateIllustrationPath: dataset.emptyStateIllustration,
       };
@@ -76,6 +91,7 @@ export default () => {
           service: this.service,
           presetType: this.presetType,
           hasFiltersApplied: this.hasFiltersApplied,
+          epicsState: this.epicsState,
           newEpicEndpoint: this.newEpicEndpoint,
           emptyStateIllustrationPath: this.emptyStateIllustrationPath,
         },
diff --git a/ee/app/assets/javascripts/roadmap/mixins/months_preset_mixin.js b/ee/app/assets/javascripts/roadmap/mixins/months_preset_mixin.js
index 01c6d33779e9f3b92d88f110ca11345f658b4c18..2b2dbf02f2562b03132ec1244b9941f1da5169f6 100644
--- a/ee/app/assets/javascripts/roadmap/mixins/months_preset_mixin.js
+++ b/ee/app/assets/javascripts/roadmap/mixins/months_preset_mixin.js
@@ -1,7 +1,5 @@
 import { totalDaysInMonth } from '~/lib/utils/datetime_utility';
 
-import { TIMELINE_END_OFFSET_HALF } from '../constants';
-
 export default {
   methods: {
     /**
@@ -17,10 +15,10 @@ export default {
      * Check if current epic ends within current month (timeline cell)
      */
     isTimeframeUnderEndDateForMonth(timeframeItem, epicEndDate) {
-      return (
-        timeframeItem.getYear() <= epicEndDate.getYear() &&
-        timeframeItem.getMonth() === epicEndDate.getMonth()
-      );
+      if (epicEndDate.getFullYear() <= timeframeItem.getFullYear()) {
+        return epicEndDate.getMonth() === timeframeItem.getMonth();
+      }
+      return epicEndDate.getTime() < timeframeItem.getTime();
     },
     /**
      * Return timeline bar width for current month (timeline cell) based on
@@ -61,16 +59,6 @@ export default {
         return 'left: 0;';
       }
 
-      // If Epic end date is out of range
-      const lastTimeframeItem = this.timeframe[this.timeframe.length - 1];
-      // Check if Epic start date falls within last month of the timeframe
-      if (
-        this.epic.startDate.getMonth() === lastTimeframeItem.getMonth() &&
-        this.epic.startDate.getFullYear() === lastTimeframeItem.getFullYear()
-      ) {
-        // Compensate for triangle size
-        return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
-      }
       // Calculate proportional offset based on startDate and total days in
       // current month.
       return `left: ${(startDate / daysInMonth) * 100}%;`;
@@ -99,7 +87,6 @@ export default {
 
       const indexOfCurrentMonth = this.timeframe.indexOf(this.timeframeItem);
       const cellWidth = this.getCellWidth();
-      const offsetEnd = this.getTimelineBarEndOffset();
       const epicStartDate = this.epic.startDate;
       const epicEndDate = this.epic.endDate;
 
@@ -149,8 +136,7 @@ export default {
         }
       }
 
-      // Reduce any offset from total width and round it off.
-      return timelineBarWidth - offsetEnd;
+      return timelineBarWidth;
     },
   },
 };
diff --git a/ee/app/assets/javascripts/roadmap/mixins/quarters_preset_mixin.js b/ee/app/assets/javascripts/roadmap/mixins/quarters_preset_mixin.js
index 9b70b17e18bbe2ef5723b443134c978034a5d6f4..402c6de03a0d50e37e99777fdc8a3b3be6b733bb 100644
--- a/ee/app/assets/javascripts/roadmap/mixins/quarters_preset_mixin.js
+++ b/ee/app/assets/javascripts/roadmap/mixins/quarters_preset_mixin.js
@@ -1,7 +1,5 @@
 import { totalDaysInQuarter, dayInQuarter } from '~/lib/utils/datetime_utility';
 
-import { TIMELINE_END_OFFSET_HALF } from '../constants';
-
 export default {
   methods: {
     /**
@@ -11,7 +9,10 @@ export default {
       const quarterStart = this.timeframeItem.range[0];
       const quarterEnd = this.timeframeItem.range[2];
 
-      return this.epic.startDate >= quarterStart && this.epic.startDate <= quarterEnd;
+      return (
+        this.epic.startDate.getTime() >= quarterStart.getTime() &&
+        this.epic.startDate.getTime() <= quarterEnd.getTime()
+      );
     },
     /**
      * Check if current epic ends within current quarter (timeline cell)
@@ -19,7 +20,7 @@ export default {
     isTimeframeUnderEndDateForQuarter(timeframeItem, epicEndDate) {
       const quarterEnd = timeframeItem.range[2];
 
-      return epicEndDate <= quarterEnd;
+      return epicEndDate.getTime() <= quarterEnd.getTime();
     },
     /**
      * Return timeline bar width for current quarter (timeline cell) based on
@@ -57,14 +58,6 @@ export default {
         return 'left: 0;';
       }
 
-      const lastTimeframeItem = this.timeframe[this.timeframe.length - 1].range[2];
-      if (
-        this.epic.startDate >= this.timeframe[this.timeframe.length - 1].range[0] &&
-        this.epic.startDate <= lastTimeframeItem
-      ) {
-        return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
-      }
-
       return `left: ${(startDay / daysInQuarter) * 100}%;`;
     },
     /**
@@ -95,7 +88,6 @@ export default {
 
       const indexOfCurrentQuarter = this.timeframe.indexOf(this.timeframeItem);
       const cellWidth = this.getCellWidth();
-      const offsetEnd = this.getTimelineBarEndOffset();
       const epicStartDate = this.epic.startDate;
       const epicEndDate = this.epic.endDate;
 
@@ -140,7 +132,7 @@ export default {
         }
       }
 
-      return timelineBarWidth - offsetEnd;
+      return timelineBarWidth;
     },
   },
 };
diff --git a/ee/app/assets/javascripts/roadmap/mixins/weeks_preset_mixin.js b/ee/app/assets/javascripts/roadmap/mixins/weeks_preset_mixin.js
index 67b3c4a9c9e423dce851b581958bae3d3fcf3958..28f332a515f66caaf0313d2fb1b25c12279f0842 100644
--- a/ee/app/assets/javascripts/roadmap/mixins/weeks_preset_mixin.js
+++ b/ee/app/assets/javascripts/roadmap/mixins/weeks_preset_mixin.js
@@ -1,4 +1,4 @@
-import { TIMELINE_END_OFFSET_HALF } from '../constants';
+import { newDate } from '~/lib/utils/datetime_utility';
 
 export default {
   methods: {
@@ -7,16 +7,19 @@ export default {
      */
     hasStartDateForWeek() {
       const firstDayOfWeek = this.timeframeItem;
-      const lastDayOfWeek = new Date(this.timeframeItem.getTime());
+      const lastDayOfWeek = newDate(this.timeframeItem);
       lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
 
-      return this.epic.startDate >= firstDayOfWeek && this.epic.startDate <= lastDayOfWeek;
+      return (
+        this.epic.startDate.getTime() >= firstDayOfWeek.getTime() &&
+        this.epic.startDate.getTime() <= lastDayOfWeek.getTime()
+      );
     },
     /**
      * Return last date of the week from provided timeframeItem
      */
     getLastDayOfWeek(timeframeItem) {
-      const lastDayOfWeek = new Date(timeframeItem.getTime());
+      const lastDayOfWeek = newDate(timeframeItem);
       lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
       return lastDayOfWeek;
     },
@@ -25,7 +28,7 @@ export default {
      */
     isTimeframeUnderEndDateForWeek(timeframeItem, epicEndDate) {
       const lastDayOfWeek = this.getLastDayOfWeek(timeframeItem);
-      return epicEndDate <= lastDayOfWeek;
+      return epicEndDate.getTime() <= lastDayOfWeek.getTime();
     },
     /**
      * Return timeline bar width for current week (timeline cell) based on
@@ -37,14 +40,6 @@ export default {
 
       return Math.min(cellWidth, barWidth);
     },
-    /**
-     * Gets timelinebar end offset based width of single day
-     * and TIMELINE_END_OFFSET_HALF
-     */
-    getTimelineBarEndOffsetHalfForWeek() {
-      const dayWidth = this.getCellWidth() / 7;
-      return TIMELINE_END_OFFSET_HALF + dayWidth * 0.5;
-    },
     /**
      * In case startDate for any epic is undefined or is out of range
      * for current timeframe, we have to provide specific offset while
@@ -73,15 +68,6 @@ export default {
         return 'left: 0;';
       }
 
-      const lastTimeframeItem = new Date(this.timeframe[this.timeframe.length - 1].getTime());
-      lastTimeframeItem.setDate(lastTimeframeItem.getDate() + 6);
-      if (
-        this.epic.startDate >= this.timeframe[this.timeframe.length - 1] &&
-        this.epic.startDate <= lastTimeframeItem
-      ) {
-        return `right: ${TIMELINE_END_OFFSET_HALF}px;`;
-      }
-
       return `left: ${startDate * dayWidth - dayWidth / 2}px;`;
     },
     /**
@@ -111,7 +97,6 @@ export default {
 
       const indexOfCurrentWeek = this.timeframe.indexOf(this.timeframeItem);
       const cellWidth = this.getCellWidth();
-      const offsetEnd = this.getTimelineBarEndOffset();
       const epicStartDate = this.epic.startDate;
       const epicEndDate = this.epic.endDate;
 
@@ -135,7 +120,7 @@ export default {
         }
       }
 
-      return timelineBarWidth - offsetEnd;
+      return timelineBarWidth;
     },
   },
 };
diff --git a/ee/app/assets/javascripts/roadmap/service/roadmap_service.js b/ee/app/assets/javascripts/roadmap/service/roadmap_service.js
index ea4d58aeff18548b91b90eddf708a4a34da7d375..4c2a0cf81ef046d9b9e99d3d18f9052634acf4b7 100644
--- a/ee/app/assets/javascripts/roadmap/service/roadmap_service.js
+++ b/ee/app/assets/javascripts/roadmap/service/roadmap_service.js
@@ -1,11 +1,28 @@
 import axios from '~/lib/utils/axios_utils';
 
+import { getEpicsPathForPreset } from '../utils/roadmap_utils';
+
 export default class RoadmapService {
-  constructor(epicsPath) {
-    this.epicsPath = epicsPath;
+  constructor({ basePath, epicsState, filterQueryString, initialEpicsPath }) {
+    this.basePath = basePath;
+    this.epicsState = epicsState;
+    this.filterQueryString = filterQueryString;
+    this.initialEpicsPath = initialEpicsPath;
   }
 
   getEpics() {
-    return axios.get(this.epicsPath);
+    return axios.get(this.initialEpicsPath);
+  }
+
+  getEpicsForTimeframe(presetType, timeframe) {
+    const epicsPath = getEpicsPathForPreset({
+      basePath: this.basePath,
+      epicsState: this.epicsState,
+      filterQueryString: this.filterQueryString,
+      presetType,
+      timeframe,
+    });
+
+    return axios.get(epicsPath);
   }
 }
diff --git a/ee/app/assets/javascripts/roadmap/store/roadmap_store.js b/ee/app/assets/javascripts/roadmap/store/roadmap_store.js
index fb4a25d17b66541882ec7c174a88bf3160bc1795..64ca6487702cc43e911814b3f2c2920ae2f421fb 100644
--- a/ee/app/assets/javascripts/roadmap/store/roadmap_store.js
+++ b/ee/app/assets/javascripts/roadmap/store/roadmap_store.js
@@ -1,16 +1,19 @@
 import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { parsePikadayDate } from '~/lib/utils/datetime_utility';
+import { newDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
 
-import { PRESET_TYPES } from '../constants';
+import { extendTimeframeForPreset, sortEpics } from '../utils/roadmap_utils';
+import { PRESET_TYPES, EXTEND_AS } from '../constants';
 
 export default class RoadmapStore {
-  constructor(groupId, timeframe, presetType) {
+  constructor({ groupId, timeframe, presetType, sortedBy }) {
     this.state = {};
     this.state.epics = [];
+    this.state.epicIds = [];
     this.state.currentGroupId = groupId;
     this.state.timeframe = timeframe;
 
     this.presetType = presetType;
+    this.sortedBy = sortedBy;
     this.initTimeframeThreshold();
   }
 
@@ -27,24 +30,33 @@ export default class RoadmapStore {
       this.timeframeEndDate = this.state.timeframe[lastTimeframeIndex];
     } else if (this.presetType === PRESET_TYPES.WEEKS) {
       this.timeframeStartDate = startFrame;
-      this.timeframeEndDate = new Date(this.state.timeframe[lastTimeframeIndex].getTime());
+      this.timeframeEndDate = newDate(this.state.timeframe[lastTimeframeIndex]);
       this.timeframeEndDate.setDate(this.timeframeEndDate.getDate() + 7);
     }
   }
 
   setEpics(epics) {
-    this.state.epics = epics.reduce((filteredEpics, epic) => {
-      const formattedEpic = RoadmapStore.formatEpicDetails(
-        epic,
-        this.timeframeStartDate,
-        this.timeframeEndDate,
-      );
-      // Exclude any Epic that has invalid dates
-      if (formattedEpic.startDate <= formattedEpic.endDate) {
-        filteredEpics.push(formattedEpic);
-      }
-      return filteredEpics;
-    }, []);
+    this.state.epicIds = [];
+    this.state.epics = RoadmapStore.filterInvalidEpics({
+      timeframeStartDate: this.timeframeStartDate,
+      timeframeEndDate: this.timeframeEndDate,
+      state: this.state,
+      epics,
+    });
+  }
+
+  addEpics(epics) {
+    this.state.epics = this.state.epics.concat(
+      RoadmapStore.filterInvalidEpics({
+        timeframeStartDate: this.timeframeStartDate,
+        timeframeEndDate: this.timeframeEndDate,
+        state: this.state,
+        newEpic: true,
+        epics,
+      }),
+    );
+
+    sortEpics(this.state.epics, this.sortedBy);
   }
 
   getEpics() {
@@ -59,6 +71,57 @@ export default class RoadmapStore {
     return this.state.timeframe;
   }
 
+  extendTimeframe(extendAs = EXTEND_AS.PREPEND) {
+    const timeframeToExtend = extendTimeframeForPreset({
+      presetType: this.presetType,
+      extendAs,
+      initialDate: extendAs === EXTEND_AS.PREPEND ? this.timeframeStartDate : this.timeframeEndDate,
+    });
+
+    if (extendAs === EXTEND_AS.PREPEND) {
+      this.state.timeframe.unshift(...timeframeToExtend);
+    } else {
+      this.state.timeframe.push(...timeframeToExtend);
+    }
+
+    this.initTimeframeThreshold();
+
+    this.state.epics.forEach(epic =>
+      RoadmapStore.processEpicDates(epic, this.timeframeStartDate, this.timeframeEndDate),
+    );
+
+    return timeframeToExtend;
+  }
+
+  static filterInvalidEpics({
+    epics,
+    timeframeStartDate,
+    timeframeEndDate,
+    state,
+    newEpic = false,
+  }) {
+    return epics.reduce((filteredEpics, epic) => {
+      const formattedEpic = RoadmapStore.formatEpicDetails(
+        epic,
+        timeframeStartDate,
+        timeframeEndDate,
+      );
+      // Exclude any Epic that has invalid dates
+      // or is already present in Roadmap timeline
+      if (
+        formattedEpic.startDate <= formattedEpic.endDate &&
+        state.epicIds.indexOf(formattedEpic.id) < 0
+      ) {
+        Object.assign(formattedEpic, {
+          newEpic,
+        });
+        filteredEpics.push(formattedEpic);
+        state.epicIds.push(formattedEpic.id);
+      }
+      return filteredEpics;
+    }, []);
+  }
+
   /**
    * This method constructs Epic object and assigns proxy dates
    * in case start or end dates are unavailable.
@@ -73,44 +136,75 @@ export default class RoadmapStore {
     if (rawEpic.start_date) {
       // If startDate is present
       const startDate = parsePikadayDate(rawEpic.start_date);
-
-      if (startDate <= timeframeStartDate) {
-        // If startDate is less than first timeframe item
-        // startDate is out of range;
-        epicItem.startDateOutOfRange = true;
-        // store original start date in different object
-        epicItem.originalStartDate = startDate;
-        // Use startDate object to set a proxy date so
-        // that timeline bar can render it.
-        epicItem.startDate = new Date(timeframeStartDate.getTime());
-      } else {
-        // startDate is within timeframe range
-        epicItem.startDate = startDate;
-      }
+      epicItem.startDate = startDate;
+      epicItem.originalStartDate = startDate;
     } else {
-      // Start date is not available
+      // startDate is not available
       epicItem.startDateUndefined = true;
-      // Set proxy date so that timeline bar can render it.
-      epicItem.startDate = new Date(timeframeStartDate.getTime());
     }
 
-    // Same as above but for endDate
-    // This entire chunk can be moved into generic method
-    // but we're keeping it here for the sake of simplicity.
     if (rawEpic.end_date) {
+      // If endDate is present
       const endDate = parsePikadayDate(rawEpic.end_date);
-      if (endDate >= timeframeEndDate) {
-        epicItem.endDateOutOfRange = true;
-        epicItem.originalEndDate = endDate;
-        epicItem.endDate = new Date(timeframeEndDate.getTime());
-      } else {
-        epicItem.endDate = endDate;
-      }
+      epicItem.endDate = endDate;
+      epicItem.originalEndDate = endDate;
     } else {
+      // endDate is not available
       epicItem.endDateUndefined = true;
-      epicItem.endDate = new Date(timeframeEndDate.getTime());
     }
 
+    RoadmapStore.processEpicDates(epicItem, timeframeStartDate, timeframeEndDate);
+
     return epicItem;
   }
+
+  static processEpicDates(epic, timeframeStartDate, timeframeEndDate) {
+    if (!epic.startDateUndefined) {
+      // If startDate is less than first timeframe item
+      if (epic.originalStartDate.getTime() < timeframeStartDate.getTime()) {
+        Object.assign(epic, {
+          // startDate is out of range
+          startDateOutOfRange: true,
+          // Use startDate object to set a proxy date so
+          // that timeline bar can render it.
+          startDate: newDate(timeframeStartDate),
+        });
+      } else {
+        Object.assign(epic, {
+          // startDate is within range
+          startDateOutOfRange: false,
+          // Set startDate to original startDate
+          startDate: newDate(epic.originalStartDate),
+        });
+      }
+    } else {
+      Object.assign(epic, {
+        startDate: newDate(timeframeStartDate),
+      });
+    }
+
+    if (!epic.endDateUndefined) {
+      // If endDate is greater than last timeframe item
+      if (epic.originalEndDate.getTime() > timeframeEndDate.getTime()) {
+        Object.assign(epic, {
+          // endDate is out of range
+          endDateOutOfRange: true,
+          // Use endDate object to set a proxy date so
+          // that timeline bar can render it.
+          endDate: newDate(timeframeEndDate),
+        });
+      } else {
+        Object.assign(epic, {
+          // startDate is within range
+          endDateOutOfRange: false,
+          // Set startDate to original startDate
+          endDate: newDate(epic.originalEndDate),
+        });
+      }
+    } else {
+      Object.assign(epic, {
+        endDate: newDate(timeframeEndDate),
+      });
+    }
+  }
 }
diff --git a/ee/app/assets/javascripts/roadmap/utils/roadmap_utils.js b/ee/app/assets/javascripts/roadmap/utils/roadmap_utils.js
index 22d2d388f5aed28129189795e8f9b961a3240c3b..d638981e3a848071abb20e973e4dbe085cadd9e4 100644
--- a/ee/app/assets/javascripts/roadmap/utils/roadmap_utils.js
+++ b/ee/app/assets/javascripts/roadmap/utils/roadmap_utils.js
@@ -1,15 +1,23 @@
-import { getTimeframeWindowFrom, totalDaysInMonth } from '~/lib/utils/datetime_utility';
+import { newDate, getTimeframeWindowFrom, totalDaysInMonth } from '~/lib/utils/datetime_utility';
 
-import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
+import { PRESET_TYPES, PRESET_DEFAULTS, EXTEND_AS, TIMELINE_CELL_MIN_WIDTH } from '../constants';
+
+const monthsForQuarters = {
+  1: [0, 1, 2],
+  2: [3, 4, 5],
+  3: [6, 7, 8],
+  4: [9, 10, 11],
+};
 
 /**
  * This method returns array of Objects representing Quarters based on provided initialDate
  *
  * For eg; If initialDate is 15th Jan 2018
  *         Then as per Roadmap specs, we need to show
- *         1 quarter before current quarter AND
+ *         2 quarters before current quarters
+ *         current quarter AND
  *         4 quarters after current quarter
- *         thus, total of 6 quarters.
+ *         thus, total of 7 quarters (21 Months).
  *
  * So returned array from this method will be;
  *        [
@@ -47,37 +55,32 @@ import { PRESET_TYPES, PRESET_DEFAULTS } from '../constants';
  *
  * @param {Date} initialDate
  */
-export const getTimeframeForQuartersView = (initialDate = new Date()) => {
-  const startDate = initialDate;
+export const getTimeframeForQuartersView = (initialDate = new Date(), timeframe = []) => {
+  const startDate = newDate(initialDate);
   startDate.setHours(0, 0, 0, 0);
 
-  const monthsForQuarters = {
-    1: [0, 1, 2],
-    2: [3, 4, 5],
-    3: [6, 7, 8],
-    4: [9, 10, 11],
-  };
-
-  // Get current quarter for current month
-  const currentQuarter = Math.floor((startDate.getMonth() + 3) / 3);
-  // Get index of current month in current quarter
-  // It could be 0, 1, 2 (i.e. first, second or third)
-  const currentMonthInCurrentQuarter = monthsForQuarters[currentQuarter].indexOf(
-    startDate.getMonth(),
-  );
+  if (!timeframe.length) {
+    // Get current quarter for current month
+    const currentQuarter = Math.floor((startDate.getMonth() + 3) / 3);
+    // Get index of current month in current quarter
+    // It could be 0, 1, 2 (i.e. first, second or third)
+    const currentMonthInCurrentQuarter = monthsForQuarters[currentQuarter].indexOf(
+      startDate.getMonth(),
+    );
 
-  // To move start back to first month of previous quarter
-  // Adding quarter size (3) to month order will give us
-  // exact number of months we need to go back in time
-  const startMonth = currentMonthInCurrentQuarter + 3;
-  const quartersTimeframe = [];
-  // Move startDate to first month of previous quarter
-  startDate.setMonth(startDate.getMonth() - startMonth);
+    // To move start back to first month of 2 quarters prior by
+    // adding quarter size (3 + 3) to month order will give us
+    // exact number of months we need to go back in time
+    const startMonth = currentMonthInCurrentQuarter + 6;
+    // Move startDate to first month of previous quarter
+    startDate.setMonth(startDate.getMonth() - startMonth);
 
-  // Get timeframe for the length we determined for this preset
-  // start from the startDate
-  const timeframe = getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH);
+    // Get timeframe for the length we determined for this preset
+    // start from the startDate
+    timeframe.push(...getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH));
+  }
 
+  const quartersTimeframe = [];
   // Iterate over the timeframe and break it down
   // in chunks of quarters
   for (let i = 0; i < timeframe.length; i += 3) {
@@ -100,83 +103,246 @@ export const getTimeframeForQuartersView = (initialDate = new Date()) => {
   return quartersTimeframe;
 };
 
+export const extendTimeframeForQuartersView = (initialDate = new Date(), length) => {
+  const startDate = newDate(initialDate);
+  startDate.setDate(1);
+
+  startDate.setMonth(startDate.getMonth() + (length > 0 ? 1 : -1));
+  const timeframe = getTimeframeWindowFrom(startDate, length);
+
+  return getTimeframeForQuartersView(startDate, length > 0 ? timeframe : timeframe.reverse());
+};
+
 /**
  * This method returns array of Dates respresenting Months based on provided initialDate
  *
  * For eg; If initialDate is 15th Jan 2018
  *         Then as per Roadmap specs, we need to show
- *         1 month before current month AND
+ *         2 months before current month,
+ *         current month AND
  *         5 months after current month
- *         thus, total of 7 months.
+ *         thus, total of 8 months.
  *
  * So returned array from this method will be;
  *        [
- *          1 Dec 2017, 1 Jan 2018, 1 Feb 2018, 1 Mar 2018,
- *          1 Apr 2018, 1 May 2018, 30 Jun 2018
+ *          1 Nov 2017, 1 Dec 2017, 1 Jan 2018, 1 Feb 2018,
+ *          1 Mar 2018, 1 Apr 2018, 1 May 2018, 30 Jun 2018
  *        ]
  *
  * @param {Date} initialDate
  */
 export const getTimeframeForMonthsView = (initialDate = new Date()) => {
-  const startDate = initialDate;
-  startDate.setHours(0, 0, 0, 0);
+  const startDate = newDate(initialDate);
 
   // Move startDate to a month prior to current month
-  startDate.setMonth(startDate.getMonth() - 1);
+  startDate.setMonth(startDate.getMonth() - 2);
 
   return getTimeframeWindowFrom(startDate, PRESET_DEFAULTS.MONTHS.TIMEFRAME_LENGTH);
 };
 
+export const extendTimeframeForMonthsView = (initialDate = new Date(), length) => {
+  const startDate = newDate(initialDate);
+
+  // When length is positive (which means extension is of type APPEND)
+  // Set initial date as first day of the month.
+  if (length > 0) {
+    startDate.setDate(1);
+  }
+
+  const timeframe = getTimeframeWindowFrom(startDate, length - 1).slice(1);
+
+  return length > 0 ? timeframe : timeframe.reverse();
+};
+
 /**
  * This method returns array of Dates respresenting Months based on provided initialDate
  *
  * For eg; If initialDate is 15th Jan 2018
  *         Then as per Roadmap specs, we need to show
- *         1 week before current week AND
+ *         2 weeks before current week,
+ *         current week AND
  *         4 weeks after current week
- *         thus, total of 6 weeks.
+ *         thus, total of 7 weeks.
  *         Note that week starts on Sunday
  *
  * So returned array from this method will be;
  *        [
- *          7 Jan 2018, 14 Jan 2018, 21 Jan 2018,
+ *          31 Dec 2017, 7 Jan 2018, 14 Jan 2018, 21 Jan 2018,
  *          28 Jan 2018, 4 Mar 2018, 11 Mar 2018
  *        ]
  *
  * @param {Date} initialDate
  */
 export const getTimeframeForWeeksView = (initialDate = new Date()) => {
-  const startDate = initialDate;
+  const startDate = newDate(initialDate);
   startDate.setHours(0, 0, 0, 0);
 
   const dayOfWeek = startDate.getDay();
-  const daysToFirstDayOfPrevWeek = dayOfWeek + 7;
+  const daysToFirstDayOfPrevWeek = dayOfWeek + 14;
   const timeframe = [];
 
-  // Move startDate to first day (Sunday) of previous week
+  // Move startDate to first day (Sunday) of 2 weeks prior
   startDate.setDate(startDate.getDate() - daysToFirstDayOfPrevWeek);
 
   // Iterate for the length of this preset
   for (let i = 0; i < PRESET_DEFAULTS.WEEKS.TIMEFRAME_LENGTH; i += 1) {
     // Push date to timeframe only when day is
-    // first day (Sunday) of the week1
-    if (startDate.getDay() === 0) {
-      timeframe.push(new Date(startDate.getTime()));
-    }
-    // Move date one day further
-    startDate.setDate(startDate.getDate() + 1);
+    // first day (Sunday) of the week
+    timeframe.push(newDate(startDate));
+
+    // Move date next Sunday
+    startDate.setDate(startDate.getDate() + 7);
   }
 
   return timeframe;
 };
 
-export const getTimeframeForPreset = (presetType = PRESET_TYPES.MONTHS) => {
+export const extendTimeframeForWeeksView = (initialDate = new Date(), length) => {
+  const startDate = newDate(initialDate);
+
+  if (length < 0) {
+    startDate.setDate(startDate.getDate() + (length + 1) * 7);
+  } else {
+    startDate.setDate(startDate.getDate() + 7);
+  }
+
+  const timeframe = getTimeframeForWeeksView(startDate, length + 1);
+  return timeframe.slice(1);
+};
+
+export const extendTimeframeForPreset = ({
+  presetType = PRESET_TYPES.MONTHS,
+  extendAs = EXTEND_AS.PREPEND,
+  extendByLength = 0,
+  initialDate,
+}) => {
   if (presetType === PRESET_TYPES.QUARTERS) {
-    return getTimeframeForQuartersView();
+    const length = extendByLength || PRESET_DEFAULTS.QUARTERS.TIMEFRAME_LENGTH;
+
+    return extendTimeframeForQuartersView(
+      initialDate,
+      extendAs === EXTEND_AS.PREPEND ? -length : length,
+    );
   } else if (presetType === PRESET_TYPES.MONTHS) {
-    return getTimeframeForMonthsView();
+    const length = extendByLength || PRESET_DEFAULTS.MONTHS.TIMEFRAME_LENGTH;
+
+    return extendTimeframeForMonthsView(
+      initialDate,
+      extendAs === EXTEND_AS.PREPEND ? -length : length,
+    );
   }
-  return getTimeframeForWeeksView();
+
+  const length = extendByLength || PRESET_DEFAULTS.WEEKS.TIMEFRAME_LENGTH;
+
+  return extendTimeframeForWeeksView(
+    initialDate,
+    extendAs === EXTEND_AS.PREPEND ? -length : length,
+  );
+};
+
+export const extendTimeframeForAvailableWidth = ({
+  timeframe,
+  timeframeStart,
+  timeframeEnd,
+  availableTimeframeWidth,
+  presetType,
+}) => {
+  let timeframeLength = timeframe.length;
+
+  // Estimate how many more timeframe columns are needed
+  // to fill in extra screen space so that timeline becomes
+  // horizontally scrollable.
+  while (availableTimeframeWidth / timeframeLength > TIMELINE_CELL_MIN_WIDTH) {
+    timeframeLength += 1;
+  }
+  // We double the increaseLengthBy to make sure there's enough room
+  // to perform horizontal scroll without triggering timeframe extension
+  // on initial page load.
+  const increaseLengthBy = (timeframeLength - timeframe.length) * 2;
+
+  // If there are timeframe items to be added
+  // to make timeline scrollable, do as follows.
+  if (increaseLengthBy > 0) {
+    // Split length in 2 parts and get
+    // count for both prepend and append.
+    const prependBy = Math.floor(increaseLengthBy / 2);
+    const appendBy = Math.ceil(increaseLengthBy / 2);
+
+    if (prependBy) {
+      // Prepend the timeline with
+      // the count as given by prependBy
+      timeframe.unshift(
+        ...extendTimeframeForPreset({
+          extendAs: EXTEND_AS.PREPEND,
+          initialDate: timeframeStart,
+          // In case of presetType `quarters`, length would represent
+          // number of months for total quarters, hence we do `* 3`.
+          extendByLength: presetType === PRESET_TYPES.QUARTERS ? prependBy * 3 : prependBy,
+          presetType,
+        }),
+      );
+    }
+
+    if (appendBy) {
+      // Append the timeline with
+      // the count as given by appendBy
+      timeframe.push(
+        ...extendTimeframeForPreset({
+          extendAs: EXTEND_AS.APPEND,
+          initialDate: timeframeEnd,
+          // In case of presetType `quarters`, length would represent
+          // number of months for total quarters, hence we do `* 3`.
+          //
+          // For other preset types, we add `2` to appendBy to compensate for
+          // last item of original timeframe (month or week)
+          extendByLength: presetType === PRESET_TYPES.QUARTERS ? appendBy * 3 : appendBy + 2,
+          presetType,
+        }),
+      );
+    }
+  }
+};
+
+export const getTimeframeForPreset = (
+  presetType = PRESET_TYPES.MONTHS,
+  availableTimeframeWidth = 0,
+) => {
+  let timeframe;
+  let timeframeStart;
+  let timeframeEnd;
+
+  // Get timeframe based on presetType and
+  // extract timeframeStart and timeframeEnd
+  // date objects
+  if (presetType === PRESET_TYPES.QUARTERS) {
+    timeframe = getTimeframeForQuartersView();
+    [timeframeStart] = timeframe[0].range;
+    // eslint-disable-next-line prefer-destructuring
+    timeframeEnd = timeframe[timeframe.length - 1].range[2];
+  } else if (presetType === PRESET_TYPES.MONTHS) {
+    timeframe = getTimeframeForMonthsView();
+    [timeframeStart] = timeframe;
+    timeframeEnd = timeframe[timeframe.length - 1];
+  } else {
+    timeframe = getTimeframeForWeeksView();
+    timeframeStart = newDate(timeframe[0]);
+    timeframeEnd = newDate(timeframe[timeframe.length - 1]);
+    timeframeStart.setDate(timeframeStart.getDate() - 7); // Move date back by a week
+    timeframeEnd.setDate(timeframeEnd.getDate() + 7); // Move date ahead by a week
+  }
+
+  // Extend timeframe on initial load to ensure
+  // timeline is horizontally scrollable in all
+  // screen sizes.
+  extendTimeframeForAvailableWidth({
+    timeframe,
+    timeframeStart,
+    timeframeEnd,
+    availableTimeframeWidth,
+    presetType,
+  });
+
+  return timeframe;
 };
 
 export const getEpicsPathForPreset = ({
@@ -184,7 +350,7 @@ export const getEpicsPathForPreset = ({
   filterQueryString = '',
   presetType = '',
   timeframe = [],
-  state = 'all',
+  epicsState = 'all',
 }) => {
   let start;
   let end;
@@ -208,13 +374,13 @@ export const getEpicsPathForPreset = ({
     end = lastTimeframe;
   } else if (presetType === PRESET_TYPES.WEEKS) {
     start = firstTimeframe;
-    end = new Date(lastTimeframe.getTime());
+    end = newDate(lastTimeframe);
     end.setDate(end.getDate() + 6);
   }
 
   const startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
   const endDate = `${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`;
-  epicsPath += `?state=${state}&start_date=${startDate}&end_date=${endDate}`;
+  epicsPath += `?state=${epicsState}&start_date=${startDate}&end_date=${endDate}`;
 
   if (filterQueryString) {
     epicsPath += `&${filterQueryString}`;
@@ -222,3 +388,43 @@ export const getEpicsPathForPreset = ({
 
   return epicsPath;
 };
+
+export const sortEpics = (epics, sortedBy) => {
+  const sortByStartDate = sortedBy.indexOf('start_date') > -1;
+  const sortOrderAsc = sortedBy.indexOf('asc') > -1;
+
+  epics.sort((a, b) => {
+    let aDate;
+    let bDate;
+
+    if (sortByStartDate) {
+      aDate = a.startDate;
+      if (a.startDateOutOfRange) {
+        aDate = a.originalStartDate;
+      }
+
+      bDate = b.startDate;
+      if (b.startDateOutOfRange) {
+        bDate = b.originalStartDate;
+      }
+    } else {
+      aDate = a.endDate;
+      if (a.endDateOutOfRange) {
+        aDate = a.originalEndDate;
+      }
+
+      bDate = b.endDate;
+      if (b.endDateOutOfRange) {
+        bDate = b.originalEndDate;
+      }
+    }
+
+    // Sort in ascending or descending order
+    if (aDate.getTime() < bDate.getTime()) {
+      return sortOrderAsc ? -1 : 1;
+    } else if (aDate.getTime() > bDate.getTime()) {
+      return sortOrderAsc ? 1 : -1;
+    }
+    return 0;
+  });
+};
diff --git a/ee/app/assets/stylesheets/pages/roadmap.scss b/ee/app/assets/stylesheets/pages/roadmap.scss
index b5d2b6196722aa5e74f0214118d5be4bc4e00722..b23766fcf74228433b8a8f8316d1c8b170e9e769 100644
--- a/ee/app/assets/stylesheets/pages/roadmap.scss
+++ b/ee/app/assets/stylesheets/pages/roadmap.scss
@@ -20,6 +20,36 @@ $column-right-gradient: linear-gradient(
   $roadmap-gradient-gray 100%
 );
 
+@keyframes colorTransitionDetailsCell {
+  from {
+    background-color: $blue-100;
+  }
+
+  to {
+    background-color: $white-light;
+  }
+}
+
+@keyframes fadeInDetails {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes fadeinTimelineBar {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 0.75;
+  }
+}
+
 @mixin roadmap-scroll-mixin {
   height: $grid-size;
   width: $details-cell-width;
@@ -229,6 +259,12 @@ $column-right-gradient: linear-gradient(
       }
     }
 
+    &.newly-added-epic {
+      .epic-details-cell {
+        animation: colorTransitionDetailsCell 3s;
+      }
+    }
+
     .epic-details-cell,
     .epic-timeline-cell {
       box-sizing: border-box;
@@ -251,6 +287,11 @@ $column-right-gradient: linear-gradient(
         height: $item-height;
       }
 
+      .epic-title,
+      .epic-group-timeframe {
+        animation: fadeInDetails 1s;
+      }
+
       .epic-title {
         display: table;
         table-layout: fixed;
@@ -294,25 +335,12 @@ $column-right-gradient: linear-gradient(
         background-color: $blue-500;
         border-radius: $border-radius-default;
         opacity: 0.75;
+        animation: fadeinTimelineBar 1s;
 
         &:hover {
           opacity: 1;
         }
 
-        &.start-date-outside::before,
-        &.end-date-outside::after {
-          content: "";
-          position: absolute;
-          top: 0;
-          height: 100%;
-        }
-
-        &.start-date-outside::before,
-        &.end-date-outside::after {
-          border-top: 12px solid transparent;
-          border-bottom: 12px solid transparent;
-        }
-
         &.start-date-undefined {
           background: linear-gradient(
             to right,
@@ -330,31 +358,6 @@ $column-right-gradient: linear-gradient(
             $roadmap-gradient-gray 100%
           );
         }
-
-        &.start-date-outside {
-          border-top-left-radius: 0;
-          border-bottom-left-radius: 0;
-
-          &::before {
-            left: -$grid-size;
-            border-right: $grid-size solid $blue-500;
-          }
-        }
-
-        &.end-date-outside {
-          border-top-right-radius: 0;
-          border-bottom-right-radius: 0;
-
-          &::after {
-            right: -$grid-size;
-            border-left: $grid-size solid $blue-500;
-          }
-        }
-
-        &.start-date-outside,
-        &.start-date-undefined.end-date-outside {
-          left: $grid-size;
-        }
       }
 
       &:last-child {
diff --git a/ee/spec/javascripts/roadmap/components/app_spec.js b/ee/spec/javascripts/roadmap/components/app_spec.js
index fa55917a9d6beefd1a6c24e6016ab81d5f349fc0..d8710cc6c0d053895585bdc5a75772d28a28d7b5 100644
--- a/ee/spec/javascripts/roadmap/components/app_spec.js
+++ b/ee/spec/javascripts/roadmap/components/app_spec.js
@@ -6,25 +6,42 @@ import axios from '~/lib/utils/axios_utils';
 import appComponent from 'ee/roadmap/components/app.vue';
 import RoadmapStore from 'ee/roadmap/store/roadmap_store';
 import RoadmapService from 'ee/roadmap/service/roadmap_service';
+import eventHub from 'ee/roadmap/event_hub';
 
-import { PRESET_TYPES } from 'ee/roadmap/constants';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
+import { PRESET_TYPES, EXTEND_AS } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
 import {
-  mockTimeframeMonths,
+  mockTimeframeInitialDate,
   mockGroupId,
+  basePath,
   epicsPath,
   mockNewEpicEndpoint,
   rawEpics,
   mockSvgPath,
+  mockSortedBy,
 } from '../mock_data';
 
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
+
 const createComponent = () => {
   const Component = Vue.extend(appComponent);
   const timeframe = mockTimeframeMonths;
 
-  const store = new RoadmapStore(mockGroupId, timeframe);
-  const service = new RoadmapService(epicsPath);
+  const store = new RoadmapStore({
+    groupId: mockGroupId,
+    presetType: PRESET_TYPES.MONTHS,
+    sortedBy: mockSortedBy,
+    timeframe,
+  });
+
+  const service = new RoadmapService({
+    initialEpicsPath: epicsPath,
+    epicsState: 'all',
+    basePath,
+  });
 
   return mountComponent(Component, {
     store,
@@ -173,6 +190,193 @@ describe('AppComponent', () => {
         }, 0);
       });
     });
+
+    describe('fetchEpicsForTimeframe', () => {
+      const roadmapTimelineEl = {
+        offsetTop: 0,
+      };
+      let mock;
+
+      beforeEach(() => {
+        mock = new MockAdapter(axios);
+        document.body.innerHTML += '<div class="flash-container"></div>';
+      });
+
+      afterEach(() => {
+        mock.restore();
+        document.querySelector('.flash-container').remove();
+      });
+
+      it('calls service.fetchEpicsForTimeframe and adds response to the store on success', done => {
+        mock.onGet(vm.service.epicsPath).reply(200, rawEpics);
+
+        spyOn(vm.service, 'getEpicsForTimeframe').and.callThrough();
+        spyOn(vm.store, 'addEpics');
+        spyOn(vm, '$nextTick').and.stub();
+
+        vm.fetchEpicsForTimeframe({
+          timeframe: mockTimeframeMonths,
+          extendType: EXTEND_AS.APPEND,
+          roadmapTimelineEl,
+        });
+
+        expect(vm.hasError).toBe(false);
+        expect(vm.service.getEpicsForTimeframe).toHaveBeenCalledWith(
+          PRESET_TYPES.MONTHS,
+          mockTimeframeMonths,
+        );
+        setTimeout(() => {
+          expect(vm.store.addEpics).toHaveBeenCalledWith(rawEpics);
+          done();
+        }, 0);
+      });
+
+      it('calls service.fetchEpicsForTimeframe and sets `hasError` to true and shows flash message when request failed', done => {
+        mock.onGet(vm.service.fetchEpicsForTimeframe).reply(500, {});
+
+        vm.fetchEpicsForTimeframe({
+          timeframe: mockTimeframeMonths,
+          extendType: EXTEND_AS.APPEND,
+          roadmapTimelineEl,
+        });
+
+        expect(vm.hasError).toBe(false);
+        setTimeout(() => {
+          expect(vm.hasError).toBe(true);
+          expect(document.querySelector('.flash-text').innerText.trim()).toBe(
+            'Something went wrong while fetching epics',
+          );
+          done();
+        }, 0);
+      });
+    });
+
+    describe('processExtendedTimeline', () => {
+      it('updates timeline by extending timeframe from the start when called with extendType as `prepend`', done => {
+        vm.store.setEpics(rawEpics);
+        vm.isLoading = false;
+
+        Vue.nextTick()
+          .then(() => {
+            const roadmapTimelineEl = vm.$el.querySelector('.roadmap-timeline-section');
+
+            spyOn(eventHub, '$emit');
+            spyOn(roadmapTimelineEl.parentElement, 'scrollBy');
+
+            vm.processExtendedTimeline({
+              extendType: EXTEND_AS.PREPEND,
+              roadmapTimelineEl,
+              itemsCount: 0,
+            });
+
+            expect(eventHub.$emit).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Object));
+            expect(roadmapTimelineEl.parentElement.scrollBy).toHaveBeenCalled();
+          })
+          .then(done)
+          .catch(done.fail);
+      });
+
+      it('updates timeline by extending timeframe from the end when called with extendType as `append`', done => {
+        vm.store.setEpics(rawEpics);
+        vm.isLoading = false;
+
+        Vue.nextTick()
+          .then(() => {
+            const roadmapTimelineEl = vm.$el.querySelector('.roadmap-timeline-section');
+
+            spyOn(eventHub, '$emit');
+
+            vm.processExtendedTimeline({
+              extendType: EXTEND_AS.PREPEND,
+              roadmapTimelineEl,
+              itemsCount: 0,
+            });
+
+            expect(eventHub.$emit).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Object));
+          })
+          .then(done)
+          .catch(done.fail);
+      });
+    });
+
+    describe('handleScrollToExtend', () => {
+      beforeEach(() => {
+        vm.store.setEpics(rawEpics);
+        vm.isLoading = false;
+      });
+
+      it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `prepend`', done => {
+        spyOn(vm.store, 'extendTimeframe');
+        spyOn(vm, 'processExtendedTimeline');
+        spyOn(vm, 'fetchEpicsForTimeframe');
+
+        const extendType = EXTEND_AS.PREPEND;
+        const roadmapTimelineEl = vm.$el.querySelector('.roadmap-timeline-section');
+
+        vm.handleScrollToExtend(roadmapTimelineEl, extendType);
+
+        expect(vm.store.extendTimeframe).toHaveBeenCalledWith(extendType);
+
+        vm.$nextTick()
+          .then(() => {
+            expect(vm.processExtendedTimeline).toHaveBeenCalledWith(
+              jasmine.objectContaining({
+                itemsCount: 0,
+                extendType,
+                roadmapTimelineEl,
+              }),
+            );
+
+            expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith(
+              jasmine.objectContaining({
+                // During tests, we don't extend timeframe
+                // as we spied on `vm.store.extendTimeframe` above
+                timeframe: undefined,
+                roadmapTimelineEl,
+                extendType,
+              }),
+            );
+          })
+          .then(done)
+          .catch(done.fail);
+      });
+
+      it('updates the store and refreshes roadmap with extended timeline when called with `extendType` param as `append`', done => {
+        spyOn(vm.store, 'extendTimeframe');
+        spyOn(vm, 'processExtendedTimeline');
+        spyOn(vm, 'fetchEpicsForTimeframe');
+
+        const extendType = EXTEND_AS.APPEND;
+        const roadmapTimelineEl = vm.$el.querySelector('.roadmap-timeline-section');
+
+        vm.handleScrollToExtend(roadmapTimelineEl, extendType);
+
+        expect(vm.store.extendTimeframe).toHaveBeenCalledWith(extendType);
+
+        vm.$nextTick()
+          .then(() => {
+            expect(vm.processExtendedTimeline).toHaveBeenCalledWith(
+              jasmine.objectContaining({
+                itemsCount: 0,
+                extendType,
+                roadmapTimelineEl,
+              }),
+            );
+
+            expect(vm.fetchEpicsForTimeframe).toHaveBeenCalledWith(
+              jasmine.objectContaining({
+                // During tests, we don't extend timeframe
+                // as we spied on `vm.store.extendTimeframe` above
+                timeframe: undefined,
+                roadmapTimelineEl,
+                extendType,
+              }),
+            );
+          })
+          .then(done)
+          .catch(done.fail);
+      });
+    });
   });
 
   describe('mounted', () => {
diff --git a/ee/spec/javascripts/roadmap/components/epic_item_spec.js b/ee/spec/javascripts/roadmap/components/epic_item_spec.js
index c8a6796f77d02e8741ff3842e3b4aa20dfc6d3b7..add5b13d888581430bd0778b07332df0107d364e 100644
--- a/ee/spec/javascripts/roadmap/components/epic_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/epic_item_spec.js
@@ -1,18 +1,24 @@
 import Vue from 'vue';
 
+import _ from 'underscore';
+
 import epicItemComponent from 'ee/roadmap/components/epic_item.vue';
 
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
 import {
-  mockTimeframeMonths,
+  mockTimeframeInitialDate,
   mockEpic,
   mockGroupId,
   mockShellWidth,
   mockItemWidth,
 } from '../mock_data';
 
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
+
 const createComponent = ({
   presetType = PRESET_TYPES.MONTHS,
   epic = mockEpic,
@@ -44,6 +50,25 @@ describe('EpicItemComponent', () => {
     vm.$destroy();
   });
 
+  describe('methods', () => {
+    describe('removeHighlight', () => {
+      it('should call _.delay after 3 seconds with a callback function which would set `epic.newEpic` to false when it is true already', done => {
+        spyOn(_, 'delay');
+
+        vm.epic.newEpic = true;
+
+        vm.removeHighlight();
+
+        vm.$nextTick()
+          .then(() => {
+            expect(_.delay).toHaveBeenCalledWith(jasmine.any(Function), 3000);
+          })
+          .then(done)
+          .catch(done.fail);
+      });
+    });
+  });
+
   describe('template', () => {
     it('renders component container element class `epics-list-item`', () => {
       expect(vm.$el.classList.contains('epics-list-item')).toBeTruthy();
diff --git a/ee/spec/javascripts/roadmap/components/epic_item_timeline_spec.js b/ee/spec/javascripts/roadmap/components/epic_item_timeline_spec.js
index 0f16e7b6596a50cca109e56ea89475f830b2e339..f85274420c202c8a9230925364c74c5fd255af89 100644
--- a/ee/spec/javascripts/roadmap/components/epic_item_timeline_spec.js
+++ b/ee/spec/javascripts/roadmap/components/epic_item_timeline_spec.js
@@ -1,15 +1,15 @@
 import Vue from 'vue';
 
 import epicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
-import {
-  TIMELINE_END_OFFSET_FULL,
-  TIMELINE_END_OFFSET_HALF,
-  TIMELINE_CELL_MIN_WIDTH,
-  PRESET_TYPES,
-} from 'ee/roadmap/constants';
+import eventHub from 'ee/roadmap/event_hub';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
+import { TIMELINE_CELL_MIN_WIDTH, PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeMonths, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.MONTHS,
@@ -62,7 +62,7 @@ describe('EpicItemTimelineComponent', () => {
       it('returns proportionate width based on timeframe length and shellWidth', () => {
         vm = createComponent({});
 
-        expect(vm.getCellWidth()).toBe(240);
+        expect(vm.getCellWidth()).toBe(210);
       });
 
       it('returns minimum fixed width when proportionate width available lower than minimum fixed width defined', () => {
@@ -74,46 +74,6 @@ describe('EpicItemTimelineComponent', () => {
       });
     });
 
-    describe('getTimelineBarEndOffset', () => {
-      it('returns full offset value when both Epic startDate and endDate is out of range', () => {
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            startDateOutOfRange: true,
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarEndOffset()).toBe(TIMELINE_END_OFFSET_FULL);
-      });
-
-      it('returns full offset value when Epic startDate is undefined and endDate is out of range', () => {
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            startDateUndefined: true,
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarEndOffset()).toBe(TIMELINE_END_OFFSET_FULL);
-      });
-
-      it('returns half offset value when Epic endDate is out of range', () => {
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarEndOffset()).toBe(TIMELINE_END_OFFSET_HALF);
-      });
-
-      it('returns 0 when both Epic startDate and endDate is defined and within range', () => {
-        vm = createComponent({});
-
-        expect(vm.getTimelineBarEndOffset()).toBe(0);
-      });
-    });
-
     describe('renderTimelineBar', () => {
       it('sets `timelineBarStyles` & `timelineBarReady` when timeframeItem has Epic.startDate', () => {
         vm = createComponent({
@@ -122,7 +82,7 @@ describe('EpicItemTimelineComponent', () => {
         });
         vm.renderTimelineBar();
 
-        expect(vm.timelineBarStyles).toBe('width: 1216px; left: 0;');
+        expect(vm.timelineBarStyles).toBe('width: 1274px; left: 0;');
         expect(vm.timelineBarReady).toBe(true);
       });
 
@@ -139,6 +99,26 @@ describe('EpicItemTimelineComponent', () => {
     });
   });
 
+  describe('mounted', () => {
+    it('binds `refreshTimeline` event listener on eventHub', () => {
+      spyOn(eventHub, '$on');
+      const vmX = createComponent({});
+
+      expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
+      vmX.$destroy();
+    });
+  });
+
+  describe('beforeDestroy', () => {
+    it('unbinds `refreshTimeline` event listener on eventHub', () => {
+      spyOn(eventHub, '$off');
+      const vmX = createComponent({});
+      vmX.$destroy();
+
+      expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
+    });
+  });
+
   describe('template', () => {
     it('renders component container element with class `epic-timeline-cell`', () => {
       vm = createComponent({});
@@ -172,7 +152,7 @@ describe('EpicItemTimelineComponent', () => {
 
       vm.renderTimelineBar();
       vm.$nextTick(() => {
-        expect(timelineBarEl.getAttribute('style')).toBe('width: 608.571px; left: 0px;');
+        expect(timelineBarEl.getAttribute('style')).toBe('width: 742.5px; left: 0px;');
         done();
       });
     });
@@ -193,23 +173,6 @@ describe('EpicItemTimelineComponent', () => {
       });
     });
 
-    it('renders timeline bar with `start-date-outside` class when Epic startDate is out of range of timeframe', done => {
-      vm = createComponent({
-        epic: Object.assign({}, mockEpic, {
-          startDateOutOfRange: true,
-          startDate: mockTimeframeMonths[0],
-          originalStartDate: new Date(2017, 0, 1),
-        }),
-      });
-      const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
-
-      vm.renderTimelineBar();
-      vm.$nextTick(() => {
-        expect(timelineBarEl.classList.contains('start-date-outside')).toBe(true);
-        done();
-      });
-    });
-
     it('renders timeline bar with `end-date-undefined` class when Epic endDate is undefined', done => {
       vm = createComponent({
         epic: Object.assign({}, mockEpic, {
@@ -226,23 +189,5 @@ describe('EpicItemTimelineComponent', () => {
         done();
       });
     });
-
-    it('renders timeline bar with `end-date-outside` class when Epic endDate is out of range of timeframe', done => {
-      vm = createComponent({
-        epic: Object.assign({}, mockEpic, {
-          startDate: mockTimeframeMonths[0],
-          endDateOutOfRange: true,
-          endDate: mockTimeframeMonths[mockTimeframeMonths.length - 1],
-          originalEndDate: new Date(2018, 11, 1),
-        }),
-      });
-      const timelineBarEl = vm.$el.querySelector('.timeline-bar-wrapper .timeline-bar');
-
-      vm.renderTimelineBar();
-      vm.$nextTick(() => {
-        expect(timelineBarEl.classList.contains('end-date-outside')).toBe(true);
-        done();
-      });
-    });
   });
 });
diff --git a/ee/spec/javascripts/roadmap/components/epics_list_empty_spec.js b/ee/spec/javascripts/roadmap/components/epics_list_empty_spec.js
index 46ad26376853d36bfa0a54c97112451516c36503..ddbb39f0896670f5704257439347504548be4be4 100644
--- a/ee/spec/javascripts/roadmap/components/epics_list_empty_spec.js
+++ b/ee/spec/javascripts/roadmap/components/epics_list_empty_spec.js
@@ -2,16 +2,20 @@ import Vue from 'vue';
 
 import epicsListEmptyComponent from 'ee/roadmap/components/epics_list_empty.vue';
 
+import {
+  getTimeframeForQuartersView,
+  getTimeframeForWeeksView,
+  getTimeframeForMonthsView,
+} from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import {
-  mockTimeframeQuarters,
-  mockTimeframeMonths,
-  mockTimeframeWeeks,
-  mockSvgPath,
-  mockNewEpicEndpoint,
-} from '../mock_data';
+import { mockTimeframeInitialDate, mockSvgPath, mockNewEpicEndpoint } from '../mock_data';
+
+const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
+const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
 
 const createComponent = ({
   hasFiltersApplied = false,
@@ -71,7 +75,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from Oct 1, 2017 to Mar 31, 2019.',
+                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Jul 1, 2017 to Mar 31, 2019.',
               );
             })
             .then(done)
@@ -83,7 +87,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To widen your search, change or remove filters. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from Oct 1, 2017 to Mar 31, 2019.',
+                'To widen your search, change or remove filters; from Jul 1, 2017 to Mar 31, 2019.',
               );
             })
             .then(done)
@@ -100,7 +104,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from Dec 1, 2017 to Jun 30, 2018.',
+                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Nov 1, 2017 to Jun 30, 2018.',
               );
             })
             .then(done)
@@ -112,7 +116,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To widen your search, change or remove filters. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from Dec 1, 2017 to Jun 30, 2018.',
+                'To widen your search, change or remove filters; from Nov 1, 2017 to Jun 30, 2018.',
               );
             })
             .then(done)
@@ -134,7 +138,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from Dec 24, 2017 to Feb 9, 2018.',
+                'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Dec 17, 2017 to Feb 9, 2018.',
               );
             })
             .then(done)
@@ -146,7 +150,7 @@ describe('EpicsListEmptyComponent', () => {
           Vue.nextTick()
             .then(() => {
               expect(vm.subMessage).toBe(
-                'To widen your search, change or remove filters. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from Dec 24, 2017 to Feb 15, 2018.',
+                'To widen your search, change or remove filters; from Dec 17, 2017 to Feb 15, 2018.',
               );
             })
             .then(done)
@@ -157,7 +161,7 @@ describe('EpicsListEmptyComponent', () => {
 
     describe('timeframeRange', () => {
       it('returns correct timeframe startDate and endDate in words', () => {
-        expect(vm.timeframeRange.startDate).toBe('Dec 1, 2017');
+        expect(vm.timeframeRange.startDate).toBe('Nov 1, 2017');
         expect(vm.timeframeRange.endDate).toBe('Jun 30, 2018');
       });
     });
diff --git a/ee/spec/javascripts/roadmap/components/epics_list_section_spec.js b/ee/spec/javascripts/roadmap/components/epics_list_section_spec.js
index 1a2b273f3cae6ef182b2e06db18f28d1e85f6506..cb37021dd8aae4a80e6d5005746ae60640958adb 100644
--- a/ee/spec/javascripts/roadmap/components/epics_list_section_spec.js
+++ b/ee/spec/javascripts/roadmap/components/epics_list_section_spec.js
@@ -3,11 +3,26 @@ import Vue from 'vue';
 import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue';
 import RoadmapStore from 'ee/roadmap/store/roadmap_store';
 import eventHub from 'ee/roadmap/event_hub';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { rawEpics, mockTimeframeMonths, mockGroupId, mockShellWidth } from '../mock_data';
+import {
+  rawEpics,
+  mockTimeframeInitialDate,
+  mockGroupId,
+  mockShellWidth,
+  mockSortedBy,
+} from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
+
+const store = new RoadmapStore({
+  groupId: mockGroupId,
+  presetType: PRESET_TYPES.MONTHS,
+  sortedBy: mockSortedBy,
+  timeframe: mockTimeframeMonths,
+});
 
-const store = new RoadmapStore(mockGroupId, mockTimeframeMonths, PRESET_TYPES.MONTHS);
 store.setEpics(rawEpics);
 const mockEpics = store.getEpics();
 
@@ -63,7 +78,7 @@ describe('EpicsListSectionComponent', () => {
 
     describe('emptyRowCellStyles', () => {
       it('returns computed style object based on sectionItemWidth prop value', () => {
-        expect(vm.emptyRowCellStyles.width).toBe('240px');
+        expect(vm.emptyRowCellStyles.width).toBe('210px');
       });
     });
 
@@ -129,15 +144,6 @@ describe('EpicsListSectionComponent', () => {
         });
       });
     });
-
-    describe('scrollToTodayIndicator', () => {
-      it('scrolls table body to put timeline today indicator in focus', () => {
-        spyOn(vm.$el, 'scrollTo');
-        vm.scrollToTodayIndicator();
-
-        expect(vm.$el.scrollTo).toHaveBeenCalledWith(jasmine.any(Number), 0);
-      });
-    });
   });
 
   describe('template', () => {
diff --git a/ee/spec/javascripts/roadmap/components/preset_months/months_header_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_months/months_header_item_spec.js
index 523a8ae5ec96a6790145d11bcd5dc2dfab8d24b2..3d478d65ae767ff556d47d061944ce0fad04c371 100644
--- a/ee/spec/javascripts/roadmap/components/preset_months/months_header_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_months/months_header_item_spec.js
@@ -1,10 +1,12 @@
 import Vue from 'vue';
 
 import MonthsHeaderItemComponent from 'ee/roadmap/components/preset_months/months_header_item.vue';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeMonths, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
 
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 const mockTimeframeIndex = 0;
 
 const createComponent = ({
@@ -56,7 +58,7 @@ describe('MonthsHeaderItemComponent', () => {
       it('returns string containing Year and Month for current timeline header item', () => {
         vm = createComponent({});
 
-        expect(vm.timelineHeaderLabel).toBe('2017 Dec');
+        expect(vm.timelineHeaderLabel).toBe('2017 Nov');
       });
 
       it('returns string containing only Month for current timeline header item when previous header contained Year', () => {
@@ -65,7 +67,7 @@ describe('MonthsHeaderItemComponent', () => {
           timeframeItem: mockTimeframeMonths[mockTimeframeIndex + 1],
         });
 
-        expect(vm.timelineHeaderLabel).toBe('2018 Jan');
+        expect(vm.timelineHeaderLabel).toBe('Dec');
       });
     });
 
@@ -117,7 +119,7 @@ describe('MonthsHeaderItemComponent', () => {
       const itemLabelEl = vm.$el.querySelector('.item-label');
 
       expect(itemLabelEl).not.toBeNull();
-      expect(itemLabelEl.innerText.trim()).toBe('2017 Dec');
+      expect(itemLabelEl.innerText.trim()).toBe('2017 Nov');
     });
   });
 });
diff --git a/ee/spec/javascripts/roadmap/components/preset_months/months_header_sub_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_months/months_header_sub_item_spec.js
index 7e62c1aa56bf8e0d91bba97e68ea43ee7c8734a3..cdd9bf04727ea89dcb238963c23f2087c4c5467e 100644
--- a/ee/spec/javascripts/roadmap/components/preset_months/months_header_sub_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_months/months_header_sub_item_spec.js
@@ -1,9 +1,12 @@
 import Vue from 'vue';
 
 import MonthsHeaderSubItemComponent from 'ee/roadmap/components/preset_months/months_header_sub_item.vue';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeMonths } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = ({
   currentDate = mockTimeframeMonths[0],
diff --git a/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_item_spec.js
index 93b6364ffb05ef4a142c33bec23b96693ca03563..611a02b9b2d184bd5a816a92c76bb79bd1e518ab 100644
--- a/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_item_spec.js
@@ -1,11 +1,13 @@
 import Vue from 'vue';
 
 import QuartersHeaderItemComponent from 'ee/roadmap/components/preset_quarters/quarters_header_item.vue';
+import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeQuarters, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
 
 const mockTimeframeIndex = 0;
+const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
 
 const createComponent = ({
   timeframeIndex = mockTimeframeIndex,
@@ -38,8 +40,6 @@ describe('QuartersHeaderItemComponent', () => {
       const currentDate = new Date();
 
       expect(vm.currentDate.getDate()).toBe(currentDate.getDate());
-      expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]);
-      expect(vm.quarterEndDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[2]);
     });
   });
 
@@ -52,11 +52,23 @@ describe('QuartersHeaderItemComponent', () => {
       });
     });
 
+    describe('quarterBeginDate', () => {
+      it('returns date object representing quarter begin date for current `timeframeItem`', () => {
+        expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[0]);
+      });
+    });
+
+    describe('quarterEndDate', () => {
+      it('returns date object representing quarter end date for current `timeframeItem`', () => {
+        expect(vm.quarterEndDate).toBe(mockTimeframeQuarters[mockTimeframeIndex].range[2]);
+      });
+    });
+
     describe('timelineHeaderLabel', () => {
       it('returns string containing Year and Quarter for current timeline header item', () => {
         vm = createComponent({});
 
-        expect(vm.timelineHeaderLabel).toBe('2017 Q4');
+        expect(vm.timelineHeaderLabel).toBe('2017 Q3');
       });
 
       it('returns string containing only Quarter for current timeline header item when previous header contained Year', () => {
@@ -65,7 +77,7 @@ describe('QuartersHeaderItemComponent', () => {
           timeframeItem: mockTimeframeQuarters[mockTimeframeIndex + 2],
         });
 
-        expect(vm.timelineHeaderLabel).toBe('Q2');
+        expect(vm.timelineHeaderLabel).toBe('2018 Q1');
       });
     });
 
@@ -118,7 +130,7 @@ describe('QuartersHeaderItemComponent', () => {
       const itemLabelEl = vm.$el.querySelector('.item-label');
 
       expect(itemLabelEl).not.toBeNull();
-      expect(itemLabelEl.innerText.trim()).toBe('2017 Q4');
+      expect(itemLabelEl.innerText.trim()).toBe('2017 Q3');
     });
   });
 });
diff --git a/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item_spec.js
index 12f84e12b8c998af336c942c54effdb89722d161..69bf9d38726adb2d15f08c4a9c99d07b0b5bb51b 100644
--- a/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_quarters/quarters_header_sub_item_spec.js
@@ -1,9 +1,12 @@
 import Vue from 'vue';
 
 import QuartersHeaderSubItemComponent from 'ee/roadmap/components/preset_quarters/quarters_header_sub_item.vue';
+import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeQuarters } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
+
+const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
 
 const createComponent = ({
   currentDate = mockTimeframeQuarters[0].range[1],
@@ -25,6 +28,22 @@ describe('QuartersHeaderSubItemComponent', () => {
   });
 
   describe('computed', () => {
+    describe('quarterBeginDate', () => {
+      it('returns first month from the `timeframeItem.range`', () => {
+        vm = createComponent({});
+
+        expect(vm.quarterBeginDate).toBe(mockTimeframeQuarters[0].range[0]);
+      });
+    });
+
+    describe('quarterEndDate', () => {
+      it('returns first month from the `timeframeItem.range`', () => {
+        vm = createComponent({});
+
+        expect(vm.quarterEndDate).toBe(mockTimeframeQuarters[0].range[2]);
+      });
+    });
+
     describe('headerSubItems', () => {
       it('returns array of dates containing Months from timeframeItem', () => {
         vm = createComponent({});
@@ -46,7 +65,7 @@ describe('QuartersHeaderSubItemComponent', () => {
       it('returns false when current quarter month is different from timeframe quarter', () => {
         vm = createComponent({
           currentDate: new Date(2017, 10, 1), // Nov 1, 2017
-          timeframeItem: mockTimeframeQuarters[1], // 2018 Apr May Jun
+          timeframeItem: mockTimeframeQuarters[0], // 2018 Apr May Jun
         });
 
         expect(vm.hasToday).toBe(false);
diff --git a/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_item_spec.js
index 26a5c1e40e13d6237c9833fa8bf3981da15db95c..1177534b097339839f5883630d1a00fb52dab5d3 100644
--- a/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_item_spec.js
@@ -1,11 +1,13 @@
 import Vue from 'vue';
 
 import WeeksHeaderItemComponent from 'ee/roadmap/components/preset_weeks/weeks_header_item.vue';
+import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeWeeks, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate, mockShellWidth, mockItemWidth } from 'ee_spec/roadmap/mock_data';
 
 const mockTimeframeIndex = 0;
+const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
 
 const createComponent = ({
   timeframeIndex = mockTimeframeIndex,
@@ -38,9 +40,6 @@ describe('WeeksHeaderItemComponent', () => {
       const currentDate = new Date();
 
       expect(vm.currentDate.getDate()).toBe(currentDate.getDate());
-      expect(vm.lastDayOfCurrentWeek.getDate()).toBe(
-        mockTimeframeWeeks[mockTimeframeIndex].getDate() + 7,
-      );
     });
   });
 
@@ -53,20 +52,37 @@ describe('WeeksHeaderItemComponent', () => {
       });
     });
 
+    describe('lastDayOfCurrentWeek', () => {
+      it('returns date object representing last day of the week as set in `timeframeItem`', () => {
+        expect(vm.lastDayOfCurrentWeek.getDate()).toBe(
+          mockTimeframeWeeks[mockTimeframeIndex].getDate() + 7,
+        );
+      });
+    });
+
     describe('timelineHeaderLabel', () => {
-      it('returns string containing Year, Month and Date for current timeline header item', () => {
+      it('returns string containing Year, Month and Date for first timeframe item of the entire timeframe', () => {
         vm = createComponent({});
 
-        expect(vm.timelineHeaderLabel).toBe('2017 Dec 24');
+        expect(vm.timelineHeaderLabel).toBe('2017 Dec 17');
+      });
+
+      it('returns string containing Year, Month and Date for timeframe item when it is first week of the year', () => {
+        vm = createComponent({
+          timeframeIndex: 3,
+          timeframeItem: new Date(2019, 0, 6),
+        });
+
+        expect(vm.timelineHeaderLabel).toBe('2019 Jan 6');
       });
 
-      it('returns string containing only Month and Date for current timeline header item when previous header contained Year', () => {
+      it('returns string containing only Month and Date timeframe item when it is somewhere in the middle of timeframe', () => {
         vm = createComponent({
           timeframeIndex: mockTimeframeIndex + 1,
           timeframeItem: mockTimeframeWeeks[mockTimeframeIndex + 1],
         });
 
-        expect(vm.timelineHeaderLabel).toBe('Dec 31');
+        expect(vm.timelineHeaderLabel).toBe('Dec 24');
       });
     });
 
@@ -112,7 +128,7 @@ describe('WeeksHeaderItemComponent', () => {
       const itemLabelEl = vm.$el.querySelector('.item-label');
 
       expect(itemLabelEl).not.toBeNull();
-      expect(itemLabelEl.innerText.trim()).toBe('2017 Dec 24');
+      expect(itemLabelEl.innerText.trim()).toBe('2017 Dec 17');
     });
   });
 });
diff --git a/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item_spec.js b/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item_spec.js
index f8a8af1a3c5771973d5520b5901e4ede418ec638..6d5a7a4dad4ac045b1b486fd2efe612d8ec08751 100644
--- a/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item_spec.js
+++ b/ee/spec/javascripts/roadmap/components/preset_weeks/weeks_header_sub_item_spec.js
@@ -1,9 +1,12 @@
 import Vue from 'vue';
 
 import WeeksHeaderSubItemComponent from 'ee/roadmap/components/preset_weeks/weeks_header_sub_item.vue';
+import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeWeeks } from 'ee_spec/roadmap/mock_data';
+import { mockTimeframeInitialDate } from 'ee_spec/roadmap/mock_data';
+
+const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
 
 const createComponent = ({
   currentDate = mockTimeframeWeeks[0],
@@ -24,19 +27,19 @@ describe('MonthsHeaderSubItemComponent', () => {
     vm.$destroy();
   });
 
-  describe('data', () => {
-    it('sets prop `headerSubItems` with array of dates containing days of week from timeframeItem', () => {
-      vm = createComponent({});
+  describe('computed', () => {
+    describe('headerSubItems', () => {
+      it('returns `headerSubItems` array of dates containing days of week from timeframeItem', () => {
+        vm = createComponent({});
 
-      expect(Array.isArray(vm.headerSubItems)).toBe(true);
-      expect(vm.headerSubItems.length).toBe(7);
-      vm.headerSubItems.forEach(subItem => {
-        expect(subItem instanceof Date).toBe(true);
+        expect(Array.isArray(vm.headerSubItems)).toBe(true);
+        expect(vm.headerSubItems.length).toBe(7);
+        vm.headerSubItems.forEach(subItem => {
+          expect(subItem instanceof Date).toBe(true);
+        });
       });
     });
-  });
 
-  describe('computed', () => {
     describe('hasToday', () => {
       it('returns true when current week is same as timeframe week', () => {
         vm = createComponent({});
diff --git a/ee/spec/javascripts/roadmap/components/roadmap_shell_spec.js b/ee/spec/javascripts/roadmap/components/roadmap_shell_spec.js
index d1a1375fb3f422db85f63e90531b99e1c9cad3e7..c9d5a91a197e1534476b8719f2fec1ddc43c0305 100644
--- a/ee/spec/javascripts/roadmap/components/roadmap_shell_spec.js
+++ b/ee/spec/javascripts/roadmap/components/roadmap_shell_spec.js
@@ -2,11 +2,14 @@ import Vue from 'vue';
 
 import roadmapShellComponent from 'ee/roadmap/components/roadmap_shell.vue';
 import eventHub from 'ee/roadmap/event_hub';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockEpic, mockTimeframeMonths, mockGroupId, mockScrollBarSize } from '../mock_data';
+import { mockEpic, mockTimeframeInitialDate, mockGroupId, mockScrollBarSize } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = (
   { epics = [mockEpic], timeframe = mockTimeframeMonths, currentGroupId = mockGroupId },
@@ -88,14 +91,29 @@ describe('RoadmapShellComponent', () => {
 
     describe('handleScroll', () => {
       beforeEach(() => {
-        spyOn(eventHub, '$emit');
+        document.body.innerHTML +=
+          '<div class="roadmap-container"><div id="roadmap-shell"></div></div>';
       });
 
-      it('emits `epicsListScrolled` event via eventHub', () => {
-        vm.noScroll = false;
-        vm.handleScroll();
+      afterEach(() => {
+        document.querySelector('.roadmap-container').remove();
+      });
+
+      it('emits `epicsListScrolled` event via eventHub', done => {
+        const vmWithParentEl = createComponent({}, document.getElementById('roadmap-shell'));
+        spyOn(eventHub, '$emit');
+
+        Vue.nextTick()
+          .then(() => {
+            vmWithParentEl.noScroll = false;
+            vmWithParentEl.handleScroll();
+
+            expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object));
 
-        expect(eventHub.$emit).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Object));
+            vmWithParentEl.$destroy();
+          })
+          .then(done)
+          .catch(done.fail);
       });
     });
   });
diff --git a/ee/spec/javascripts/roadmap/components/roadmap_timeline_section_spec.js b/ee/spec/javascripts/roadmap/components/roadmap_timeline_section_spec.js
index 246c540f8579bf3a18f7a035c147459719a51ea2..1cb3369f6d3941c465bd5b930f4a06cb5b93e839 100644
--- a/ee/spec/javascripts/roadmap/components/roadmap_timeline_section_spec.js
+++ b/ee/spec/javascripts/roadmap/components/roadmap_timeline_section_spec.js
@@ -2,11 +2,14 @@ import Vue from 'vue';
 
 import roadmapTimelineSectionComponent from 'ee/roadmap/components/roadmap_timeline_section.vue';
 import eventHub from 'ee/roadmap/event_hub';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockEpic, mockTimeframeMonths, mockShellWidth } from '../mock_data';
+import { mockEpic, mockTimeframeInitialDate, mockShellWidth } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.MONTHS,
diff --git a/ee/spec/javascripts/roadmap/components/timeline_today_indicator_spec.js b/ee/spec/javascripts/roadmap/components/timeline_today_indicator_spec.js
index 0ad21a725d57529db00b95502edb0eadee7a129e..12f07560e4b112cea7d25810be356d5847dba3ff 100644
--- a/ee/spec/javascripts/roadmap/components/timeline_today_indicator_spec.js
+++ b/ee/spec/javascripts/roadmap/components/timeline_today_indicator_spec.js
@@ -2,10 +2,14 @@ import Vue from 'vue';
 
 import timelineTodayIndicatorComponent from 'ee/roadmap/components/timeline_today_indicator.vue';
 import eventHub from 'ee/roadmap/event_hub';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeMonths } from '../mock_data';
+import { mockTimeframeInitialDate } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const mockCurrentDate = new Date(
   mockTimeframeMonths[0].getFullYear(),
@@ -53,29 +57,33 @@ describe('TimelineTodayIndicatorComponent', () => {
         const stylesObj = vm.todayBarStyles;
 
         expect(stylesObj.height).toBe('120px');
-        expect(stylesObj.left).toBe('48%');
+        expect(stylesObj.left).toBe('50%');
         expect(vm.todayBarReady).toBe(true);
       });
     });
   });
 
   describe('mounted', () => {
-    it('binds `epicsListRendered` event listener via eventHub', () => {
+    it('binds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
       spyOn(eventHub, '$on');
       const vmX = createComponent({});
 
       expect(eventHub.$on).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
+      expect(eventHub.$on).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
+      expect(eventHub.$on).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
       vmX.$destroy();
     });
   });
 
   describe('beforeDestroy', () => {
-    it('unbinds `epicsListRendered` event listener via eventHub', () => {
+    it('unbinds `epicsListRendered`, `epicsListScrolled` and `refreshTimeline` event listeners via eventHub', () => {
       spyOn(eventHub, '$off');
       const vmX = createComponent({});
       vmX.$destroy();
 
       expect(eventHub.$off).toHaveBeenCalledWith('epicsListRendered', jasmine.any(Function));
+      expect(eventHub.$off).toHaveBeenCalledWith('epicsListScrolled', jasmine.any(Function));
+      expect(eventHub.$off).toHaveBeenCalledWith('refreshTimeline', jasmine.any(Function));
     });
   });
 
diff --git a/ee/spec/javascripts/roadmap/mixins/months_preset_mixin_spec.js b/ee/spec/javascripts/roadmap/mixins/months_preset_mixin_spec.js
index ec2bd35090b994a29c2767aa8a96de61088f3dbb..9eca1073609a92ca568df025e1c8c37bf214c50c 100644
--- a/ee/spec/javascripts/roadmap/mixins/months_preset_mixin_spec.js
+++ b/ee/spec/javascripts/roadmap/mixins/months_preset_mixin_spec.js
@@ -1,10 +1,14 @@
 import Vue from 'vue';
 
 import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeMonths, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.MONTHS,
@@ -114,17 +118,6 @@ describe('MonthsPresetMixin', () => {
         expect(vm.getTimelineBarStartOffsetForMonths()).toBe('left: 0;');
       });
 
-      it('returns `right: 8px;` when Epic startDate is in last timeframe month and endDate is out of range', () => {
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            startDate: mockTimeframeMonths[mockTimeframeMonths.length - 1],
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarStartOffsetForMonths()).toBe('right: 8px;');
-      });
-
       it('returns proportional `left` value based on Epic startDate and days in the month', () => {
         vm = createComponent({
           epic: Object.assign({}, mockEpic, {
@@ -132,7 +125,7 @@ describe('MonthsPresetMixin', () => {
           }),
         });
 
-        expect(vm.getTimelineBarStartOffsetForMonths()).toContain('left: 48');
+        expect(vm.getTimelineBarStartOffsetForMonths()).toContain('left: 50%');
       });
     });
 
@@ -147,7 +140,7 @@ describe('MonthsPresetMixin', () => {
           }),
         });
 
-        expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(492);
+        expect(Math.floor(vm.getTimelineBarWidthForMonths())).toBe(637);
       });
     });
   });
diff --git a/ee/spec/javascripts/roadmap/mixins/quarters_preset_mixin_spec.js b/ee/spec/javascripts/roadmap/mixins/quarters_preset_mixin_spec.js
index a047cd1f61271d9871946115b5afe014257a02f4..360606a6f7016b59b1b7d0ded545b8a622f08baf 100644
--- a/ee/spec/javascripts/roadmap/mixins/quarters_preset_mixin_spec.js
+++ b/ee/spec/javascripts/roadmap/mixins/quarters_preset_mixin_spec.js
@@ -1,10 +1,14 @@
 import Vue from 'vue';
 
 import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
+import { getTimeframeForQuartersView } from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeQuarters, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+
+const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.QUARTERS,
@@ -114,17 +118,6 @@ describe('QuartersPresetMixin', () => {
         expect(vm.getTimelineBarStartOffsetForQuarters()).toBe('left: 0;');
       });
 
-      it('returns `right: 8px;` when Epic startDate is in last timeframe month and endDate is out of range', () => {
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            startDate: mockTimeframeQuarters[mockTimeframeQuarters.length - 1].range[1],
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarStartOffsetForQuarters()).toBe('right: 8px;');
-      });
-
       it('returns proportional `left` value based on Epic startDate and days in the quarter', () => {
         vm = createComponent({
           epic: Object.assign({}, mockEpic, {
@@ -147,7 +140,7 @@ describe('QuartersPresetMixin', () => {
           }),
         });
 
-        expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(282);
+        expect(Math.floor(vm.getTimelineBarWidthForQuarters())).toBe(240);
       });
     });
   });
diff --git a/ee/spec/javascripts/roadmap/mixins/section_mixin_spec.js b/ee/spec/javascripts/roadmap/mixins/section_mixin_spec.js
index 9bc67f011cde9e91f95a3a56f1f03acab729acf7..7698b3a7701620b0cb7b1f5a9c752bb7b4eff9b4 100644
--- a/ee/spec/javascripts/roadmap/mixins/section_mixin_spec.js
+++ b/ee/spec/javascripts/roadmap/mixins/section_mixin_spec.js
@@ -1,11 +1,19 @@
 import Vue from 'vue';
 
 import roadmapTimelineSectionComponent from 'ee/roadmap/components/roadmap_timeline_section.vue';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
 
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockEpic, mockTimeframeMonths, mockShellWidth, mockScrollBarSize } from '../mock_data';
+import {
+  mockEpic,
+  mockTimeframeInitialDate,
+  mockShellWidth,
+  mockScrollBarSize,
+} from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.MONTHS,
@@ -52,7 +60,7 @@ describe('SectionMixin', () => {
 
     describe('sectionItemWidth', () => {
       it('returns calculated item width based on sectionShellWidth and timeframe size', () => {
-        expect(vm.sectionItemWidth).toBe(240);
+        expect(vm.sectionItemWidth).toBe(210);
       });
     });
 
diff --git a/ee/spec/javascripts/roadmap/mixins/weeks_preset_mixin_spec.js b/ee/spec/javascripts/roadmap/mixins/weeks_preset_mixin_spec.js
index 3ac996bc907f92eafd4cac26fd95291a31ef7933..83a23a9299f1c3c0df1243223d2247518694bfd9 100644
--- a/ee/spec/javascripts/roadmap/mixins/weeks_preset_mixin_spec.js
+++ b/ee/spec/javascripts/roadmap/mixins/weeks_preset_mixin_spec.js
@@ -1,10 +1,14 @@
 import Vue from 'vue';
 
 import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
+import { getTimeframeForWeeksView } from 'ee/roadmap/utils/roadmap_utils';
+
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
 import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTimeframeWeeks, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+import { mockTimeframeInitialDate, mockEpic, mockShellWidth, mockItemWidth } from '../mock_data';
+
+const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
 
 const createComponent = ({
   presetType = PRESET_TYPES.WEEKS,
@@ -59,7 +63,7 @@ describe('WeeksPresetMixin', () => {
         vm = createComponent({});
         const lastDayOfWeek = vm.getLastDayOfWeek(mockTimeframeWeeks[0]);
 
-        expect(lastDayOfWeek.getDate()).toBe(30);
+        expect(lastDayOfWeek.getDate()).toBe(23);
         expect(lastDayOfWeek.getMonth()).toBe(11);
         expect(lastDayOfWeek.getFullYear()).toBe(2017);
       });
@@ -95,14 +99,6 @@ describe('WeeksPresetMixin', () => {
       });
     });
 
-    describe('getTimelineBarEndOffsetHalfForWeek', () => {
-      it('returns timeline bar end offset for Weeks view', () => {
-        vm = createComponent({});
-
-        expect(vm.getTimelineBarEndOffsetHalfForWeek()).toBe(28);
-      });
-    });
-
     describe('getTimelineBarStartOffsetForWeeks', () => {
       it('returns empty string when Epic startDate is out of range', () => {
         vm = createComponent({
@@ -133,19 +129,6 @@ describe('WeeksPresetMixin', () => {
         expect(vm.getTimelineBarStartOffsetForWeeks()).toBe('left: 0;');
       });
 
-      it('returns `right: 8px;` when Epic startDate is in last timeframe month and endDate is out of range', () => {
-        const startDate = new Date(mockTimeframeWeeks[mockTimeframeWeeks.length - 1].getTime());
-        startDate.setDate(startDate.getDate() + 1);
-        vm = createComponent({
-          epic: Object.assign({}, mockEpic, {
-            startDate,
-            endDateOutOfRange: true,
-          }),
-        });
-
-        expect(vm.getTimelineBarStartOffsetForWeeks()).toBe('right: 8px;');
-      });
-
       it('returns proportional `left` value based on Epic startDate and days in the month', () => {
         vm = createComponent({
           epic: Object.assign({}, mockEpic, {
@@ -153,7 +136,7 @@ describe('WeeksPresetMixin', () => {
           }),
         });
 
-        expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 60');
+        expect(vm.getTimelineBarStartOffsetForWeeks()).toContain('left: 51');
       });
     });
 
@@ -168,7 +151,7 @@ describe('WeeksPresetMixin', () => {
           }),
         });
 
-        expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1600);
+        expect(Math.floor(vm.getTimelineBarWidthForWeeks())).toBe(1611);
       });
     });
   });
diff --git a/ee/spec/javascripts/roadmap/mock_data.js b/ee/spec/javascripts/roadmap/mock_data.js
index 799a743b492414d1d05833306fec812523b8bf8a..56c32ea02782c067ca6f41f9017a30d5996ac6a0 100644
--- a/ee/spec/javascripts/roadmap/mock_data.js
+++ b/ee/spec/javascripts/roadmap/mock_data.js
@@ -1,9 +1,3 @@
-import {
-  getTimeframeForQuartersView,
-  getTimeframeForMonthsView,
-  getTimeframeForWeeksView,
-} from 'ee/roadmap/utils/roadmap_utils';
-
 export const mockScrollBarSize = 15;
 
 export const mockGroupId = 2;
@@ -12,15 +6,85 @@ export const mockShellWidth = 2000;
 
 export const mockItemWidth = 180;
 
+export const mockSortedBy = 'start_date_asc';
+
+export const basePath = '/groups/gitlab-org/-/epics.json';
+
 export const epicsPath = '/groups/gitlab-org/-/epics.json?start_date=2017-11-1&end_date=2018-4-30';
 
 export const mockNewEpicEndpoint = '/groups/gitlab-org/-/epics';
 
 export const mockSvgPath = '/foo/bar.svg';
 
-export const mockTimeframeQuarters = getTimeframeForQuartersView(new Date(2018, 0, 1));
-export const mockTimeframeMonths = getTimeframeForMonthsView(new Date(2018, 0, 1));
-export const mockTimeframeWeeks = getTimeframeForWeeksView(new Date(2018, 0, 1));
+export const mockTimeframeInitialDate = new Date(2018, 0, 1);
+
+export const mockTimeframeQuartersPrepend = [
+  {
+    year: 2016,
+    quarterSequence: 4,
+    range: [new Date(2016, 9, 1), new Date(2016, 10, 1), new Date(2016, 11, 31)],
+  },
+  {
+    year: 2017,
+    quarterSequence: 1,
+    range: [new Date(2017, 0, 1), new Date(2017, 1, 1), new Date(2017, 2, 31)],
+  },
+  {
+    year: 2017,
+    quarterSequence: 2,
+    range: [new Date(2017, 3, 1), new Date(2017, 4, 1), new Date(2017, 5, 30)],
+  },
+];
+export const mockTimeframeQuartersAppend = [
+  {
+    year: 2019,
+    quarterSequence: 2,
+    range: [new Date(2019, 3, 1), new Date(2019, 4, 1), new Date(2019, 5, 30)],
+  },
+  {
+    year: 2019,
+    quarterSequence: 3,
+    range: [new Date(2019, 6, 1), new Date(2019, 7, 1), new Date(2019, 8, 30)],
+  },
+  {
+    year: 2019,
+    quarterSequence: 4,
+    range: [new Date(2019, 9, 1), new Date(2019, 10, 1), new Date(2019, 11, 31)],
+  },
+];
+
+export const mockTimeframeMonthsPrepend = [
+  new Date(2017, 4, 1),
+  new Date(2017, 5, 1),
+  new Date(2017, 6, 1),
+  new Date(2017, 7, 1),
+  new Date(2017, 8, 1),
+  new Date(2017, 9, 1),
+];
+export const mockTimeframeMonthsAppend = [
+  new Date(2018, 6, 1),
+  new Date(2018, 7, 1),
+  new Date(2018, 8, 1),
+  new Date(2018, 9, 1),
+  new Date(2018, 10, 30),
+];
+
+export const mockTimeframeWeeksPrepend = [
+  new Date(2017, 10, 5),
+  new Date(2017, 10, 12),
+  new Date(2017, 10, 19),
+  new Date(2017, 10, 26),
+  new Date(2017, 11, 3),
+  new Date(2017, 11, 10),
+];
+export const mockTimeframeWeeksAppend = [
+  new Date(2018, 0, 28),
+  new Date(2018, 1, 4),
+  new Date(2018, 1, 11),
+  new Date(2018, 1, 18),
+  new Date(2018, 1, 25),
+  new Date(2018, 2, 4),
+];
 
 export const mockEpic = {
   id: 1,
@@ -188,3 +252,22 @@ export const rawEpics = [
     web_url: '/groups/gitlab-org/marketing/-/epics/22',
   },
 ];
+
+export const mockUnsortedEpics = [
+  {
+    startDate: new Date(2017, 2, 12),
+    endDate: new Date(2017, 7, 20),
+  },
+  {
+    startDate: new Date(2015, 5, 8),
+    endDate: new Date(2016, 3, 1),
+  },
+  {
+    startDate: new Date(2019, 4, 12),
+    endDate: new Date(2019, 7, 30),
+  },
+  {
+    startDate: new Date(2014, 3, 17),
+    endDate: new Date(2015, 7, 15),
+  },
+];
diff --git a/ee/spec/javascripts/roadmap/service/roadmap_service_spec.js b/ee/spec/javascripts/roadmap/service/roadmap_service_spec.js
index 9f68ecf41f51e24daa3445024c7682b580c80489..f374698bee7ac01848f054607a25d004b0e6845a 100644
--- a/ee/spec/javascripts/roadmap/service/roadmap_service_spec.js
+++ b/ee/spec/javascripts/roadmap/service/roadmap_service_spec.js
@@ -1,13 +1,23 @@
 import axios from '~/lib/utils/axios_utils';
 
 import RoadmapService from 'ee/roadmap/service/roadmap_service';
-import { epicsPath } from '../mock_data';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
+import { PRESET_TYPES } from 'ee/roadmap/constants';
+import { basePath, epicsPath, mockTimeframeInitialDate } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 describe('RoadmapService', () => {
   let service;
 
   beforeEach(() => {
-    service = new RoadmapService(epicsPath);
+    service = new RoadmapService({
+      initialEpicsPath: epicsPath,
+      epicsState: 'all',
+      filterQueryString: '',
+      basePath,
+    });
   });
 
   describe('getEpics', () => {
@@ -15,7 +25,30 @@ describe('RoadmapService', () => {
       spyOn(axios, 'get').and.stub();
       service.getEpics();
 
-      expect(axios.get).toHaveBeenCalledWith(service.epicsPath);
+      expect(axios.get).toHaveBeenCalledWith(
+        '/groups/gitlab-org/-/epics.json?start_date=2017-11-1&end_date=2018-4-30',
+      );
+    });
+  });
+
+  describe('getEpicsForTimeframe', () => {
+    it('calls `getEpicsPathForPreset` to construct epics path', () => {
+      const getEpicsPathSpy = spyOnDependency(RoadmapService, 'getEpicsPathForPreset');
+      spyOn(axios, 'get').and.stub();
+
+      const presetType = PRESET_TYPES.MONTHS;
+
+      service.getEpicsForTimeframe(presetType, mockTimeframeMonths);
+
+      expect(getEpicsPathSpy).toHaveBeenCalledWith(
+        jasmine.objectContaining({
+          timeframe: mockTimeframeMonths,
+          epicsState: 'all',
+          filterQueryString: '',
+          basePath,
+          presetType,
+        }),
+      );
     });
   });
 });
diff --git a/ee/spec/javascripts/roadmap/store/roadmap_store_spec.js b/ee/spec/javascripts/roadmap/store/roadmap_store_spec.js
index 0754354896cbf9b1586a76fc33215e0881c1fc6c..1ad1f29269b13c1e292b3040a5171659c09a0a20 100644
--- a/ee/spec/javascripts/roadmap/store/roadmap_store_spec.js
+++ b/ee/spec/javascripts/roadmap/store/roadmap_store_spec.js
@@ -1,21 +1,32 @@
 import RoadmapStore from 'ee/roadmap/store/roadmap_store';
-import { PRESET_TYPES } from 'ee/roadmap/constants';
-import { mockGroupId, mockTimeframeMonths, rawEpics } from '../mock_data';
+import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils';
+
+import { PRESET_TYPES, EXTEND_AS } from 'ee/roadmap/constants';
+import { mockGroupId, mockTimeframeInitialDate, rawEpics, mockSortedBy } from '../mock_data';
+
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
 
 describe('RoadmapStore', () => {
   let store;
 
   beforeEach(() => {
-    store = new RoadmapStore(mockGroupId, mockTimeframeMonths, PRESET_TYPES.MONTHS);
+    store = new RoadmapStore({
+      groupId: mockGroupId,
+      timeframe: mockTimeframeMonths,
+      presetType: PRESET_TYPES.MONTHS,
+      sortedBy: mockSortedBy,
+    });
   });
 
   describe('constructor', () => {
     it('initializes default state', () => {
       expect(store.state).toBeDefined();
       expect(Array.isArray(store.state.epics)).toBe(true);
+      expect(Array.isArray(store.state.epicIds)).toBe(true);
       expect(store.state.currentGroupId).toBe(mockGroupId);
       expect(store.state.timeframe).toBe(mockTimeframeMonths);
       expect(store.presetType).toBe(PRESET_TYPES.MONTHS);
+      expect(store.sortedBy).toBe(mockSortedBy);
       expect(store.timeframeStartDate).toBeDefined();
       expect(store.timeframeEndDate).toBeDefined();
     });
@@ -23,9 +34,52 @@ describe('RoadmapStore', () => {
 
   describe('setEpics', () => {
     it('sets Epics list to state while filtering out Epics with invalid dates', () => {
+      spyOn(RoadmapStore, 'filterInvalidEpics').and.callThrough();
       store.setEpics(rawEpics);
 
-      expect(store.getEpics().length).toBe(rawEpics.length - 2); // 2 epics have invalid dates
+      expect(RoadmapStore.filterInvalidEpics).toHaveBeenCalledWith(
+        jasmine.objectContaining({
+          timeframeStartDate: store.timeframeStartDate,
+          timeframeEndDate: store.timeframeEndDate,
+          state: store.state,
+          epics: rawEpics,
+        }),
+      );
+
+      expect(store.getEpics().length).toBe(rawEpics.length - 1); // There is only 1 invalid epic
+    });
+  });
+
+  describe('addEpics', () => {
+    beforeEach(() => {
+      store.setEpics(rawEpics);
+    });
+
+    it('adds new Epics to the epics list within state while filtering out existing epics or epics with invalid dates and sorts the list based on `sortedBy` value', () => {
+      spyOn(RoadmapStore, 'filterInvalidEpics');
+      const sortEpicsSpy = spyOnDependency(RoadmapStore, 'sortEpics').and.stub();
+
+      const newEpic = Object.assign({}, rawEpics[0], {
+        id: 999,
+      });
+
+      store.addEpics([newEpic]);
+
+      expect(RoadmapStore.filterInvalidEpics).toHaveBeenCalledWith(
+        jasmine.objectContaining({
+          timeframeStartDate: store.timeframeStartDate,
+          timeframeEndDate: store.timeframeEndDate,
+          state: store.state,
+          epics: [newEpic],
+          newEpic: true,
+        }),
+      );
+
+      expect(sortEpicsSpy).toHaveBeenCalledWith(jasmine.any(Object), mockSortedBy);
+
+      // rawEpics contain 2 invalid epics but now that we added 1
+      // new epic, length will be `rawEpics.length - 1 (invalid) + 1 (new epic)`
+      expect(store.getEpics().length).toBe(rawEpics.length);
     });
   });
 
@@ -41,12 +95,98 @@ describe('RoadmapStore', () => {
     });
   });
 
+  describe('extendTimeframe', () => {
+    beforeEach(() => {
+      store.setEpics(rawEpics);
+      store.state.timeframe = [];
+    });
+
+    it('calls `extendTimeframeForPreset` and prepends items to the timeframe when called with `extendAs` param as `prepend`', () => {
+      const extendTimeframeSpy = spyOnDependency(
+        RoadmapStore,
+        'extendTimeframeForPreset',
+      ).and.returnValue([]);
+      spyOn(store.state.timeframe, 'unshift');
+      spyOn(store, 'initTimeframeThreshold');
+      spyOn(RoadmapStore, 'processEpicDates');
+
+      const itemCount = store.extendTimeframe(EXTEND_AS.PREPEND).length;
+
+      expect(extendTimeframeSpy).toHaveBeenCalledWith(jasmine.any(Object));
+      expect(store.state.timeframe.unshift).toHaveBeenCalled();
+      expect(store.initTimeframeThreshold).toHaveBeenCalled();
+      expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
+      expect(itemCount).toBe(0);
+    });
+
+    it('calls `extendTimeframeForPreset` and appends items to the timeframe when called with `extendAs` param as `append`', () => {
+      const extendTimeframeSpy = spyOnDependency(
+        RoadmapStore,
+        'extendTimeframeForPreset',
+      ).and.returnValue([]);
+      spyOn(store.state.timeframe, 'push');
+      spyOn(store, 'initTimeframeThreshold');
+      spyOn(RoadmapStore, 'processEpicDates');
+
+      const itemCount = store.extendTimeframe(EXTEND_AS.APPEND).length;
+
+      expect(extendTimeframeSpy).toHaveBeenCalledWith(jasmine.any(Object));
+      expect(store.state.timeframe.push).toHaveBeenCalled();
+      expect(store.initTimeframeThreshold).toHaveBeenCalled();
+      expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
+      expect(itemCount).toBe(0);
+    });
+  });
+
+  describe('filterInvalidEpics', () => {
+    it('returns formatted epics list by filtering out epics with invalid dates', () => {
+      spyOn(RoadmapStore, 'formatEpicDetails').and.callThrough();
+
+      const epicsList = RoadmapStore.filterInvalidEpics({
+        epics: rawEpics,
+        timeframeStartDate: store.timeframeStartDate,
+        timeframeEndDate: store.timeframeEndDate,
+        state: store.state,
+      });
+
+      expect(RoadmapStore.formatEpicDetails).toHaveBeenCalled();
+
+      expect(epicsList.length).toBe(rawEpics.length - 1); // There are is only 1 invalid epic
+    });
+
+    it('returns formatted epics list by filtering out existing epics', () => {
+      store.setEpics(rawEpics);
+
+      spyOn(RoadmapStore, 'formatEpicDetails').and.callThrough();
+
+      const newEpic = Object.assign({}, rawEpics[0]);
+
+      const epicsList = RoadmapStore.filterInvalidEpics({
+        epics: [newEpic],
+        timeframeStartDate: store.timeframeStartDate,
+        timeframeEndDate: store.timeframeEndDate,
+        state: store.state,
+      });
+
+      expect(RoadmapStore.formatEpicDetails).toHaveBeenCalled();
+
+      expect(epicsList.length).toBe(0); // No epics are eligible to be added
+    });
+  });
+
   describe('formatEpicDetails', () => {
     const rawEpic = rawEpics[0];
 
     it('returns formatted Epic object from raw Epic object', () => {
-      const epic = RoadmapStore.formatEpicDetails(rawEpic);
+      spyOn(RoadmapStore, 'processEpicDates');
+
+      const epic = RoadmapStore.formatEpicDetails(
+        rawEpic,
+        store.timeframeStartDate,
+        store.timeframeEndDate,
+      );
 
+      expect(RoadmapStore.processEpicDates).toHaveBeenCalled();
       expect(epic.id).toBe(rawEpic.id);
       expect(epic.name).toBe(rawEpic.name);
       expect(epic.groupId).toBe(rawEpic.group_id);
diff --git a/ee/spec/javascripts/roadmap/utils/roadmap_utils_spec.js b/ee/spec/javascripts/roadmap/utils/roadmap_utils_spec.js
index 94b85b2473c084edaeeaa6c8c683604f2980fe0c..b41722b52c53ce50fc05f596bd1a0b76d7fd29fc 100644
--- a/ee/spec/javascripts/roadmap/utils/roadmap_utils_spec.js
+++ b/ee/spec/javascripts/roadmap/utils/roadmap_utils_spec.js
@@ -1,12 +1,32 @@
 import {
   getTimeframeForQuartersView,
+  extendTimeframeForQuartersView,
   getTimeframeForMonthsView,
+  extendTimeframeForMonthsView,
   getTimeframeForWeeksView,
+  extendTimeframeForWeeksView,
+  extendTimeframeForAvailableWidth,
   getEpicsPathForPreset,
+  sortEpics,
 } from 'ee/roadmap/utils/roadmap_utils';
 
 import { PRESET_TYPES } from 'ee/roadmap/constants';
 
+import {
+  mockTimeframeInitialDate,
+  mockTimeframeQuartersPrepend,
+  mockTimeframeQuartersAppend,
+  mockTimeframeMonthsPrepend,
+  mockTimeframeMonthsAppend,
+  mockTimeframeWeeksPrepend,
+  mockTimeframeWeeksAppend,
+  mockUnsortedEpics,
+} from '../mock_data';
+
+const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
+const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
+const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
+
 describe('getTimeframeForQuartersView', () => {
   let timeframe;
 
@@ -14,8 +34,8 @@ describe('getTimeframeForQuartersView', () => {
     timeframe = getTimeframeForQuartersView(new Date(2018, 0, 1));
   });
 
-  it('returns timeframe with total of 6 quarters', () => {
-    expect(timeframe.length).toBe(6);
+  it('returns timeframe with total of 7 quarters', () => {
+    expect(timeframe.length).toBe(7);
   });
 
   it('each timeframe item has `quarterSequence`, `year` and `range` present', () => {
@@ -26,15 +46,15 @@ describe('getTimeframeForQuartersView', () => {
     expect(Array.isArray(timeframeItem.range)).toBe(true);
   });
 
-  it('first timeframe item refers to quarter prior to current quarter', () => {
+  it('first timeframe item refers to 2 quarters prior to current quarter', () => {
     const timeframeItem = timeframe[0];
     const expectedQuarter = {
-      0: { month: 9, date: 1 }, // 1 Oct 2017
-      1: { month: 10, date: 1 }, // 1 Nov 2017
-      2: { month: 11, date: 31 }, // 31 Dec 2017
+      0: { month: 6, date: 1 }, // 1 Jul 2017
+      1: { month: 7, date: 1 }, // 1 Aug 2017
+      2: { month: 8, date: 30 }, // 30 Sep 2017
     };
 
-    expect(timeframeItem.quarterSequence).toEqual(4);
+    expect(timeframeItem.quarterSequence).toEqual(3);
     expect(timeframeItem.year).toEqual(2017);
     timeframeItem.range.forEach((month, index) => {
       expect(month.getFullYear()).toBe(2017);
@@ -61,6 +81,44 @@ describe('getTimeframeForQuartersView', () => {
   });
 });
 
+describe('extendTimeframeForQuartersView', () => {
+  it('returns extended timeframe into the past from current timeframe startDate', () => {
+    const initialDate = mockTimeframeQuarters[0].range[0];
+
+    const extendedTimeframe = extendTimeframeForQuartersView(initialDate, -9);
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeQuartersPrepend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.year).toBe(mockTimeframeQuartersPrepend[index].year);
+      expect(timeframeItem.quarterSequence).toBe(
+        mockTimeframeQuartersPrepend[index].quarterSequence,
+      );
+
+      timeframeItem.range.forEach((rangeItem, j) => {
+        expect(rangeItem.getTime()).toBe(mockTimeframeQuartersPrepend[index].range[j].getTime());
+      });
+    });
+  });
+
+  it('returns extended timeframe into the future from current timeframe endDate', () => {
+    const initialDate = mockTimeframeQuarters[mockTimeframeQuarters.length - 1].range[2];
+
+    const extendedTimeframe = extendTimeframeForQuartersView(initialDate, 9);
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeQuartersAppend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.year).toBe(mockTimeframeQuartersAppend[index].year);
+      expect(timeframeItem.quarterSequence).toBe(
+        mockTimeframeQuartersAppend[index].quarterSequence,
+      );
+
+      timeframeItem.range.forEach((rangeItem, j) => {
+        expect(rangeItem.getTime()).toBe(mockTimeframeQuartersAppend[index].range[j].getTime());
+      });
+    });
+  });
+});
+
 describe('getTimeframeForMonthsView', () => {
   let timeframe;
 
@@ -68,15 +126,15 @@ describe('getTimeframeForMonthsView', () => {
     timeframe = getTimeframeForMonthsView(new Date(2018, 0, 1));
   });
 
-  it('returns timeframe with total of 7 months', () => {
-    expect(timeframe.length).toBe(7);
+  it('returns timeframe with total of 8 months', () => {
+    expect(timeframe.length).toBe(8);
   });
 
-  it('first timeframe item refers to month prior to current month', () => {
+  it('first timeframe item refers to 2 months prior to current month', () => {
     const timeframeItem = timeframe[0];
     const expectedMonth = {
       year: 2017,
-      month: 11,
+      month: 10,
       date: 1,
     };
 
@@ -99,6 +157,28 @@ describe('getTimeframeForMonthsView', () => {
   });
 });
 
+describe('extendTimeframeForMonthsView', () => {
+  it('returns extended timeframe into the past from current timeframe startDate', () => {
+    const initialDate = mockTimeframeMonths[0];
+    const extendedTimeframe = extendTimeframeForMonthsView(initialDate, -6);
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeMonthsPrepend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.getTime()).toBe(mockTimeframeMonthsPrepend[index].getTime());
+    });
+  });
+
+  it('returns extended timeframe into the future from current timeframe endDate', () => {
+    const initialDate = mockTimeframeMonths[mockTimeframeMonths.length - 1];
+    const extendedTimeframe = extendTimeframeForMonthsView(initialDate, 7);
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeMonthsAppend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.getTime()).toBe(mockTimeframeMonthsAppend[index].getTime());
+    });
+  });
+});
+
 describe('getTimeframeForWeeksView', () => {
   let timeframe;
 
@@ -106,16 +186,16 @@ describe('getTimeframeForWeeksView', () => {
     timeframe = getTimeframeForWeeksView(new Date(2018, 0, 1));
   });
 
-  it('returns timeframe with total of 6 weeks', () => {
-    expect(timeframe.length).toBe(6);
+  it('returns timeframe with total of 7 weeks', () => {
+    expect(timeframe.length).toBe(7);
   });
 
-  it('first timeframe item refers to week prior to current week', () => {
+  it('first timeframe item refers to 2 weeks prior to current week', () => {
     const timeframeItem = timeframe[0];
     const expectedMonth = {
       year: 2017,
       month: 11,
-      date: 24,
+      date: 17,
     };
 
     expect(timeframeItem.getFullYear()).toBe(expectedMonth.year);
@@ -137,6 +217,67 @@ describe('getTimeframeForWeeksView', () => {
   });
 });
 
+describe('extendTimeframeForWeeksView', () => {
+  it('returns extended timeframe into the past from current timeframe startDate', () => {
+    const extendedTimeframe = extendTimeframeForWeeksView(mockTimeframeWeeks[0], -6); // initialDate: 17 Dec 2017
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeWeeksPrepend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.getTime()).toBe(mockTimeframeWeeksPrepend[index].getTime());
+    });
+  });
+
+  it('returns extended timeframe into the future from current timeframe endDate', () => {
+    const extendedTimeframe = extendTimeframeForWeeksView(
+      mockTimeframeWeeks[mockTimeframeWeeks.length - 1], // initialDate: 28 Jan 2018
+      6,
+    );
+
+    expect(extendedTimeframe.length).toBe(mockTimeframeWeeksAppend.length);
+    extendedTimeframe.forEach((timeframeItem, index) => {
+      expect(timeframeItem.getTime()).toBe(mockTimeframeWeeksAppend[index].getTime());
+    });
+  });
+});
+
+describe('extendTimeframeForAvailableWidth', () => {
+  let timeframe;
+  let timeframeStart;
+  let timeframeEnd;
+
+  beforeEach(() => {
+    timeframe = mockTimeframeMonths.slice();
+    [timeframeStart] = timeframe;
+    timeframeEnd = timeframe[timeframe.length - 1];
+  });
+
+  it('should not extend `timeframe` when availableTimeframeWidth is small enough to force horizontal scrollbar to show up', () => {
+    extendTimeframeForAvailableWidth({
+      availableTimeframeWidth: 100,
+      presetType: PRESET_TYPES.MONTHS,
+      timeframe,
+      timeframeStart,
+      timeframeEnd,
+    });
+
+    expect(timeframe.length).toBe(mockTimeframeMonths.length);
+  });
+
+  it('should extend `timeframe` when availableTimeframeWidth is large enough that it can fit more timeframe items to show up horizontal scrollbar', () => {
+    extendTimeframeForAvailableWidth({
+      availableTimeframeWidth: 2000,
+      presetType: PRESET_TYPES.MONTHS,
+      timeframe,
+      timeframeStart,
+      timeframeEnd,
+    });
+
+    expect(timeframe.length).toBe(16);
+    expect(timeframe[0].getTime()).toBe(1498867200000); // 1 July 2017
+    expect(timeframe[timeframe.length - 1].getTime()).toBe(1540944000000); // 31 Oct 2018
+  });
+});
+
 describe('getEpicsPathForPreset', () => {
   const basePath = '/groups/gitlab-org/-/epics.json';
   const filterQueryString = 'scope=all&utf8=✓&state=opened&label_name[]=Bug';
@@ -149,7 +290,7 @@ describe('getEpicsPathForPreset', () => {
       presetType: PRESET_TYPES.QUARTERS,
     });
 
-    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-10-1&end_date=2019-3-31`);
+    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-7-1&end_date=2019-3-31`);
   });
 
   it('returns epics path string based on provided basePath and timeframe for Months', () => {
@@ -160,7 +301,7 @@ describe('getEpicsPathForPreset', () => {
       presetType: PRESET_TYPES.MONTHS,
     });
 
-    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-12-1&end_date=2018-6-30`);
+    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-11-1&end_date=2018-6-30`);
   });
 
   it('returns epics path string based on provided basePath and timeframe for Weeks', () => {
@@ -171,7 +312,7 @@ describe('getEpicsPathForPreset', () => {
       presetType: PRESET_TYPES.WEEKS,
     });
 
-    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-12-24&end_date=2018-2-3`);
+    expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-12-17&end_date=2018-2-3`);
   });
 
   it('returns epics path string while preserving filterQueryString', () => {
@@ -184,7 +325,94 @@ describe('getEpicsPathForPreset', () => {
     });
 
     expect(epicsPath).toBe(
-      `${basePath}?state=all&start_date=2017-12-1&end_date=2018-6-30&scope=all&utf8=✓&state=opened&label_name[]=Bug`,
+      `${basePath}?state=all&start_date=2017-11-1&end_date=2018-6-30&scope=all&utf8=✓&state=opened&label_name[]=Bug`,
     );
   });
+
+  it('returns epics path string containing epicsState', () => {
+    const epicsState = 'opened';
+    const timeframe = getTimeframeForMonthsView(new Date(2018, 0, 1));
+    const epicsPath = getEpicsPathForPreset({
+      presetType: PRESET_TYPES.MONTHS,
+      basePath,
+      timeframe,
+      epicsState,
+    });
+
+    expect(epicsPath).toContain(`state=${epicsState}`);
+  });
+});
+
+describe('sortEpics', () => {
+  it('sorts epics list by startDate in ascending order when `sortedBy` param is `start_date_asc`', () => {
+    const epics = mockUnsortedEpics.slice();
+    const sortedOrder = [
+      new Date(2014, 3, 17),
+      new Date(2015, 5, 8),
+      new Date(2017, 2, 12),
+      new Date(2019, 4, 12),
+    ];
+
+    sortEpics(epics, 'start_date_asc');
+
+    expect(epics.length).toBe(mockUnsortedEpics.length);
+
+    epics.forEach((epic, index) => {
+      expect(epic.startDate.getTime()).toBe(sortedOrder[index].getTime());
+    });
+  });
+
+  it('sorts epics list by startDate in descending order when `sortedBy` param is `start_date_desc`', () => {
+    const epics = mockUnsortedEpics.slice();
+    const sortedOrder = [
+      new Date(2019, 4, 12),
+      new Date(2017, 2, 12),
+      new Date(2015, 5, 8),
+      new Date(2014, 3, 17),
+    ];
+
+    sortEpics(epics, 'start_date_desc');
+
+    expect(epics.length).toBe(mockUnsortedEpics.length);
+
+    epics.forEach((epic, index) => {
+      expect(epic.startDate.getTime()).toBe(sortedOrder[index].getTime());
+    });
+  });
+
+  it('sorts epics list by endDate in ascending order when `sortedBy` param is `end_date_asc`', () => {
+    const epics = mockUnsortedEpics.slice();
+    const sortedOrder = [
+      new Date(2015, 7, 15),
+      new Date(2016, 3, 1),
+      new Date(2017, 7, 20),
+      new Date(2019, 7, 30),
+    ];
+
+    sortEpics(epics, 'end_date_asc');
+
+    expect(epics.length).toBe(mockUnsortedEpics.length);
+
+    epics.forEach((epic, index) => {
+      expect(epic.endDate.getTime()).toBe(sortedOrder[index].getTime());
+    });
+  });
+
+  it('sorts epics list by endDate in descending order when `sortedBy` param is `end_date_desc`', () => {
+    const epics = mockUnsortedEpics.slice();
+    const sortedOrder = [
+      new Date(2019, 7, 30),
+      new Date(2017, 7, 20),
+      new Date(2016, 3, 1),
+      new Date(2015, 7, 15),
+    ];
+
+    sortEpics(epics, 'end_date_desc');
+
+    expect(epics.length).toBe(mockUnsortedEpics.length);
+
+    epics.forEach((epic, index) => {
+      expect(epic.endDate.getTime()).toBe(sortedOrder[index].getTime());
+    });
+  });
 });
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6858c2811b1cd16faa788c8b827cbb33b37341c9..0e9344f1bbb73f3c24c0928ea13b13c42ead3b9f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4538,22 +4538,10 @@ msgstr ""
 msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
 msgstr ""
 
-msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from %{startDate} to %{endDate}."
+msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from %{startDate} to %{endDate}."
 msgstr ""
 
-msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|To widen your search, change or remove filters. In the months view, only epics in the past month, current month, and next 5 months are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|To widen your search, change or remove filters. In the quarters view, only epics in the past quarter, current quarter, and next 4 quarters are shown &ndash; from %{startDate} to %{endDate}."
-msgstr ""
-
-msgid "GroupRoadmap|To widen your search, change or remove filters. In the weeks view, only epics in the past week, current week, and next 4 weeks are shown &ndash; from %{startDate} to %{endDate}."
+msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
 msgstr ""
 
 msgid "GroupRoadmap|Until %{dateWord}"